From 049486093aa8ba485f2d5dc7e511d85909918b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 4 May 2026 13:26:50 +0100 Subject: [PATCH] Add Sentry user context (#348) ## Summary - identify authenticated users on Sentry web requests with id and email - add user and banking connection context to banking sync jobs - cover Sentry context behavior with feature tests ## Tests - vendor/bin/pint --dirty --format agent - php artisan test --compact tests/Feature/SentryUserMiddlewareTest.php - php artisan test --compact tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php --- app/Http/Middleware/SetSentryUser.php | 34 ++++++++++++++ app/Jobs/SyncBankingConnectionJob.php | 25 +++++++++++ bootstrap/app.php | 2 + .../SyncBankingConnectionJobTest.php | 36 +++++++++++++++ tests/Feature/SentryUserMiddlewareTest.php | 44 +++++++++++++++++++ 5 files changed, 141 insertions(+) create mode 100644 app/Http/Middleware/SetSentryUser.php create mode 100644 tests/Feature/SentryUserMiddlewareTest.php diff --git a/app/Http/Middleware/SetSentryUser.php b/app/Http/Middleware/SetSentryUser.php new file mode 100644 index 00000000..51858037 --- /dev/null +++ b/app/Http/Middleware/SetSentryUser.php @@ -0,0 +1,34 @@ +user(); + + if ($user === null) { + return; + } + + $scope->setUser([ + 'id' => (string) $user->getAuthIdentifier(), + 'email' => $user->email, + ]); + }); + + return $next($request); + } +} diff --git a/app/Jobs/SyncBankingConnectionJob.php b/app/Jobs/SyncBankingConnectionJob.php index dac9af20..7109cfb3 100644 --- a/app/Jobs/SyncBankingConnectionJob.php +++ b/app/Jobs/SyncBankingConnectionJob.php @@ -24,6 +24,9 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; +use Sentry\State\Scope; + +use function Sentry\configureScope; class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue { @@ -58,6 +61,7 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue $syncedAt = now(); $connection->loadMissing('user'); + $this->setSentryContext($connection); if (! $connection->user) { Log::info('Banking connection belongs to deleted user, skipping sync', ['connection_id' => $connection->id]); @@ -214,6 +218,27 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue ]); } + private function setSentryContext(BankingConnection $connection): void + { + configureScope(function (Scope $scope) use ($connection): void { + $scope->setTag('banking_connection_id', (string) $connection->id); + $scope->setContext('banking_connection', [ + 'id' => $connection->id, + 'provider' => $connection->provider, + 'status' => $connection->status->value, + ]); + + if ($connection->user === null) { + return; + } + + $scope->setUser([ + 'id' => (string) $connection->user->getAuthIdentifier(), + 'email' => $connection->user->email, + ]); + }); + } + private function logSyncAttempt( BankingConnection $connection, BankingSyncLogStatus $status, diff --git a/bootstrap/app.php b/bootstrap/app.php index 58d435d1..972f7362 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -6,6 +6,7 @@ use App\Http\Middleware\EnsureUserIsSubscribed; use App\Http\Middleware\HandleAppearance; use App\Http\Middleware\HandleInertiaRequests; use App\Http\Middleware\SetLocale; +use App\Http\Middleware\SetSentryUser; use App\Services\AuthEntryPointService; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; @@ -37,6 +38,7 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ HandleAppearance::class, SetLocale::class, + SetSentryUser::class, HandleInertiaRequests::class, AddLinkHeadersForPreloadedAssets::class, BlockDemoAccountActions::class.':auto', diff --git a/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php b/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php index c06349e1..7d38a3ef 100644 --- a/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php +++ b/tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php @@ -23,6 +23,42 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Queue; +use Sentry\SentrySdk; +use Sentry\State\Hub; +use Sentry\State\Scope; + +it('sets sentry user and banking connection context for sync jobs', function () { + SentrySdk::setCurrentHub(new Hub); + + $user = User::factory()->create([ + 'name' => 'Private Name', + 'email' => 'sync-user@example.com', + ]); + $connection = BankingConnection::factory()->awaitingMapping()->create([ + 'user_id' => $user->id, + 'provider' => 'binance', + ]); + + $job = new SyncBankingConnectionJob($connection); + $job->handle(Mockery::mock(TransactionSyncService::class), Mockery::mock(BalanceSyncService::class)); + + SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($user, $connection): void { + $sentryUser = $scope->getUser(); + $tags = (fn (): array => $this->tags)->call($scope); + $contexts = (fn (): array => $this->contexts)->call($scope); + + expect($sentryUser)->not->toBeNull() + ->and($sentryUser->getId())->toBe((string) $user->id) + ->and($sentryUser->getEmail())->toBe('sync-user@example.com') + ->and($sentryUser->getUsername())->toBeNull() + ->and($tags['banking_connection_id'])->toBe((string) $connection->id) + ->and($contexts['banking_connection'])->toMatchArray([ + 'id' => $connection->id, + 'provider' => 'binance', + 'status' => BankingConnectionStatus::AwaitingMapping->value, + ]); + }); +}); test('first sync calculates historical balances', function () { $user = User::factory()->onboarded()->create(); diff --git a/tests/Feature/SentryUserMiddlewareTest.php b/tests/Feature/SentryUserMiddlewareTest.php new file mode 100644 index 00000000..55fec726 --- /dev/null +++ b/tests/Feature/SentryUserMiddlewareTest.php @@ -0,0 +1,44 @@ +create([ + 'name' => 'Secret Name', + 'email' => 'affected@example.com', + ]); + + $request = Request::create('/dashboard'); + $request->setUserResolver(fn () => $user); + + (new SetSentryUser)->handle($request, fn () => new Response); + + SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($user): void { + $sentryUser = $scope->getUser(); + + expect($sentryUser)->not->toBeNull() + ->and($sentryUser->getId())->toBe((string) $user->id) + ->and($sentryUser->getEmail())->toBe('affected@example.com') + ->and($sentryUser->getUsername())->toBeNull(); + }); +}); + +it('leaves sentry user context empty for guests', function () { + SentrySdk::setCurrentHub(new Hub); + + $request = Request::create('/'); + + (new SetSentryUser)->handle($request, fn () => new Response); + + SentrySdk::getCurrentHub()->configureScope(function (Scope $scope): void { + expect($scope->getUser())->toBeNull(); + }); +});