Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode): void
->{interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->booleanNode('capture_soft_fails')->defaultTrue()->end()
->booleanNode('isolate_breadcrumbs_by_message')->defaultFalse()->end()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue that having isolate_breadcrumbs_by_message set to false by default is correct for now as it otherwise would break bc, but should be deprecated right away as it should be true by default in the future. The main argument is that messages (like requests) are handled separately already, and many of the services in the framework do already get reset on kernel.terminate.

Maybe you can even rely on the Symfony\Contracts\Service\ResetInterface in favor of the WorkerMessageReceivedEvent that you are currently using?

Copy link
Contributor Author

@Litarnus Litarnus Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I agree that it should be true by default and we should do this in the next major.

I will also explore Symfony\Contracts\Service\ResetInterface and see if we can use it here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage of the Symfony\Contracts\Service\ResetInterface would also resolve the "leak" when using FrankenPHP.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... in which you are referring to its worker mode, I guess?

->end()
->end()
->end();
Expand Down
2 changes: 1 addition & 1 deletion src/DependencyInjection/SentryExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private function registerMessengerListenerConfiguration(ContainerBuilder $contai
return;
}

$container->getDefinition(MessengerListener::class)->setArgument(1, $config['capture_soft_fails']);
$container->getDefinition(MessengerListener::class)->setArgument(1, $config['capture_soft_fails'])->setArgument(2, $config['isolate_breadcrumbs_by_message']);
}

/**
Expand Down
79 changes: 58 additions & 21 deletions src/EventListener/MessengerListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Sentry\State\Scope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\WrappedExceptionsInterface;
Expand All @@ -30,15 +31,25 @@ final class MessengerListener
private $captureSoftFails;

/**
* @param HubInterface $hub The current hub
* @param bool $captureSoftFails Whether to capture errors thrown
* while processing a message that
* will be retried
* @var bool When this is enabled, a new scope is pushed on the stack when a message
* is received and will pop it again after the message was finished (either success or fail).
* This allows us to have breadcrumbs only for one message and no breadcrumb is leaked into
* future messages.
*/
public function __construct(HubInterface $hub, bool $captureSoftFails = true)
private $isolateBreadcrumbsByMessage;

/**
* @param HubInterface $hub The current hub
* @param bool $captureSoftFails Whether to capture errors thrown
* while processing a message that
* will be retried
* @param bool $isolateBreadcrumbsByMessage Whether messages should have isolated breadcrumbs
*/
public function __construct(HubInterface $hub, bool $captureSoftFails = true, bool $isolateBreadcrumbsByMessage = false)
{
$this->hub = $hub;
$this->captureSoftFails = $captureSoftFails;
$this->isolateBreadcrumbsByMessage = $isolateBreadcrumbsByMessage;
}

/**
Expand All @@ -48,28 +59,36 @@ public function __construct(HubInterface $hub, bool $captureSoftFails = true)
*/
public function handleWorkerMessageFailedEvent(WorkerMessageFailedEvent $event): void
{
if (!$this->captureSoftFails && $event->willRetry()) {
return;
}
try {
if (!$this->captureSoftFails && $event->willRetry()) {
return;
}

$this->hub->withScope(function (Scope $scope) use ($event): void {
$envelope = $event->getEnvelope();
$exception = $event->getThrowable();
$this->hub->withScope(function (Scope $scope) use ($event): void {
$envelope = $event->getEnvelope();
$exception = $event->getThrowable();

$scope->setTag('messenger.receiver_name', $event->getReceiverName());
$scope->setTag('messenger.message_class', \get_class($envelope->getMessage()));
$scope->setTag('messenger.receiver_name', $event->getReceiverName());
$scope->setTag('messenger.message_class', \get_class($envelope->getMessage()));

/** @var BusNameStamp|null $messageBusStamp */
$messageBusStamp = $envelope->last(BusNameStamp::class);
/** @var BusNameStamp|null $messageBusStamp */
$messageBusStamp = $envelope->last(BusNameStamp::class);

if (null !== $messageBusStamp) {
$scope->setTag('messenger.message_bus', $messageBusStamp->getBusName());
}
if (null !== $messageBusStamp) {
$scope->setTag('messenger.message_bus', $messageBusStamp->getBusName());
}

$this->captureException($exception, $event->willRetry());
});
$this->captureException($exception, $event->willRetry());
});

$this->flushClient();
$this->flushClient();
} finally {
// We always want to pop the scope at the end of this method to add the breadcrumbs
// to any potential event that is produced.
if ($this->isolateBreadcrumbsByMessage) {
$this->hub->popScope();
}
}
}

