Skip to content

Commit df45d65

Browse files
authored
feat(core): Capture # of dropped spans through beforeSendTransaction (#13022)
Fixes #12849. This is a bit tricky because `beforeSendTransaction` can return a promise, so in order to avoid dealing with this I put the # of spans on the sdk metadata, and then compare that afterwards.
1 parent a880712 commit df45d65

File tree

3 files changed

+58
-12
lines changed

3 files changed

+58
-12
lines changed

.size-limit.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ module.exports = [
4848
path: 'packages/browser/build/npm/esm/index.js',
4949
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
5050
gzip: true,
51-
limit: '76 KB',
51+
limit: '78 KB',
5252
},
5353
{
5454
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
5555
path: 'packages/browser/build/npm/esm/index.js',
5656
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
5757
gzip: true,
58-
limit: '89 KB',
58+
limit: '90 KB',
5959
},
6060
{
6161
name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)',

packages/core/src/baseclient.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -371,20 +371,21 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
371371
/**
372372
* @inheritDoc
373373
*/
374-
public recordDroppedEvent(reason: EventDropReason, category: DataCategory, _event?: Event): void {
375-
// Note: we use `event` in replay, where we overwrite this hook.
376-
374+
public recordDroppedEvent(reason: EventDropReason, category: DataCategory, eventOrCount?: Event | number): void {
377375
if (this._options.sendClientReports) {
376+
// TODO v9: We do not need the `event` passed as third argument anymore, and can possibly remove this overload
377+
// If event is passed as third argument, we assume this is a count of 1
378+
const count = typeof eventOrCount === 'number' ? eventOrCount : 1;
379+
378380
// We want to track each category (error, transaction, session, replay_event) separately
379381
// but still keep the distinction between different type of outcomes.
380382
// We could use nested maps, but it's much easier to read and type this way.
381383
// A correct type for map-based implementation if we want to go that route
382384
// would be `Partial<Record<SentryRequestType, Partial<Record<Outcome, number>>>>`
383385
// With typescript 4.1 we could even use template literal types
384386
const key = `${reason}:${category}`;
385-
DEBUG_BUILD && logger.log(`Adding outcome: "${key}"`);
386-
387-
this._outcomes[key] = (this._outcomes[key] || 0) + 1;
387+
DEBUG_BUILD && logger.log(`Recording outcome: "${key}"${count > 1 ? ` (${count} times)` : ''}`);
388+
this._outcomes[key] = (this._outcomes[key] || 0) + count;
388389
}
389390
}
390391

@@ -794,11 +795,11 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
794795
.then(processedEvent => {
795796
if (processedEvent === null) {
796797
this.recordDroppedEvent('before_send', dataCategory, event);
797-
if (isTransactionEvent(event)) {
798+
if (isTransaction) {
798799
const spans = event.spans || [];
799800
// the transaction itself counts as one span, plus all the child spans that are added
800801
const spanCount = 1 + spans.length;
801-
this._outcomes['span'] = (this._outcomes['span'] || 0) + spanCount;
802+
this.recordDroppedEvent('before_send', 'span', spanCount);
802803
}
803804
throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
804805
}
@@ -808,6 +809,18 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
808809
this._updateSessionFromEvent(session, processedEvent);
809810
}
810811

812+
if (isTransaction) {
813+
const spanCountBefore =
814+
(processedEvent.sdkProcessingMetadata && processedEvent.sdkProcessingMetadata.spanCountBeforeProcessing) ||
815+
0;
816+
const spanCountAfter = processedEvent.spans ? processedEvent.spans.length : 0;
817+
818+
const droppedSpanCount = spanCountBefore - spanCountAfter;
819+
if (droppedSpanCount > 0) {
820+
this.recordDroppedEvent('before_send', 'span', droppedSpanCount);
821+
}
822+
}
823+
811824
// None of the Sentry built event processor will update transaction name,
812825
// so if the transaction name has been changed by an event processor, we know
813826
// it has to come from custom event processor added by a user
@@ -973,6 +986,15 @@ function processBeforeSend(
973986
}
974987

975988
if (beforeSendTransaction) {
989+
if (event.spans) {
990+
// We store the # of spans before processing in SDK metadata,
991+
// so we can compare it afterwards to determine how many spans were dropped
992+
const spanCountBefore = event.spans.length;
993+
event.sdkProcessingMetadata = {
994+
...event.sdkProcessingMetadata,
995+
spanCountBeforeProcessing: spanCountBefore,
996+
};
997+
}
976998
return beforeSendTransaction(event, hint);
977999
}
9781000
}

packages/core/test/lib/base.test.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,30 @@ describe('BaseClient', () => {
10421042
expect(TestClient.instance!.event!.transaction).toBe('/adopt/dont/shop');
10431043
});
10441044

1045+
test('calls `beforeSendTransaction` and drops spans', () => {
1046+
const beforeSendTransaction = jest.fn(event => {
1047+
event.spans = [{ span_id: 'span5', trace_id: 'trace1', start_timestamp: 1234 }];
1048+
return event;
1049+
});
1050+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction });
1051+
const client = new TestClient(options);
1052+
1053+
client.captureEvent({
1054+
transaction: '/dogs/are/great',
1055+
type: 'transaction',
1056+
spans: [
1057+
{ span_id: 'span1', trace_id: 'trace1', start_timestamp: 1234 },
1058+
{ span_id: 'span2', trace_id: 'trace1', start_timestamp: 1234 },
1059+
{ span_id: 'span3', trace_id: 'trace1', start_timestamp: 1234 },
1060+
],
1061+
});
1062+
1063+
expect(beforeSendTransaction).toHaveBeenCalled();
1064+
expect(TestClient.instance!.event!.spans?.length).toBe(1);
1065+
1066+
expect(client['_outcomes']).toEqual({ 'before_send:span': 2 });
1067+
});
1068+
10451069
test('calls `beforeSendSpan` and uses the modified spans', () => {
10461070
expect.assertions(3);
10471071

@@ -1120,8 +1144,6 @@ describe('BaseClient', () => {
11201144
});
11211145

11221146
test('calls `beforeSendSpan` and discards the span', () => {
1123-
expect.assertions(2);
1124-
11251147
const beforeSendSpan = jest.fn(() => null);
11261148
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan });
11271149
const client = new TestClient(options);
@@ -1149,6 +1171,7 @@ describe('BaseClient', () => {
11491171
expect(beforeSendSpan).toHaveBeenCalledTimes(2);
11501172
const capturedEvent = TestClient.instance!.event!;
11511173
expect(capturedEvent.spans).toHaveLength(0);
1174+
expect(client['_outcomes']).toEqual({ 'before_send:span': 2 });
11521175
});
11531176

11541177
test('calls `beforeSend` and logs info about invalid return value', () => {
@@ -2017,6 +2040,7 @@ describe('BaseClient', () => {
20172040

20182041
describe('hook removal with `on`', () => {
20192042
it('should return a cleanup function that, when executed, unregisters a hook', async () => {
2043+
jest.useFakeTimers();
20202044
expect.assertions(8);
20212045

20222046
const client = new TestClient(

0 commit comments

Comments
 (0)