Skip to content

Commit f5f7140

Browse files
authored
#20 - Support creating analytics via queued job (#21)
* feat(#20): Introduce support for tracking requests via a queued job BREAKING CHANGE: Changes to the API to support tracking requests via a queued job * refactor(#20): Only support PHP 8.0 for this version * refactor(#20): Update ubuntu version and node version
1 parent 124271f commit f5f7140

16 files changed

+241
-294
lines changed

.github/workflows/Build.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ on: [push]
33
jobs:
44
test:
55
name: Test
6-
runs-on: ubuntu-18.04
6+
runs-on: ubuntu-20.04
77
strategy:
88
matrix:
9-
php: [7.3, 8.0]
9+
php: [8.0]
1010
steps:
1111
- uses: actions/checkout@master
1212
- name: Checkout
@@ -31,14 +31,14 @@ jobs:
3131
name: Release
3232
if: github.ref == 'refs/heads/master'
3333
needs: test
34-
runs-on: ubuntu-18.04
34+
runs-on: ubuntu-20.04
3535
steps:
3636
- name: Checkout
3737
uses: actions/checkout@v1
3838
- name: Setup Node.js
3939
uses: actions/setup-node@v1
4040
with:
41-
node-version: 12
41+
node-version: 16
4242
- name: Install dependencies
4343
run: yarn install
4444
- name: Release

README.md

+22-98
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ The default migration assumes you users are stored in a `users` table with a `BI
7979
When logging a request, the package will use this code to insert the `user_id` into the `analytics` table:
8080

8181
```php
82-
if ($user = $request->user()) {
83-
$userId = $user->id;
84-
}
82+
$userId = $request->user()?->id ?? null
8583
```
8684

8785
### Excluding Routes
@@ -133,104 +131,38 @@ return [
133131
];
134132
```
135133

136-
### Post Request Hooks
137-
138-
We provide an optional hook you can use to run custom logic after an analytics record has been created. You can provide as many hooks as you want, calling `addPostHook` will add a new hook rather than replace existing hooks.
139-
140-
The hook closure is based an instance of `RequestDetails` and the `Analytics` record that was created for the request. You can access both the request and response inside the `RequestDetails` class. If you're using a custom `RequestDetails` class, you'll be given an instance of your custom class.
141-
142-
```php
143-
// AppServiceProvider
144-
145-
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
146-
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
147-
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;
148-
149-
public function boot()
150-
{
151-
ServerAnalytics::addPostHook(
152-
function (RequestDetails $requestDetails, Analytics $analytics) {
153-
// Do whatever you want with the request, response, and created analytics record
154-
}
155-
);
156-
}
157-
```
134+
### Request hooks
158135

159-
### Attach Entities to Analytics Records
136+
We provide took optional hooks that you can use to automatically associate extra data with your analytics records: `addMetaHook` and `addRelationHook`. Both hooks return an array of data that should be associated to the analytics record. You can add as many of each as you'd like throughout your request's lifecycle.
160137

161-
If you want to attach your application's entities to an analytics record, you can use the `addRelation(Model $model, ?string $reason = null)` on the Analytics model in combination with a hook:
138+
`addMetaHook` example:
162139

163140
```php
164-
// AppServiceProvider
165-
166-
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
167-
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
168-
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;
169-
170-
public function boot()
171-
{
172-
ServerAnalytics::addPostHook(
173-
function (RequestDetails $requestDetails, Analytics $analytics) {
174-
// Attach the logged-in user to the analytics request record
175-
if ($user = $requestDetails->request->user()) {
176-
$analytics->addRelation($user);
177-
}
178-
}
179-
);
180-
}
181-
```
182-
183-
There's also a helper method available if you want to attach a relation without checking the request, response, or analytics record:
184-
185-
```php
186-
// Controller
187-
188-
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
189-
190-
public function __invoke(Post $post)
191-
{
192-
ServerAnalytics::addRelation($post);
193-
194-
// ...finish controller logic
195-
}
141+
ServerAnalytics::addMetaHook(function (RequestDetails $requestDetails) {
142+
return [
143+
'key' => 'some-key',
144+
'value' => 'some-value'
145+
];
146+
});
196147
```
197148

198-
The method provides an optional second argument, `$reason`, which allows you to add a reason for the relation attachment. The `reason` column is a `string, VARCHAR(255)` column. If not passed, the value will be `null`.
199-
200-
### Attach Metadata to Analytics Records
201-
202-
In addition to attaching entities to your analytics records, you can attach custom metadata (key/value).
149+
`addRelationHook` example:
203150

204151
```php
205-
// AppServiceProvider
206-
207-
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
208-
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
209-
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;
210-
211-
public function boot()
212-
{
213-
ServerAnalytics::addPostHook(
214-
function (RequestDetails $requestDetails, Analytics $analytics) {
215-
$analytics->addMeta('foo', 'bar');
216-
}
217-
);
218-
}
152+
ServerAnalytics::addRelationHook(function (RequestDetails $requestDetails) use ($post) {
153+
return [
154+
'model' => $post,
155+
'reason' => 'Post that was deleted.'
156+
];
157+
});
219158
```
220159

221-
There's also a helper method available if you want to attach metadata without checking the request, response, or analytics record:
160+
There's also helper methods available which call the above hooks for you:
222161

223162
```php
224-
// Controller
225-
226-
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
163+
ServerAnalytics::addMeta('test', 'value');
227164

228-
public function __invoke(Post $post)
229-
{
230-
ServerAnalytics::addMeta('some-key', 'some-value');
231-
232-
// ...finish controller logic
233-
}
165+
ServerAnalytics::addRelation($post);
234166
```
235167

236168
### Providing Custom Request Details
@@ -247,21 +179,13 @@ class CustomRequestDetails extends RequestDetails
247179
{
248180
public function getMethod(): string
249181
{
182+
// Set the stored `method` as "TEST" for all requests
250183
return 'TEST';
251184
}
252185
}
253-
254-
// AppServiceProvider
255-
256-
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
257-
258-
public function boot()
259-
{
260-
ServerAnalytics::setRequestDetails(new CustomRequestDetails);
261-
}
262186
```
263187

264-
With the above example, the `method` variable for every request will be set to "TEST".
188+
There's a config value, `request_details_class`, which you can set to the FQN of your request details class. We will attempt to resolve that class out of the container when logging requests.
265189

266190
### Querying Analytics Records
267191

composer.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
}
1919
],
2020
"require": {
21-
"php": ">=7.3",
21+
"php": ">=8.0",
2222
"doctrine/dbal": "^3.1",
23-
"illuminate/support": ">=6",
23+
"illuminate/bus": ">=8",
24+
"illuminate/queue": ">=8",
25+
"illuminate/support": ">=8",
2426
"jaybizzle/crawler-detect": "^1.2"
2527
},
2628
"require-dev": {

config/config.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,19 @@
2828
* Controls whether or not the page views
2929
* from bots will be recorded.
3030
*/
31-
'ignore_bot_requests' => true
31+
'ignore_bot_requests' => true,
32+
33+
/**
34+
* The FQN of the class used to generate the details for the request.
35+
*/
36+
'request_details_class' => \OhSeeSoftware\LaravelServerAnalytics\RequestDetails::class,
37+
38+
/**
39+
* The name of the queue connection to use
40+
* to process the analytics requests.
41+
*
42+
* If left null, the analytics records will
43+
* be created synchronously at the end of the request.
44+
*/
45+
'queue_connection' => null
3246
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace OhSeeSoftware\LaravelServerAnalytics\Exceptions;
4+
5+
use Exception;
6+
7+
class MissingMetaKeyException extends Exception
8+
{
9+
}

src/Facades/ServerAnalytics.php

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
* @method static void addRouteExclusions(array $routes)
1111
* @method static void addMethodExclusions(array $methods)
1212
* @method static bool shouldTrackRequest(Request $request)
13-
* @method static void addPostHook($callback)
1413
* @method static void clearPostHooks()
1514
* @method static void addRelation(Model $model, ?string $reason = null)
1615
* @method static void addMeta(string $key, $value)

src/Http/Middleware/LogRequest.php

+41-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
namespace OhSeeSoftware\LaravelServerAnalytics\Http\Middleware;
44

55
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Http\Response;
68
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
9+
use OhSeeSoftware\LaravelServerAnalytics\Jobs\LogRequestRecord;
10+
use OhSeeSoftware\LaravelServerAnalytics\LaravelServerAnalytics;
11+
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
712

813
class LogRequest
914
{
@@ -34,6 +39,41 @@ public function terminate($request, $response)
3439
return;
3540
}
3641

37-
ServerAnalytics::logRequest($request, $response);
42+
$requestData = $this->getRequestData($request, $response);
43+
44+
$queueConnection = ServerAnalytics::getQueueConnection();
45+
if ($queueConnection) {
46+
LogRequestRecord::dispatch($requestData)->onQueue($queueConnection);
47+
} else {
48+
LogRequestRecord::dispatchSync($requestData);
49+
}
50+
}
51+
52+
private function getRequestData($request, $response): array
53+
{
54+
/** @var RequestDetails $requestDetails */
55+
$requestDetails = resolve(ServerAnalytics::getRequestDetailsClass());
56+
$requestDetails->setRequest($request);
57+
$requestDetails->setResponse($response);
58+
59+
$duration = 0;
60+
if ($request->analyticsRequestStartTime) {
61+
$duration = round(microtime(true) * 1000) - $request->analyticsRequestStartTime;
62+
}
63+
64+
return [
65+
'user_id' => $request->user()?->id ?? null,
66+
'method' => $requestDetails->getMethod(),
67+
'host' => $requestDetails->getHost(),
68+
'path' => $requestDetails->getPath(),
69+
'status_code' => $requestDetails->getStatusCode(),
70+
'user_agent' => $requestDetails->getUserAgent(),
71+
'ip_address' => $requestDetails->getIpAddress(),
72+
'referrer' => $requestDetails->getReferrer(),
73+
'query_params' => $requestDetails->getQueryParams(),
74+
'duration_ms' => $duration,
75+
'meta' => ServerAnalytics::runMetaHooks($requestDetails),
76+
'relations' => ServerAnalytics::runRelationHooks($requestDetails)
77+
];
3878
}
3979
}

src/Jobs/LogRequestRecord.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace OhSeeSoftware\LaravelServerAnalytics\Jobs;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Contracts\Queue\ShouldQueue;
7+
use Illuminate\Foundation\Bus\Dispatchable;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Http\Response;
10+
use Illuminate\Queue\InteractsWithQueue;
11+
use Illuminate\Queue\SerializesModels;
12+
use Illuminate\Support\Carbon;
13+
use OhSeeSoftware\LaravelServerAnalytics\Exceptions\MissingMetaKeyException;
14+
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
15+
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;
16+
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
17+
18+
class LogRequestRecord implements ShouldQueue
19+
{
20+
use Dispatchable;
21+
use InteractsWithQueue;
22+
use Queueable;
23+
use SerializesModels;
24+
25+
public $tries = 5;
26+
27+
public $backoff = 10;
28+
29+
public function __construct(public array $requestData)
30+
{
31+
}
32+
33+
public function handle()
34+
{
35+
$analytics = Analytics::create($this->requestData);
36+
37+
collect($this->requestData['meta'] ?? [])
38+
->each(function ($meta) use ($analytics) {
39+
$key = $meta['key'] ?? null;
40+
if (!$key) {
41+
throw new MissingMetaKeyException('Missing meta `key`!');
42+
}
43+
$analytics->addMeta($key, $meta['value'] ?? null);
44+
});
45+
46+
collect($this->requestData['relations'] ?? [])
47+
->each(function ($relation) use ($analytics) {
48+
$model = $relation['model'] ?? null;
49+
if (!$model) {
50+
throw new MissingMetaKeyException('Missing relation `model`!');
51+
}
52+
$analytics->addRelation($model, $relation['reason'] ?? null);
53+
});
54+
}
55+
}

0 commit comments

Comments
 (0)