Skip to content

Commit bf687e4

Browse files
committed
feat(core): Drop spans if beforeSendSpan returns null
1 parent 85df82e commit bf687e4

File tree

7 files changed

+142
-15
lines changed

7 files changed

+142
-15
lines changed

packages/core/src/baseclient.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
Span,
2525
SpanAttributes,
2626
SpanContextData,
27+
SpanJSON,
2728
StartSpanOptions,
2829
TransactionEvent,
2930
Transport,
@@ -904,7 +905,14 @@ function processBeforeSend(
904905

905906
if (isTransactionEvent(event)) {
906907
if (event.spans && beforeSendSpan) {
907-
event.spans = event.spans.map(span => beforeSendSpan(span));
908+
const processedSpans: SpanJSON[] = [];
909+
for (const span of event.spans) {
910+
const processedSpan = beforeSendSpan(span);
911+
if (processedSpan) {
912+
processedSpans.push(processedSpan);
913+
}
914+
}
915+
event.spans = processedSpans;
908916
}
909917

910918
if (beforeSendTransaction) {

packages/core/src/envelope.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
SessionEnvelope,
1313
SessionItem,
1414
SpanEnvelope,
15+
SpanItem,
1516
SpanJSON,
1617
} from '@sentry/types';
1718
import {
@@ -98,8 +99,9 @@ export function createEventEnvelope(
9899
* Create envelope from Span item.
99100
*
100101
* Takes an optional client and runs spans through `beforeSendSpan` if available.
102+
* @returns A SpanEnvelope or null if all spans have been dropped
101103
*/
102-
export function createSpanEnvelope(spans: SentrySpan[], client?: Client): SpanEnvelope {
104+
export function createSpanEnvelope(spans: SentrySpan[], client?: Client): SpanEnvelope | null {
103105
function dscHasRequiredProps(dsc: Partial<DynamicSamplingContext>): dsc is DynamicSamplingContext {
104106
return !!dsc.trace_id && !!dsc.public_key;
105107
}
@@ -118,7 +120,14 @@ export function createSpanEnvelope(spans: SentrySpan[], client?: Client): SpanEn
118120
const convertToSpanJSON = beforeSendSpan
119121
? (span: SentrySpan) => beforeSendSpan(spanToJSON(span) as SpanJSON)
120122
: (span: SentrySpan) => spanToJSON(span);
121-
const items = spans.map(span => createSpanEnvelopeItem(convertToSpanJSON(span)));
122123

123-
return createEnvelope<SpanEnvelope>(headers, items);
124+
const items: SpanItem[] = [];
125+
for (const span of spans) {
126+
const spanJson = convertToSpanJSON(span);
127+
if (spanJson) {
128+
items.push(createSpanEnvelopeItem(spanJson));
129+
}
130+
}
131+
132+
return items.length > 0 ? createEnvelope<SpanEnvelope>(headers, items) : null;
124133
}

packages/core/src/tracing/sentrySpan.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,12 @@ export class SentrySpan implements Span {
9797

9898
this._events = [];
9999

100+
this._isStandaloneSpan = spanContext.isStandalone;
101+
100102
// If the span is already ended, ensure we finalize the span immediately
101103
if (this._endTime) {
102104
this._onSpanEnded();
103105
}
104-
105-
this._isStandaloneSpan = spanContext.isStandalone;
106106
}
107107

108108
/** @inheritdoc */
@@ -259,7 +259,14 @@ export class SentrySpan implements Span {
259259

260260
// if this is a standalone span, we send it immediately
261261
if (this._isStandaloneSpan) {
262-
sendSpanEnvelope(createSpanEnvelope([this], client));
262+
const spanEnvelope = createSpanEnvelope([this], client);
263+
if (spanEnvelope) {
264+
sendSpanEnvelope(spanEnvelope);
265+
} else {
266+
if (client) {
267+
client.recordDroppedEvent('before_send', 'span');
268+
}
269+
}
263270
return;
264271
}
265272

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,38 @@ describe('BaseClient', () => {
11191119
expect(loggerWarnSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.');
11201120
});
11211121

1122+
test('calls `beforeSendSpan` and discards the span', () => {
1123+
expect.assertions(2);
1124+
1125+
const beforeSendSpan = jest.fn(() => null);
1126+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan });
1127+
const client = new TestClient(options);
1128+
1129+
const transaction: Event = {
1130+
transaction: '/cats/are/great',
1131+
type: 'transaction',
1132+
spans: [
1133+
{
1134+
description: 'first span',
1135+
span_id: '9e15bf99fbe4bc80',
1136+
start_timestamp: 1591603196.637835,
1137+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1138+
},
1139+
{
1140+
description: 'second span',
1141+
span_id: 'aa554c1f506b0783',
1142+
start_timestamp: 1591603196.637835,
1143+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1144+
},
1145+
],
1146+
};
1147+
client.captureEvent(transaction);
1148+
1149+
expect(beforeSendSpan).toHaveBeenCalledTimes(2);
1150+
const capturedEvent = TestClient.instance!.event!;
1151+
expect(capturedEvent.spans).toHaveLength(0);
1152+
});
1153+
11221154
test('calls `beforeSend` and logs info about invalid return value', () => {
11231155
const invalidValues = [undefined, false, true, [], 1];
11241156
expect.assertions(invalidValues.length * 3);

packages/core/test/lib/envelope.test.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ describe('createSpanEnvelope', () => {
109109

110110
const spanEnvelope = createSpanEnvelope([span]);
111111

112-
const spanItem = spanEnvelope[1][0][1];
112+
const spanItem = spanEnvelope![1][0][1];
113113
expect(spanItem).toEqual({
114114
data: {
115115
'sentry.origin': 'manual',
@@ -131,7 +131,7 @@ describe('createSpanEnvelope', () => {
131131
new SentrySpan({ name: 'test', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' } }),
132132
]);
133133

134-
const spanEnvelopeHeaders = spanEnvelope[0];
134+
const spanEnvelopeHeaders = spanEnvelope![0];
135135
expect(spanEnvelopeHeaders).toEqual({
136136
sent_at: expect.any(String),
137137
trace: {
@@ -152,7 +152,7 @@ describe('createSpanEnvelope', () => {
152152

153153
const spanEnvelope = createSpanEnvelope([new SentrySpan()]);
154154

155-
const spanEnvelopeHeaders = spanEnvelope[0];
155+
const spanEnvelopeHeaders = spanEnvelope![0];
156156
expect(spanEnvelopeHeaders).toEqual({
157157
sent_at: expect.any(String),
158158
});
@@ -174,7 +174,7 @@ describe('createSpanEnvelope', () => {
174174

175175
expect(beforeSendSpan).toHaveBeenCalled();
176176

177-
const spanItem = spanEnvelope[1][0][1];
177+
const spanItem = spanEnvelope![1][0][1];
178178
expect(spanItem).toEqual({
179179
data: {
180180
'sentry.origin': 'manual',
@@ -209,7 +209,7 @@ describe('createSpanEnvelope', () => {
209209

210210
expect(beforeSendSpan).toHaveBeenCalled();
211211

212-
const spanItem = spanEnvelope[1][0][1];
212+
const spanItem = spanEnvelope![1][0][1];
213213
expect(spanItem).toEqual({
214214
data: {
215215
'sentry.origin': 'manual',
@@ -224,4 +224,22 @@ describe('createSpanEnvelope', () => {
224224
trace_id: expect.stringMatching(/^[0-9a-f]{32}$/),
225225
});
226226
});
227+
228+
it('calls `beforeSendSpan` and discards the envelope', () => {
229+
const beforeSendSpan = jest.fn(() => null);
230+
const options = getDefaultTestClientOptions({ dsn: 'https://domain/123', beforeSendSpan });
231+
const client = new TestClient(options);
232+
233+
const span = new SentrySpan({
234+
name: 'test',
235+
isStandalone: true,
236+
startTimestamp: 1,
237+
endTimestamp: 2,
238+
});
239+
240+
const spanEnvelope = createSpanEnvelope([span], client);
241+
242+
expect(beforeSendSpan).toHaveBeenCalled();
243+
expect(spanEnvelope).toBeNull();
244+
});
227245
});

packages/core/test/lib/tracing/sentrySpan.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { timestampInSeconds } from '@sentry/utils';
2+
import { setCurrentClient } from '../../../src';
23
import { SentrySpan } from '../../../src/tracing/sentrySpan';
34
import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus';
45
import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, spanToJSON } from '../../../src/utils/spanUtils';
6+
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
57

68
describe('SentrySpan', () => {
79
describe('name', () => {
@@ -88,6 +90,57 @@ describe('SentrySpan', () => {
8890
span.end();
8991
expect(spanToJSON(span).timestamp).toBeGreaterThan(1);
9092
});
93+
94+
test('sends the span if `beforeSendSpan` does not modify the span ', () => {
95+
const beforeSendSpan = jest.fn(span => span);
96+
const client = new TestClient(
97+
getDefaultTestClientOptions({
98+
dsn: 'https://username@domain/123',
99+
enableSend: true,
100+
beforeSendSpan,
101+
}),
102+
);
103+
setCurrentClient(client);
104+
105+
// @ts-expect-error Accessing private transport API
106+
const mockSend = jest.spyOn(client._transport, 'send');
107+
const span = new SentrySpan({
108+
name: 'test',
109+
isStandalone: true,
110+
startTimestamp: 1,
111+
endTimestamp: 2,
112+
sampled: true,
113+
});
114+
span.end();
115+
expect(mockSend).toHaveBeenCalled();
116+
});
117+
118+
test('does not send the span if `beforeSendSpan` drops the span', () => {
119+
const beforeSendSpan = jest.fn(() => null);
120+
const client = new TestClient(
121+
getDefaultTestClientOptions({
122+
dsn: 'https://username@domain/123',
123+
enableSend: true,
124+
beforeSendSpan,
125+
}),
126+
);
127+
setCurrentClient(client);
128+
129+
const recordDroppedEventSpy = jest.spyOn(client, 'recordDroppedEvent');
130+
// @ts-expect-error Accessing private transport API
131+
const mockSend = jest.spyOn(client._transport, 'send');
132+
const span = new SentrySpan({
133+
name: 'test',
134+
isStandalone: true,
135+
startTimestamp: 1,
136+
endTimestamp: 2,
137+
sampled: true,
138+
});
139+
span.end();
140+
141+
expect(mockSend).not.toHaveBeenCalled();
142+
expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'span');
143+
});
91144
});
92145

93146
describe('end', () => {

packages/types/src/options.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,11 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
285285
/**
286286
* An event-processing callback for spans. This allows a span to be modified before it's sent.
287287
*
288-
* Note that you must return a valid span from this callback. If you do not wish to modify the span, simply return
289-
* it at the end.
288+
* Returning `null` will cause this span to be dropped.
290289
* @param span The span generated by the SDK.
290+
* @returns A new span that will be sent | null.
291291
*/
292-
beforeSendSpan?: (span: SpanJSON) => SpanJSON;
292+
beforeSendSpan?: (span: SpanJSON) => SpanJSON | null;
293293

294294
/**
295295
* An event-processing callback for transaction events, guaranteed to be invoked after all other event

0 commit comments

Comments
 (0)