diff --git a/composer.json b/composer.json index 7d6ff50..f4afdf4 100644 --- a/composer.json +++ b/composer.json @@ -35,10 +35,12 @@ "phpstan/phpstan": "^1.10", "phpstan/phpstan-symfony": "^1.3", "phpunit/phpunit": "^10.4", - "symfony/messenger": "^5.4 || ^6.0 || ^7.0" + "symfony/messenger": "^5.4 || ^6.0 || ^7.0", + "symfony/security-bundle": "^5.4 || ^6.0 || ^7.0" }, "suggest": { - "ext-newrelic": "Install the New Relic PHP agent on your system" + "ext-newrelic": "Install the New Relic PHP agent on your system", + "symfony/security-bundle": "Track users impacted by transaction error" }, "autoload": { "psr-4": { diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index b69258a..3b7998c 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -14,6 +14,7 @@ namespace Check24\NewRelicBundle\EventListener; use Check24\NewRelicBundle\NewRelic\NewRelicInteractorInterface; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpKernel\Event\ExceptionEvent; @@ -26,12 +27,18 @@ public function __construct( private array $excludedExceptions, private NewRelicInteractorInterface $interactor, + private ?Security $security = null, ) { } public function __invoke(ExceptionEvent $event): void { $exception = $event->getThrowable(); + // link users to transactions (anonymously), so that we can monitor # of users impacted by errors in error inbox + // see https://docs.newrelic.com/docs/errors-inbox/error-users-impacted/#attributes + if ($user = $this->security?->getUser()) { + $this->interactor->addCustomParameter('enduser.id', crc32($user->getUserIdentifier())); + } foreach ($this->excludedExceptions as $excludedException) { if ($exception instanceof $excludedException) { diff --git a/tests/App/TestKernel.php b/tests/App/TestKernel.php index 91888bd..f2ed060 100644 --- a/tests/App/TestKernel.php +++ b/tests/App/TestKernel.php @@ -16,6 +16,7 @@ use Check24\NewRelicBundle\Check24NewRelicBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\MonologBundle\MonologBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; @@ -29,6 +30,7 @@ public function registerBundles(): iterable new FrameworkBundle(), new MonologBundle(), new Check24NewRelicBundle(), + new SecurityBundle(), ]; } @@ -46,6 +48,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void ], ], ], + 'router' => ['resource' => 'kernel::loadRoutes', 'type' => 'service', 'utf8' => true], ]); $container->loadFromExtension('check24_new_relic', [ @@ -76,6 +79,10 @@ public function registerContainerConfiguration(LoaderInterface $loader): void ], ], ]); + $container->loadFromExtension('security', [ + 'providers' => ['users_in_memory' => ['memory' => null]], + 'firewalls' => ['main' => ['provider' => 'users_in_memory']], + ]); }); } diff --git a/tests/EventListener/ExceptionListenerTest.php b/tests/EventListener/ExceptionListenerTest.php index 9a9f6a2..657fcc8 100644 --- a/tests/EventListener/ExceptionListenerTest.php +++ b/tests/EventListener/ExceptionListenerTest.php @@ -17,21 +17,25 @@ use Check24\NewRelicBundle\NewRelic\NewRelicInteractorInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Security\Core\User\InMemoryUser; use Tests\Check24\NewRelicBundle\DummyException; class ExceptionListenerTest extends TestCase { private ExceptionListener $listener; private NewRelicInteractorInterface&MockObject $interactor; + private Security&MockObject $security; protected function setUp(): void { $this->listener = new ExceptionListener( [DummyException::class], $this->interactor = $this->createMock(NewRelicInteractorInterface::class), + $this->security = $this->createMock(Security::class), ); } @@ -51,6 +55,19 @@ public function testItSendErrorToNewRelic(): void $this->listener->__invoke($event); } + public function testItAttachesUserId(): void + { + $this->security->method('getUser')->willReturn(new InMemoryUser('foo@bar.baz', null)); + $this->interactor->expects(self::once())->method('addCustomParameter')->with('enduser.id', '3260418537'); + + $this->listener->__invoke(new ExceptionEvent( + $this->createMock(HttpKernelInterface::class), + $this->createMock(Request::class), + HttpKernelInterface::MAIN_REQUEST, + new \RuntimeException(), + )); + } + public function testItIgnoreExcludedErrors(): void { $event = new ExceptionEvent( diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index c544574..4e20abe 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -13,6 +13,7 @@ namespace Tests\Check24\NewRelicBundle; +use Check24\NewRelicBundle\EventListener\ExceptionListener; use Check24\NewRelicBundle\NewRelic\Config; use Check24\NewRelicBundle\NewRelic\NewRelicInteractor; use Check24\NewRelicBundle\NewRelic\NewRelicInteractorInterface; @@ -38,6 +39,7 @@ public function testKernelIsBootableAndServicesAreUsable(): void self::assertInstanceOf(TraceUuidFactory::class, $container->get(TraceIdFactoryInterface::class)); self::assertInstanceOf(RouteNameStrategy::class, $container->get(RequestTransactionNameStrategyInterface::class)); self::assertInstanceOf(MessageNameStrategy::class, $container->get(MessengerTransactionNameStrategyInterface::class)); + self::assertInstanceOf(ExceptionListener::class, $container->get(ExceptionListener::class)); $config = $container->get(Config::class);