feat: BindSentryContext middleware + queue job attempt tagging

WS-7 PR-2 commit 2.

- app/Http/Middleware/BindSentryContext.php: sets RFC §3.6 tags on the
  active Sentry scope (app, http.method, route_name, actor_type,
  user_id, organisation_id, event_id, impersonation). Multi-tenant
  invariant: throws RuntimeException in local/testing when an auth
  request to a tenant-scoped route lacks organisation_id; logs a
  warning in production so the user flow still completes.
- app/Listeners/Observability/TagJobAttemptOnSentry.php: tags
  queue.attempt on the scope from the JobProcessing event. Default
  stack-trace grouping preserved per §3.11.
- ActorType: VOLUNTEER case reserved for a future role split. Current
  resolver maps non-admin authenticated users to ORG_MEMBER.
- bootstrap/app.php: registers sentry.context alias. Applied inside
  auth:sanctum groups in routes/api.php so it runs after auth.
- AppServiceProvider::boot registers the queue listener.

Test count: 1507 to 1523. Larastan clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 09:13:55 +02:00
parent bdb89a2479
commit b1d5bcda76
7 changed files with 575 additions and 9 deletions

View File

@@ -15,9 +15,13 @@ use Illuminate\Http\Request;
* 1. Portal-token request PORTAL_TOKEN
* 2. Authenticated super_admin SUPER_ADMIN
* 3. Authenticated org_admin ORGANIZER_ADMIN
* 4. Authenticated volunteer (role match) VOLUNTEER
* 5. Other authenticated user ORG_MEMBER
* 6. None of the above UNAUTHENTICATED
* 4. Other authenticated user ORG_MEMBER (covers volunteers Crewli has
* no `volunteer` Spatie role today; volunteers join an organisation as
* org_member with org-pivot semantics. Promote to a dedicated case once
* the role model differentiates them at the user level.)
* 5. None of the above UNAUTHENTICATED
*
* The VOLUNTEER case is reserved for that future split.
*/
enum ActorType: string
{
@@ -46,10 +50,6 @@ enum ActorType: string
return self::ORGANIZER_ADMIN;
}
if ($user->hasRole('volunteer')) {
return self::VOLUNTEER;
}
return self::ORG_MEMBER;
}
}