Skip to content

Commit 97f67fc

Browse files
mydeaLms24
andauthored
feat(core): Add ignoreSpans option (#17078)
This adds a new `ignoreSpans` option to all SDKs which can be used as follows: ```js Sentry.init({ ignoreSpans: ['partial match', /regex/, { name: 'span name', op: 'http.client' }] }); ``` this will drop spans before they are sent. Eventual child spans in the same envelope will be re-parented, if possible. Closes #16820 --------- Co-authored-by: Lukas Stracke <[email protected]>
1 parent 815fc27 commit 97f67fc

File tree

7 files changed

+464
-18
lines changed

7 files changed

+464
-18
lines changed

dev-packages/e2e-tests/test-applications/webpack-4/build.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ webpack(
1717
minimize: true,
1818
minimizer: [new TerserPlugin()],
1919
},
20+
performance: {
21+
hints: false,
22+
maxEntrypointSize: 512000,
23+
maxAssetSize: 512000
24+
},
2025
plugins: [new HtmlWebpackPlugin(), new webpack.EnvironmentPlugin(['E2E_TEST_DSN'])],
2126
mode: 'production',
2227
// webpack 4 does not support ES2020 features out of the box, so we need to transpile them

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 (ignoreSpans?.length && 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 (ignoreSpans?.length && 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: 13 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,17 @@ 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?.length
129+
? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans))
130+
: spans;
131+
const droppedSpans = spans.length - filteredSpans.length;
132+
133+
if (droppedSpans) {
134+
client?.recordDroppedEvent('before_send', 'span', droppedSpans);
135+
}
136+
126137
const convertToSpanJSON = beforeSendSpan
127138
? (span: SentrySpan) => {
128139
const spanJson = spanToJSON(span);
@@ -138,7 +149,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
138149
: spanToJSON;
139150

140151
const items: SpanItem[] = [];
141-
for (const span of spans) {
152+
for (const span of filteredSpans) {
142153
const spanJson = convertToSpanJSON(span);
143154
if (spanJson) {
144155
items.push(createSpanEnvelopeItem(spanJson));

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

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

13+
/**
14+
* A filter object for ignoring spans.
15+
* At least one of the properties (`op` or `name`) must be set.
16+
*/
17+
type IgnoreSpanFilter =
18+
| {
19+
/**
20+
* Spans with a name matching this pattern will be ignored.
21+
*/
22+
name: string | RegExp;
23+
/**
24+
* Spans with an op matching this pattern will be ignored.
25+
*/
26+
op?: string | RegExp;
27+
}
28+
| {
29+
/**
30+
* Spans with a name matching this pattern will be ignored.
31+
*/
32+
name?: string | RegExp;
33+
/**
34+
* Spans with an op matching this pattern will be ignored.
35+
*/
36+
op: string | RegExp;
37+
};
38+
1339
export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOptions> {
1440
/**
1541
* Enable debug functionality in the SDK itself. If `debug` is set to `true` the SDK will attempt
@@ -208,6 +234,16 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
208234
*/
209235
ignoreTransactions?: Array<string | RegExp>;
210236

237+
/**
238+
* A list of span names or patterns to ignore.
239+
*
240+
* If you specify a pattern {@link IgnoreSpanFilter}, at least one
241+
* of the properties (`op` or `name`) must be set.
242+
*
243+
* @default []
244+
*/
245+
ignoreSpans?: (string | RegExp | IgnoreSpanFilter)[];
246+
211247
/**
212248
* A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint
213249
* that accepts Sentry envelopes for forwarding. This can be used to force data
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { ClientOptions } from '../types-hoist/options';
2+
import type { SpanJSON } from '../types-hoist/span';
3+
import { isMatchingPattern } 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: Required<ClientOptions>['ignoreSpans'],
11+
): boolean {
12+
if (!ignoreSpans?.length || !span.description) {
13+
return false;
14+
}
15+
16+
for (const pattern of ignoreSpans) {
17+
if (isStringOrRegExp(pattern)) {
18+
if (isMatchingPattern(span.description, pattern)) {
19+
return true;
20+
}
21+
continue;
22+
}
23+
24+
if (!pattern.name && !pattern.op) {
25+
continue;
26+
}
27+
28+
const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true;
29+
const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true;
30+
31+
// This check here is only correct because we can guarantee that we ran `isMatchingPattern`
32+
// for at least one of `nameMatches` and `opMatches`. So in contrary to how this looks,
33+
// not both op and name actually have to match. This is the most efficient way to check
34+
// for all combinations of name and op patterns.
35+
if (nameMatches && opMatches) {
36+
return true;
37+
}
38+
}
39+
40+
return false;
41+
}
42+
43+
/**
44+
* 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.
45+
* This mutates the spans array in place!
46+
*/
47+
export function reparentChildSpans(spans: SpanJSON[], dropSpan: SpanJSON): void {
48+
const droppedSpanParentId = dropSpan.parent_span_id;
49+
const droppedSpanId = dropSpan.span_id;
50+
51+
// This should generally not happen, as we do not apply this on root spans
52+
// but to be safe, we just bail in this case
53+
if (!droppedSpanParentId) {
54+
return;
55+
}
56+
57+
for (const span of spans) {
58+
if (span.parent_span_id === droppedSpanId) {
59+
span.parent_span_id = droppedSpanParentId;
60+
}
61+
}
62+
}
63+
64+
function isStringOrRegExp(value: unknown): value is string | RegExp {
65+
return typeof value === 'string' || value instanceof RegExp;
66+
}

0 commit comments

Comments
 (0)