From 3b44e7d8a7052509bb57e77c16fa2d927cad03ab Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 20 Oct 2026 17:44:24 +0200 Subject: [PATCH 1/3] feat(client-reports): extend aggregator to collect event facts --- analysis-baseline.toml | 6 + phpstan-baseline.neon | 6 + src/ClientReport/ClientReportAggregator.php | 154 ++++++++++++++++++ src/Options.php | 20 +++ src/Transport/DataCategory.php | 15 ++ src/functions.php | 1 + .../ClientReportAggregatorTest.php | 135 +++++++++++++++ tests/ClientReport/ClientReportAssertions.php | 56 +++++++ tests/OptionsTest.php | 8 + tests/SentrySdkExtension.php | 10 ++ 10 files changed, 411 insertions(+) create mode 100644 tests/ClientReport/ClientReportAssertions.php diff --git a/analysis-baseline.toml b/analysis-baseline.toml index dc9b1c7fe..e512c0438 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -1038,6 +1038,12 @@ code = "mixed-return-statement" message = 'Could not infer a precise return type for function `Sentry\Options::shouldCaptureSilencedErrors`. Saw type `mixed`.' count = 1 +[[issues]] +file = "src/Options.php" +code = "mixed-return-statement" +message = 'Could not infer a precise return type for function `Sentry\Options::shouldSendClientReports`. Saw type `mixed`.' +count = 1 + [[issues]] file = "src/Options.php" code = "mixed-return-statement" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f1d50a504..c682a015d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -486,6 +486,12 @@ parameters: count: 1 path: src/Options.php + - + message: '#^Method Sentry\\Options\:\:shouldSendClientReports\(\) should return bool but returns mixed\.$#' + identifier: return.type + count: 1 + path: src/Options.php + - message: '#^Method Sentry\\Options\:\:shouldSendDefaultPii\(\) should return bool but returns mixed\.$#' identifier: return.type diff --git a/src/ClientReport/ClientReportAggregator.php b/src/ClientReport/ClientReportAggregator.php index 8045a51b2..f17ef4d23 100644 --- a/src/ClientReport/ClientReportAggregator.php +++ b/src/ClientReport/ClientReportAggregator.php @@ -4,12 +4,31 @@ namespace Sentry\ClientReport; +use Sentry\Attributes\AttributeBag; use Sentry\Event; +use Sentry\EventType; +use Sentry\Logs\Log; +use Sentry\Metrics\Types\Metric; use Sentry\State\HubAdapter; use Sentry\Transport\DataCategory; class ClientReportAggregator { + /** + * The JSON structure of a serialized log without its values, see {@see \Sentry\Serializer\EnvelopItems\LogsItem}. + */ + private const LOG_JSON_STRUCTURE = '{"timestamp":,"trace_id":"","level":"","body":"","attributes":{}},'; + + /** + * The JSON structure of a serialized metric without its values, see {@see \Sentry\Serializer\EnvelopItems\MetricsItem}. + */ + private const METRIC_JSON_STRUCTURE = '{"timestamp":,"trace_id":"","span_id":"","name":"","value":,"unit":,"type":"","attributes":{}},'; + + /** + * The JSON structure of a serialized attribute without its values. + */ + private const ATTRIBUTE_JSON_STRUCTURE = '"":{"type":"","value":},'; + /** * @var self */ @@ -32,6 +51,10 @@ class ClientReportAggregator public function add(DataCategory $category, Reason $reason, int $quantity): void { + if (!$this->isEnabled()) { + return; + } + $category = $category->getValue(); $reason = $reason->getValue(); if ($quantity <= 0) { @@ -50,6 +73,52 @@ public function add(DataCategory $category, Reason $reason, int $quantity): void $this->reports[$category][$reason] = ($this->reports[$category][$reason] ?? 0) + $quantity; } + /** + * Records a discarded event in all the data categories it accounts for, e.g. a + * transaction also discards its spans and its profile. + */ + public function addEvent(Event $event, Reason $reason): void + { + if (!$this->isEnabled()) { + return; + } + + switch ($event->getType()) { + case EventType::event(): + $this->add(DataCategory::error(), $reason, 1); + $this->addAttachments($event, $reason); + break; + case EventType::transaction(): + $this->add(DataCategory::transaction(), $reason, 1); + // Relay extracts an additional span from the transaction itself + $this->add(DataCategory::span(), $reason, \count($event->getSpans()) + 1); + + if ($event->getSdkMetadata('profile') !== null) { + $this->add(DataCategory::profile(), $reason, 1); + } + + $this->addAttachments($event, $reason); + break; + case EventType::checkIn(): + $this->add(DataCategory::checkIn(), $reason, 1); + break; + case EventType::logs(): + $logs = $event->getLogs(); + $this->add(DataCategory::logItem(), $reason, \count($logs)); + $this->add(DataCategory::logBytes(), $reason, array_sum(array_map(static function (Log $log): int { + return self::getLogSize($log); + }, $logs))); + break; + case EventType::metrics(): + $metrics = $event->getMetrics(); + $this->add(DataCategory::metric(), $reason, \count($metrics)); + $this->add(DataCategory::metricBytes(), $reason, array_sum(array_map(static function (Metric $metric): int { + return self::getMetricSize($metric); + }, $metrics))); + break; + } + } + public function flush(): void { if (empty($this->reports)) { @@ -73,6 +142,91 @@ public function flush(): void } } + private function isEnabled(): bool + { + $client = HubAdapter::getInstance()->getClient(); + + return $client !== null && $client->getOptions()->shouldSendClientReports(); + } + + private function addAttachments(Event $event, Reason $reason): void + { + $bytes = 0; + + foreach ($event->getAttachments() as $attachment) { + $bytes += $attachment->getSize() ?? 0; + } + + if ($bytes > 0) { + $this->add(DataCategory::attachment(), $reason, $bytes); + } + } + + /** + * Estimates the size the log has once serialized, without serializing it. + */ + private static function getLogSize(Log $log): int + { + return \strlen(self::LOG_JSON_STRUCTURE) + + \strlen((string) $log->getTimestamp()) + + \strlen($log->getTraceId()) + + \strlen((string) $log->getLevel()) + + \strlen($log->getBody()) + + self::getAttributesSize($log->attributes()); + } + + /** + * Estimates the size the metric has once serialized, without serializing it. + */ + private static function getMetricSize(Metric $metric): int + { + $unit = $metric->getUnit(); + + return \strlen(self::METRIC_JSON_STRUCTURE) + + \strlen((string) $metric->getTimestamp()) + + \strlen((string) $metric->getTraceId()) + + \strlen((string) $metric->getSpanId()) + + \strlen($metric->getName()) + + self::getValueSize($metric->getValue()) + + self::getValueSize($unit !== null ? (string) $unit : null) + + \strlen($metric->getType()) + + self::getAttributesSize($metric->getAttributes()); + } + + private static function getAttributesSize(AttributeBag $attributes): int + { + $size = 0; + + foreach ($attributes->all() as $key => $attribute) { + $size += \strlen(self::ATTRIBUTE_JSON_STRUCTURE) + \strlen($key) + \strlen($attribute->getType()) + self::getValueSize($attribute->getValue()); + } + + return $size; + } + + /** + * Estimates the size of a scalar once serialized, ignoring escaped characters. + * + * @param string|bool|int|float|null $value + */ + private static function getValueSize($value): int + { + if (\is_string($value)) { + // The value is wrapped in quotes + return \strlen($value) + 2; + } + + if (\is_bool($value)) { + return $value ? 4 : 5; + } + + if ($value === null) { + return 4; + } + + return \strlen((string) $value); + } + public static function getInstance(): self { if (self::$instance === null) { diff --git a/src/Options.php b/src/Options.php index c6d4b002f..3c977e570 100644 --- a/src/Options.php +++ b/src/Options.php @@ -978,6 +978,24 @@ public function setSendDefaultPii(bool $enable): self return $this->updateOptions(['send_default_pii' => $enable]); } + /** + * Returns whether the SDK should record and send client reports about discarded telemetry. + */ + public function shouldSendClientReports(): bool + { + return $this->options['send_client_reports']; + } + + /** + * Sets whether the SDK should record and send client reports about discarded telemetry. + * + * @param bool $enable Flag indicating if client reports will be sent + */ + public function setSendClientReports(bool $enable): self + { + return $this->updateOptions(['send_client_reports' => $enable]); + } + /** * Returns whether the default integrations are enabled. */ @@ -1290,6 +1308,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('before_breadcrumb', ['callable']); $resolver->setAllowedTypes('integrations', ['Sentry\\Integration\\IntegrationInterface[]', 'callable']); $resolver->setAllowedTypes('send_default_pii', 'bool'); + $resolver->setAllowedTypes('send_client_reports', 'bool'); $resolver->setAllowedTypes('default_integrations', 'bool'); $resolver->setAllowedTypes('max_value_length', 'int'); $resolver->setAllowedTypes('transport', ['null', TransportInterface::class]); @@ -1400,6 +1419,7 @@ private function configureOptions(OptionsResolver $resolver): void 'in_app_exclude' => [], 'in_app_include' => [], 'send_default_pii' => false, + 'send_client_reports' => true, 'max_value_length' => 1024, 'transport' => null, 'http_client' => null, diff --git a/src/Transport/DataCategory.php b/src/Transport/DataCategory.php index b2ee0884e..751925a88 100644 --- a/src/Transport/DataCategory.php +++ b/src/Transport/DataCategory.php @@ -31,6 +31,11 @@ public static function transaction(): self return self::getInstance('transaction'); } + public static function span(): self + { + return self::getInstance('span'); + } + // TODO: not sure if this should be called monitor or checkIn. public static function checkIn(): self { @@ -52,11 +57,21 @@ public static function profile(): self return self::getInstance('profile'); } + public static function attachment(): self + { + return self::getInstance('attachment'); + } + public static function metric(): self { return self::getInstance('trace_metric'); } + public static function metricBytes(): self + { + return self::getInstance('trace_metric_byte'); + } + public static function internal(): self { return self::getInstance('internal'); diff --git a/src/functions.php b/src/functions.php index 432f00f57..9e270087a 100644 --- a/src/functions.php +++ b/src/functions.php @@ -80,6 +80,7 @@ * release?: string|null, * sample_rate?: float|int, * send_attempts?: int, + * send_client_reports?: bool, * send_default_pii?: bool, * server_name?: string, * spotlight?: bool, diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php index ce82c4d50..6b6202574 100644 --- a/tests/ClientReport/ClientReportAggregatorTest.php +++ b/tests/ClientReport/ClientReportAggregatorTest.php @@ -5,18 +5,33 @@ namespace Sentry\Tests\ClientReport; use PHPUnit\Framework\TestCase; +use Sentry\Attachment\Attachment; use Sentry\Client; use Sentry\ClientReport\ClientReportAggregator; use Sentry\ClientReport\Reason; +use Sentry\Event; +use Sentry\Logs\Log; +use Sentry\Logs\LogLevel; +use Sentry\Metrics\Types\CounterMetric; +use Sentry\Metrics\Types\DistributionMetric; use Sentry\Options; +use Sentry\Profiling\Profile; use Sentry\SentrySdk; +use Sentry\Serializer\EnvelopItems\LogsItem; +use Sentry\Serializer\EnvelopItems\MetricsItem; use Sentry\State\Hub; use Sentry\Tests\StubLogger; use Sentry\Tests\StubTransport; +use Sentry\Tracing\Span; +use Sentry\Tracing\SpanId; +use Sentry\Tracing\TraceId; use Sentry\Transport\DataCategory; +use Sentry\Unit; class ClientReportAggregatorTest extends TestCase { + use ClientReportAssertions; + protected function setUp(): void { ini_set('zend.exception_ignore_args', '0'); @@ -115,4 +130,124 @@ public function testNegativeQuantityDiscardedWhenNoClientIsBound(): void $this->assertEmpty(StubTransport::$events); $this->assertEmpty(StubLogger::$logs); } + + public function testAddEventForError(): void + { + $event = Event::createEvent(); + $event->setAttachments([ + Attachment::fromBytes('foo.txt', 'foo'), + Attachment::fromBytes('bar.txt', 'barbaz'), + ]); + + ClientReportAggregator::getInstance()->addEvent($event, Reason::beforeSend()); + + $this->assertClientReports([ + 'error' => ['before_send' => 1], + 'attachment' => ['before_send' => 9], + ]); + } + + public function testAddEventForTransaction(): void + { + $event = Event::createTransaction(); + $event->setSpans([new Span(), new Span()]); + $event->setSdkMetadata('profile', new Profile()); + + ClientReportAggregator::getInstance()->addEvent($event, Reason::ratelimitBackoff()); + + $this->assertClientReports([ + 'transaction' => ['ratelimit_backoff' => 1], + 'span' => ['ratelimit_backoff' => 3], + 'profile' => ['ratelimit_backoff' => 1], + ]); + } + + public function testAddEventForTransactionWithoutSpans(): void + { + ClientReportAggregator::getInstance()->addEvent(Event::createTransaction(), Reason::sampleRate()); + + $this->assertClientReports([ + 'transaction' => ['sample_rate' => 1], + 'span' => ['sample_rate' => 1], + ]); + } + + public function testAddEventForCheckIn(): void + { + ClientReportAggregator::getInstance()->addEvent(Event::createCheckIn(), Reason::beforeSend()); + + $this->assertClientReports([ + 'monitor' => ['before_send' => 1], + ]); + } + + public function testAddEventForLogs(): void + { + $event = Event::createLogs()->setLogs([ + (new Log(1597790835.123456, '566e3688a61d4bc888951642d6f14a19', LogLevel::info(), 'User jane logged in from 10.0.0.1')) + ->setAttribute('sentry.release', '1.2.3') + ->setAttribute('sentry.environment', 'production') + ->setAttribute('sentry.sdk.name', 'sentry.php') + ->setAttribute('sentry.message.template', 'User %s logged in from %s') + ->setAttribute('sentry.message.parameter.0', 'jane') + ->setAttribute('user.id', 42) + ->setAttribute('duration', 12.5) + ->setAttribute('http.ok', true), + new Log(1597790835.123456, '566e3688a61d4bc888951642d6f14a19', LogLevel::error(), 'Something went wrong'), + ]); + + ClientReportAggregator::getInstance()->addEvent($event, Reason::ratelimitBackoff()); + + $reports = self::getClientReports(); + + $this->assertSame(['log_item', 'log_byte'], array_keys($reports)); + $this->assertSame(['ratelimit_backoff' => 2], $reports['log_item']); + $this->assertEstimatedSize(LogsItem::toEnvelopeItem($event), $reports['log_byte']['ratelimit_backoff']); + } + + public function testAddEventForMetrics(): void + { + $event = Event::createMetrics()->setMetrics([ + new CounterMetric('api.requests', 1, new TraceId('566e3688a61d4bc888951642d6f14a19'), new SpanId('566e3688a61d4bc8'), ['sentry.environment' => 'production', 'route' => 'users.show', 'cached' => false], 1597790835.123456, null), + new DistributionMetric('api.duration', 12.75, new TraceId('566e3688a61d4bc888951642d6f14a19'), new SpanId('566e3688a61d4bc8'), ['sentry.environment' => 'production'], 1597790835.123456, Unit::millisecond()), + ]); + + ClientReportAggregator::getInstance()->addEvent($event, Reason::ratelimitBackoff()); + + $reports = self::getClientReports(); + + $this->assertSame(['trace_metric', 'trace_metric_byte'], array_keys($reports)); + $this->assertSame(['ratelimit_backoff' => 2], $reports['trace_metric']); + $this->assertEstimatedSize(MetricsItem::toEnvelopeItem($event), $reports['trace_metric_byte']['ratelimit_backoff']); + } + + public function testAddEventIgnoresClientReports(): void + { + ClientReportAggregator::getInstance()->addEvent(Event::createClientReport(), Reason::sendError()); + + $this->assertClientReports([]); + } + + public function testNothingIsRecordedWhenClientReportsAreDisabled(): void + { + SentrySdk::getCurrentHub()->bindClient(new Client(new Options([ + 'send_client_reports' => false, + ]), StubTransport::getInstance())); + + ClientReportAggregator::getInstance()->add(DataCategory::error(), Reason::beforeSend(), 1); + ClientReportAggregator::getInstance()->addEvent(Event::createEvent(), Reason::beforeSend()); + + $this->assertClientReports([]); + } + + /** + * Asserts that the estimated size is within 5% of the serialized items, so that a change of the + * serialized format must be reflected in the estimate. + */ + private function assertEstimatedSize(string $envelopeItem, int $estimatedSize): void + { + $serializedSize = \strlen(explode("\n", $envelopeItem, 2)[1]); + + $this->assertEqualsWithDelta($serializedSize, $estimatedSize, $serializedSize * 0.05); + } } diff --git a/tests/ClientReport/ClientReportAssertions.php b/tests/ClientReport/ClientReportAssertions.php new file mode 100644 index 000000000..6e05202f4 --- /dev/null +++ b/tests/ClientReport/ClientReportAssertions.php @@ -0,0 +1,56 @@ +> $expected the quantities keyed by category and reason + */ + protected function assertClientReports(array $expected): void + { + $this->assertSame(self::sortClientReports($expected), self::sortClientReports(self::getClientReports())); + } + + /** + * Returns the client reports recorded so far. + * + * @return array> the quantities keyed by category and reason + */ + protected static function getClientReports(): array + { + $reflectionProperty = new \ReflectionProperty(ClientReportAggregator::class, 'reports'); + if (\PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } + /** @var array> $reports */ + $reports = $reflectionProperty->getValue(ClientReportAggregator::getInstance()); + if (\PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(false); + } + + return $reports; + } + + /** + * @param array> $reports + * + * @return array> + */ + private static function sortClientReports(array $reports): array + { + ksort($reports); + + foreach ($reports as &$reasons) { + ksort($reasons); + } + + return $reports; + } +} diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index 050285612..f8312fe60 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -379,6 +379,13 @@ static function (): void {}, 'setSendDefaultPii', ]; + yield [ + 'send_client_reports', + false, + 'shouldSendClientReports', + 'setSendClientReports', + ]; + yield [ 'data_collection', (new DataCollectionOptions())->setUserInfo(false), @@ -635,6 +642,7 @@ public function testDefaultOptionValues(): void 'in_app_exclude' => [], 'in_app_include' => [], 'send_default_pii' => false, + 'send_client_reports' => true, 'data_collection' => null, 'max_value_length' => 1024, 'transport' => null, diff --git a/tests/SentrySdkExtension.php b/tests/SentrySdkExtension.php index 145e35251..4d93d6e74 100644 --- a/tests/SentrySdkExtension.php +++ b/tests/SentrySdkExtension.php @@ -5,6 +5,7 @@ namespace Sentry\Tests; use PHPUnit\Runner\BeforeTestHook as BeforeTestHookInterface; +use Sentry\ClientReport\ClientReportAggregator; use Sentry\Integration\IntegrationRegistry; use Sentry\SentrySdk; use Sentry\State\Scope; @@ -68,5 +69,14 @@ public function executeBeforeTest(string $test): void if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(false); } + + $reflectionProperty = new \ReflectionProperty(ClientReportAggregator::class, 'instance'); + if (\PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(true); + } + $reflectionProperty->setValue(null, null); + if (\PHP_VERSION_ID < 80100) { + $reflectionProperty->setAccessible(false); + } } } From a350af180a869fe715d2fddf96e470f0a5cf0f61 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 22 Oct 2026 13:50:02 +0200 Subject: [PATCH 2/3] dont do metrics/logs bytes --- src/ClientReport/ClientReportAggregator.php | 95 +------------------ src/Transport/DataCategory.php | 5 - .../ClientReportAggregatorTest.php | 45 ++------- tests/ClientReport/ClientReportAssertions.php | 16 +--- 4 files changed, 15 insertions(+), 146 deletions(-) diff --git a/src/ClientReport/ClientReportAggregator.php b/src/ClientReport/ClientReportAggregator.php index f17ef4d23..e744d32db 100644 --- a/src/ClientReport/ClientReportAggregator.php +++ b/src/ClientReport/ClientReportAggregator.php @@ -4,31 +4,13 @@ namespace Sentry\ClientReport; -use Sentry\Attributes\AttributeBag; use Sentry\Event; use Sentry\EventType; -use Sentry\Logs\Log; -use Sentry\Metrics\Types\Metric; use Sentry\State\HubAdapter; use Sentry\Transport\DataCategory; class ClientReportAggregator { - /** - * The JSON structure of a serialized log without its values, see {@see \Sentry\Serializer\EnvelopItems\LogsItem}. - */ - private const LOG_JSON_STRUCTURE = '{"timestamp":,"trace_id":"","level":"","body":"","attributes":{}},'; - - /** - * The JSON structure of a serialized metric without its values, see {@see \Sentry\Serializer\EnvelopItems\MetricsItem}. - */ - private const METRIC_JSON_STRUCTURE = '{"timestamp":,"trace_id":"","span_id":"","name":"","value":,"unit":,"type":"","attributes":{}},'; - - /** - * The JSON structure of a serialized attribute without its values. - */ - private const ATTRIBUTE_JSON_STRUCTURE = '"":{"type":"","value":},'; - /** * @var self */ @@ -103,18 +85,10 @@ public function addEvent(Event $event, Reason $reason): void $this->add(DataCategory::checkIn(), $reason, 1); break; case EventType::logs(): - $logs = $event->getLogs(); - $this->add(DataCategory::logItem(), $reason, \count($logs)); - $this->add(DataCategory::logBytes(), $reason, array_sum(array_map(static function (Log $log): int { - return self::getLogSize($log); - }, $logs))); + $this->add(DataCategory::logItem(), $reason, \count($event->getLogs())); break; case EventType::metrics(): - $metrics = $event->getMetrics(); - $this->add(DataCategory::metric(), $reason, \count($metrics)); - $this->add(DataCategory::metricBytes(), $reason, array_sum(array_map(static function (Metric $metric): int { - return self::getMetricSize($metric); - }, $metrics))); + $this->add(DataCategory::metric(), $reason, \count($event->getMetrics())); break; } } @@ -162,71 +136,6 @@ private function addAttachments(Event $event, Reason $reason): void } } - /** - * Estimates the size the log has once serialized, without serializing it. - */ - private static function getLogSize(Log $log): int - { - return \strlen(self::LOG_JSON_STRUCTURE) - + \strlen((string) $log->getTimestamp()) - + \strlen($log->getTraceId()) - + \strlen((string) $log->getLevel()) - + \strlen($log->getBody()) - + self::getAttributesSize($log->attributes()); - } - - /** - * Estimates the size the metric has once serialized, without serializing it. - */ - private static function getMetricSize(Metric $metric): int - { - $unit = $metric->getUnit(); - - return \strlen(self::METRIC_JSON_STRUCTURE) - + \strlen((string) $metric->getTimestamp()) - + \strlen((string) $metric->getTraceId()) - + \strlen((string) $metric->getSpanId()) - + \strlen($metric->getName()) - + self::getValueSize($metric->getValue()) - + self::getValueSize($unit !== null ? (string) $unit : null) - + \strlen($metric->getType()) - + self::getAttributesSize($metric->getAttributes()); - } - - private static function getAttributesSize(AttributeBag $attributes): int - { - $size = 0; - - foreach ($attributes->all() as $key => $attribute) { - $size += \strlen(self::ATTRIBUTE_JSON_STRUCTURE) + \strlen($key) + \strlen($attribute->getType()) + self::getValueSize($attribute->getValue()); - } - - return $size; - } - - /** - * Estimates the size of a scalar once serialized, ignoring escaped characters. - * - * @param string|bool|int|float|null $value - */ - private static function getValueSize($value): int - { - if (\is_string($value)) { - // The value is wrapped in quotes - return \strlen($value) + 2; - } - - if (\is_bool($value)) { - return $value ? 4 : 5; - } - - if ($value === null) { - return 4; - } - - return \strlen((string) $value); - } - public static function getInstance(): self { if (self::$instance === null) { diff --git a/src/Transport/DataCategory.php b/src/Transport/DataCategory.php index 751925a88..3e7e14feb 100644 --- a/src/Transport/DataCategory.php +++ b/src/Transport/DataCategory.php @@ -67,11 +67,6 @@ public static function metric(): self return self::getInstance('trace_metric'); } - public static function metricBytes(): self - { - return self::getInstance('trace_metric_byte'); - } - public static function internal(): self { return self::getInstance('internal'); diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php index 6b6202574..a74d17bef 100644 --- a/tests/ClientReport/ClientReportAggregatorTest.php +++ b/tests/ClientReport/ClientReportAggregatorTest.php @@ -17,8 +17,6 @@ use Sentry\Options; use Sentry\Profiling\Profile; use Sentry\SentrySdk; -use Sentry\Serializer\EnvelopItems\LogsItem; -use Sentry\Serializer\EnvelopItems\MetricsItem; use Sentry\State\Hub; use Sentry\Tests\StubLogger; use Sentry\Tests\StubTransport; @@ -184,41 +182,29 @@ public function testAddEventForCheckIn(): void public function testAddEventForLogs(): void { $event = Event::createLogs()->setLogs([ - (new Log(1597790835.123456, '566e3688a61d4bc888951642d6f14a19', LogLevel::info(), 'User jane logged in from 10.0.0.1')) - ->setAttribute('sentry.release', '1.2.3') - ->setAttribute('sentry.environment', 'production') - ->setAttribute('sentry.sdk.name', 'sentry.php') - ->setAttribute('sentry.message.template', 'User %s logged in from %s') - ->setAttribute('sentry.message.parameter.0', 'jane') - ->setAttribute('user.id', 42) - ->setAttribute('duration', 12.5) - ->setAttribute('http.ok', true), - new Log(1597790835.123456, '566e3688a61d4bc888951642d6f14a19', LogLevel::error(), 'Something went wrong'), + new Log(1597790835.0, '566e3688a61d4bc888951642d6f14a19', LogLevel::info(), 'foo'), + new Log(1597790835.0, '566e3688a61d4bc888951642d6f14a19', LogLevel::error(), 'bar'), ]); ClientReportAggregator::getInstance()->addEvent($event, Reason::ratelimitBackoff()); - $reports = self::getClientReports(); - - $this->assertSame(['log_item', 'log_byte'], array_keys($reports)); - $this->assertSame(['ratelimit_backoff' => 2], $reports['log_item']); - $this->assertEstimatedSize(LogsItem::toEnvelopeItem($event), $reports['log_byte']['ratelimit_backoff']); + $this->assertClientReports([ + 'log_item' => ['ratelimit_backoff' => 2], + ]); } public function testAddEventForMetrics(): void { $event = Event::createMetrics()->setMetrics([ - new CounterMetric('api.requests', 1, new TraceId('566e3688a61d4bc888951642d6f14a19'), new SpanId('566e3688a61d4bc8'), ['sentry.environment' => 'production', 'route' => 'users.show', 'cached' => false], 1597790835.123456, null), - new DistributionMetric('api.duration', 12.75, new TraceId('566e3688a61d4bc888951642d6f14a19'), new SpanId('566e3688a61d4bc8'), ['sentry.environment' => 'production'], 1597790835.123456, Unit::millisecond()), + new CounterMetric('foo', 1, new TraceId('566e3688a61d4bc888951642d6f14a19'), new SpanId('566e3688a61d4bc8'), [], 1597790835.0, null), + new DistributionMetric('bar', 12.75, new TraceId('566e3688a61d4bc888951642d6f14a19'), new SpanId('566e3688a61d4bc8'), [], 1597790835.0, Unit::millisecond()), ]); ClientReportAggregator::getInstance()->addEvent($event, Reason::ratelimitBackoff()); - $reports = self::getClientReports(); - - $this->assertSame(['trace_metric', 'trace_metric_byte'], array_keys($reports)); - $this->assertSame(['ratelimit_backoff' => 2], $reports['trace_metric']); - $this->assertEstimatedSize(MetricsItem::toEnvelopeItem($event), $reports['trace_metric_byte']['ratelimit_backoff']); + $this->assertClientReports([ + 'trace_metric' => ['ratelimit_backoff' => 2], + ]); } public function testAddEventIgnoresClientReports(): void @@ -239,15 +225,4 @@ public function testNothingIsRecordedWhenClientReportsAreDisabled(): void $this->assertClientReports([]); } - - /** - * Asserts that the estimated size is within 5% of the serialized items, so that a change of the - * serialized format must be reflected in the estimate. - */ - private function assertEstimatedSize(string $envelopeItem, int $estimatedSize): void - { - $serializedSize = \strlen(explode("\n", $envelopeItem, 2)[1]); - - $this->assertEqualsWithDelta($serializedSize, $estimatedSize, $serializedSize * 0.05); - } } diff --git a/tests/ClientReport/ClientReportAssertions.php b/tests/ClientReport/ClientReportAssertions.php index 6e05202f4..84a1e4d03 100644 --- a/tests/ClientReport/ClientReportAssertions.php +++ b/tests/ClientReport/ClientReportAssertions.php @@ -14,28 +14,18 @@ trait ClientReportAssertions * @param array> $expected the quantities keyed by category and reason */ protected function assertClientReports(array $expected): void - { - $this->assertSame(self::sortClientReports($expected), self::sortClientReports(self::getClientReports())); - } - - /** - * Returns the client reports recorded so far. - * - * @return array> the quantities keyed by category and reason - */ - protected static function getClientReports(): array { $reflectionProperty = new \ReflectionProperty(ClientReportAggregator::class, 'reports'); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(true); } - /** @var array> $reports */ - $reports = $reflectionProperty->getValue(ClientReportAggregator::getInstance()); + /** @var array> $actual */ + $actual = $reflectionProperty->getValue(ClientReportAggregator::getInstance()); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(false); } - return $reports; + $this->assertSame(self::sortClientReports($expected), self::sortClientReports($actual)); } /** From fb0891e094d20b9d99e2c96f4bf5bb3067a6652b Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 22 Oct 2026 14:17:01 +0200 Subject: [PATCH 3/3] disabled by default --- src/Options.php | 2 +- tests/ClientReport/ClientReportAggregatorTest.php | 1 + tests/OptionsTest.php | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Options.php b/src/Options.php index fb2ca065a..6789b3d5f 100644 --- a/src/Options.php +++ b/src/Options.php @@ -1471,7 +1471,7 @@ private function configureOptions(OptionsResolver $resolver): void 'in_app_exclude' => [], 'in_app_include' => [], 'send_default_pii' => false, - 'send_client_reports' => true, + 'send_client_reports' => false, 'max_value_length' => 1024, 'max_breadth' => null, 'transport' => null, diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php index a74d17bef..2097d0e87 100644 --- a/tests/ClientReport/ClientReportAggregatorTest.php +++ b/tests/ClientReport/ClientReportAggregatorTest.php @@ -37,6 +37,7 @@ protected function setUp(): void StubLogger::$logs = []; SentrySdk::init()->bindClient(new Client(new Options([ 'logger' => StubLogger::getInstance(), + 'send_client_reports' => true, ]), StubTransport::getInstance())); } diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php index ac4c9eeec..4956a7230 100644 --- a/tests/OptionsTest.php +++ b/tests/OptionsTest.php @@ -388,7 +388,7 @@ static function (): void {}, yield [ 'send_client_reports', - false, + true, 'shouldSendClientReports', 'setSendClientReports', ]; @@ -657,7 +657,7 @@ public function testDefaultOptionValues(): void 'in_app_exclude' => [], 'in_app_include' => [], 'send_default_pii' => false, - 'send_client_reports' => true, + 'send_client_reports' => false, 'data_collection' => null, 'max_value_length' => 1024, 'max_breadth' => null,