Skip to content

Commit bbd8a82

Browse files
committed
implement
1 parent dbf1af1 commit bbd8a82

8 files changed

Lines changed: 150 additions & 18 deletions

File tree

packages/core/src/client.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,7 +1600,13 @@ function processBeforeSend(
16001600
const rootSpanJson = convertTransactionEventToSpanJson(processedEvent);
16011601

16021602
// 1.1 If the root span should be ignored, drop the whole transaction
1603-
if (ignoreSpans?.length && shouldIgnoreSpan(rootSpanJson, ignoreSpans)) {
1603+
if (
1604+
ignoreSpans?.length &&
1605+
shouldIgnoreSpan(
1606+
{ description: rootSpanJson.description, op: rootSpanJson.op, attributes: rootSpanJson.data },
1607+
ignoreSpans,
1608+
)
1609+
) {
16041610
// dropping the whole transaction!
16051611
return null;
16061612
}
@@ -1624,7 +1630,10 @@ function processBeforeSend(
16241630

16251631
for (const span of initialSpans) {
16261632
// 2.a If the child span should be ignored, reparent it to the root span
1627-
if (ignoreSpans?.length && shouldIgnoreSpan(span, ignoreSpans)) {
1633+
if (
1634+
ignoreSpans?.length &&
1635+
shouldIgnoreSpan({ description: span.description, op: span.op, attributes: span.data }, ignoreSpans)
1636+
) {
16281637
reparentChildSpans(initialSpans, span);
16291638
continue;
16301639
}

packages/core/src/envelope.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
142142
const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {};
143143

144144
const filteredSpans = ignoreSpans?.length
145-
? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans))
145+
? spans.filter(span => {
146+
const json = spanToJSON(span);
147+
return !shouldIgnoreSpan({ description: json.description, op: json.op, attributes: json.data }, ignoreSpans);
148+
})
146149
: spans;
147150
const droppedSpans = spans.length - filteredSpans.length;
148151

packages/core/src/tracing/idleSpan.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,13 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
179179
// Ignored spans will get dropped later (in the client) but since we already adjust
180180
// the idle span end timestamp here, we can already take to-be-ignored spans out of
181181
// the calculation here.
182-
if (ignoreSpans && shouldIgnoreSpan(currentSpanJson, ignoreSpans)) {
182+
if (
183+
ignoreSpans &&
184+
shouldIgnoreSpan(
185+
{ description: currentSpanJson.description, op: currentSpanJson.op, attributes: currentSpanJson.data },
186+
ignoreSpans,
187+
)
188+
) {
183189
return acc;
184190
}
185191
return acc ? Math.max(acc, currentSpanJson.timestamp) : currentSpanJson.timestamp;

packages/core/src/tracing/trace.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ function _shouldIgnoreStreamedSpan(client: Client | undefined, spanArguments: Se
610610
{
611611
description: spanArguments.name || '',
612612
op: spanArguments.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] || spanArguments.op,
613+
attributes: spanArguments.attributes,
613614
},
614615
ignoreSpans,
615616
);

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,17 @@ export interface ServerRuntimeOptions {
100100
onFatalError?(this: void, error: Error): void;
101101
}
102102

103+
/**
104+
* Allowed attribute value matchers in `ignoreSpans` filters.
105+
* String span attributes use pattern matching (substring or RegExp).
106+
* Non-string attribute values match by strict equality (arrays element-wise).
107+
*/
108+
export type IgnoreSpanAttributeValue = string | boolean | number | string[] | boolean[] | number[] | RegExp;
109+
103110
/**
104111
* A filter object for ignoring spans.
105-
* At least one of the properties (`op` or `name`) must be set.
112+
* At least one of the properties (`name`, `op`, or `attributes`) must be set.
113+
* If multiple are set, all must match for the span to be ignored.
106114
*/
107115
type IgnoreSpanFilter =
108116
| {
@@ -114,6 +122,12 @@ type IgnoreSpanFilter =
114122
* Spans with an op matching this pattern will be ignored.
115123
*/
116124
op?: string | RegExp;
125+
/**
126+
* Spans whose attributes ALL match the corresponding entries will be ignored.
127+
* String attribute values are matched as patterns (substring or RegExp).
128+
* Non-string values match by strict equality (arrays element-wise).
129+
*/
130+
attributes?: Record<string, IgnoreSpanAttributeValue>;
117131
}
118132
| {
119133
/**
@@ -124,6 +138,28 @@ type IgnoreSpanFilter =
124138
* Spans with an op matching this pattern will be ignored.
125139
*/
126140
op: string | RegExp;
141+
/**
142+
* Spans whose attributes ALL match the corresponding entries will be ignored.
143+
* String attribute values are matched as patterns (substring or RegExp).
144+
* Non-string values match by strict equality (arrays element-wise).
145+
*/
146+
attributes?: Record<string, IgnoreSpanAttributeValue>;
147+
}
148+
| {
149+
/**
150+
* Spans with a name matching this pattern will be ignored.
151+
*/
152+
name?: string | RegExp;
153+
/**
154+
* Spans with an op matching this pattern will be ignored.
155+
*/
156+
op?: string | RegExp;
157+
/**
158+
* Spans whose attributes ALL match the corresponding entries will be ignored.
159+
* String attribute values are matched as patterns (substring or RegExp).
160+
* Non-string values match by strict equality (arrays element-wise).
161+
*/
162+
attributes: Record<string, IgnoreSpanAttributeValue>;
127163
};
128164

129165
export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOptions> {
@@ -326,7 +362,8 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
326362
* A list of span names or patterns to ignore.
327363
*
328364
* If you specify a pattern {@link IgnoreSpanFilter}, at least one
329-
* of the properties (`op` or `name`) must be set.
365+
* of the properties (`name`, `op`, or `attributes`) must be set.
366+
* When multiple properties are set, all must match for the span to be ignored.
330367
*
331368
* @default []
332369
*/

packages/core/src/utils/should-ignore-span.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DEBUG_BUILD } from '../debug-build';
2-
import type { ClientOptions } from '../types-hoist/options';
2+
import type { ClientOptions, IgnoreSpanAttributeValue } from '../types-hoist/options';
33
import type { SpanJSON } from '../types-hoist/span';
44
import { debug } from './debug-logger';
55
import { isMatchingPattern } from './string';
@@ -12,34 +12,39 @@ function logIgnoredSpan(droppedSpan: Pick<SpanJSON, 'description' | 'op'>): void
1212
* Check if a span should be ignored based on the ignoreSpans configuration.
1313
*/
1414
export function shouldIgnoreSpan(
15-
span: Pick<SpanJSON, 'description' | 'op'>,
15+
span: Pick<SpanJSON, 'description' | 'op'> & { attributes?: Record<string, unknown> },
1616
ignoreSpans: Required<ClientOptions>['ignoreSpans'],
1717
): boolean {
18-
if (!ignoreSpans?.length || !span.description) {
18+
if (!ignoreSpans?.length) {
1919
return false;
2020
}
2121

2222
for (const pattern of ignoreSpans) {
2323
if (isStringOrRegExp(pattern)) {
24-
if (isMatchingPattern(span.description, pattern)) {
24+
if (span.description && isMatchingPattern(span.description, pattern)) {
2525
DEBUG_BUILD && logIgnoredSpan(span);
2626
return true;
2727
}
2828
continue;
2929
}
3030

31-
if (!pattern.name && !pattern.op) {
31+
if (!pattern.name && !pattern.op && !pattern.attributes) {
3232
continue;
3333
}
3434

35-
const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true;
35+
const nameMatches = pattern.name ? span.description && isMatchingPattern(span.description, pattern.name) : true;
3636
const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true;
37+
const attrsMatch = pattern.attributes
38+
? Object.entries(pattern.attributes).every(([key, valuePattern]) =>
39+
_matchesAttributeValue(span.attributes?.[key], valuePattern),
40+
)
41+
: true;
3742

3843
// This check here is only correct because we can guarantee that we ran `isMatchingPattern`
39-
// for at least one of `nameMatches` and `opMatches`. So in contrary to how this looks,
40-
// not both op and name actually have to match. This is the most efficient way to check
41-
// for all combinations of name and op patterns.
42-
if (nameMatches && opMatches) {
44+
// for at least one of `nameMatches`, `opMatches`, or `attrsMatch`. So in contrary to how this looks,
45+
// not all of op, name, and attributes actually have to match. This is the most efficient way to check
46+
// for all combinations of name, op, and attribute patterns.
47+
if (nameMatches && opMatches && attrsMatch) {
4348
DEBUG_BUILD && logIgnoredSpan(span);
4449
return true;
4550
}
@@ -48,6 +53,19 @@ export function shouldIgnoreSpan(
4853
return false;
4954
}
5055

56+
function _matchesAttributeValue(actual: unknown, pat: IgnoreSpanAttributeValue): boolean {
57+
// String values support pattern matching
58+
if (typeof actual === 'string' && (typeof pat === 'string' || pat instanceof RegExp)) {
59+
return isMatchingPattern(actual, pat);
60+
}
61+
// Arrays: element-wise strict equality
62+
if (Array.isArray(actual) && Array.isArray(pat)) {
63+
return actual.length === pat.length && actual.every((v, i) => v === pat[i]);
64+
}
65+
// Primitives: strict equality
66+
return actual === pat;
67+
}
68+
5169
/**
5270
* 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.
5371
* This mutates the spans array in place!

packages/core/test/lib/utils/should-ignore-span.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,56 @@ describe('shouldIgnoreSpan', () => {
103103
expect(shouldIgnoreSpan({ description: 'GET /health', op: 'http.server' }, [{ op: 'http.server' }])).toBe(true);
104104
});
105105

106+
describe('attribute matching', () => {
107+
it.each([
108+
// strings: pattern matching (substring + regex)
109+
['GET', 'GE', true],
110+
['GET', 'POST', false],
111+
['GET', /^GET$/, true],
112+
['GET', /^POST$/, false],
113+
// numbers: strict equality
114+
[200, 200, true],
115+
[404, 200, false],
116+
// booleans: strict equality
117+
[true, true, true],
118+
[true, false, false],
119+
// no type coercion across primitive types
120+
[true, 'true', false],
121+
// arrays: element-wise strict equality (one positive per element type, plus mismatch shapes)
122+
[['a', 'b'], ['a', 'b'], true],
123+
[['a', 'b'], ['a', 'c'], false],
124+
[['a', 'b'], ['a'], false],
125+
[[1, 2], [1, 2], true],
126+
[[true, false], [true, false], true],
127+
])('matches attribute value %j against pattern %j → %s', (actual, pattern, expected) => {
128+
const span = { description: 'span', op: 'op', attributes: { x: actual } };
129+
expect(shouldIgnoreSpan(span, [{ attributes: { x: pattern } }])).toBe(expected);
130+
});
131+
132+
it('does not match when the attribute key is absent on the span', () => {
133+
const span = { description: 'span', op: 'op', attributes: {} };
134+
expect(shouldIgnoreSpan(span, [{ attributes: { 'missing.key': 'x' } }])).toBe(false);
135+
});
136+
137+
it('requires every attribute entry to match', () => {
138+
const span = { description: 'span', op: 'op', attributes: { a: 1, b: 2 } };
139+
expect(shouldIgnoreSpan(span, [{ attributes: { a: 1, b: 2 } }])).toBe(true);
140+
expect(shouldIgnoreSpan(span, [{ attributes: { a: 1, b: 3 } }])).toBe(false);
141+
});
142+
143+
it('requires both name and attributes to match', () => {
144+
const span = { description: 'GET /healthz', op: 'http.server', attributes: { 'http.method': 'GET' } };
145+
expect(shouldIgnoreSpan(span, [{ name: /healthz?/, attributes: { 'http.method': 'GET' } }])).toBe(true);
146+
expect(shouldIgnoreSpan(span, [{ name: /healthz?/, attributes: { 'http.method': 'POST' } }])).toBe(false);
147+
expect(shouldIgnoreSpan(span, [{ name: /other/, attributes: { 'http.method': 'GET' } }])).toBe(false);
148+
});
149+
150+
it('still matches an attribute-only filter on a span without a description', () => {
151+
const span = { description: undefined as unknown as string, op: undefined, attributes: { foo: 'bar' } };
152+
expect(shouldIgnoreSpan(span, [{ attributes: { foo: 'bar' } }])).toBe(true);
153+
});
154+
});
155+
106156
it('emits a debug log when a span is ignored', () => {
107157
const debugLogSpy = vi.spyOn(debug, 'log');
108158
const span = { description: 'testDescription', op: 'testOp' };

packages/opentelemetry/src/sampler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ export class SentrySampler implements Sampler {
9696
const { description: inferredChildName, op: childOp } = inferSpanData(spanName, spanAttributes, spanKind);
9797
if (
9898
shouldIgnoreSpan(
99-
{ description: inferredChildName, op: spanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] ?? childOp },
99+
{
100+
description: inferredChildName,
101+
op: spanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] ?? childOp,
102+
attributes: spanAttributes,
103+
},
100104
ignoreSpans,
101105
)
102106
) {
@@ -144,7 +148,11 @@ export class SentrySampler implements Sampler {
144148
this._isSpanStreaming &&
145149
ignoreSpans?.length &&
146150
shouldIgnoreSpan(
147-
{ description: inferredSpanName, op: mergedAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] ?? op },
151+
{
152+
description: inferredSpanName,
153+
op: mergedAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] ?? op,
154+
attributes: mergedAttributes,
155+
},
148156
ignoreSpans,
149157
)
150158
) {

0 commit comments

Comments
 (0)