Skip to content

Commit d5bba58

Browse files
authored
feat(opentelemetry): Expose sampling helper (#12674)
When users want to use a custom sampler, they can use these new helpers to still have sentry working nicely with whatever they decide to do in there. For e.g. trace propagation etc. to work correctly with Sentry, we need to attach some things to trace state etc. These helpers encapsulate this for the user, while still allowing them to decide however they want if the span should be sampled or not. This was brought up here: #12191 (reply in thread)
1 parent 0c1e877 commit d5bba58

File tree

2 files changed

+52
-20
lines changed

2 files changed

+52
-20
lines changed

packages/opentelemetry/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrat
3636
export { wrapContextManagerClass } from './contextManager';
3737
export { SentryPropagator } from './propagator';
3838
export { SentrySpanProcessor } from './spanProcessor';
39-
export { SentrySampler } from './sampler';
39+
export {
40+
SentrySampler,
41+
wrapSamplingDecision,
42+
} from './sampler';
4043

4144
export { openTelemetrySetupCheck } from './utils/setupCheck';
4245

packages/opentelemetry/src/sampler.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Attributes, Context, Span } from '@opentelemetry/api';
1+
import type { Attributes, Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api';
22
import { SpanKind } from '@opentelemetry/api';
33
import { isSpanContextValid, trace } from '@opentelemetry/api';
44
import { TraceState } from '@opentelemetry/core';
@@ -40,16 +40,8 @@ export class SentrySampler implements Sampler {
4040
const parentSpan = trace.getSpan(context);
4141
const parentContext = parentSpan?.spanContext();
4242

43-
let traceState = parentContext?.traceState || new TraceState();
44-
45-
// We always keep the URL on the trace state, so we can access it in the propagator
46-
const url = spanAttributes[SEMATTRS_HTTP_URL];
47-
if (url && typeof url === 'string') {
48-
traceState = traceState.set(SENTRY_TRACE_STATE_URL, url);
49-
}
50-
5143
if (!hasTracingEnabled(options)) {
52-
return { decision: SamplingDecision.NOT_RECORD, traceState };
44+
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
5345
}
5446

5547
// If we have a http.client span that has no local parent, we never want to sample it
@@ -59,7 +51,7 @@ export class SentrySampler implements Sampler {
5951
spanAttributes[SEMATTRS_HTTP_METHOD] &&
6052
(!parentSpan || parentContext?.isRemote)
6153
) {
62-
return { decision: SamplingDecision.NOT_RECORD, traceState };
54+
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
6355
}
6456

6557
const parentSampled = parentSpan ? getParentSampled(parentSpan, traceId, spanName) : undefined;
@@ -76,7 +68,7 @@ export class SentrySampler implements Sampler {
7668
mutableSamplingDecision,
7769
);
7870
if (!mutableSamplingDecision.decision) {
79-
return { decision: SamplingDecision.NOT_RECORD, traceState: traceState };
71+
return wrapSamplingDecision({ decision: undefined, context, spanAttributes });
8072
}
8173

8274
const [sampled, sampleRate] = sampleSpan(options, {
@@ -96,25 +88,22 @@ export class SentrySampler implements Sampler {
9688
const method = `${spanAttributes[SEMATTRS_HTTP_METHOD]}`.toUpperCase();
9789
if (method === 'OPTIONS' || method === 'HEAD') {
9890
DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`);
91+
9992
return {
100-
decision: SamplingDecision.NOT_RECORD,
93+
...wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, context, spanAttributes }),
10194
attributes,
102-
traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'),
10395
};
10496
}
10597

10698
if (!sampled) {
10799
return {
108-
decision: SamplingDecision.NOT_RECORD,
100+
...wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, context, spanAttributes }),
109101
attributes,
110-
traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'),
111102
};
112103
}
113-
114104
return {
115-
decision: SamplingDecision.RECORD_AND_SAMPLED,
105+
...wrapSamplingDecision({ decision: SamplingDecision.RECORD_AND_SAMPLED, context, spanAttributes }),
116106
attributes,
117-
traceState,
118107
};
119108
}
120109

@@ -152,3 +141,43 @@ function getParentSampled(parentSpan: Span, traceId: string, spanName: string):
152141

153142
return undefined;
154143
}
144+
145+
/**
146+
* Wrap a sampling decision with data that Sentry needs to work properly with it.
147+
* If you pass `decision: undefined`, it will be treated as `NOT_RECORDING`, but in contrast to passing `NOT_RECORDING`
148+
* it will not propagate this decision to downstream Sentry SDKs.
149+
*/
150+
export function wrapSamplingDecision({
151+
decision,
152+
context,
153+
spanAttributes,
154+
}: { decision: SamplingDecision | undefined; context: Context; spanAttributes: SpanAttributes }): SamplingResult {
155+
const traceState = getBaseTraceState(context, spanAttributes);
156+
157+
// If the decision is undefined, we treat it as NOT_RECORDING, but we don't propagate this decision to downstream SDKs
158+
// Which is done by not setting `SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING` traceState
159+
if (decision == undefined) {
160+
return { decision: SamplingDecision.NOT_RECORD, traceState };
161+
}
162+
163+
if (decision === SamplingDecision.NOT_RECORD) {
164+
return { decision, traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1') };
165+
}
166+
167+
return { decision, traceState };
168+
}
169+
170+
function getBaseTraceState(context: Context, spanAttributes: SpanAttributes): TraceStateInterface {
171+
const parentSpan = trace.getSpan(context);
172+
const parentContext = parentSpan?.spanContext();
173+
174+
let traceState = parentContext?.traceState || new TraceState();
175+
176+
// We always keep the URL on the trace state, so we can access it in the propagator
177+
const url = spanAttributes[SEMATTRS_HTTP_URL];
178+
if (url && typeof url === 'string') {
179+
traceState = traceState.set(SENTRY_TRACE_STATE_URL, url);
180+
}
181+
182+
return traceState;
183+
}

0 commit comments

Comments
 (0)