diff --git a/src/Client.php b/src/Client.php index 4941b9074..4adf3565c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -285,6 +285,14 @@ public function getSdkVersion(): string */ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?Event { + $sampleRate = $this->options->getSampleRate(); + + if (EventType::event() === $event->getType() && $sampleRate < 1 && mt_rand(1, 100) / 100.0 > $sampleRate) { + $this->logger->info(\sprintf('The %s will be discarded because it has been sampled.', $this->getEventDescription($event)), ['event' => $event]); + + return null; + } + if ($hint !== null) { if ($hint->exception !== null && empty($event->getExceptions())) { $this->addThrowableToEvent($event, $hint->exception, $hint); @@ -314,22 +322,7 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco $event->setEnvironment($this->options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT); } - $eventDescription = \sprintf( - '%s%s [%s]', - $event->getLevel() !== null ? $event->getLevel() . ' ' : '', - (string) $event->getType(), - (string) $event->getId() - ); - - $isEvent = EventType::event() === $event->getType(); - $sampleRate = $this->options->getSampleRate(); - - // only sample with the `sample_rate` on errors/messages - if ($isEvent && $sampleRate < 1 && mt_rand(1, 100) / 100.0 > $sampleRate) { - $this->logger->info(\sprintf('The %s will be discarded because it has been sampled.', $eventDescription), ['event' => $event]); - - return null; - } + $eventDescription = $this->getEventDescription($event); $event = $this->applyIgnoreOptions($event, $eventDescription); @@ -368,6 +361,16 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco return $event; } + private function getEventDescription(Event $event): string + { + return \sprintf( + '%s%s [%s]', + $event->getLevel() !== null ? $event->getLevel() . ' ' : '', + (string) $event->getType(), + (string) $event->getId() + ); + } + /** * Checks if an exception should be ignored based on configured patterns. * Supports both class hierarchy matching and regex patterns. @@ -528,6 +531,11 @@ private function addMissingStacktraceToEvent(Event $event): void return; } + // Only error events carry a stacktrace, for any other type it would be discarded + if ($event->getType() !== EventType::event()) { + return; + } + // We should not add a stacktrace when the event already has one or contains exceptions if ($event->getStacktrace() !== null || !empty($event->getExceptions())) { return; diff --git a/tests/ClientTest.php b/tests/ClientTest.php index b2ad69e46..837aea905 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -361,6 +361,40 @@ public static function captureEventAttachesStacktraceAccordingToAttachStacktrace ]; } + /** + * @dataProvider captureEventDoesNotAttachStacktraceToNonErrorEventsDataProvider + */ + public function testCaptureEventDoesNotAttachStacktraceToNonErrorEvents(Event $event): void + { + /** @var TransportInterface&MockObject $transport */ + $transport = $this->createMock(TransportInterface::class); + $transport->expects($this->once()) + ->method('send') + ->with($this->callback(static function (Event $event): bool { + return $event->getStacktrace() === null; + })) + ->willReturnCallback(static function (Event $event): Result { + return new Result(ResultStatus::success(), $event); + }); + + $client = ClientBuilder::create(['attach_stacktrace' => true]) + ->setTransport($transport) + ->getClient(); + + $this->assertNotNull($client->captureEvent($event)); + } + + public static function captureEventDoesNotAttachStacktraceToNonErrorEventsDataProvider(): \Generator + { + yield 'transaction' => [Event::createTransaction()]; + + yield 'check-in' => [Event::createCheckIn()]; + + yield 'logs' => [Event::createLogs()]; + + yield 'metrics' => [Event::createMetrics()]; + } + public function testCaptureEventPrefersExplicitStacktrace(): void { $stacktrace = new Stacktrace([ @@ -652,6 +686,29 @@ public function testProcessEventDiscardsEventWhenSampleRateOptionIsZero(): void $client->captureEvent(Event::createEvent()); } + public function testProcessEventDoesNotBuildStacktracesWhenEventIsNotSampled(): void + { + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->exactly(2)) + ->method('info') + ->with( + new StringMatchesFormatDescription('The event [%s] will be discarded because it has been sampled.'), + $this->callback(static function (array $context): bool { + return $context['event'] instanceof Event + && $context['event']->getExceptions() === [] + && $context['event']->getStacktrace() === null; + }) + ); + + $client = ClientBuilder::create(['sample_rate' => 0, 'attach_stacktrace' => true]) + ->setTransport($this->createMock(TransportInterface::class)) + ->setLogger($logger) + ->getClient(); + + $client->captureException(new \Exception('foo')); + $client->captureMessage('foo'); + } + public function testProcessEventCapturesEventWhenSampleRateOptionIsAboveZero(): void { $transport = $this->createMock(TransportInterface::class);