From 0b6338a423855f3d793d00681cee8a0ebf42ccfc Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 18 Jul 2025 12:07:34 +0200 Subject: [PATCH 1/4] feat(core): Add `ignoreSpans` option Closes https://github.com/getsentry/sentry-javascript/issues/16820 --- packages/core/src/client.ts | 64 +++++-- packages/core/src/envelope.ts | 13 +- packages/core/src/types-hoist/options.ts | 12 ++ packages/core/src/utils/should-ignore-span.ts | 67 +++++++ packages/core/test/lib/client.test.ts | 172 ++++++++++++++++++ .../test/lib/utils/should-ignore-span.test.ts | 124 +++++++++++++ 6 files changed, 434 insertions(+), 18 deletions(-) create mode 100644 packages/core/src/utils/should-ignore-span.ts create mode 100644 packages/core/test/lib/utils/should-ignore-span.test.ts diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 8b03392106d9..2b1a96266a4d 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -42,6 +42,7 @@ import { merge } from './utils/merge'; import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; +import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils'; import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils/syncpromise'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; @@ -1281,7 +1282,7 @@ function processBeforeSend( event: Event, hint: EventHint, ): PromiseLike | Event | null { - const { beforeSend, beforeSendTransaction, beforeSendSpan } = options; + const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options; let processedEvent = event; if (isErrorEvent(processedEvent) && beforeSend) { @@ -1289,28 +1290,59 @@ function processBeforeSend( } if (isTransactionEvent(processedEvent)) { - if (beforeSendSpan) { - // process root span - const processedRootSpanJson = beforeSendSpan(convertTransactionEventToSpanJson(processedEvent)); - if (!processedRootSpanJson) { - showSpanDropWarning(); - } else { - // update event with processed root span values - processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson)); + // Avoid processing if we don't have to + if (beforeSendSpan || ignoreSpans) { + // 1. Process root span + const rootSpanJson = convertTransactionEventToSpanJson(processedEvent); + + // 1.1 If the root span should be ignored, drop the whole transaction + if (shouldIgnoreSpan(rootSpanJson, ignoreSpans)) { + // dropping the whole transaction! + return null; } - // process child spans + // 1.2 If a `beforeSendSpan` callback is defined, process the root span + if (beforeSendSpan) { + const processedRootSpanJson = beforeSendSpan(rootSpanJson); + if (!processedRootSpanJson) { + showSpanDropWarning(); + } else { + // update event with processed root span values + processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson)); + } + } + + // 2. Process child spans if (processedEvent.spans) { const processedSpans: SpanJSON[] = []; - for (const span of processedEvent.spans) { - const processedSpan = beforeSendSpan(span); - if (!processedSpan) { - showSpanDropWarning(); - processedSpans.push(span); + + const initialSpans = processedEvent.spans; + + for (const span of initialSpans) { + // 2.a If the child span should be ignored, reparent it to the root span + if (shouldIgnoreSpan(span, ignoreSpans)) { + reparentChildSpans(initialSpans, span); + continue; + } + + // 2.b If a `beforeSendSpan` callback is defined, process the child span + if (beforeSendSpan) { + const processedSpan = beforeSendSpan(span); + if (!processedSpan) { + showSpanDropWarning(); + processedSpans.push(span); + } else { + processedSpans.push(processedSpan); + } } else { - processedSpans.push(processedSpan); + processedSpans.push(span); } } + + const droppedSpans = processedEvent.spans.length - processedSpans.length; + if (droppedSpans) { + client.recordDroppedEvent('before_send', 'span', droppedSpans); + } processedEvent.spans = processedSpans; } } diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index dc094d218812..f01a1c214083 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -26,6 +26,7 @@ import { getSdkMetadataForEnvelopeHeader, } from './utils/envelope'; import { uuid4 } from './utils/misc'; +import { shouldIgnoreSpan } from './utils/should-ignore-span'; import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; /** @@ -122,7 +123,15 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }), }; - const beforeSendSpan = client?.getOptions().beforeSendSpan; + const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {}; + + const filteredSpans = ignoreSpans ? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans)) : spans; + const droppedSpans = spans.length - filteredSpans.length; + + if (droppedSpans) { + client?.recordDroppedEvent('before_send', 'span', droppedSpans); + } + const convertToSpanJSON = beforeSendSpan ? (span: SentrySpan) => { const spanJson = spanToJSON(span); @@ -138,7 +147,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? : spanToJSON; const items: SpanItem[] = []; - for (const span of spans) { + for (const span of filteredSpans) { const spanJson = convertToSpanJSON(span); if (spanJson) { items.push(createSpanEnvelopeItem(spanJson)); diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index ce0851f940a5..81c5261eac86 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -10,6 +10,11 @@ import type { StackLineParser, StackParser } from './stacktrace'; import type { TracePropagationTargets } from './tracing'; import type { BaseTransportOptions, Transport } from './transport'; +interface IgnoreSpanFilter { + name?: string | RegExp; + op?: string | RegExp; +} + export interface ClientOptions { /** * Enable debug functionality in the SDK itself. If `debug` is set to `true` the SDK will attempt @@ -208,6 +213,13 @@ export interface ClientOptions; + /** + * A list of span names or patterns to ignore. + * + * @default [] + */ + ignoreSpans?: (string | RegExp | IgnoreSpanFilter)[]; + /** * A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint * that accepts Sentry envelopes for forwarding. This can be used to force data diff --git a/packages/core/src/utils/should-ignore-span.ts b/packages/core/src/utils/should-ignore-span.ts new file mode 100644 index 000000000000..58040c2ecdd1 --- /dev/null +++ b/packages/core/src/utils/should-ignore-span.ts @@ -0,0 +1,67 @@ +import type { ClientOptions } from '../types-hoist/options'; +import type { SpanJSON } from '../types-hoist/span'; +import { isMatchingPattern, stringMatchesSomePattern } from './string'; + +/** + * Check if a span should be ignored based on the ignoreSpans configuration. + */ +export function shouldIgnoreSpan( + span: Pick, + ignoreSpans: ClientOptions['ignoreSpans'], +): boolean { + if (!ignoreSpans?.length) { + return false; + } + + if (!span.description) { + return false; + } + + // First we check the simple string/regex patterns - if the name matches any of them, we ignore the span + const simplePatterns = ignoreSpans.filter(isStringOrRegExp); + if (simplePatterns.length && stringMatchesSomePattern(span.description, simplePatterns)) { + return true; + } + + // Then we check the more complex patterns, where both parts must match + for (const pattern of ignoreSpans) { + // Have already checked for simple patterns, so we can skip these + if (isStringOrRegExp(pattern) || (!pattern.name && !pattern.op)) { + continue; + } + + const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true; + const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true; + + if (nameMatches && opMatches) { + return true; + } + } + + return false; +} + +/** + * 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. + * This mutates the spans array in place! + */ +export function reparentChildSpans(spans: SpanJSON[], dropSpan: SpanJSON): void { + const droppedSpanParentId = dropSpan.parent_span_id; + const droppedSpanId = dropSpan.span_id; + + // This should generally not happen, as we do not apply this on root spans + // but to be safe, we just bail in this case + if (!droppedSpanParentId) { + return; + } + + for (const span of spans) { + if (span.parent_span_id === droppedSpanId) { + span.parent_span_id = droppedSpanParentId; + } + } +} + +function isStringOrRegExp(value: unknown): value is string | RegExp { + return typeof value === 'string' || value instanceof RegExp; +} diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 4b1c7a378114..0d4c0dfae8cf 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -1046,6 +1046,178 @@ describe('Client', () => { expect(capturedEvent.transaction).toEqual(transaction.transaction); }); + test('uses `ignoreSpans` to drop root spans', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, ignoreSpans: ['root span'] }); + const client = new TestClient(options); + + const captureExceptionSpy = vi.spyOn(client, 'captureException'); + const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log'); + + const transaction: Event = { + transaction: 'root span', + type: 'transaction', + spans: [ + { + description: 'first span', + span_id: '9e15bf99fbe4bc80', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + { + description: 'second span', + span_id: 'aa554c1f506b0783', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + ], + }; + client.captureEvent(transaction); + + expect(TestClient.instance!.event).toBeUndefined(); + // This proves that the reason the event didn't send/didn't get set on the test client is not because there was an + // error, but because the event processor returned `null` + expect(captureExceptionSpy).not.toBeCalled(); + expect(loggerLogSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.'); + }); + + test('uses `ignoreSpans` to drop child spans', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, ignoreSpans: ['first span'] }); + const client = new TestClient(options); + const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent'); + + const transaction: Event = { + contexts: { + trace: { + span_id: 'root-span-id', + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + }, + }, + transaction: 'root span', + type: 'transaction', + spans: [ + { + description: 'first span', + span_id: '9e15bf99fbe4bc80', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + { + description: 'second span', + span_id: 'aa554c1f506b0783', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + { + description: 'third span', + span_id: 'aa554c1f506b0783', + parent_span_id: '9e15bf99fbe4bc80', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + ], + }; + client.captureEvent(transaction); + + const capturedEvent = TestClient.instance!.event!; + expect(capturedEvent.spans).toEqual([ + { + description: 'second span', + span_id: 'aa554c1f506b0783', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + { + description: 'third span', + span_id: 'aa554c1f506b0783', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + ]); + expect(recordDroppedEventSpy).toBeCalledWith('before_send', 'span', 1); + }); + + test('uses complex `ignoreSpans` to drop child spans', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + ignoreSpans: [ + { + name: 'first span', + }, + { + name: 'span', + op: 'op1', + }, + ], + }); + const client = new TestClient(options); + const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent'); + + const transaction: Event = { + contexts: { + trace: { + span_id: 'root-span-id', + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + }, + }, + transaction: 'root span', + type: 'transaction', + spans: [ + { + description: 'first span', + span_id: '9e15bf99fbe4bc80', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + { + description: 'second span', + op: 'op1', + span_id: 'aa554c1f506b0783', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + { + description: 'third span', + op: 'other op', + span_id: 'aa554c1f506b0783', + parent_span_id: '9e15bf99fbe4bc80', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + ], + }; + client.captureEvent(transaction); + + const capturedEvent = TestClient.instance!.event!; + expect(capturedEvent.spans).toEqual([ + { + description: 'third span', + op: 'other op', + span_id: 'aa554c1f506b0783', + parent_span_id: 'root-span-id', + start_timestamp: 1591603196.637835, + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + data: {}, + }, + ]); + expect(recordDroppedEventSpy).toBeCalledWith('before_send', 'span', 2); + }); + test('does not modify existing contexts for root span in `beforeSendSpan`', () => { const beforeSendSpan = vi.fn((span: SpanJSON) => { return { diff --git a/packages/core/test/lib/utils/should-ignore-span.test.ts b/packages/core/test/lib/utils/should-ignore-span.test.ts new file mode 100644 index 000000000000..434cb8911518 --- /dev/null +++ b/packages/core/test/lib/utils/should-ignore-span.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; +import type { ClientOptions, SpanJSON } from '../../../src'; +import { reparentChildSpans, shouldIgnoreSpan } from '../../../src/utils/should-ignore-span'; + +describe('shouldIgnoreSpan', () => { + it('should not ignore spans with empty ignoreSpans', () => { + const span = { description: 'test', op: 'test' }; + const ignoreSpans = [] as ClientOptions['ignoreSpans']; + expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(false); + }); + + describe('string patterns', () => { + it.each([ + ['test', 'test', true], + ['test', 'test2', false], + ['test2', 'test', true], + ])('should ignore spans with description %s & ignoreSpans=%s', (description, ignoreSpansPattern, expected) => { + const span = { description, op: 'default' }; + const ignoreSpans = [ignoreSpansPattern]; + expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(expected); + }); + }); + + describe('regex patterns', () => { + it.each([ + ['test', /test/, true], + ['test', /test2/, false], + ['test2', /test/, true], + ])('should ignore spans with description %s & ignoreSpans=%s', (description, ignoreSpansPattern, expected) => { + const span = { description, op: 'default' }; + const ignoreSpans = [ignoreSpansPattern]; + expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(expected); + }); + }); + + describe('complex patterns', () => { + it.each([ + [{ name: 'test' }, true], + [{ name: 'test2' }, false], + [{ op: 'test' }, true], + [{ op: 'test2' }, false], + [{ name: 'test', op: 'test' }, true], + [{ name: 'test2', op: 'test' }, false], + [{ name: 'test', op: 'test2' }, false], + [{ name: 'test2', op: 'test2' }, false], + [{ name: 'test', op: 'test2' }, false], + ])('should ignore spans with description %s & ignoreSpans=%s', (ignoreSpansPattern, expected) => { + const span = { description: 'test span name', op: 'test span op' }; + const ignoreSpans = [ignoreSpansPattern]; + expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(expected); + }); + }); + + it('works with multiple patterns', () => { + const ignoreSpans = ['test', /test2/, { op: 'test2' }]; + + // All of these are ignored because the name matches + const span1 = { description: 'test span name', op: 'test span op' }; + const span2 = { description: 'test span name2', op: 'test span op2' }; + const span3 = { description: 'test span name3', op: 'test span op3' }; + const span4 = { description: 'test span name4', op: 'test span op4' }; + + expect(shouldIgnoreSpan(span1, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan(span2, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan(span3, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan(span4, ignoreSpans)).toBe(true); + + // All of these are ignored because the op matches + const span5 = { description: 'custom 1', op: 'test2' }; + const span6 = { description: 'custom 2', op: 'test2' }; + const span7 = { description: 'custom 3', op: 'test2' }; + const span8 = { description: 'custom 4', op: 'test2' }; + + expect(shouldIgnoreSpan(span5, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan(span6, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan(span7, ignoreSpans)).toBe(true); + expect(shouldIgnoreSpan(span8, ignoreSpans)).toBe(true); + + // None of these are ignored because the name and op don't match + const span9 = { description: 'custom 5', op: 'test' }; + const span10 = { description: 'custom 6', op: 'test' }; + const span11 = { description: 'custom 7', op: 'test' }; + const span12 = { description: 'custom 8', op: 'test' }; + + expect(shouldIgnoreSpan(span9, ignoreSpans)).toBe(false); + expect(shouldIgnoreSpan(span10, ignoreSpans)).toBe(false); + expect(shouldIgnoreSpan(span11, ignoreSpans)).toBe(false); + expect(shouldIgnoreSpan(span12, ignoreSpans)).toBe(false); + }); +}); + +describe('reparentChildSpans', () => { + it('should ignore dropped root spans', () => { + const span1 = { span_id: '1' } as SpanJSON; + const span2 = { span_id: '2', parent_span_id: '1' } as SpanJSON; + const span3 = { span_id: '3', parent_span_id: '2' } as SpanJSON; + + const spans = [span1, span2, span3]; + + reparentChildSpans(spans, span1); + + expect(spans).toEqual([span1, span2, span3]); + expect(span1.parent_span_id).toBeUndefined(); + expect(span2.parent_span_id).toBe('1'); + expect(span3.parent_span_id).toBe('2'); + }); + + it('should reparent child spans of the dropped span', () => { + const span1 = { span_id: '1' } as SpanJSON; + const span2 = { span_id: '2', parent_span_id: '1' } as SpanJSON; + const span3 = { span_id: '3', parent_span_id: '2' } as SpanJSON; + const span4 = { span_id: '4', parent_span_id: '3' } as SpanJSON; + + const spans = [span1, span2, span3, span4]; + + reparentChildSpans(spans, span2); + + expect(spans).toEqual([span1, span2, span3, span4]); + expect(span1.parent_span_id).toBeUndefined(); + expect(span2.parent_span_id).toBe('1'); + expect(span3.parent_span_id).toBe('1'); + expect(span4.parent_span_id).toBe('3'); + }); +}); From 1c505295dd1c699964a97fcc1384fdea2fe640cd Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 18 Jul 2025 13:25:20 +0200 Subject: [PATCH 2/4] fix tests --- .../e2e-tests/test-applications/webpack-4/build.mjs | 5 +++++ packages/core/test/lib/client.test.ts | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs index 0818243ad9ee..32d475aac75c 100644 --- a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs +++ b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs @@ -17,6 +17,11 @@ webpack( minimize: true, minimizer: [new TerserPlugin()], }, + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000 + }, plugins: [new HtmlWebpackPlugin(), new webpack.EnvironmentPlugin(['E2E_TEST_DSN'])], mode: 'production', // webpack 4 does not support ES2020 features out of the box, so we need to transpile them diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 0d4c0dfae8cf..3cae5ed65787 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -1115,7 +1115,7 @@ describe('Client', () => { }, { description: 'third span', - span_id: 'aa554c1f506b0783', + span_id: 'aa554c1f506b0784', parent_span_id: '9e15bf99fbe4bc80', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', @@ -1137,7 +1137,7 @@ describe('Client', () => { }, { description: 'third span', - span_id: 'aa554c1f506b0783', + span_id: 'aa554c1f506b0784', parent_span_id: 'root-span-id', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', @@ -1193,7 +1193,7 @@ describe('Client', () => { { description: 'third span', op: 'other op', - span_id: 'aa554c1f506b0783', + span_id: 'aa554c1f506b0784', parent_span_id: '9e15bf99fbe4bc80', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', @@ -1208,7 +1208,7 @@ describe('Client', () => { { description: 'third span', op: 'other op', - span_id: 'aa554c1f506b0783', + span_id: 'aa554c1f506b0784', parent_span_id: 'root-span-id', start_timestamp: 1591603196.637835, trace_id: '86f39e84263a4de99c326acab3bfe3bd', From 520816192e3ea879d6ecf8e97f02feac9064f448 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 30 Jul 2025 15:23:21 +0200 Subject: [PATCH 3/4] union type options object, improve filtering performance --- packages/core/src/client.ts | 4 +-- packages/core/src/envelope.ts | 4 ++- packages/core/src/types-hoist/options.ts | 32 ++++++++++++++++--- packages/core/src/utils/should-ignore-span.ts | 31 +++++++++--------- .../test/lib/utils/should-ignore-span.test.ts | 2 +- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 2b1a96266a4d..3f1105de5c15 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1296,7 +1296,7 @@ function processBeforeSend( const rootSpanJson = convertTransactionEventToSpanJson(processedEvent); // 1.1 If the root span should be ignored, drop the whole transaction - if (shouldIgnoreSpan(rootSpanJson, ignoreSpans)) { + if (ignoreSpans?.length && shouldIgnoreSpan(rootSpanJson, ignoreSpans)) { // dropping the whole transaction! return null; } @@ -1320,7 +1320,7 @@ function processBeforeSend( for (const span of initialSpans) { // 2.a If the child span should be ignored, reparent it to the root span - if (shouldIgnoreSpan(span, ignoreSpans)) { + if (ignoreSpans?.length && shouldIgnoreSpan(span, ignoreSpans)) { reparentChildSpans(initialSpans, span); continue; } diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index f01a1c214083..6ae8fa718638 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -125,7 +125,9 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {}; - const filteredSpans = ignoreSpans ? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans)) : spans; + const filteredSpans = ignoreSpans?.length + ? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans)) + : spans; const droppedSpans = spans.length - filteredSpans.length; if (droppedSpans) { diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 81c5261eac86..142313b76c25 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -10,10 +10,31 @@ import type { StackLineParser, StackParser } from './stacktrace'; import type { TracePropagationTargets } from './tracing'; import type { BaseTransportOptions, Transport } from './transport'; -interface IgnoreSpanFilter { - name?: string | RegExp; - op?: string | RegExp; -} +/** + * A filter object for ignoring spans. + * At least one of the properties (`op` or `name`) must be set. + */ +type IgnoreSpanFilter = + | { + /** + * Spans with a name matching this pattern will be ignored. + */ + name: string | RegExp; + /** + * Spans with an op matching this pattern will be ignored. + */ + op?: string | RegExp; + } + | { + /** + * Spans with a name matching this pattern will be ignored. + */ + name?: string | RegExp; + /** + * Spans with an op matching this pattern will be ignored. + */ + op: string | RegExp; + }; export interface ClientOptions { /** @@ -216,6 +237,9 @@ export interface ClientOptions, - ignoreSpans: ClientOptions['ignoreSpans'], + ignoreSpans: Required['ignoreSpans'], ): boolean { - if (!ignoreSpans?.length) { + if (!ignoreSpans?.length || !span.description) { return false; } - if (!span.description) { - return false; - } - - // First we check the simple string/regex patterns - if the name matches any of them, we ignore the span - const simplePatterns = ignoreSpans.filter(isStringOrRegExp); - if (simplePatterns.length && stringMatchesSomePattern(span.description, simplePatterns)) { - return true; - } - - // Then we check the more complex patterns, where both parts must match for (const pattern of ignoreSpans) { - // Have already checked for simple patterns, so we can skip these - if (isStringOrRegExp(pattern) || (!pattern.name && !pattern.op)) { + if (isStringOrRegExp(pattern)) { + if (isMatchingPattern(span.description, pattern)) { + return true; + } + continue; + } + + if (!pattern.name && !pattern.op) { continue; } const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true; const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true; + // This check here is only correct because we can guarantee that we ran `isMatchingPattern` + // for at least one of `nameMatches` and `opMatches`. So in contrary to how this looks, + // not both op and name actually have to match. This is the most efficient way to check + // for all combinations of name and op patterns. if (nameMatches && opMatches) { return true; } diff --git a/packages/core/test/lib/utils/should-ignore-span.test.ts b/packages/core/test/lib/utils/should-ignore-span.test.ts index 434cb8911518..92dc0a1435ee 100644 --- a/packages/core/test/lib/utils/should-ignore-span.test.ts +++ b/packages/core/test/lib/utils/should-ignore-span.test.ts @@ -5,7 +5,7 @@ import { reparentChildSpans, shouldIgnoreSpan } from '../../../src/utils/should- describe('shouldIgnoreSpan', () => { it('should not ignore spans with empty ignoreSpans', () => { const span = { description: 'test', op: 'test' }; - const ignoreSpans = [] as ClientOptions['ignoreSpans']; + const ignoreSpans = [] as Required['ignoreSpans']; expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(false); }); From 415714fda7c1116ce01d67fdcc18964bdcde0eb7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 30 Jul 2025 15:45:14 +0200 Subject: [PATCH 4/4] size increase --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index a46d905fe89f..d53eaae56712 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -233,7 +233,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '146 KB', + limit: '147 KB', }, { name: '@sentry/node - without tracing',