Skip to content

Commit

Permalink
#20 - Support creating analytics via queued job (#21)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
owenconti authored Aug 10, 2022
1 parent 124271f commit f5f7140
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 294 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/Build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ on: [push]
jobs:
test:
name: Test
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
strategy:
matrix:
php: [7.3, 8.0]
php: [8.0]
steps:
- uses: actions/checkout@master
- name: Checkout
Expand All @@ -31,14 +31,14 @@ jobs:
name: Release
if: github.ref == 'refs/heads/master'
needs: test
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
node-version: 16
- name: Install dependencies
run: yarn install
- name: Release
Expand Down
120 changes: 22 additions & 98 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ The default migration assumes you users are stored in a `users` table with a `BI
When logging a request, the package will use this code to insert the `user_id` into the `analytics` table:

```php
if ($user = $request->user()) {
$userId = $user->id;
}
$userId = $request->user()?->id ?? null
```

### Excluding Routes
Expand Down Expand Up @@ -133,104 +131,38 @@ return [
];
```

### Post Request Hooks

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.

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.

```php
// AppServiceProvider

use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;

public function boot()
{
ServerAnalytics::addPostHook(
function (RequestDetails $requestDetails, Analytics $analytics) {
// Do whatever you want with the request, response, and created analytics record
}
);
}
```
### Request hooks

### Attach Entities to Analytics Records
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.

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:
`addMetaHook` example:

```php
// AppServiceProvider

use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;

public function boot()
{
ServerAnalytics::addPostHook(
function (RequestDetails $requestDetails, Analytics $analytics) {
// Attach the logged-in user to the analytics request record
if ($user = $requestDetails->request->user()) {
$analytics->addRelation($user);
}
}
);
}
```

There's also a helper method available if you want to attach a relation without checking the request, response, or analytics record:

```php
// Controller

use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;

public function __invoke(Post $post)
{
ServerAnalytics::addRelation($post);

// ...finish controller logic
}
ServerAnalytics::addMetaHook(function (RequestDetails $requestDetails) {
return [
'key' => 'some-key',
'value' => 'some-value'
];
});
```

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`.

### Attach Metadata to Analytics Records

In addition to attaching entities to your analytics records, you can attach custom metadata (key/value).
`addRelationHook` example:

```php
// AppServiceProvider

use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;

public function boot()
{
ServerAnalytics::addPostHook(
function (RequestDetails $requestDetails, Analytics $analytics) {
$analytics->addMeta('foo', 'bar');
}
);
}
ServerAnalytics::addRelationHook(function (RequestDetails $requestDetails) use ($post) {
return [
'model' => $post,
'reason' => 'Post that was deleted.'
];
});
```

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

```php
// Controller

use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
ServerAnalytics::addMeta('test', 'value');

public function __invoke(Post $post)
{
ServerAnalytics::addMeta('some-key', 'some-value');

// ...finish controller logic
}
ServerAnalytics::addRelation($post);
```

### Providing Custom Request Details
Expand All @@ -247,21 +179,13 @@ class CustomRequestDetails extends RequestDetails
{
public function getMethod(): string
{
// Set the stored `method` as "TEST" for all requests
return 'TEST';
}
}

// AppServiceProvider

use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;

public function boot()
{
ServerAnalytics::setRequestDetails(new CustomRequestDetails);
}
```

With the above example, the `method` variable for every request will be set to "TEST".
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.

### Querying Analytics Records

Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
}
],
"require": {
"php": ">=7.3",
"php": ">=8.0",
"doctrine/dbal": "^3.1",
"illuminate/support": ">=6",
"illuminate/bus": ">=8",
"illuminate/queue": ">=8",
"illuminate/support": ">=8",
"jaybizzle/crawler-detect": "^1.2"
},
"require-dev": {
Expand Down
16 changes: 15 additions & 1 deletion config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,19 @@
* Controls whether or not the page views
* from bots will be recorded.
*/
'ignore_bot_requests' => true
'ignore_bot_requests' => true,

/**
* The FQN of the class used to generate the details for the request.
*/
'request_details_class' => \OhSeeSoftware\LaravelServerAnalytics\RequestDetails::class,

/**
* The name of the queue connection to use
* to process the analytics requests.
*
* If left null, the analytics records will
* be created synchronously at the end of the request.
*/
'queue_connection' => null
];
9 changes: 9 additions & 0 deletions src/Exceptions/MissingMetaKeyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace OhSeeSoftware\LaravelServerAnalytics\Exceptions;

