Skip to content

Commit f54c97a

Browse files
backport(tracing): Report dropped spans for transactions (#13343)
Co-authored-by: Francesco Novy <francesco.novy@sentry.io>
1 parent eeae3cf commit f54c97a

File tree

4 files changed

+87
-10
lines changed

4 files changed

+87
-10
lines changed

.size-limit.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ module.exports = [
9393
name: '@sentry/browser (incl. Tracing) - ES6 CDN Bundle (gzipped)',
9494
path: 'packages/browser/build/bundles/bundle.tracing.min.js',
9595
gzip: true,
96-
limit: '37 KB',
96+
limit: '38 KB',
9797
},
9898
{
9999
name: '@sentry/browser - ES6 CDN Bundle (gzipped)',
@@ -115,7 +115,7 @@ module.exports = [
115115
path: 'packages/browser/build/bundles/bundle.tracing.min.js',
116116
gzip: false,
117117
brotli: false,
118-
limit: '112 KB',
118+
limit: '113 KB',
119119
},
120120
{
121121
name: '@sentry/browser - ES6 CDN Bundle (minified & uncompressed)',

packages/core/src/baseclient.ts

+34-7
Original file line numberDiff line numberDiff line change
@@ -419,21 +419,21 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
419419
/**
420420
* @inheritDoc
421421
*/
422-
public recordDroppedEvent(reason: EventDropReason, category: DataCategory, _event?: Event): void {
423-
// Note: we use `event` in replay, where we overwrite this hook.
424-
422+
public recordDroppedEvent(reason: EventDropReason, category: DataCategory, eventOrCount?: Event | number): void {
425423
if (this._options.sendClientReports) {
424+
// TODO v9: We do not need the `event` passed as third argument anymore, and can possibly remove this overload
425+
// If event is passed as third argument, we assume this is a count of 1
426+
const count = typeof eventOrCount === 'number' ? eventOrCount : 1;
427+
426428
// We want to track each category (error, transaction, session, replay_event) separately
427429
// but still keep the distinction between different type of outcomes.
428430
// We could use nested maps, but it's much easier to read and type this way.
429431
// A correct type for map-based implementation if we want to go that route
430432
// would be `Partial<Record<SentryRequestType, Partial<Record<Outcome, number>>>>`
431433
// With typescript 4.1 we could even use template literal types
432434
const key = `${reason}:${category}`;
433-
DEBUG_BUILD && logger.log(`Adding outcome: "${key}"`);
434-
435-
// The following works because undefined + 1 === NaN and NaN is falsy
436-
this._outcomes[key] = this._outcomes[key] + 1 || 1;
435+
DEBUG_BUILD && logger.log(`Recording outcome: "${key}"${count > 1 ? ` (${count} times)` : ''}`);
436+
this._outcomes[key] = (this._outcomes[key] || 0) + count;
437437
}
438438
}
439439

@@ -778,6 +778,12 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
778778
.then(processedEvent => {
779779
if (processedEvent === null) {
780780
this.recordDroppedEvent('before_send', dataCategory, event);
781+
if (isTransaction) {
782+
const spans = event.spans || [];
783+
// the transaction itself counts as one span, plus all the child spans that are added
784+
const spanCount = 1 + spans.length;
785+
this.recordDroppedEvent('before_send', 'span', spanCount);
786+
}
781787
throw new SentryError(`${beforeSendLabel} returned \`null\`, will not send event.`, 'log');
782788
}
783789

@@ -786,6 +792,18 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
786792
this._updateSessionFromEvent(session, processedEvent);
787793
}
788794

795+
if (isTransaction) {
796+
const spanCountBefore =
797+
(processedEvent.sdkProcessingMetadata && processedEvent.sdkProcessingMetadata.spanCountBeforeProcessing) ||
798+
0;
799+
const spanCountAfter = processedEvent.spans ? processedEvent.spans.length : 0;
800+
801+
const droppedSpanCount = spanCountBefore - spanCountAfter;
802+
if (droppedSpanCount > 0) {
803+
this.recordDroppedEvent('before_send', 'span', droppedSpanCount);
804+
}
805+
}
806+
789807
// None of the Sentry built event processor will update transaction name,
790808
// so if the transaction name has been changed by an event processor, we know
791809
// it has to come from custom event processor added by a user
@@ -924,6 +942,15 @@ function processBeforeSend(
924942
}
925943

926944
if (isTransactionEvent(event) && beforeSendTransaction) {
945+
if (event.spans) {
946+
// We store the # of spans before processing in SDK metadata,
947+
// so we can compare it afterwards to determine how many spans were dropped
948+
const spanCountBefore = event.spans.length;
949+
event.sdkProcessingMetadata = {
950+
...event.sdkProcessingMetadata,
951+
spanCountBeforeProcessing: spanCountBefore,
952+
};
953+
}
927954
return beforeSendTransaction(event, hint);
928955
}
929956

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

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Client, Envelope, Event, Span, Transaction } from '@sentry/types';
22
import { SentryError, SyncPromise, dsnToString, logger } from '@sentry/utils';
33

4-
import { Hub, Scope, makeSession, setGlobalScope } from '../../src';
4+
import { Hub, Scope, Span as CoreSpan, makeSession, setGlobalScope } from '../../src';
55
import * as integrationModule from '../../src/integration';
66
import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
77
import { AdHocIntegration, TestIntegration } from '../mocks/integration';
@@ -1004,6 +1004,55 @@ describe('BaseClient', () => {
10041004
expect(TestClient.instance!.event!.transaction).toBe('/adopt/dont/shop');
10051005
});
10061006

1007+
test('calls `beforeSendTransaction` and drops spans', () => {
1008+
const beforeSendTransaction = jest.fn(event => {
1009+
event.spans = [new CoreSpan({ spanId: 'span5', traceId: 'trace1', startTimestamp: 1234 })];
1010+
return event;
1011+
});
1012+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction });
1013+
const client = new TestClient(options);
1014+
1015+
client.captureEvent({
1016+
transaction: '/dogs/are/great',
1017+
type: 'transaction',
1018+
spans: [
1019+
new CoreSpan({ spanId: 'span1', traceId: 'trace1', startTimestamp: 1234 }),
1020+
new CoreSpan({ spanId: 'span2', traceId: 'trace1', startTimestamp: 1234 }),
1021+
new CoreSpan({ spanId: 'span3', traceId: 'trace1', startTimestamp: 1234 }),
1022+
],
1023+
});
1024+
1025+
expect(beforeSendTransaction).toHaveBeenCalled();
1026+
expect(TestClient.instance!.event!.spans?.length).toBe(1);
1027+
1028+
expect(client['_outcomes']).toEqual({ 'before_send:span': 2 });
1029+
});
1030+
1031+
test('calls `beforeSendTransaction` and drops spans + 1 if tx null', () => {
1032+
const beforeSendTransaction = jest.fn(() => {
1033+
return null;
1034+
});
1035+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction });
1036+
const client = new TestClient(options);
1037+
1038+
client.captureEvent({
1039+
transaction: '/dogs/are/great',
1040+
type: 'transaction',
1041+
spans: [
1042+
new CoreSpan({ spanId: 'span1', traceId: 'trace1', startTimestamp: 1234 }),
1043+
new CoreSpan({ spanId: 'span2', traceId: 'trace1', startTimestamp: 1234 }),
1044+
new CoreSpan({ spanId: 'span3', traceId: 'trace1', startTimestamp: 1234 }),
1045+
],
1046+
});
1047+
1048+
expect(beforeSendTransaction).toHaveBeenCalled();
1049+
1050+
expect(client['_outcomes']).toEqual({
1051+
'before_send:span': 4,
1052+
'before_send:transaction': 1,
1053+
});
1054+
});
1055+
10071056
test('calls `beforeSend` and discards the event', () => {
10081057
expect.assertions(4);
10091058

packages/node-experimental/test/integration/transactions.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ describe('Integration | Transactions', () => {
110110
}),
111111
sampleRate: 1,
112112
source: 'task',
113+
spanCountBeforeProcessing: 2,
113114
spanMetadata: expect.any(Object),
114115
requestPath: 'test-path',
115116
},

0 commit comments

Comments
 (0)