Skip to content

feat(core): Add ignoreSpans option #17078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions dev-packages/e2e-tests/test-applications/webpack-4/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 48 additions & 16 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1281,36 +1282,67 @@ function processBeforeSend(
event: Event,
hint: EventHint,
): PromiseLike<Event | null> | Event | null {
const { beforeSend, beforeSendTransaction, beforeSendSpan } = options;
const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
let processedEvent = event;

if (isErrorEvent(processedEvent) && beforeSend) {
return beforeSend(processedEvent, hint);
}

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 (ignoreSpans?.length && 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 (ignoreSpans?.length && 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;
}
}
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -122,7 +123,17 @@ 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?.length
? 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);
Expand All @@ -138,7 +149,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));
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/types-hoist/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ import type { StackLineParser, StackParser } from './stacktrace';
import type { TracePropagationTargets } from './tracing';
import type { BaseTransportOptions, Transport } from './transport';

/**
* 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<TO extends BaseTransportOptions = BaseTransportOptions> {
/**
* Enable debug functionality in the SDK itself. If `debug` is set to `true` the SDK will attempt
Expand Down Expand Up @@ -208,6 +234,16 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*/
ignoreTransactions?: Array<string | RegExp>;

/**
* A list of span names or patterns to ignore.
*
* If you specify a pattern {@link IgnoreSpanFilter}, at least one
* of the properties (`op` or `name`) must be set.
*
* @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
Expand Down
66 changes: 66 additions & 0 deletions packages/core/src/utils/should-ignore-span.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { ClientOptions } from '../types-hoist/options';
import type { SpanJSON } from '../types-hoist/span';
import { isMatchingPattern } from './string';

/**
* Check if a span should be ignored based on the ignoreSpans configuration.
*/
export function shouldIgnoreSpan(
span: Pick<SpanJSON, 'description' | 'op'>,
ignoreSpans: Required<ClientOptions>['ignoreSpans'],
): boolean {
if (!ignoreSpans?.length || !span.description) {
return false;
}

for (const pattern of ignoreSpans) {
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Span Ignoring Fails for Empty Operations

The shouldIgnoreSpan function fails to correctly evaluate ignoreSpans patterns for spans with an empty op value. The condition span.op && isMatchingPattern(span.op, pattern.op) short-circuits when span.op is an empty string (""), as empty strings are falsy. This prevents isMatchingPattern from being called, causing spans with empty op values to not be ignored even when pattern.op is configured to match them (e.g., /^$/). The check should explicitly distinguish empty strings from undefined (e.g., span.op != null) to ensure proper pattern matching.

Locations (1)
Fix in Cursor Fix in Web


// 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;
}
}

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;
}
Loading
Loading