Skip to content

Commit 5568d2c

Browse files
committed
feat(core): Add ignoreSpans option
Closes #16820
1 parent 5310112 commit 5568d2c

File tree

6 files changed

+434
-18
lines changed

6 files changed

+434
-18
lines changed

packages/core/src/client.ts

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { merge } from './utils/merge';
4242
import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc';
4343
import { parseSampleRate } from './utils/parseSampleRate';
4444
import { prepareEvent } from './utils/prepareEvent';
45+
import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span';
4546
import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils';
4647
import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils/syncpromise';
4748
import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';
@@ -1293,36 +1294,67 @@ function processBeforeSend(
12931294
event: Event,
12941295
hint: EventHint,
12951296
): PromiseLike<Event | null> | Event | null {
1296-
const { beforeSend, beforeSendTransaction, beforeSendSpan } = options;
1297+
const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
12971298
let processedEvent = event;
12981299

12991300
if (isErrorEvent(processedEvent) && beforeSend) {
13001301
return beforeSend(processedEvent, hint);
13011302
}
13021303

13031304
if (isTransactionEvent(processedEvent)) {
1304-
if (beforeSendSpan) {
1305-
// process root span
1306-
const processedRootSpanJson = beforeSendSpan(convertTransactionEventToSpanJson(processedEvent));
1307-
if (!processedRootSpanJson) {
1308-
showSpanDropWarning();
1309-
} else {
1310-
// update event with processed root span values
1311-
processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
1305+
// Avoid processing if we don't have to
1306+
if (beforeSendSpan || ignoreSpans) {
1307+
// 1. Process root span
1308+
const rootSpanJson = convertTransactionEventToSpanJson(processedEvent);
1309+
1310+
// 1.1 If the root span should be ignored, drop the whole transaction
1311+
if (shouldIgnoreSpan(rootSpanJson, ignoreSpans)) {
1312+
// dropping the whole transaction!
1313+
return null;
13121314
}
13131315

1314-
// process child spans
1316+
// 1.2 If a `beforeSendSpan` callback is defined, process the root span
1317+
if (beforeSendSpan) {
1318+
const processedRootSpanJson = beforeSendSpan(rootSpanJson);
1319+
if (!processedRootSpanJson) {
1320+
showSpanDropWarning();
1321+
} else {
1322+
// update event with processed root span values
1323+
processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
1324+
}
1325+
}
1326+
1327+
// 2. Process child spans
13151328
if (processedEvent.spans) {
13161329
const processedSpans: SpanJSON[] = [];
1317-
for (const span of processedEvent.spans) {
1318-
const processedSpan = beforeSendSpan(span);
1319-
if (!processedSpan) {
1320-
showSpanDropWarning();
1321-
processedSpans.push(span);
1330+
1331+
const initialSpans = processedEvent.spans;
1332+
1333+
for (const span of initialSpans) {
1334+
// 2.a If the child span should be ignored, reparent it to the root span
1335+
if (shouldIgnoreSpan(span, ignoreSpans)) {
1336+
reparentChildSpans(initialSpans, span);
1337+
continue;
1338+
}
1339+
1340+
// 2.b If a `beforeSendSpan` callback is defined, process the child span
1341+
if (beforeSendSpan) {
1342+
const processedSpan = beforeSendSpan(span);
1343+
if (!processedSpan) {
1344+
showSpanDropWarning();
1345+
processedSpans.push(span);
1346+
} else {
1347+
processedSpans.push(processedSpan);
1348+
}
13221349
} else {
1323-
processedSpans.push(processedSpan);
1350+
processedSpans.push(span);
13241351
}
13251352
}
1353+
1354+
const droppedSpans = processedEvent.spans.length - processedSpans.length;
1355+
if (droppedSpans) {
1356+
client.recordDroppedEvent('before_send', 'span', droppedSpans);
1357+
}
13261358
processedEvent.spans = processedSpans;
13271359
}
13281360
}

packages/core/src/envelope.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
getSdkMetadataForEnvelopeHeader,
2727
} from './utils/envelope';
2828
import { uuid4 } from './utils/misc';
29+
import { shouldIgnoreSpan } from './utils/should-ignore-span';
2930
import { showSpanDropWarning, spanToJSON } from './utils/spanUtils';
3031

3132
/**
@@ -122,7 +123,15 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
122123
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
123124
};
124125

125-
const beforeSendSpan = client?.getOptions().beforeSendSpan;
126+
const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {};
127+
128+
const filteredSpans = ignoreSpans ? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans)) : spans;
129+
const droppedSpans = spans.length - filteredSpans.length;
130+
131+
if (droppedSpans) {
132+
client?.recordDroppedEvent('before_send', 'span', droppedSpans);
133+
}
134+
126135
const convertToSpanJSON = beforeSendSpan
127136
? (span: SentrySpan) => {
128137
const spanJson = spanToJSON(span);
@@ -138,7 +147,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
138147
: spanToJSON;
139148

140149
const items: SpanItem[] = [];
141-
for (const span of spans) {
150+
for (const span of filteredSpans) {
142151
const spanJson = convertToSpanJSON(span);
143152
if (spanJson) {
144153
items.push(createSpanEnvelopeItem(spanJson));

packages/core/src/types-hoist/options.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import type { StackLineParser, StackParser } from './stacktrace';
1010
import type { TracePropagationTargets } from './tracing';
1111
import type { BaseTransportOptions, Transport } from './transport';
1212

13+
interface IgnoreSpanFilter {
14+
name?: string | RegExp;
15+
op?: string | RegExp;
16+
}
17+
1318
export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOptions> {
1419
/**
1520
* Enable debug functionality in the SDK itself. If `debug` is set to `true` the SDK will attempt
@@ -208,6 +213,13 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
208213
*/
209214
ignoreTransactions?: Array<string | RegExp>;
210215

216+
/**
217+
* A list of span names or patterns to ignore.
218+
*
219+
* @default []
220+
*/
221+
ignoreSpans?: (string | RegExp | IgnoreSpanFilter)[];
222+
211223
/**
212224
* A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint
213225
* that accepts Sentry envelopes for forwarding. This can be used to force data
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { ClientOptions } from '../types-hoist/options';
2+
import type { SpanJSON } from '../types-hoist/span';
3+
import { isMatchingPattern, stringMatchesSomePattern } from './string';
4+
5+
/**
6+
* Check if a span should be ignored based on the ignoreSpans configuration.
7+
*/
8+
export function shouldIgnoreSpan(
9+
span: Pick<SpanJSON, 'description' | 'op'>,
10+
ignoreSpans: ClientOptions['ignoreSpans'],
11+
): boolean {
12+
if (!ignoreSpans?.length) {
13+
return false;
14+
}
15+
16+
if (!span.description) {
17+
return false;
18+
}
19+
20+
// First we check the simple string/regex patterns - if the name matches any of them, we ignore the span
21+
const simplePatterns = ignoreSpans.filter(isStringOrRegExp);
22+
if (simplePatterns.length && stringMatchesSomePattern(span.description, simplePatterns)) {
23+
return true;
24+
}
25+
26+
// Then we check the more complex patterns, where both parts must match
27+
for (const pattern of ignoreSpans) {
28+
// Have already checked for simple patterns, so we can skip these
29+
if (isStringOrRegExp(pattern) || (!pattern.name && !pattern.op)) {
30+
continue;
31+
}
32+
33+
const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true;
34+
const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true;
35+
36+
if (nameMatches && opMatches) {
37+
return true;
38+
}
39+
}
40+
41+
return false;
42+
}
43+
44+
/**
45+
* Takes a list of spans, and a span that was dropped, and re-parents the child spans of the dropped span to the parent of the dropped span, if possible.
46+
* This mutates the spans array in place!
47+
*/
48+
export function reparentChildSpans(spans: SpanJSON[], dropSpan: SpanJSON): void {
49+
const droppedSpanParentId = dropSpan.parent_span_id;
50+
const droppedSpanId = dropSpan.span_id;
51+
52+
// This should generally not happen, as we do not apply this on root spans
53+
// but to be safe, we just bail in this case
54+
if (!droppedSpanParentId) {
55+
return;
56+
}
57+
58+
for (const span of spans) {
59+
if (span.parent_span_id === droppedSpanId) {
60+
span.parent_span_id = droppedSpanParentId;
61+
}
62+
}
63+
}
64+
65+
function isStringOrRegExp(value: unknown): value is string | RegExp {
66+
return typeof value === 'string' || value instanceof RegExp;
67+
}

packages/core/test/lib/client.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,178 @@ describe('Client', () => {
10471047
expect(capturedEvent.transaction).toEqual(transaction.transaction);
10481048
});
10491049

1050+
test('uses `ignoreSpans` to drop root spans', () => {
1051+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, ignoreSpans: ['root span'] });
1052+
const client = new TestClient(options);
1053+
1054+
const captureExceptionSpy = vi.spyOn(client, 'captureException');
1055+
const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log');
1056+
1057+
const transaction: Event = {
1058+
transaction: 'root span',
1059+
type: 'transaction',
1060+
spans: [
1061+
{
1062+
description: 'first span',
1063+
span_id: '9e15bf99fbe4bc80',
1064+
start_timestamp: 1591603196.637835,
1065+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1066+
data: {},
1067+
},
1068+
{
1069+
description: 'second span',
1070+
span_id: 'aa554c1f506b0783',
1071+
start_timestamp: 1591603196.637835,
1072+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1073+
data: {},
1074+
},
1075+
],
1076+
};
1077+
client.captureEvent(transaction);
1078+
1079+
expect(TestClient.instance!.event).toBeUndefined();
1080+
// This proves that the reason the event didn't send/didn't get set on the test client is not because there was an
1081+
// error, but because the event processor returned `null`
1082+
expect(captureExceptionSpy).not.toBeCalled();
1083+
expect(loggerLogSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.');
1084+
});
1085+
1086+
test('uses `ignoreSpans` to drop child spans', () => {
1087+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, ignoreSpans: ['first span'] });
1088+
const client = new TestClient(options);
1089+
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
1090+
1091+
const transaction: Event = {
1092+
contexts: {
1093+
trace: {
1094+
span_id: 'root-span-id',
1095+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1096+
},
1097+
},
1098+
transaction: 'root span',
1099+
type: 'transaction',
1100+
spans: [
1101+
{
1102+
description: 'first span',
1103+
span_id: '9e15bf99fbe4bc80',
1104+
parent_span_id: 'root-span-id',
1105+
start_timestamp: 1591603196.637835,
1106+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1107+
data: {},
1108+
},
1109+
{
1110+
description: 'second span',
1111+
span_id: 'aa554c1f506b0783',
1112+
parent_span_id: 'root-span-id',
1113+
start_timestamp: 1591603196.637835,
1114+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1115+
data: {},
1116+
},
1117+
{
1118+
description: 'third span',
1119+
span_id: 'aa554c1f506b0783',
1120+
parent_span_id: '9e15bf99fbe4bc80',
1121+
start_timestamp: 1591603196.637835,
1122+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1123+
data: {},
1124+
},
1125+
],
1126+
};
1127+
client.captureEvent(transaction);
1128+
1129+
const capturedEvent = TestClient.instance!.event!;
1130+
expect(capturedEvent.spans).toEqual([
1131+
{
1132+
description: 'second span',
1133+
span_id: 'aa554c1f506b0783',
1134+
parent_span_id: 'root-span-id',
1135+
start_timestamp: 1591603196.637835,
1136+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1137+
data: {},
1138+
},
1139+
{
1140+
description: 'third span',
1141+
span_id: 'aa554c1f506b0783',
1142+
parent_span_id: 'root-span-id',
1143+
start_timestamp: 1591603196.637835,
1144+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1145+
data: {},
1146+
},
1147+
]);
1148+
expect(recordDroppedEventSpy).toBeCalledWith('before_send', 'span', 1);
1149+
});
1150+
1151+
test('uses complex `ignoreSpans` to drop child spans', () => {
1152+
const options = getDefaultTestClientOptions({
1153+
dsn: PUBLIC_DSN,
1154+
ignoreSpans: [
1155+
{
1156+
name: 'first span',
1157+
},
1158+
{
1159+
name: 'span',
1160+
op: 'op1',
1161+
},
1162+
],
1163+
});
1164+
const client = new TestClient(options);
1165+
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
1166+
1167+
const transaction: Event = {
1168+
contexts: {
1169+
trace: {
1170+
span_id: 'root-span-id',
1171+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1172+
},
1173+
},
1174+
transaction: 'root span',
1175+
type: 'transaction',
1176+
spans: [
1177+
{
1178+
description: 'first span',
1179+
span_id: '9e15bf99fbe4bc80',
1180+
parent_span_id: 'root-span-id',
1181+
start_timestamp: 1591603196.637835,
1182+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1183+
data: {},
1184+
},
1185+
{
1186+
description: 'second span',
1187+
op: 'op1',
1188+
span_id: 'aa554c1f506b0783',
1189+
parent_span_id: 'root-span-id',
1190+
start_timestamp: 1591603196.637835,
1191+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1192+
data: {},
1193+
},
1194+
{
1195+
description: 'third span',
1196+
op: 'other op',
1197+
span_id: 'aa554c1f506b0783',
1198+
parent_span_id: '9e15bf99fbe4bc80',
1199+
start_timestamp: 1591603196.637835,
1200+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1201+
data: {},
1202+
},
1203+
],
1204+
};
1205+
client.captureEvent(transaction);
1206+
1207+
const capturedEvent = TestClient.instance!.event!;
1208+
expect(capturedEvent.spans).toEqual([
1209+
{
1210+
description: 'third span',
1211+
op: 'other op',
1212+
span_id: 'aa554c1f506b0783',
1213+
parent_span_id: 'root-span-id',
1214+
start_timestamp: 1591603196.637835,
1215+
trace_id: '86f39e84263a4de99c326acab3bfe3bd',
1216+
data: {},
1217+
},
1218+
]);
1219+
expect(recordDroppedEventSpy).toBeCalledWith('before_send', 'span', 2);
1220+
});
1221+
10501222
test('does not modify existing contexts for root span in `beforeSendSpan`', () => {
10511223
const beforeSendSpan = vi.fn((span: SpanJSON) => {
10521224
return {

0 commit comments

Comments
 (0)