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
This commit is contained in:
Víctor Falcón 2026-05-04 13:26:50 +01:00 committed by GitHub
parent cd2462d451
commit 049486093a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 141 additions and 0 deletions

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Sentry\State\Scope;
use Symfony\Component\HttpFoundation\Response;
use function Sentry\configureScope;
class SetSentryUser
{
/**
* @param Closure(Request): Response $next
*/
public function handle(Request $request, Closure $next): Response
{
configureScope(function (Scope $scope) use ($request): void {
$user = $request->user();
if ($user === null) {
return;
}
$scope->setUser([
'id' => (string) $user->getAuthIdentifier(),
'email' => $user->email,
]);
});
return $next($request);
}
}

View File

@ -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,

View File

@ -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',

View File

@ -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();

View File

@ -0,0 +1,44 @@
<?php
use App\Http\Middleware\SetSentryUser;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Sentry\SentrySdk;
use Sentry\State\Hub;
use Sentry\State\Scope;
it('sets sentry user context with id and email only', function () {
SentrySdk::setCurrentHub(new Hub);
$user = User::factory()->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();
});
});