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:
2026-05-06 12:53:14 +02:00
parent 42994522eb
commit 9414d09472
11 changed files with 401 additions and 532 deletions

View File

@@ -34,17 +34,18 @@ return Application::configure(basePath: dirname(__DIR__))
// round-trip. Runs early so unauthenticated 4xx responses
// still carry a request_id header.
\App\Http\Middleware\BindRequestLogContext::class,
// RFC-WS-7 §3.6 — route-scope Sentry tags (app/route_name/
// http.method). Auth-scope tags (user_id/actor_type/
// organisation_id/actor_scope/impersonation.*) bind in
// AuthScopeContextListener on the Authenticated event,
// not in middleware. See the listener for rationale.
\App\Http\Middleware\BindSentryRouteContext::class,
]);
$middleware->alias([
'portal.token' => \App\Http\Middleware\PortalTokenMiddleware::class,
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'impersonation' => \App\Http\Middleware\HandleImpersonation::class,
// RFC-WS-7 §3.6 — applied inside auth:sanctum groups so it runs
// after authentication and can read $request->user(). Cannot live
// on the api group because route-level auth middleware runs after
// group middleware in Laravel.
'sentry.context' => \App\Http\Middleware\BindSentryContext::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {