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
6 changes: 6 additions & 0 deletions analysis-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,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"
Expand Down
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,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
Expand Down
63 changes: 63 additions & 0 deletions src/ClientReport/ClientReportAggregator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Sentry\ClientReport;

use Sentry\Event;
use Sentry\EventType;
use Sentry\State\HubAdapter;
use Sentry\Transport\DataCategory;

Expand Down Expand Up @@ -32,6 +33,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) {
Expand All @@ -50,6 +55,44 @@ 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():
$this->add(DataCategory::logItem(), $reason, \count($event->getLogs()));
break;
case EventType::metrics():
$this->add(DataCategory::metric(), $reason, \count($event->getMetrics()));
break;
}
}

public function flush(): void
{
if (empty($this->reports)) {
Expand All @@ -73,6 +116,26 @@ 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);
}
}

public static function getInstance(): self
{
if (self::$instance === null) {
Expand Down
20 changes: 20 additions & 0 deletions src/Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,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.
*/
Expand Down Expand Up @@ -1338,6 +1356,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('max_breadth', ['null', 'int']);
Expand Down Expand Up @@ -1452,6 +1471,7 @@ private function configureOptions(OptionsResolver $resolver): void
'in_app_exclude' => [],
'in_app_include' => [],
'send_default_pii' => false,
'send_client_reports' => false,
'max_value_length' => 1024,
'max_breadth' => null,
'transport' => null,
Expand Down
10 changes: 10 additions & 0 deletions src/Transport/DataCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -52,6 +57,11 @@ 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');
Expand Down
1 change: 1 addition & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
* release?: string|null,
* sample_rate?: float|int,
* send_attempts?: int,
* send_client_reports?: bool,
* send_default_pii?: bool,
* server_name?: string,
* spotlight?: bool,
Expand Down
111 changes: 111 additions & 0 deletions tests/ClientReport/ClientReportAggregatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,39 @@
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\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');
StubTransport::$events = [];
StubLogger::$logs = [];
SentrySdk::init()->bindClient(new Client(new Options([
'logger' => StubLogger::getInstance(),
'send_client_reports' => true,
]), StubTransport::getInstance()));
}

Expand Down Expand Up @@ -115,4 +129,101 @@ 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.0, '566e3688a61d4bc888951642d6f14a19', LogLevel::info(), 'foo'),
new Log(1597790835.0, '566e3688a61d4bc888951642d6f14a19', LogLevel::error(), 'bar'),
]);

ClientReportAggregator::getInstance()->addEvent($event, Reason::ratelimitBackoff());

$this->assertClientReports([
'log_item' => ['ratelimit_backoff' => 2],
]);
}

public function testAddEventForMetrics(): void
{
$event = Event::createMetrics()->setMetrics([
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());

$this->assertClientReports([
'trace_metric' => ['ratelimit_backoff' => 2],
]);
}

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([]);
}
}
46 changes: 46 additions & 0 deletions tests/ClientReport/ClientReportAssertions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Sentry\Tests\ClientReport;

use Sentry\ClientReport\ClientReportAggregator;

trait ClientReportAssertions
{
/**
* Asserts the client reports recorded so far, independent of their order.
*
* @param array<string, array<string, int>> $expected the quantities keyed by category and reason
*/
protected function assertClientReports(array $expected): void
{
$reflectionProperty = new \ReflectionProperty(ClientReportAggregator::class, 'reports');
if (\PHP_VERSION_ID < 80100) {
$reflectionProperty->setAccessible(true);
}
/** @var array<string, array<string, int>> $actual */
$actual = $reflectionProperty->getValue(ClientReportAggregator::getInstance());
if (\PHP_VERSION_ID < 80100) {
$reflectionProperty->setAccessible(false);
}

$this->assertSame(self::sortClientReports($expected), self::sortClientReports($actual));
}

/**
* @param array<string, array<string, int>> $reports
*
* @return array<string, array<string, int>>
*/
private static function sortClientReports(array $reports): array
{
ksort($reports);

foreach ($reports as &$reasons) {
ksort($reasons);
}

return $reports;
}
}
Loading
Loading