Skip to content

Commit 77de264

Browse files
committed
feat(core): Add ignoreSpans option
Closes #16820
1 parent 74b680d commit 77de264

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';
@@ -1281,36 +1282,67 @@ function processBeforeSend(
12811282
event: Event,
12821283
hint: EventHint,
12831284
): PromiseLike<Event | null> | Event | null {
1284-
const { beforeSend, beforeSendTransaction, beforeSendSpan } = options;
1285+
const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
12851286
let processedEvent = event;
12861287

12871288
if (isErrorEvent(processedEvent) && beforeSend) {
12881289
return beforeSend(processedEvent, hint);
12891290
}
12901291

12911292
if (isTransactionEvent(processedEvent)) {
1292-
if (beforeSendSpan) {
1293-
// process root span
1294-
const processedRootSpanJson = beforeSendSpan(convertTransactionEventToSpanJson(processedEvent));
1295-
if (!processedRootSpanJson) {
1296-
showSpanDropWarning();
1297-
} else {
1298-
// update event with processed root span values
1299-
processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
1293+
// Avoid processing if we don't have to
1294+
if (beforeSendSpan || ignoreSpans) {
1295+
// 1. Process root span
1296+
const rootSpanJson = convertTransactionEventToSpanJson(processedEvent);
1297+
1298+
// 1.1 If the root span should be ignored, drop the whole transaction
1299+
if (shouldIgnoreSpan(rootSpanJson, ignoreSpans)) {
1300+
// dropping the whole transaction!
1301+
return null;
13001302
}
13011303

1302-
// process child spans
1304+
// 1.2 If a `beforeSendSpan` callback is defined, process the root span
1305+
if (beforeSendSpan) {
1306+
const processedRootSpanJson = beforeSendSpan(rootSpanJson);
1307+
if (!processedRootSpanJson) {
1308+
showSpanDropWarning();
1309+
} else {
1310+
// update event with processed root span values
1311+
processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
1312+
}
1313+
}
1314+
1315+
// 2. Process child spans
13031316
if (processedEvent.spans) {
13041317
const processedSpans: SpanJSON[] = [];
1305-
for (const span of processedEvent.spans) {
1306-
const processedSpan = beforeSendSpan(span);
1307-
if (!processedSpan) {
1308-
showSpanDropWarning();
1309-
processedSpans.push(span);
1318+
1319+
const initialSpans = processedEvent.spans;
1320+
1321+
for (const span of initialSpans) {
1322+
// 2.a If the child span should be ignored, reparent it to the root span
1323+
if (shouldIgnoreSpan(span, ignoreSpans)) {
1324+
reparentChildSpans(initialSpans, span);
1325+
continue;
1326+
}
1327+
1328+
// 2.b If a `beforeSendSpan` callback is defined, process the child span
1329+
if (beforeSendSpan) {
1330+
const processedSpan = beforeSendSpan(span);
1331+
if (!processedSpan) {
1332+
showSpanDropWarning();
1333+
processedSpans.push(span);
1334+
} else {
1335+
processedSpans.push(processedSpan);
1336+
}
13101337
} else {
1311-
processedSpans.push(processedSpan);
1338+
processedSpans.push(span);
13121339
}
13131340
}
1341+
1342+
const droppedSpans = processedEvent.spans.length - processedSpans.length;
1343+
if (droppedSpans) {
1344+
client.recordDroppedEvent('before_send', 'span', droppedSpans);
1345+
}
13141346
processedEvent.spans = processedSpans;
13151347
}
13161348
}

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
@@ -1046,6 +1046,178 @@ describe('Client', () => {
10461046
expect(capturedEvent.transaction).toEqual(transaction.transaction);
10471047
});
10481048

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

0 commit comments

Comments
 (0)