Skip to content

Commit 20590fe

Browse files
committed
apply ignorespans, improve beforesendspan, handle segment span being dropped
1 parent 2d2eb10 commit 20590fe

File tree

4 files changed

+148
-52
lines changed

4 files changed

+148
-52
lines changed
Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import type { IntegrationFn, Span, SpanV2JSON } from '@sentry/core';
1+
import type { Client, IntegrationFn, Span, SpanV2JSON } from '@sentry/core';
22
import {
33
createSpanV2Envelope,
44
debug,
55
defineIntegration,
66
getDynamicSamplingContextFromSpan,
7+
getRootSpan as getSegmentSpan,
78
isV2BeforeSendSpanCallback,
9+
reparentChildSpans,
10+
shouldIgnoreSpan,
11+
showSpanDropWarning,
812
spanToV2JSON,
913
} from '@sentry/core';
1014
import { DEBUG_BUILD } from '../debug-build';
@@ -13,7 +17,7 @@ export interface SpanStreamingOptions {
1317
batchLimit: number;
1418
}
1519

16-
const _spanStreamingIntegration = ((userOptions?: Partial<SpanStreamingOptions>) => {
20+
export const spanStreamingIntegration = defineIntegration(((userOptions?: Partial<SpanStreamingOptions>) => {
1721
const validatedUserProvidedBatchLimit =
1822
userOptions?.batchLimit && userOptions.batchLimit <= 1000 && userOptions.batchLimit >= 1
1923
? userOptions.batchLimit
@@ -31,7 +35,8 @@ const _spanStreamingIntegration = ((userOptions?: Partial<SpanStreamingOptions>)
3135
...userOptions,
3236
};
3337

34-
const traceMap = new Map<string, Set<Span>>();
38+
// key: traceId-segmentSpanId
39+
const spanTreeMap = new Map<string, Set<Span>>();
3540

3641
return {
3742
name: 'SpanStreaming',
@@ -54,57 +59,118 @@ const _spanStreamingIntegration = ((userOptions?: Partial<SpanStreamingOptions>)
5459
}
5560

5661
client.on('spanEnd', span => {
57-
const spanBuffer = traceMap.get(span.spanContext().traceId);
62+
const spanTreeMapKey = getSpanTreeMapKey(span);
63+
const spanBuffer = spanTreeMap.get(spanTreeMapKey);
5864
if (spanBuffer) {
5965
spanBuffer.add(span);
6066
} else {
61-
traceMap.set(span.spanContext().traceId, new Set([span]));
67+
spanTreeMap.set(spanTreeMapKey, new Set([span]));
6268
}
6369
});
6470

6571
// For now, we send all spans on local segment (root) span end.
6672
// TODO: This will change once we have more concrete ideas about a universal SDK data buffer.
67-
client.on('segmentSpanEnd', segmentSpan => {
68-
const traceId = segmentSpan.spanContext().traceId;
69-
const spansOfTrace = traceMap.get(traceId);
73+
client.on(
74+
'segmentSpanEnd',
75+
segmentSpan => () =>
76+
processAndSendSpans(segmentSpan, {
77+
spanTreeMap: spanTreeMap,
78+
client,
79+
batchLimit: options.batchLimit,
80+
beforeSendSpan,
81+
}),
82+
);
83+
},
84+
};
85+
}) satisfies IntegrationFn);
7086

71-
if (!spansOfTrace?.size) {
72-
traceMap.delete(traceId);
73-
return;
74-
}
87+
interface SpanProcessingOptions {
88+
client: Client;
89+
spanTreeMap: Map<string, Set<Span>>;
90+
batchLimit: number;
91+
beforeSendSpan: ((span: SpanV2JSON) => SpanV2JSON) | undefined;
92+
}
7593

76-
const serializedSpans = Array.from(spansOfTrace ?? []).map(span => {
77-
const serializedSpan = spanToV2JSON(span);
78-
return beforeSendSpan ? beforeSendSpan(serializedSpan) : serializedSpan;
79-
});
94+
function getSpanTreeMapKey(span: Span): string {
95+
return `${span.spanContext().traceId}-${getSegmentSpan(span).spanContext().spanId}`;
96+
}
8097

81-
const batches: SpanV2JSON[][] = [];
82-
for (let i = 0; i < serializedSpans.length; i += options.batchLimit) {
83-
batches.push(serializedSpans.slice(i, i + options.batchLimit));
84-
}
98+
function processAndSendSpans(
99+
segmentSpan: Span,
100+
{ client, spanTreeMap, batchLimit, beforeSendSpan }: SpanProcessingOptions,
101+
): void {
102+
const traceId = segmentSpan.spanContext().traceId;
103+
const spanTreeMapKey = getSpanTreeMapKey(segmentSpan);
104+
const spansOfTrace = spanTreeMap.get(spanTreeMapKey);
105+
106+
if (!spansOfTrace?.size) {
107+
spanTreeMap.delete(spanTreeMapKey);
108+
return;
109+
}
85110

86-
DEBUG_BUILD &&
87-
debug.log(`Sending trace ${traceId} in ${batches.length} batche${batches.length === 1 ? '' : 's'}`);
111+
const { ignoreSpans } = client.getOptions();
88112

89-
// TODO: Apply scopes to spans
90-
// TODO: Apply ignoreSpans to spans
113+
// TODO: Apply scopes to spans
91114

92-
const dsc = getDynamicSamplingContextFromSpan(segmentSpan);
115+
// 1. Check if the entire span tree is ignored by ignoreSpans
116+
const segmentSpanJson = spanToV2JSON(segmentSpan);
117+
if (ignoreSpans?.length && shouldIgnoreSpan(segmentSpanJson, ignoreSpans)) {
118+
client.recordDroppedEvent('before_send', 'span', spansOfTrace.size);
119+
spanTreeMap.delete(spanTreeMapKey);
120+
return;
121+
}
93122

94-
for (const batch of batches) {
95-
const envelope = createSpanV2Envelope(batch, dsc, client);
96-
// no need to handle client reports for network errors,
97-
// buffer overflows or rate limiting here. All of this is handled
98-
// by client and transport.
99-
client.sendEnvelope(envelope).then(null, reason => {
100-
DEBUG_BUILD && debug.error('Error while sending span stream envelope:', reason);
101-
});
102-
}
123+
const serializedSpans = Array.from(spansOfTrace ?? []).map(spanToV2JSON);
103124

104-
traceMap.delete(traceId);
105-
});
106-
},
107-
};
108-
}) satisfies IntegrationFn;
125+
const processedSpans = [];
126+
let ignoredSpanCount = 0;
127+
128+
for (const span of serializedSpans) {
129+
// 2. Check if child spans should be ignored
130+
const isChildSpan = span.span_id !== segmentSpan.spanContext().spanId;
131+
if (ignoreSpans?.length && isChildSpan && shouldIgnoreSpan(span, ignoreSpans)) {
132+
reparentChildSpans(serializedSpans, span);
133+
ignoredSpanCount++;
134+
// drop this span by not adding it to the processedSpans array
135+
continue;
136+
}
109137

110-
export const spanStreamingIntegration = defineIntegration(_spanStreamingIntegration);
138+
// 3. Apply beforeSendSpan callback
139+
const processedSpan = beforeSendSpan ? applyBeforeSendSpanCallback(span, beforeSendSpan) : span;
140+
processedSpans.push(processedSpan);
141+
}
142+
143+
if (ignoredSpanCount) {
144+
client.recordDroppedEvent('before_send', 'span', ignoredSpanCount);
145+
}
146+
147+
const batches: SpanV2JSON[][] = [];
148+
for (let i = 0; i < processedSpans.length; i += batchLimit) {
149+
batches.push(processedSpans.slice(i, i + batchLimit));
150+
}
151+
152+
DEBUG_BUILD && debug.log(`Sending trace ${traceId} in ${batches.length} batche${batches.length === 1 ? '' : 's'}`);
153+
154+
const dsc = getDynamicSamplingContextFromSpan(segmentSpan);
155+
156+
for (const batch of batches) {
157+
const envelope = createSpanV2Envelope(batch, dsc, client);
158+
// no need to handle client reports for network errors,
159+
// buffer overflows or rate limiting here. All of this is handled
160+
// by client and transport.
161+
client.sendEnvelope(envelope).then(null, reason => {
162+
DEBUG_BUILD && debug.error('Error while sending span stream envelope:', reason);
163+
});
164+
}
165+
166+
spanTreeMap.delete(spanTreeMapKey);
167+
}
168+
169+
function applyBeforeSendSpanCallback(span: SpanV2JSON, beforeSendSpan: (span: SpanV2JSON) => SpanV2JSON): SpanV2JSON {
170+
const modifedSpan = beforeSendSpan(span);
171+
if (!modifedSpan) {
172+
showSpanDropWarning();
173+
return span;
174+
}
175+
return modifedSpan;
176+
}

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ export {
7676
getSpanDescendants,
7777
getStatusMessage,
7878
getRootSpan,
79+
getSegmentSpan,
7980
getActiveSpan,
8081
addChildSpanToSpan,
8182
spanTimeInputToSeconds,
8283
updateSpanName,
8384
spanToV2JSON,
85+
showSpanDropWarning,
8486
} from './utils/spanUtils';
8587
export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope';
8688
export { parseSampleRate } from './utils/parseSampleRate';
@@ -304,6 +306,7 @@ export { SDK_VERSION } from './utils/version';
304306
export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids';
305307
export { escapeStringForRegex } from './vendor/escapeStringForRegex';
306308
export { isV2BeforeSendSpanCallback, makeV2Callback } from './utils/beforeSendSpan';
309+
export { shouldIgnoreSpan, reparentChildSpans } from './utils/should-ignore-span';
307310