/**
Expand All @@ -82,6 +101,24 @@ public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event
// Flush normally happens at shutdown... which only happens in the worker if it is run with a lifecycle limit
// such as --time=X or --limit=Y. Flush immediately in a background worker.
$this->flushClient();
if ($this->isolateBreadcrumbsByMessage) {
$this->hub->popScope();
}
}

/**
* Method that will push a new scope on the hub to create message local breadcrumbs that will not
* "leak" into future messages.
*
* @param WorkerMessageReceivedEvent $event
*
* @return void
*/
public function handleWorkerMessageReceivedEvent(WorkerMessageReceivedEvent $event): void
{
if ($this->isolateBreadcrumbsByMessage) {
$this->hub->pushScope();
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Resources/config/schema/sentry-1.0.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
<xsd:complexType name="messenger">
<xsd:attribute name="enabled" type="xsd:boolean" />
<xsd:attribute name="capture-soft-fails" type="xsd:boolean" />
<xsd:attribute name="isolate-breadcrumbs-by-message" type="xsd:boolean" />
</xsd:complexType>

<xsd:complexType name="tracing">
Expand Down
1 change: 1 addition & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@

<tag name="kernel.event_listener" event="Symfony\Component\Messenger\Event\WorkerMessageFailedEvent" method="handleWorkerMessageFailedEvent" priority="50" />
<tag name="kernel.event_listener" event="Symfony\Component\Messenger\Event\WorkerMessageHandledEvent" method="handleWorkerMessageHandledEvent" priority="50" />
<tag name="kernel.event_listener" event="Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent" method="handleWorkerMessageReceivedEvent" priority="50" />
</service>

<service id="Sentry\SentryBundle\EventListener\LoginListener" class="Sentry\SentryBundle\EventListener\LoginListener">
Expand Down
1 change: 1 addition & 0 deletions tests/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function testProcessConfigurationWithDefaultConfiguration(): void
'messenger' => [
'enabled' => interface_exists(MessageBusInterface::class),
'capture_soft_fails' => true,
'isolate_breadcrumbs_by_message' => false,
],
'tracing' => [
'enabled' => true,
Expand Down
1 change: 1 addition & 0 deletions tests/DependencyInjection/Fixtures/php/full.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
'messenger' => [
'enabled' => true,
'capture_soft_fails' => false,
'isolate_breadcrumbs_by_message' => true,
],
'tracing' => [
'dbal' => [
Expand Down
2 changes: 1 addition & 1 deletion tests/DependencyInjection/Fixtures/xml/full.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<sentry:ignore-exception>Symfony\Component\HttpKernel\Exception\BadRequestHttpException</sentry:ignore-exception>
<sentry:ignore-transaction>GET tracing_ignored_transaction</sentry:ignore-transaction>
</sentry:options>
<sentry:messenger enabled="true" capture-soft-fails="false" />
<sentry:messenger enabled="true" capture-soft-fails="false" isolate-breadcrumbs-by-message="true"/>
<sentry:tracing>
<sentry:dbal enabled="false">
<sentry:connection>default</sentry:connection>
Expand Down
1 change: 1 addition & 0 deletions tests/DependencyInjection/Fixtures/yml/full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ sentry:
messenger:
enabled: true
capture_soft_fails: false
isolate_breadcrumbs_by_message: true
tracing:
dbal:
enabled: false
Expand Down
7 changes: 7 additions & 0 deletions tests/DependencyInjection/SentryExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\MessageBusInterface;

abstract class SentryExtensionTest extends TestCase
Expand Down Expand Up @@ -138,10 +139,16 @@ public function testMessengerListener(): void
'method' => 'handleWorkerMessageHandledEvent',
'priority' => 50,
],
[
'event' => WorkerMessageReceivedEvent::class,
'method' => 'handleWorkerMessageReceivedEvent',
'priority' => 50,
],
],
], $definition->getTags());

$this->assertFalse($definition->getArgument(1));
$this->assertTrue($definition->getArgument(2));
}

