feat: sentry-laravel install + scrubber + ignored exceptions

WS-7 PR-2 commit 1. Wires sentry-laravel into the app behind a
config-only no-op when SENTRY_DSN_BACKEND is empty (RFC §3.3).

- composer require sentry/sentry-laravel ^4.15 (resolved 4.25.1)
- config/sentry.php: DSN env mapped to SENTRY_DSN_BACKEND, environment
  falls back to APP_ENV, traces/profiles forced to 0.0 (RFC §2
  amendment B), send_default_pii hard-pinned false, before_send to
  SentryEventScrubber, ignore_exceptions covers ValidationException /
  AuthenticationException / AuthorizationException.
- app/Services/Observability/SentryEventScrubber.php: recursive body /
  header / query-string scrubber + form_values wholesale replacement +
  HttpException sub-500 drop (status filter that ignore_exceptions
  cannot do class-only). Max-depth guard against malicious payloads.
- app/Enums/Observability/ActorType.php: enum + resolver for §3.6
  actor_type tag (consumed by BindSentryContext in commit 2).
- tests/Feature/Observability/PiiScrubbingTest.php: 20 cases.
- api/.env.example: SENTRY_DSN_BACKEND + SENTRY_RELEASE entries.

Larastan: clean. Test count: 1487 to 1507.

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

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Enums\Observability;
use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
/**
* Actor classification used as the `actor_type` Sentry tag (RFC-WS-7 §3.6).
*
* Resolution precedence (most specific first):
* 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
*/
enum ActorType: string
{
case ORGANIZER_ADMIN = 'organizer_admin';
case SUPER_ADMIN = 'super_admin';
case PORTAL_TOKEN = 'portal_token';
case VOLUNTEER = 'volunteer';
case ORG_MEMBER = 'org_member';
case UNAUTHENTICATED = 'unauthenticated';
public static function resolve(?Authenticatable $user, ?Request $request): self
{
if ($request !== null && $request->attributes->get('portal_context') !== null) {
return self::PORTAL_TOKEN;
}
if (! $user instanceof User) {
return self::UNAUTHENTICATED;
}
if ($user->hasRole('super_admin')) {
return self::SUPER_ADMIN;
}
if ($user->hasRole('org_admin')) {
return self::ORGANIZER_ADMIN;
}
if ($user->hasRole('volunteer')) {
return self::VOLUNTEER;
}
return self::ORG_MEMBER;
}
}