308311
export type { Attachment } from './types-hoist/attachment';
309312
export type {

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

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
import { DEBUG_BUILD } from '../debug-build';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes';
23
import type { ClientOptions } from '../types-hoist/options';
3-
import type { SpanJSON } from '../types-hoist/span';
4+
import type { SpanJSON, SpanV2JSON } from '../types-hoist/span';
45
import { debug } from './debug-logger';
56
import { isMatchingPattern } from './string';
67

7-
function logIgnoredSpan(droppedSpan: Pick<SpanJSON, 'description' | 'op'>): void {
8-
debug.log(`Ignoring span ${droppedSpan.op} - ${droppedSpan.description} because it matches \`ignoreSpans\`.`);
8+
function logIgnoredSpan(spanName: string, spanOp: string | undefined): void {
9+
debug.log(`Ignoring span ${spanOp ? `${spanOp} - ` : ''}${spanName} because it matches \`ignoreSpans\`.`);
910
}
1011

1112
/**
1213
* Check if a span should be ignored based on the ignoreSpans configuration.
1314
*/
1415
export function shouldIgnoreSpan(
15-
span: Pick<SpanJSON, 'description' | 'op'>,
16+
span: Pick<SpanJSON, 'description' | 'op'> | Pick<SpanV2JSON, 'name' | 'attributes'>,
1617
ignoreSpans: Required<ClientOptions>['ignoreSpans'],
1718
): boolean {
18-
if (!ignoreSpans?.length || !span.description) {
19+
if (!ignoreSpans?.length) {
20+
return false;
21+
}
22+
23+
const { spanName, spanOp: spanOpAttributeOrString } =
24+
'description' in span
25+
? { spanName: span.description, spanOp: span.op }
26+
: 'name' in span
27+
? { spanName: span.name, spanOp: span.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] }
28+
: { spanName: '', spanOp: '' };
29+
30+
const spanOp =
31+
typeof spanOpAttributeOrString === 'string'
32+
? spanOpAttributeOrString
33+
: spanOpAttributeOrString?.type === 'string'
34+
? spanOpAttributeOrString.value
35+
: undefined;
36+
37+
if (!spanName) {
1938
return false;
2039
}
2140

2241
for (const pattern of ignoreSpans) {
2342
if (isStringOrRegExp(pattern)) {
24-
if (isMatchingPattern(span.description, pattern)) {
25-
DEBUG_BUILD && logIgnoredSpan(span);
43+
if (isMatchingPattern(spanName, pattern)) {
44+
DEBUG_BUILD && logIgnoredSpan(spanName, spanOp);
2645
return true;
2746
}
2847
continue;
@@ -32,15 +51,15 @@ export function shouldIgnoreSpan(
3251
continue;
3352
}
3453

35-
const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true;
36-
const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true;
54+
const nameMatches = pattern.name ? isMatchingPattern(spanName, pattern.name) : true;
55+
const opMatches = pattern.op ? spanOp && isMatchingPattern(spanOp, pattern.op) : true;
3756

3857
// This check here is only correct because we can guarantee that we ran `isMatchingPattern`
3958
// for at least one of `nameMatches` and `opMatches`. So in contrary to how this looks,
4059
// not both op and name actually have to match. This is the most efficient way to check
4160
// for all combinations of name and op patterns.
4261
if (nameMatches && opMatches) {
43-
DEBUG_BUILD && logIgnoredSpan(span);
62+
DEBUG_BUILD && logIgnoredSpan(spanName, spanOp);
4463
return true;
4564
}
4665
}
@@ -52,7 +71,10 @@ export function shouldIgnoreSpan(
5271
* 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.
5372
* This mutates the spans array in place!
5473
*/
55-
export function reparentChildSpans(spans: SpanJSON[], dropSpan: SpanJSON): void {
74+
export function reparentChildSpans(
75+
spans: Pick<SpanV2JSON, 'parent_span_id' | 'span_id'>[],
76+
dropSpan: Pick<SpanV2JSON, 'parent_span_id' | 'span_id'>,
77+
): void {
5678
const droppedSpanParentId = dropSpan.parent_span_id;
5779
const droppedSpanId = dropSpan.span_id;
5880

packages/core/src/utils/spanUtils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,12 @@ export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] {
394394
/**
395395
* Returns the root span of a given span.
396396
*/
397-
export function getRootSpan(span: SpanWithPotentialChildren): Span {
397+
export const getRootSpan = getSegmentSpan;
398+
399+
/**
400+
* Returns the segment span of a given span.
401+
*/
402+
export function getSegmentSpan(span: SpanWithPotentialChildren): Span {
398403
return span[ROOT_SPAN_FIELD] || span;
399404
}
400405

0 commit comments

Comments
 (0)