public function testMessengerListenerIsRemovedWhenDisabled(): void
Expand Down
10 changes: 9 additions & 1 deletion tests/End2End/App/Controller/MessengerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Sentry\SentryBundle\Tests\End2End\App\Controller;

use Psr\Log\LoggerInterface;
use Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessage;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
Expand All @@ -15,9 +16,15 @@ class MessengerController
*/
private $messenger;

public function __construct(MessageBusInterface $messenger)
/**
* @var LoggerInterface
*/
private $logger;

public function __construct(MessageBusInterface $messenger, LoggerInterface $logger)
{
$this->messenger = $messenger;
$this->logger = $logger;
}

public function dispatchMessage(): Response
Expand All @@ -29,6 +36,7 @@ public function dispatchMessage(): Response

public function dispatchUnrecoverableMessage(): Response
{
$this->logger->warning('Dispatch FooMessage');
$this->messenger->dispatch(new FooMessage(false));

return new Response('Success');
Expand Down
15 changes: 14 additions & 1 deletion tests/End2End/App/Messenger/FooMessageHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,27 @@

namespace Sentry\SentryBundle\Tests\End2End\App\Messenger;

use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;

class FooMessageHandler
{
/**
* @var LoggerInterface
*/
private $logger;

public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}

public function __invoke(FooMessage $message): void
{
$this->logger->warning('Handle FooMessage');
if (!$message->shouldRetry()) {
throw new class extends \Exception implements UnrecoverableExceptionInterface { };
throw new class extends \Exception implements UnrecoverableExceptionInterface {
};
}

throw new \Exception('This is an intentional failure while handling a message of class ' . \get_class($message));
Expand Down
14 changes: 14 additions & 0 deletions tests/End2End/App/messenger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
class: \Sentry\SentryBundle\Tests\End2End\App\Messenger\StaticInMemoryTransportFactory

Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessageHandler:
autowire: true
class: \Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessageHandler
tags:
- { name: messenger.message_handler }
Expand All @@ -15,6 +16,18 @@ services:
tags:
- controller.service_arguments

Sentry\Monolog\BreadcrumbHandler:
arguments:
- '@Sentry\State\HubInterface'
- !php/const Monolog\Logger::WARNING

monolog:
handlers:
sentry_breadcrumbs:
type: service
name: sentry_breadcrumbs
id: Sentry\Monolog\BreadcrumbHandler

framework:
messenger:
transports:
Expand All @@ -28,3 +41,4 @@ framework:
sentry:
messenger:
capture_soft_fails: false
isolate_breadcrumbs_by_message: true
26 changes: 26 additions & 0 deletions tests/End2End/End2EndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ protected function setUp(): void
{
parent::setUp();

StubTransport::$events = [];

file_put_contents(self::SENT_EVENTS_LOG, '');
}

Expand Down Expand Up @@ -250,6 +252,30 @@ public function testMessengerCaptureSoftFailCanBeDisabled(): void
$this->assertLastEventIdIsNull($client);
}

public function testIsolateBreadcrumbsByMessage(): void
{
$this->skipIfMessengerIsMissing();

$client = static::createClient();

// Create two messages
$client->request('GET', '/dispatch-unrecoverable-message');
$this->assertSame(200, $client->getResponse()->getStatusCode());

$client->request('GET', '/dispatch-unrecoverable-message');
$this->assertSame(200, $client->getResponse()->getStatusCode());

$this->consumeOneMessage($client->getKernel());
$this->consumeOneMessage($client->getKernel());

$events = StubTransport::$events;
$this->assertCount(2, $events);

// Asser that both have the same number of breadcrumbs meaning that each event has isolated breadcrumbs
$this->assertCount(4, $events[0]->getBreadcrumbs());
$this->assertCount(4, $events[1]->getBreadcrumbs());
}

private function consumeOneMessage(KernelInterface $kernel): void
{
$application = new Application($kernel);
Expand Down
Loading
Loading