Skip to content

Commit 6a6e05b

Browse files
authored
feat(opentelemetry)!: Exclusively pass root spans through sampling pipeline (#14951)
1 parent 1048a43 commit 6a6e05b

File tree

5 files changed

+84
-39
lines changed

5 files changed

+84
-39
lines changed

docs/migration/v8-to-v9.md

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ In v9, an `undefined` value will be treated the same as if the value is not defi
8888

8989
- The `requestDataIntegration` will no longer automatically set the user from `request.user`. This is an express-specific, undocumented behavior, and also conflicts with our privacy-by-default strategy. Starting in v9, you'll need to manually call `Sentry.setUser()` e.g. in a middleware to set the user on Sentry events.
9090

91+
- The `tracesSampler` hook will no longer be called for _every_ span. Instead, it will only be called for "root spans". Root spans are spans that have no local parent span. Root spans may however have incoming trace data from a different service, for example when using distributed tracing.
92+
9193
### `@sentry/browser`
9294

9395
- The `captureUserFeedback` method has been removed. Use `captureFeedback` instead and update the `comments` field to `message`.

packages/core/src/semanticAttributes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source';
77

88
/**
99
* Use this attribute to represent the sample rate used for a span.
10+
*
11+
* NOTE: Is only defined on root spans.
1012
*/
1113
export const SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE = 'sentry.sample_rate';
1214

packages/opentelemetry/src/sampler.ts

+42-25
Original file line numberDiff line numberDiff line change
@@ -95,45 +95,62 @@ export class SentrySampler implements Sampler {
9595
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
9696
}
9797

98-
const [sampled, sampleRate] = sampleSpan(options, {
99-
name: inferredSpanName,
100-
attributes: mergedAttributes,
101-
transactionContext: {
98+
const isRootSpan = !parentSpan || parentContext?.isRemote;
99+
100+
// We only sample based on parameters (like tracesSampleRate or tracesSampler) for root spans (which is done in sampleSpan).
101+
// Non-root-spans simply inherit the sampling decision from their parent.
102+
if (isRootSpan) {
103+
const [sampled, sampleRate] = sampleSpan(options, {
102104
name: inferredSpanName,
105+
attributes: mergedAttributes,
106+
transactionContext: {
107+
name: inferredSpanName,
108+
parentSampled,
109+
},
103110
parentSampled,
104-
},
105-
parentSampled,
106-
});
111+
});
107112

108-
const attributes: Attributes = {
109-
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate,
110-
};
113+
const attributes: Attributes = {
114+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate,
115+
};
111116

112-
const method = `${maybeSpanHttpMethod}`.toUpperCase();
113-
if (method === 'OPTIONS' || method === 'HEAD') {
114-
DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`);
117+
const method = `${maybeSpanHttpMethod}`.toUpperCase();
118+
if (method === 'OPTIONS' || method === 'HEAD') {
119+
DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`);
115120

116-
return {
117-
...wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, context, spanAttributes }),
118-
attributes,
119-
};
120-
}
121+
return {
122+
...wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, context, spanAttributes }),
123+
attributes,
124+
};
125+
}
121126

122-
if (!sampled) {
123-
if (parentSampled === undefined) {
127+
if (
128+
!sampled &&
129+
// We check for `parentSampled === undefined` because we only want to record client reports for spans that are trace roots (ie. when there was incoming trace)
130+
parentSampled === undefined
131+
) {
124132
DEBUG_BUILD && logger.log('[Tracing] Discarding root span because its trace was not chosen to be sampled.');
125133
this._client.recordDroppedEvent('sample_rate', 'transaction');
126134
}
127135

128136
return {
129-
...wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, context, spanAttributes }),
137+
...wrapSamplingDecision({
138+
decision: sampled ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,
139+
context,
140+
spanAttributes,
141+
}),
130142
attributes,
131143
};
144+
} else {
145+
return {
146+
...wrapSamplingDecision({
147+
decision: parentSampled ? SamplingDecision.RECORD_AND_SAMPLED : SamplingDecision.NOT_RECORD,
148+
context,
149+
spanAttributes,
150+
}),
151+
attributes: {},
152+
};
132153
}
133-
return {
134-
...wrapSamplingDecision({ decision: SamplingDecision.RECORD_AND_SAMPLED, context, spanAttributes }),
135-
attributes,
136-
};
137154
}
138155