use Exception;

class MissingMetaKeyException extends Exception
{
}
1 change: 0 additions & 1 deletion src/Facades/ServerAnalytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
* @method static void addRouteExclusions(array $routes)
* @method static void addMethodExclusions(array $methods)
* @method static bool shouldTrackRequest(Request $request)
* @method static void addPostHook($callback)
* @method static void clearPostHooks()
* @method static void addRelation(Model $model, ?string $reason = null)
* @method static void addMeta(string $key, $value)
Expand Down
42 changes: 41 additions & 1 deletion src/Http/Middleware/LogRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
namespace OhSeeSoftware\LaravelServerAnalytics\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
use OhSeeSoftware\LaravelServerAnalytics\Jobs\LogRequestRecord;
use OhSeeSoftware\LaravelServerAnalytics\LaravelServerAnalytics;
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;

class LogRequest
{
Expand Down Expand Up @@ -34,6 +39,41 @@ public function terminate($request, $response)
return;
}

ServerAnalytics::logRequest($request, $response);
$requestData = $this->getRequestData($request, $response);

$queueConnection = ServerAnalytics::getQueueConnection();
if ($queueConnection) {
LogRequestRecord::dispatch($requestData)->onQueue($queueConnection);
} else {
LogRequestRecord::dispatchSync($requestData);
}
}

private function getRequestData($request, $response): array
{
/** @var RequestDetails $requestDetails */
$requestDetails = resolve(ServerAnalytics::getRequestDetailsClass());
$requestDetails->setRequest($request);
$requestDetails->setResponse($response);

$duration = 0;
if ($request->analyticsRequestStartTime) {
$duration = round(microtime(true) * 1000) - $request->analyticsRequestStartTime;
}

return [
'user_id' => $request->user()?->id ?? null,
'method' => $requestDetails->getMethod(),
'host' => $requestDetails->getHost(),
'path' => $requestDetails->getPath(),
'status_code' => $requestDetails->getStatusCode(),
'user_agent' => $requestDetails->getUserAgent(),
'ip_address' => $requestDetails->getIpAddress(),
'referrer' => $requestDetails->getReferrer(),
'query_params' => $requestDetails->getQueryParams(),
'duration_ms' => $duration,
'meta' => ServerAnalytics::runMetaHooks($requestDetails),
'relations' => ServerAnalytics::runRelationHooks($requestDetails)
];
}
}
55 changes: 55 additions & 0 deletions src/Jobs/LogRequestRecord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace OhSeeSoftware\LaravelServerAnalytics\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use OhSeeSoftware\LaravelServerAnalytics\Exceptions\MissingMetaKeyException;
use OhSeeSoftware\LaravelServerAnalytics\Facades\ServerAnalytics;
use OhSeeSoftware\LaravelServerAnalytics\Models\Analytics;
use OhSeeSoftware\LaravelServerAnalytics\RequestDetails;

class LogRequestRecord implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

public $tries = 5;

public $backoff = 10;

public function __construct(public array $requestData)
{
}

public function handle()
{
$analytics = Analytics::create($this->requestData);

collect($this->requestData['meta'] ?? [])
->each(function ($meta) use ($analytics) {
$key = $meta['key'] ?? null;
if (!$key) {
throw new MissingMetaKeyException('Missing meta `key`!');
}
$analytics->addMeta($key, $meta['value'] ?? null);
});

collect($this->requestData['relations'] ?? [])
->each(function ($relation) use ($analytics) {
$model = $relation['model'] ?? null;
if (!$model) {
throw new MissingMetaKeyException('Missing relation `model`!');
}
$analytics->addRelation($model, $relation['reason'] ?? null);
});
}
}
Loading

0 comments on commit f5f7140

Please sign in to comment.