Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ The logging will even log fatal errors caused by your saloon requests so you can
> [!TIP]
> We will be adding more features soon, so keep an eye out for updates!

## Choosing what gets recorded

By default, Barstool records every Saloon request. You can narrow that down in two directions:

- **`only`** — an allowlist. If any connectors or requests are listed, only those are recorded and everything else is skipped automatically. Handy when you have lots of connectors but only care about a few.
- **`ignore`** — a denylist. Listed connectors or requests are never recorded.

```php
// config/barstool.php
'only' => [
'connectors' => [
StripeConnector::class, // record everything sent through this connector...
],
'requests' => [],
],

'ignore' => [
'connectors' => [],
'requests' => [
StripeHealthCheckRequest::class, // ...except this noisy request
],
],
```

A request is recorded if it matches either `only` list (or both lists are empty), and the `ignore` list always takes precedence — so you can allow a whole connector and still ignore individual requests on it.

## Adding context to recordings

Sometimes the request and response alone don't tell the whole story. You can attach your own context to recordings — the current user, tenant, job name, anything you like — and it will be stored in the `context` column of the `barstools` table as JSON:
Expand Down
15 changes: 15 additions & 0 deletions config/barstool.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@
*/
'keep_successful_responses' => true,

/*
* If any connectors or requests are listed here, ONLY those will be recorded
* and everything else is excluded automatically.
* Leave both lists empty to record everything (default).
* The `ignore` list below always takes precedence over this list.
*/
'only' => [
'connectors' => [
// SomeConnector::class,
],
'requests' => [
// SomeRequest::class,
],
],

/*
* Any connectors or requests that should be ignored from recording.
*/
Expand Down
43 changes: 41 additions & 2 deletions src/Barstool.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,55 @@ public static function shouldRecord(PendingRequest|Response|FatalRequestExceptio
return false;
}

[$connector, $request] = self::resolveClasses($data);

return self::passesOnlyList($connector, $request)
&& self::passesIgnoreList($connector, $request);
}

/**
* @return array{class-string, class-string}
*/
private static function resolveClasses(PendingRequest|Response|FatalRequestException $data): array
{
[$connector, $request] = match (true) {
$data instanceof PendingRequest => [$data->getConnector(), $data->getRequest()],
$data instanceof Response, $data instanceof FatalRequestException => [$data->getPendingRequest()->getConnector(), $data->getPendingRequest()->getRequest()],
};

if (in_array(get_class($connector), config('barstool.ignore.connectors', []))) {
return [get_class($connector), get_class($request)];
}

/**
* When either `only` list is configured, a request must match one of them
* to be recorded. Empty lists mean everything passes.
*
* @param class-string $connector
* @param class-string $request
*/
private static function passesOnlyList(string $connector, string $request): bool
{
$onlyConnectors = config('barstool.only.connectors', []);
$onlyRequests = config('barstool.only.requests', []);

if ($onlyConnectors === [] && $onlyRequests === []) {
return true;
}

return in_array($connector, $onlyConnectors) || in_array($request, $onlyRequests);
}

/**
* @param class-string $connector
* @param class-string $request
*/
private static function passesIgnoreList(string $connector, string $request): bool
{
if (in_array($connector, config('barstool.ignore.connectors', []))) {
return false;
}

if (in_array(get_class($request), config('barstool.ignore.requests', []))) {
if (in_array($request, config('barstool.ignore.requests', []))) {
return false;
}

Expand Down
110 changes: 110 additions & 0 deletions tests/BarstoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -895,3 +895,113 @@

expect($barstool->uuid)->toBe($uuid);
});

it('only records connectors on the only list when configured', function () {
config()->set('barstool.enabled', true);
config()->set('barstool.only.connectors', [RandomConnector::class]);

MockClient::global([
SoloUserRequest::class => MockResponse::make(body: ['data' => 'solo'], status: 200),
RequestWithConnector::class => MockResponse::make(body: ['data' => 'connector'], status: 200),
]);

$response = (new SoloUserRequest)->send();

expect($response->getPendingRequest()->headers()->get('X-Barstool-UUID'))->toBeNull();
assertDatabaseCount('barstools', 0);

$response = (new RandomConnector)->send(new RequestWithConnector);

expect($response->getPendingRequest()->headers()->get('X-Barstool-UUID'))->not()->toBeNull();
assertDatabaseCount('barstools', 1);
});

it('only records requests on the only list when configured', function () {
config()->set('barstool.enabled', true);
config()->set('barstool.only.requests', [SoloUserRequest::class]);

MockClient::global([
SoloUserRequest::class => MockResponse::make(body: ['data' => 'solo'], status: 200),
RequestWithConnector::class => MockResponse::make(body: ['data' => 'connector'], status: 200),
]);

(new RandomConnector)->send(new RequestWithConnector);

assertDatabaseCount('barstools', 0);

(new SoloUserRequest)->send();

assertDatabaseCount('barstools', 1);
});

it('records a request matching either only list', function () {
config()->set('barstool.enabled', true);
config()->set('barstool.only.connectors', [RandomConnector::class]);
config()->set('barstool.only.requests', [SoloUserRequest::class]);

MockClient::global([
SoloUserRequest::class => MockResponse::make(body: ['data' => 'solo'], status: 200),
RequestWithConnector::class => MockResponse::make(body: ['data' => 'connector'], status: 200),
]);

// Allowed via only.requests
(new SoloUserRequest)->send();

// Allowed via only.connectors
(new RandomConnector)->send(new RequestWithConnector);

assertDatabaseCount('barstools', 2);
});

it('applies the ignore list on top of the only list', function () {
config()->set('barstool.enabled', true);
config()->set('barstool.only.connectors', [RandomConnector::class]);
config()->set('barstool.ignore.requests', [RequestWithConnector::class]);

MockClient::global([
RequestWithConnector::class => MockResponse::make(body: ['data' => 'ignored'], status: 200),
PostRequest::class => MockResponse::make(body: ['data' => 'sibling'], status: 200),
]);

$connector = new RandomConnector;

// Connector is allowed, but this request is ignored - ignore wins
$connector->send(new RequestWithConnector);

assertDatabaseCount('barstools', 0);

// Sibling request on the same allowed connector is still recorded
$connector->send(new PostRequest);

assertDatabaseCount('barstools', 1);
});

it('does not record a class listed in both only and ignore', function () {
config()->set('barstool.enabled', true);
config()->set('barstool.only.connectors', [RandomConnector::class]);
config()->set('barstool.ignore.connectors', [RandomConnector::class]);

MockClient::global([
RequestWithConnector::class => MockResponse::make(body: ['data' => 'conflict'], status: 200),
]);

(new RandomConnector)->send(new RequestWithConnector);

assertDatabaseCount('barstools', 0);
});

it('records everything when the only lists are empty', function () {
config()->set('barstool.enabled', true);
config()->set('barstool.only.connectors', []);
config()->set('barstool.only.requests', []);

MockClient::global([
SoloUserRequest::class => MockResponse::make(body: ['data' => 'solo'], status: 200),
RequestWithConnector::class => MockResponse::make(body: ['data' => 'connector'], status: 200),
]);

(new SoloUserRequest)->send();
(new RandomConnector)->send(new RequestWithConnector);

assertDatabaseCount('barstools', 2);
});