139156
/** Returns the sampler name or short description with the configuration. */

packages/opentelemetry/test/sampler.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('SentrySampler', () => {
5757
const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links);
5858
expect(actual).toEqual({
5959
decision: SamplingDecision.NOT_RECORD,
60-
attributes: { 'sentry.sample_rate': 0 },
60+
attributes: {},
6161
traceState: new TraceState().set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'),
6262
});
6363
expect(spyOnDroppedEvent).toHaveBeenCalledTimes(0);

packages/opentelemetry/test/trace.test.ts

+37-13
Original file line numberDiff line numberDiff line change
@@ -1336,12 +1336,27 @@ describe('trace (sampling)', () => {
13361336
});
13371337
});
13381338

1339-
expect(tracesSampler).toHaveBeenCalledTimes(3);
1340-
expect(tracesSampler).toHaveBeenLastCalledWith({
1341-
parentSampled: false,
1339+
expect(tracesSampler).toHaveBeenCalledTimes(2);
1340+
expect(tracesSampler).toHaveBeenCalledWith(
1341+
expect.objectContaining({
1342+
parentSampled: undefined,
1343+
name: 'outer',
1344+
attributes: {},
1345+
transactionContext: { name: 'outer', parentSampled: undefined },
1346+
}),
1347+
);
1348+
expect(tracesSampler).toHaveBeenCalledWith(
1349+
expect.objectContaining({
1350+
parentSampled: undefined,
1351+
name: 'outer2',
1352+
attributes: {},
1353+
transactionContext: { name: 'outer2', parentSampled: undefined },
1354+
}),
1355+
);
1356+
1357+
// Only root spans should go through the sampler
1358+
expect(tracesSampler).not.toHaveBeenLastCalledWith({
13421359
name: 'inner2',
1343-
attributes: {},
1344-
transactionContext: { name: 'inner2', parentSampled: false },
13451360
});
13461361
});
13471362

@@ -1390,13 +1405,22 @@ describe('trace (sampling)', () => {
13901405
});
13911406
});
13921407

1393-
expect(tracesSampler).toHaveBeenCalledTimes(3);
1394-
expect(tracesSampler).toHaveBeenLastCalledWith({
1395-
parentSampled: false,
1396-
name: 'inner2',
1397-
attributes: {},
1398-
transactionContext: { name: 'inner2', parentSampled: false },
1399-
});
1408+
expect(tracesSampler).toHaveBeenCalledTimes(2);
1409+
expect(tracesSampler).toHaveBeenCalledWith(
1410+
expect.objectContaining({
1411+
parentSampled: undefined,
1412+
name: 'outer2',
1413+
attributes: {},
1414+
transactionContext: { name: 'outer2', parentSampled: undefined },
1415+
}),
1416+
);
1417+
1418+
// Only root spans should be passed to tracesSampler
1419+
expect(tracesSampler).not.toHaveBeenLastCalledWith(
1420+
expect.objectContaining({
1421+
name: 'inner2',
1422+
}),
1423+
);
14001424

14011425
// Now return `0.4`, it should not sample
14021426
tracesSamplerResponse = 0.4;
@@ -1405,7 +1429,7 @@ describe('trace (sampling)', () => {
14051429
expect(outerSpan.isRecording()).toBe(false);
14061430
});
14071431

1408-
expect(tracesSampler).toHaveBeenCalledTimes(4);
1432+
expect(tracesSampler).toHaveBeenCalledTimes(3);
14091433
expect(tracesSampler).toHaveBeenLastCalledWith({
14101434
parentSampled: undefined,
14111435
name: 'outer3',

0 commit comments

Comments
 (0)