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:
parent
cd2462d451
commit
049486093a
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue