Skip to content

feat(client-reports): Add support for client reports#1978

Open
Litarnus wants to merge 21 commits intomasterfrom
client-reports
Open

feat(client-reports): Add support for client reports#1978
Litarnus wants to merge 21 commits intomasterfrom
client-reports

Conversation

@Litarnus
Copy link
Contributor

@Litarnus Litarnus commented Dec 1, 2025

Adds support for ClientReports.

This PR only adds the possibility to send client reports, it does not collect them yet

@Litarnus Litarnus self-assigned this Dec 17, 2025
@Litarnus Litarnus marked this pull request as ready for review February 23, 2026 14:26
}

if ($envelopeHeader === null) {
return \sprintf("{}\n%s", implode("\n", array_filter($items)));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client report envelopes omit standard headers

Medium Severity

PayloadSerializer::serialize() special-cases EventType::clientReport() by emitting an empty envelope header ({}) instead of the usual sent_at/dsn/sdk fields. If Sentry/Relay expects these envelope headers for routing or validation, client reports may be rejected even though other event types succeed.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

@giortzisg giortzisg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, left some comments to verify that we don't rate-limit the reports

];
$envelopeHeader = null;
if ($event->getType() !== EventType::clientReport()) {
// @see https://develop.sentry.dev/sdk/envelopes/#envelope-headers

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: The spec mentions that it's recommended to send sent_at headers always. Let's verify if we should send it or not here.

Comment on lines +181 to +184
// Client reports don't need to be augmented in the prepareEvent pipeline.
if ($event->getType() !== EventType::clientReport()) {
$event = $this->prepareEvent($event, $hint, $scope);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: From the comment that I left above when using captureEvent, I would consider just sending the reports immediately to the transport instead of complicating the logic here. We should also verify that we don't rate limit the reports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have plans to change the way events are processed and sent, right now it all goes through the same captureEvent workflow but we will split it up into separate pipelines. I would keep this as is for now and do the refactoring later

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Client reports overwrite Hub's lastEventId tracking
    • Client report flush now sends via the bound client directly instead of Hub::captureEvent(), so flushing no longer overwrites the hub’s user-facing lastEventId.

Create PR

Or push these changes by commenting:

@cursor push e7cd5c73a3
Preview (e7cd5c73a3)
diff --git a/src/ClientReport/ClientReportAggregator.php b/src/ClientReport/ClientReportAggregator.php
--- a/src/ClientReport/ClientReportAggregator.php
+++ b/src/ClientReport/ClientReportAggregator.php
@@ -64,9 +64,11 @@
         $event = Event::createClientReport();
         $event->setClientReports($reports);
 
+        $client = HubAdapter::getInstance()->getClient();
+
         // Reset the client reports only if we successfully sent an event. If it fails it
         // can be sent on the next flush, or it gets discarded anyway.
-        if (HubAdapter::getInstance()->captureEvent($event) !== null) {
+        if ($client !== null && $client->captureEvent($event) !== null) {
             $this->reports = [];
         }
     }

diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php
--- a/tests/ClientReport/ClientReportAggregatorTest.php
+++ b/tests/ClientReport/ClientReportAggregatorTest.php
@@ -66,6 +66,20 @@
         $this->assertSame(40, $report->getQuantity());
     }
 
+    public function testFlushDoesNotOverwriteLastEventId(): void
+    {
+        $hub = SentrySdk::getCurrentHub();
+        $eventId = $hub->captureMessage('foo');
+
+        $this->assertNotNull($eventId);
+        $this->assertSame($eventId, $hub->getLastEventId());
+
+        ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), 10);
+        ClientReportAggregator::getInstance()->flush();
+
+        $this->assertSame($eventId, $hub->getLastEventId());
+    }
+
     public function testNegativeQuantityDiscarded(): void
     {
         ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), -10);
This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link

@giortzisg giortzisg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@Litarnus
Copy link
Contributor Author

Litarnus commented Mar 9, 2026

@cursor push e7cd5c7

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

// Client reports don't need to be augmented in the prepareEvent pipeline.
if ($event->getType() !== EventType::clientReport()) {
$event = $this->prepareEvent($event, $hint, $scope);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client reports route through full captureEvent pipeline unnecessarily

Low Severity

Client reports are sent by calling $client->captureEvent() from ClientReportAggregator::flush(), which forces an event-type check into captureEvent to skip prepareEvent. This adds branching complexity to a core method and couples internal telemetry to the user-facing event capture path. The LogsAggregator and MetricsAggregator use $hub->captureEvent(), making this approach inconsistent. Sending directly through the transport (as the reviewer suggested) would avoid complicating captureEvent and naturally bypass rate limiting, scope enrichment, and lastEventId concerns.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good feedback and we will restructure the flow of different telemetry and non telemetry events in the future so we can get independent e.g. captureClientReports or captureLogs pipelines which will only enrich and augment data that is necessary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants