refactor: BindSentryContext to AuthScopeContextListener for auth-scope tags
Sentry-context binding split into two responsibilities: - Route-scope (app, http.method, route_name) stays in middleware on the api group as BindSentryRouteContext — works on every request, no auth required. - Auth-scope (user_id, actor_type) moves to AuthScopeContextListener on Illuminate\Auth\Events\Authenticated — works on every authentication mechanism (Sanctum, portal-tokens, future authenticators) without per-route middleware-attachment. Listener also augments Log::withContext with user_id (closes OBS-2). Architecturally fault-preventing rather than fault-detecting: new authenticated route groups need no separate sentry.context aliasing, so silent observability gaps are no longer possible (closes OBS-3). Impersonation tagging is co-located with HandleImpersonation: after the user-swap, the middleware re-tags Sentry scope with the target user_id/actor_type and adds impersonation.active / impersonation.impersonator_user_id / impersonation.session_id. The Authenticated event fires for the admin (Sanctum's natural flow), the listener tags the admin, then HandleImpersonation overwrites post-swap. Files renamed: - BindSentryContext -> BindSentryRouteContext (route-scope only) - BindSentryContextTest -> BindSentryRouteContextTest (4 cases) Files added: - AuthScopeContextListener - AuthScopeContextListenerTest (6 cases) bootstrap/app.php drops the sentry.context alias and prepends BindSentryRouteContext to the api group. routes/api.php drops every sentry.context middleware string from auth:sanctum groups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
172
api/tests/Feature/Observability/AuthScopeContextListenerTest.php
Normal file
172
api/tests/Feature/Observability/AuthScopeContextListenerTest.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Observability;
|
||||
|
||||
use App\Http\Middleware\HandleImpersonation;
|
||||
use App\Models\ImpersonationSession;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Auth\Events\Authenticated;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Sentry\Event as SentryEvent;
|
||||
use Sentry\SentrySdk;
|
||||
use Sentry\State\Scope;
|
||||
use Tests\TestCase;
|
||||
|
||||
use function Sentry\configureScope;
|
||||
|
||||
/**
|
||||
* Auth-scope Sentry tags + Log::withContext applied via the
|
||||
* {@see \App\Listeners\Observability\AuthScopeContextListener} on every
|
||||
* Authenticated event.
|
||||
*
|
||||
* Impersonation re-binding (target user_id/actor_type plus impersonation.*
|
||||
* tags) is co-located in {@see HandleImpersonation} and exercised by
|
||||
* the relevant tests in this file.
|
||||
*/
|
||||
final class AuthScopeContextListenerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RoleSeeder::class);
|
||||
SentrySdk::getCurrentHub()->pushScope();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function captureScopeTags(): array
|
||||
{
|
||||
$event = SentryEvent::createEvent();
|
||||
|
||||
configureScope(static function (Scope $scope) use ($event): void {
|
||||
$scope->applyToEvent($event);
|
||||
});
|
||||
|
||||
return $event->getTags();
|
||||
}
|
||||
|
||||
private function captureScopeUserId(): ?string
|
||||
{
|
||||
$event = SentryEvent::createEvent();
|
||||
|
||||
configureScope(static function (Scope $scope) use ($event): void {
|
||||
$scope->applyToEvent($event);
|
||||
});
|
||||
|
||||
return $event->getUser()?->getId();
|
||||
}
|
||||
|
||||
public function test_authenticated_event_tags_user_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
event(new Authenticated('web', $user));
|
||||
|
||||
$tags = $this->captureScopeTags();
|
||||
$this->assertSame($user->id, $tags['user_id'] ?? null);
|
||||
$this->assertSame($user->id, $this->captureScopeUserId());
|
||||
}
|
||||
|
||||
public function test_authenticated_event_tags_actor_type_super_admin(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('super_admin');
|
||||
|
||||
event(new Authenticated('web', $user));
|
||||
|
||||
$this->assertSame('super_admin', $this->captureScopeTags()['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_authenticated_event_tags_actor_type_organizer_admin(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
event(new Authenticated('web', $user));
|
||||
|
||||
$this->assertSame('organizer_admin', $this->captureScopeTags()['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_authenticated_event_tags_actor_type_org_member(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_member');
|
||||
|
||||
event(new Authenticated('web', $user));
|
||||
|
||||
$this->assertSame('org_member', $this->captureScopeTags()['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_authenticated_event_does_not_set_impersonation_tags(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
event(new Authenticated('web', $user));
|
||||
|
||||
$tags = $this->captureScopeTags();
|
||||
$this->assertArrayNotHasKey('impersonation.active', $tags);
|
||||
$this->assertArrayNotHasKey('impersonation.impersonator_user_id', $tags);
|
||||
}
|
||||
|
||||
public function test_handle_impersonation_rebinds_user_id_and_tags_impersonation_after_swap(): void
|
||||
{
|
||||
Organisation::factory()->create(); // tenancy fixture
|
||||
|
||||
$admin = User::factory()->create([
|
||||
'mfa_enabled' => true,
|
||||
'mfa_method' => \App\Enums\MfaMethod::TOTP->value,
|
||||
'mfa_secret' => encrypt('JBSWY3DPEHPK3PXP'),
|
||||
'mfa_confirmed_at' => now(),
|
||||
]);
|
||||
$admin->assignRole('super_admin');
|
||||
|
||||
$target = User::factory()->create();
|
||||
$target->assignRole('org_admin');
|
||||
|
||||
// Authenticated event for the admin (Sanctum's normal flow).
|
||||
event(new Authenticated('web', $admin));
|
||||
$this->assertSame($admin->id, $this->captureScopeTags()['user_id'] ?? null);
|
||||
$this->assertSame('super_admin', $this->captureScopeTags()['actor_type'] ?? null);
|
||||
|
||||
// Manufacture an impersonation session and run HandleImpersonation
|
||||
// through to the post-swap re-binding logic.
|
||||
$session = ImpersonationSession::create([
|
||||
'admin_id' => $admin->id,
|
||||
'target_user_id' => $target->id,
|
||||
'reason' => 'test',
|
||||
'mfa_method' => \App\Enums\MfaMethod::TOTP->value,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'started_at' => now(),
|
||||
'expires_at' => now()->addHour(),
|
||||
]);
|
||||
\Illuminate\Support\Facades\Cache::put(
|
||||
'impersonation:'.$admin->id.':'.$target->id,
|
||||
$session->id,
|
||||
now()->addHour(),
|
||||
);
|
||||
|
||||
$request = Request::create('http://localhost/api/v1/me/profile', 'GET');
|
||||
$request->headers->set('X-Impersonate-User', $target->id);
|
||||
$request->setUserResolver(static fn () => $admin);
|
||||
$request->server->set('REMOTE_ADDR', '127.0.0.1');
|
||||
|
||||
$middleware = app(HandleImpersonation::class);
|
||||
$middleware->handle($request, static fn () => response('ok'));
|
||||
|
||||
$tags = $this->captureScopeTags();
|
||||
$this->assertSame($target->id, $tags['user_id'] ?? null);
|
||||
$this->assertSame('organizer_admin', $tags['actor_type']);
|
||||
$this->assertSame('true', $tags['impersonation.active'] ?? null);
|
||||
$this->assertSame($admin->id, $tags['impersonation.impersonator_user_id'] ?? null);
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Observability;
|
||||
|
||||
use App\Http\Middleware\BindSentryContext;
|
||||
use App\Models\Event;
|
||||
use App\Models\ImpersonationSession;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
use Sentry\Event as SentryEvent;
|
||||
use Sentry\SentrySdk;
|
||||
use Sentry\State\Scope;
|
||||
use Tests\TestCase;
|
||||
|
||||
use function Sentry\configureScope;
|
||||
|
||||
final class BindSentryContextTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RoleSeeder::class);
|
||||
$this->resetSentryScope();
|
||||
}
|
||||
|
||||
private function resetSentryScope(): void
|
||||
{
|
||||
SentrySdk::getCurrentHub()->pushScope();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current Sentry scope by applying it to a fresh Event and
|
||||
* harvesting the tags / user context.
|
||||
*
|
||||
* @return array{tags: array<string, string>, user: ?array<string, mixed>}
|
||||
*/
|
||||
private function captureScope(): array
|
||||
{
|
||||
$event = SentryEvent::createEvent();
|
||||
|
||||
configureScope(static function (Scope $scope) use ($event): void {
|
||||
$scope->applyToEvent($event);
|
||||
});
|
||||
|
||||
$userBag = $event->getUser();
|
||||
|
||||
return [
|
||||
'tags' => $event->getTags(),
|
||||
'user' => $userBag === null ? null : array_filter([
|
||||
'id' => $userBag->getId(),
|
||||
'username' => $userBag->getUsername(),
|
||||
], static fn ($v) => $v !== null),
|
||||
];
|
||||
}
|
||||
|
||||
private function runMiddleware(Request $request): void
|
||||
{
|
||||
(new BindSentryContext)->handle($request, static fn (Request $req) => response('ok'));
|
||||
}
|
||||
|
||||
private function makeAuthenticatedRequest(User $user, string $path = 'api/v1/me/profile'): Request
|
||||
{
|
||||
$request = Request::create('http://localhost/'.$path, 'GET');
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function test_authenticated_org_admin_request_tags_organisation_id(): void
|
||||
{
|
||||
$org = Organisation::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$org->users()->attach($user, ['role' => 'org_admin']);
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/organisations/'.$org->id.'/some-path');
|
||||
$request->setRouteResolver(function () use ($org, $request) {
|
||||
$route = new \Illuminate\Routing\Route(['GET'], 'organisations/{organisation}/some-path', static fn () => null);
|
||||
$route->bind($request);
|
||||
$route->setParameter('organisation', $org);
|
||||
$route->name('organisations.test');
|
||||
|
||||
return $route;
|
||||
});
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame($org->id, $scope['tags']['organisation_id'] ?? null);
|
||||
}
|
||||
|
||||
public function test_authenticated_org_admin_request_tags_user_id(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame($user->id, $scope['tags']['user_id'] ?? null);
|
||||
$this->assertSame($user->id, $scope['user']['id'] ?? null);
|
||||
$this->assertSame($user->id, $scope['user']['username'] ?? null);
|
||||
}
|
||||
|
||||
public function test_authenticated_org_admin_request_tags_actor_type_organizer_admin(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('organizer_admin', $scope['tags']['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_super_admin_request_tags_actor_type_super_admin(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('super_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/admin/organisations');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('super_admin', $scope['tags']['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_org_member_authenticated_user_tags_actor_type_org_member(): void
|
||||
{
|
||||
// Volunteer-ness is behaviour in Crewli (shift assignments), not
|
||||
// identity — non-admin authenticated users resolve to ORG_MEMBER.
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_member');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('org_member', $scope['tags']['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_portal_token_request_tags_actor_type_portal_token(): void
|
||||
{
|
||||
$org = Organisation::factory()->create();
|
||||
$event = Event::factory()->create(['organisation_id' => $org->id]);
|
||||
|
||||
$request = Request::create('http://localhost/api/v1/portal/me', 'GET');
|
||||
$request->attributes->set('portal_context', 'artist');
|
||||
$request->attributes->set('portal_event', $event);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('portal_token', $scope['tags']['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_portal_token_request_tags_organisation_id_from_token(): void
|
||||
{
|
||||
$org = Organisation::factory()->create();
|
||||
$event = Event::factory()->create(['organisation_id' => $org->id]);
|
||||
|
||||
$request = Request::create('http://localhost/api/v1/portal/me', 'GET');
|
||||
$request->attributes->set('portal_context', 'artist');
|
||||
$request->attributes->set('portal_event', $event);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame($org->id, $scope['tags']['organisation_id'] ?? null);
|
||||
$this->assertSame($event->id, $scope['tags']['event_id'] ?? null);
|
||||
}
|
||||
|
||||
public function test_unauthenticated_request_tags_actor_type_unauthenticated(): void
|
||||
{
|
||||
$request = Request::create('http://localhost/api/v1/auth/login', 'POST');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('unauthenticated', $scope['tags']['actor_type'] ?? null);
|
||||
$this->assertArrayNotHasKey('user_id', $scope['tags']);
|
||||
}
|
||||
|
||||
public function test_authenticated_request_to_tenant_scoped_route_without_org_throws_in_test_environment(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/organisations/missing/events');
|
||||
// Synthesise a route that DECLARES {organisation} but has no
|
||||
// bound parameter — the invariant must fire.
|
||||
$route = new \Illuminate\Routing\Route(['GET'], 'organisations/{organisation}/events', static fn () => null);
|
||||
$route->bind($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessageMatches('/lacks resolvable organisation_id/');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
}
|
||||
|
||||
public function test_authenticated_request_to_user_scoped_route_skips_invariant(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
// /me/profile route declares no {organisation} or {event} param —
|
||||
// user-scoped, invariant skipped.
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile');
|
||||
$route = new \Illuminate\Routing\Route(['GET'], 'me/profile', static fn () => null);
|
||||
$route->bind($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
|
||||
$scope = $this->captureScope();
|
||||
$this->assertSame('organizer_admin', $scope['tags']['actor_type'] ?? null);
|
||||
}
|
||||
|
||||
public function test_impersonation_active_tag_when_session_active(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('super_admin');
|
||||
$target = User::factory()->create();
|
||||
$target->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($target, 'api/v1/me/profile');
|
||||
$request->attributes->set('impersonator', $admin);
|
||||
$session = new ImpersonationSession;
|
||||
$session->id = '01J0000000000000000000IMPS';
|
||||
$session->admin_id = $admin->id;
|
||||
$session->target_user_id = $target->id;
|
||||
$request->attributes->set('impersonation_session', $session);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('true', $scope['tags']['impersonation.active'] ?? null);
|
||||
}
|
||||
|
||||
public function test_impersonation_impersonator_user_id_tag_when_session_active(): void
|
||||
{
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('super_admin');
|
||||
$target = User::factory()->create();
|
||||
$target->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($target, 'api/v1/me/profile');
|
||||
$request->attributes->set('impersonator', $admin);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame($admin->id, $scope['tags']['impersonation.impersonator_user_id'] ?? null);
|
||||
}
|
||||
|
||||
public function test_impersonation_active_false_when_no_session(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('false', $scope['tags']['impersonation.active'] ?? null);
|
||||
$this->assertArrayNotHasKey('impersonation.impersonator_user_id', $scope['tags']);
|
||||
}
|
||||
|
||||
public function test_route_name_tag_present(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user, 'api/v1/me/profile');
|
||||
$route = new \Illuminate\Routing\Route(['GET'], 'me/profile', static fn () => null);
|
||||
$route->name('me.profile');
|
||||
$route->bind($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('me.profile', $scope['tags']['route_name'] ?? null);
|
||||
}
|
||||
|
||||
public function test_http_method_tag_present(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = Request::create('http://localhost/api/v1/me/profile', 'PATCH');
|
||||
$request->setUserResolver(static fn () => $user);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('PATCH', $scope['tags']['http.method'] ?? null);
|
||||
}
|
||||
|
||||
public function test_app_tag_is_api(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('org_admin');
|
||||
|
||||
$request = $this->makeAuthenticatedRequest($user);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
$scope = $this->captureScope();
|
||||
|
||||
$this->assertSame('api', $scope['tags']['app'] ?? null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Observability;
|
||||
|
||||
use App\Http\Middleware\BindSentryRouteContext;
|
||||
use Illuminate\Http\Request;
|
||||
use Sentry\Event as SentryEvent;
|
||||
use Sentry\SentrySdk;
|
||||
use Sentry\State\Scope;
|
||||
use Tests\TestCase;
|
||||
|
||||
use function Sentry\configureScope;
|
||||
|
||||
/**
|
||||
* Route-scope tags (app, http.method, route_name) on every API request.
|
||||
*
|
||||
* Auth-scope assertions (user_id, actor_type, organisation_id, etc.) live
|
||||
* in {@see AuthScopeContextListenerTest} — that's the file to look at if
|
||||
* you're changing what gets tagged on authenticated events.
|
||||
*/
|
||||
final class BindSentryRouteContextTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
SentrySdk::getCurrentHub()->pushScope();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function captureScopeTags(): array
|
||||
{
|
||||
$event = SentryEvent::createEvent();
|
||||
|
||||
configureScope(static function (Scope $scope) use ($event): void {
|
||||
$scope->applyToEvent($event);
|
||||
});
|
||||
|
||||
return $event->getTags();
|
||||
}
|
||||
|
||||
private function runMiddleware(Request $request): void
|
||||
{
|
||||
(new BindSentryRouteContext())->handle($request, static fn (Request $req) => response('ok'));
|
||||
}
|
||||
|
||||
public function test_app_tag_is_api(): void
|
||||
{
|
||||
$request = Request::create('http://localhost/api/v1/_anything', 'GET');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
|
||||
$this->assertSame('api', $this->captureScopeTags()['app'] ?? null);
|
||||
}
|
||||
|
||||
public function test_http_method_tag_present(): void
|
||||
{
|
||||
$request = Request::create('http://localhost/api/v1/me/profile', 'PATCH');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
|
||||
$this->assertSame('PATCH', $this->captureScopeTags()['http.method'] ?? null);
|
||||
}
|
||||
|
||||
public function test_route_name_tag_present(): void
|
||||
{
|
||||
$request = Request::create('http://localhost/api/v1/me/profile', 'GET');
|
||||
$route = new \Illuminate\Routing\Route(['GET'], 'me/profile', static fn () => null);
|
||||
$route->name('me.profile');
|
||||
$route->bind($request);
|
||||
$request->setRouteResolver(static fn () => $route);
|
||||
|
||||
$this->runMiddleware($request);
|
||||
|
||||
$this->assertSame('me.profile', $this->captureScopeTags()['route_name'] ?? null);
|
||||
}
|
||||
|
||||
public function test_route_name_tag_omitted_when_route_has_no_name(): void
|
||||
{
|
||||
$request = Request::create('http://localhost/api/v1/anonymous', 'GET');
|
||||
|
||||
$this->runMiddleware($request);
|
||||
|
||||
$this->assertArrayNotHasKey('route_name', $this->captureScopeTags());
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ final class ExceptionReportingTest extends TestCase
|
||||
|
||||
// Test-only routes that exercise each branch of the
|
||||
// ignore_exceptions / before_send / capture pipeline.
|
||||
Route::middleware(['auth:sanctum', 'sentry.context'])->group(function (): void {
|
||||
Route::middleware(['auth:sanctum', \App\Http\Middleware\BindSentryRouteContext::class])->group(function (): void {
|
||||
Route::get('_obs_runtime', static fn () => throw new RuntimeException('boom'))
|
||||
->name('test.obs.runtime');
|
||||
Route::get('_obs_validation', static function (): never {
|
||||
|
||||
Reference in New Issue
Block a user