Skip to content

Commit 3869fd5

Browse files
feat(client-reports): Add support for client reports (#1978)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent d6cc8f1 commit 3869fd5

14 files changed

Lines changed: 639 additions & 33 deletions

‎src/Client.php‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ public function captureException(\Throwable $exception, ?Scope $scope = null, ?E
178178
*/
179179
public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId
180180
{
181-
$event = $this->prepareEvent($event, $hint, $scope);
181+
// Client reports don't need to be augmented in the prepareEvent pipeline.
182+
if ($event->getType() !== EventType::clientReport()) {
183+
$event = $this->prepareEvent($event, $hint, $scope);
184+
}
182185

183186
if ($event === null) {
184187
return null;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\ClientReport;
6+
7+
use Sentry\Event;
8+
use Sentry\State\HubAdapter;
9+
use Sentry\Transport\DataCategory;
10+
11+
class ClientReportAggregator
12+
{
13+
/**
14+
* @var self
15+
*/
16+
private static $instance;
17+
18+
/**
19+
* Nested array for local aggregation. The first key is the category and the second one is the reason.
20+
*
21+
* ```
22+
* [
23+
* 'example-category' => [
24+
* 'example-reason' => 10
25+
* ]
26+
* ]
27+
*```
28+
*
29+
* @var array<array<string, int>>
30+
*/
31+
private $reports = [];
32+
33+
public function add(DataCategory $category, Reason $reason, int $quantity): void
34+
{
35+
$category = $category->getValue();
36+
$reason = $reason->getValue();
37+
if ($quantity <= 0) {
38+
$client = HubAdapter::getInstance()->getClient();
39+
if ($client !== null) {
40+
$logger = $client->getOptions()->getLoggerOrNullLogger();
41+
$logger->debug('Dropping Client report with category={category} and reason={reason} because quantity is zero or negative ({quantity})', [
42+
'category' => $category,
43+
'reason' => $reason,
44+
'quantity' => $quantity,
45+
]);
46+
}
47+
48+
return;
49+
}
50+
$this->reports[$category][$reason] = ($this->reports[$category][$reason] ?? 0) + $quantity;
51+
}
52+
53+
public function flush(): void
54+
{
55+
if (empty($this->reports)) {
56+
return;
57+
}
58+
$reports = [];
59+
foreach ($this->reports as $category => $reasons) {
60+
foreach ($reasons as $reason => $quantity) {
61+
$reports[] = new DiscardedEvent($category, $reason, $quantity);
62+
}
63+
}
64+
$event = Event::createClientReport();
65+
$event->setClientReports($reports);
66+
67+
$client = HubAdapter::getInstance()->getClient();
68+
69+
// Reset the client reports only if we successfully sent an event. If it fails it
70+
// can be sent on the next flush, or it gets discarded anyway.
71+
if ($client !== null && $client->captureEvent($event) !== null) {
72+
$this->reports = [];
73+
}
74+
}
75+
76+
public static function getInstance(): self
77+
{
78+
if (self::$instance === null) {
79+
self::$instance = new self();
80+
}
81+
82+
return self::$instance;
83+
}
84+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\ClientReport;
6+
7+
class DiscardedEvent
8+
{
9+
/**
10+
* @var string
11+
*/
12+
private $reason;
13+
14+
/**
15+
* @var string
16+
*/
17+
private $category;
18+
19+
/**
20+
* @var int
21+
*/
22+
private $quantity;
23+
24+
public function __construct(string $category, string $reason, int $quantity)
25+
{
26+
$this->category = $category;
27+
$this->reason = $reason;
28+
$this->quantity = $quantity;
29+
}
30+
31+
public function getCategory(): string
32+
{
33+
return $this->category;
34+
}
35+
36+
public function getQuantity(): int
37+
{
38+
return $this->quantity;
39+
}
40+
41+
public function getReason(): string
42+
{
43+
return $this->reason;
44+
}
45+
}

‎src/ClientReport/Reason.php‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\ClientReport;
6+
7+
class Reason
8+
{
9+
/**
10+
* @var string
11+
*/
12+
private $value;
13+
14+
/**
15+
* @var array<self>
16+
*/
17+
private static $instances = [];
18+
19+
public function __construct(string $value)
20+
{
21+
$this->value = $value;
22+
}
23+
24+
public static function queueOverflow(): self
25+
{
26+
return self::getInstance('queue_overflow');
27+
}
28+
29+
public static function cacheOverflow(): self
30+
{
31+
return self::getInstance('cache_overflow');
32+
}
33+
34+
public static function bufferOverflow(): self
35+
{
36+
return self::getInstance('buffer_overflow');
37+
}
38+
39+
public static function ratelimitBackoff(): self
40+
{
41+
return self::getInstance('ratelimit_backoff');
42+
}
43+
44+
public static function networkError(): self
45+
{
46+
return self::getInstance('network_error');
47+
}
48+
49+
public static function sampleRate(): self
50+
{
51+
return self::getInstance('sample_rate');
52+
}
53+
54+
public static function beforeSend(): self
55+
{
56+
return self::getInstance('before_send');
57+
}
58+
59+
public static function eventProcessor(): self
60+
{
61+
return self::getInstance('event_processor');
62+
}
63+
64+
public static function sendError(): self
65+
{
66+
return self::getInstance('send_error');
67+
}
68+
69+
public static function internalSdkError(): self
70+
{
71+
return self::getInstance('internal_sdk_error');
72+
}
73+
74+
public static function insufficientData(): self
75+
{
76+
return self::getInstance('insufficient_data');
77+
}
78+
79+
public static function backpressure(): self
80+
{
81+
return self::getInstance('backpressure');
82+
}
83+
84+
public function getValue(): string
85+
{
86+
return $this->value;
87+
}
88+
89+
public function __toString()
90+
{
91+
return $this->value;
92+
}
93+
94+
private static function getInstance(string $value): self
95+
{
96+
if (!isset(self::$instances[$value])) {
97+
self::$instances[$value] = new self($value);
98+
}
99+
100+
return self::$instances[$value];
101+
}
102+
}

‎src/Event.php‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Sentry;
66

7+
use Sentry\ClientReport\DiscardedEvent;
78
use Sentry\Context\OsContext;
89
use Sentry\Context\RuntimeContext;
910
use Sentry\Logs\Log;
@@ -210,6 +211,11 @@ final class Event
210211
*/
211212
private $profile;
212213

214+
/**
215+
* @var DiscardedEvent[]
216+
*/
217+
private $clientReports = [];
218+
213219
private function __construct(?EventId $eventId, EventType $eventType)
214220
{
215221
$this->id = $eventId ?? EventId::generate();
@@ -252,6 +258,11 @@ public static function createMetrics(?EventId $eventId = null): self
252258
return new self($eventId, EventType::metrics());
253259
}
254260

261+
public static function createClientReport(?EventId $eventId = null): self
262+
{
263+
return new self($eventId, EventType::clientReport());
264+
}
265+
255266
/**
256267
* Gets the ID of this event.
257268
*/
@@ -978,4 +989,22 @@ public function getTraceId(): ?string
978989

979990
return null;
980991
}
992+
993+
/**
994+
* @param DiscardedEvent[] $clientReports
995+
*/
996+
public function setClientReports(array $clientReports): self
997+
{
998+
$this->clientReports = $clientReports;
999+
1000+
return $this;
1001+
}
1002+
1003+
/**
1004+
* @return DiscardedEvent[]
1005+
*/
1006+
public function getClientReports(): array
1007+
{
1008+
return $this->clientReports;
1009+
}
9811010
}

‎src/EventType.php‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public static function metrics(): self
5252
return self::getInstance('trace_metric');
5353
}
5454

55+
public static function clientReport(): self
56+
{
57+
return self::getInstance('client_report');
58+
}
59+
5560
/**
5661
* List of all cases on the enum.
5762
*
@@ -65,6 +70,7 @@ public static function cases(): array
6570
self::checkIn(),
6671
self::logs(),
6772
self::metrics(),
73+
self::clientReport(),
6874
];
6975
}
7076

@@ -73,12 +79,21 @@ public function requiresEventId(): bool
7379
switch ($this) {
7480
case self::metrics():
7581
case self::logs():
82+
case self::clientReport():
7683
return false;
7784
default:
7885
return true;
7986
}
8087
}
8188

89+
/**
90+
* Returns false if rate limiting should not be applied.
91+
*/
92+
public function requiresRateLimiting(): bool
93+
{
94+
return $this !== self::clientReport();
95+
}
96+
8297
public function __toString(): string
8398
{
8499
return $this->value;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Serializer\EnvelopItems;
6+
7+
use Sentry\ClientReport\DiscardedEvent;
8+
use Sentry\Event;
9+
use Sentry\Util\JSON;
10+
11+
class ClientReportItem implements EnvelopeItemInterface
12+
{
13+
public static function toEnvelopeItem(Event $event): ?string
14+
{
15+
$reports = $event->getClientReports();
16+
17+
$headers = ['type' => 'client_report'];
18+
$body = [
19+
'timestamp' => $event->getTimestamp(),
20+
'discarded_events' => array_map(static function (DiscardedEvent $report) {
21+
return [
22+
'category' => $report->getCategory(),
23+
'reason' => $report->getReason(),
24+
'quantity' => $report->getQuantity(),
25+
];
26+
}, $reports),
27+
];
28+
29+
return \sprintf("%s\n%s", JSON::encode($headers), JSON::encode($body));
30+
}
31+
}

0 commit comments

Comments
 (0)