diff --git a/README.md b/README.md index 3690b2a..e8dfc03 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/config/barstool.php b/config/barstool.php index e0b0fe2..37db9ee 100644 --- a/config/barstool.php +++ b/config/barstool.php @@ -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. */ diff --git a/src/Barstool.php b/src/Barstool.php index 7dc930a..66e9de3 100755 --- a/src/Barstool.php +++ b/src/Barstool.php @@ -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; } diff --git a/tests/BarstoolTest.php b/tests/BarstoolTest.php index f255d06..2e85294 100644 --- a/tests/BarstoolTest.php +++ b/tests/BarstoolTest.php @@ -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); +});