<?php
declare(strict_types=1);
namespace Sentry\SentryBundle\EventListener;
use Sentry\Event;
use Sentry\EventHint;
use Sentry\ExceptionMechanism;
use Sentry\State\HubInterface;
use Sentry\State\Scope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\WrappedExceptionsInterface;
use Symfony\Component\Messenger\Stamp\BusNameStamp;
final class MessengerListener
{
/**
* @var HubInterface The current hub
*/
private $hub;
/**
* @var bool Whether to capture errors thrown while processing a message that
* will be retried
*/
private $captureSoftFails;
/**
* @param HubInterface $hub The current hub
* @param bool $captureSoftFails Whether to capture errors thrown
* while processing a message that
* will be retried
*/
public function __construct(HubInterface $hub, bool $captureSoftFails = true)
{
$this->hub = $hub;
$this->captureSoftFails = $captureSoftFails;
}
/**
* This method is called for each message that failed to be handled.
*
* @param WorkerMessageFailedEvent $event The event
*/
public function handleWorkerMessageFailedEvent(WorkerMessageFailedEvent $event): void
{
if (!$this->captureSoftFails && $event->willRetry()) {
return;
}
$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()));
/** @var BusNameStamp|null $messageBusStamp */
$messageBusStamp = $envelope->last(BusNameStamp::class);
if (null !== $messageBusStamp) {
$scope->setTag('messenger.message_bus', $messageBusStamp->getBusName());
}
$this->captureException($exception, $event->willRetry());
});
$this->flushClient();
}
/**
* This method is called for each handled message.
*
* @param WorkerMessageHandledEvent $event The event
*/
public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event): void
{
// 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();
}
/**
* Creates Sentry events from the given exception.
*
* Unpacks multiple exceptions wrapped in a HandlerFailedException and notifies
* Sentry of each individual exception.
*
* If the message will be retried the exceptions will be marked as handled
* in Sentry.
*/
private function captureException(\Throwable $exception, bool $willRetry): void
{
if ($exception instanceof WrappedExceptionsInterface) {
$exception = $exception->getWrappedExceptions();
} elseif ($exception instanceof HandlerFailedException && method_exists($exception, 'getNestedExceptions')) {
$exception = $exception->getNestedExceptions();
} elseif ($exception instanceof DelayedMessageHandlingException && method_exists($exception, 'getExceptions')) {
$exception = $exception->getExceptions();
}
if (\is_array($exception)) {
foreach ($exception as $nestedException) {
$this->captureException($nestedException, $willRetry);
}
return;
}
$hint = EventHint::fromArray([
'exception' => $exception,
'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, $willRetry),
]);
$this->hub->captureEvent(Event::createEvent(), $hint);
}
private function flushClient(): void
{
$client = $this->hub->getClient();
if (null !== $client) {
$client->flush();
}
}
}