Skip to content

Commit 0e9d0a9

Browse files
committed
feat(opentelemetry): Ensure DSC & attributes are correctly set
1 parent 1eeea62 commit 0e9d0a9

File tree

9 files changed

+163
-31
lines changed

9 files changed

+163
-31
lines changed

packages/core/src/tracing/dynamicSamplingContext.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly<Partial<
4747
return {};
4848
}
4949

50-
// passing emit=false here to only emit later once the DSC is actually populated
5150
const dsc = getDynamicSamplingContextFromClient(spanToJSON(span).trace_id || '', client);
5251

5352
// TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext

packages/opentelemetry/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createContextKey } from '@opentelemetry/api';
22

33
export const SENTRY_TRACE_HEADER = 'sentry-trace';
44
export const SENTRY_BAGGAGE_HEADER = 'baggage';
5+
export const SENTRY_TRACE_STATE_DSC = 'sentry.trace';
56

67
/** Context Key to hold a PropagationContext. */
78
export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY');

packages/opentelemetry/src/propagator.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api';
22
import { TraceFlags, propagation, trace } from '@opentelemetry/api';
3-
import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core';
3+
import { TraceState, W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core';
44
import { getClient, getDynamicSamplingContextFromClient } from '@sentry/core';
55
import type { DynamicSamplingContext, PropagationContext } from '@sentry/types';
6-
import { SENTRY_BAGGAGE_KEY_PREFIX, generateSentryTraceHeader, propagationContextFromHeaders } from '@sentry/utils';
7-
8-
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from './constants';
6+
import {
7+
SENTRY_BAGGAGE_KEY_PREFIX,
8+
dynamicSamplingContextToSentryBaggageHeader,
9+
generateSentryTraceHeader,
10+
propagationContextFromHeaders,
11+
} from '@sentry/utils';
12+
13+
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC } from './constants';
914
import { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData';
1015

1116
/**
@@ -24,7 +29,9 @@ export class SentryPropagator extends W3CBaggagePropagator {
2429

2530
const propagationContext = getPropagationContextFromContext(context);
2631
const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext);
27-
const dynamicSamplingContext = propagationContext ? getDsc(propagationContext, traceId) : undefined;
32+
const dynamicSamplingContext = propagationContext
33+
? getDynamicSamplingContext(propagationContext, traceId)
34+
: undefined;
2835

2936
if (dynamicSamplingContext) {
3037
baggage = Object.entries(dynamicSamplingContext).reduce<Baggage>((b, [dscKey, dscValue]) => {
@@ -58,11 +65,19 @@ export class SentryPropagator extends W3CBaggagePropagator {
5865
// Add propagation context to context
5966
const contextWithPropagationContext = setPropagationContextOnContext(context, propagationContext);
6067

68+
// We store the DSC as OTEL trace state on the span context
69+
const dscString = propagationContext.dsc
70+
? dynamicSamplingContextToSentryBaggageHeader(propagationContext.dsc)
71+
: undefined;
72+
73+
const traceState = dscString ? new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString) : undefined;
74+
6175
const spanContext: SpanContext = {
6276
traceId: propagationContext.traceId,
6377
spanId: propagationContext.parentSpanId || '',
6478
isRemote: true,
6579
traceFlags: propagationContext.sampled === true ? TraceFlags.SAMPLED : TraceFlags.NONE,
80+
traceState,
6681
};
6782

6883
// Add remote parent span context
@@ -77,7 +92,8 @@ export class SentryPropagator extends W3CBaggagePropagator {
7792
}
7893
}
7994

80-
function getDsc(
95+
/** Get the DSC. */
96+
function getDynamicSamplingContext(
8197
propagationContext: PropagationContext,
8298
traceId: string | undefined,
8399
): Partial<DynamicSamplingContext> | undefined {
@@ -96,6 +112,7 @@ function getDsc(
96112
return undefined;
97113
}
98114

115+
/** Get the trace data for propagation. */
99116
function getSentryTraceData(
100117
context: Context,
101118
propagationContext: PropagationContext | undefined,

packages/opentelemetry/src/semanticAttributes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export const InternalSentrySemanticAttributes = {
66
ORIGIN: 'sentry.origin',
77
OP: 'sentry.op',
88
SOURCE: 'sentry.source',
9-
SAMPLE_RATE: 'sentry.sample_rate',
109
PARENT_SAMPLED: 'sentry.parentSampled',
1110
BREADCRUMB_TYPE: 'sentry.breadcrumb.type',
1211
BREADCRUMB_LEVEL: 'sentry.breadcrumb.level',
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
11
import type { Client } from '@sentry/types';
2+
import { dropUndefinedKeys } from '@sentry/utils';
23

3-
import { getActiveSpan } from './utils/getActiveSpan';
4-
import { spanHasParentId } from './utils/spanTypes';
4+
import { getActiveSpan, getRootSpan } from './utils/getActiveSpan';
5+
import { spanHasName, spanHasParentId } from './utils/spanTypes';
56

67
/** Ensure the `trace` context is set on all events. */
78
export function setupEventContextTrace(client: Client): void {
89
client.addEventProcessor(event => {
910
const span = getActiveSpan();
10-
if (!span) {
11+
// For transaction events, this is handled separately
12+
// Because the active span may not be the span that is actually the transaction event
13+
if (!span || event.type === 'transaction') {
1114
return event;
1215
}
1316

1417
const spanContext = span.spanContext();
1518

1619
// If event has already set `trace` context, use that one.
1720
event.contexts = {
18-
trace: {
21+
trace: dropUndefinedKeys({
1922
trace_id: spanContext.traceId,
2023
span_id: spanContext.spanId,
2124
parent_span_id: spanHasParentId(span) ? span.parentSpanId : undefined,
22-
},
25+
}),
2326
...event.contexts,
2427
};
2528

29+
const rootSpan = getRootSpan(span);
30+
const transactionName = spanHasName(rootSpan) ? rootSpan.name : undefined;
31+
if (transactionName) {
32+
event.tags = { transaction: transactionName, ...event.tags };
33+
}
34+
2635
return event;
2736
});
2837
}

packages/opentelemetry/src/spanExporter.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import type { Transaction } from '@sentry/core';
77
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
88
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, flush, getCurrentHub } from '@sentry/core';
99
import type { Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types';
10-
import { addNonEnumerableProperty, logger } from '@sentry/utils';
10+
import { addNonEnumerableProperty, dropUndefinedKeys, logger } from '@sentry/utils';
1111
import { startTransaction } from './custom/transaction';
1212

1313
import { DEBUG_BUILD } from './debug-build';
1414
import { InternalSentrySemanticAttributes } from './semanticAttributes';
1515
import { convertOtelTimeToSeconds } from './utils/convertOtelTimeToSeconds';
16+
import { getDynamicSamplingContextFromSpan } from './utils/dynamicSamplingContext';
1617
import { getRequestSpanData } from './utils/getRequestSpanData';
1718
import type { SpanNode } from './utils/groupSpansWithParents';
1819
import { groupSpansWithParents } from './utils/groupSpansWithParents';
@@ -149,6 +150,17 @@ function createTransactionForOtelSpan(span: ReadableSpan): Transaction {
149150
const metadata = getSpanMetadata(span);
150151
const capturedSpanScopes = getSpanScopes(span);
151152

153+
const sampleRate = span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as number | undefined;
154+
155+
const attributes = {
156+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
157+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate,
158+
...data,
159+
...removeSentryAttributes(span.attributes),
160+
};
161+
162+
const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span);
163+
152164
const transaction = startTransaction(hub, {
153165
spanId,
154166
traceId,
@@ -159,13 +171,13 @@ function createTransactionForOtelSpan(span: ReadableSpan): Transaction {
159171
status: mapStatus(span),
160172
startTimestamp: convertOtelTimeToSeconds(span.startTime),
161173
metadata: {
162-
sampleRate: span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as number | undefined,
174+
...dropUndefinedKeys({
175+
dynamicSamplingContext,
176+
sampleRate,
177+
}),
163178
...metadata,
164179
},
165-
attributes: {
166-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
167-
},
168-
data: removeSentryAttributes(data),
180+
attributes,
169181
origin,
170182
tags,
171183
sampled: true,
@@ -180,7 +192,14 @@ function createTransactionForOtelSpan(span: ReadableSpan): Transaction {
180192
});
181193

182194
if (capturedSpanScopes) {
183-
setCapturedScopesOnTransaction(transaction, capturedSpanScopes.scope, capturedSpanScopes.isolationScope);
195+
// Ensure the `transaction` tag is correctly set on the transaction event
196+
const scope = capturedSpanScopes.scope.clone();
197+
scope.addEventProcessor(event => {
198+
event.tags = { transaction: description, ...event.tags };
199+
return event;
200+
});
201+
202+
setCapturedScopesOnTransaction(transaction, scope, capturedSpanScopes.isolationScope);
184203
}
185204

186205
return transaction;
@@ -264,6 +283,8 @@ function removeSentryAttributes(data: Record<string, unknown>): Record<string, u
264283
delete cleanedData[InternalSentrySemanticAttributes.ORIGIN];
265284
delete cleanedData[InternalSentrySemanticAttributes.OP];
266285
delete cleanedData[InternalSentrySemanticAttributes.SOURCE];
286+
// We want to avoid having this on each span (as that is set by the Sampler)
287+
// We only want this on the transaction, where we manually add it to `attributes`
267288
delete cleanedData[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE];
268289
/* eslint-enable @typescript-eslint/no-dynamic-delete */
269290

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { TraceFlags } from '@opentelemetry/api';
2+
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
4+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
5+
getClient,
6+
getDynamicSamplingContextFromClient,
7+
} from '@sentry/core';
8+
import type { DynamicSamplingContext } from '@sentry/types';
9+
import { baggageHeaderToDynamicSamplingContext } from '@sentry/utils';
10+
import { SENTRY_TRACE_STATE_DSC } from '../constants';
11+
import type { AbstractSpan } from '../types';
12+
import { spanHasAttributes, spanHasName } from './spanTypes';
13+
14+
/**
15+
* Creates a dynamic sampling context from a span (and client and scope)
16+
*
17+
* @param span the span from which a few values like the root span name and sample rate are extracted.
18+
*
19+
* @returns a dynamic sampling context
20+
*/
21+
export function getDynamicSamplingContextFromSpan(span: AbstractSpan): Readonly<Partial<DynamicSamplingContext>> {
22+
const client = getClient();
23+
if (!client) {
24+
return {};
25+
}
26+
27+
const dsc = getDynamicSamplingContextFromClient(span.spanContext().traceId, client);
28+
29+
const traceState = span.spanContext().traceState;
30+
const traceStateDsc = traceState?.get(SENTRY_TRACE_STATE_DSC);
31+
32+
if (traceStateDsc) {
33+
const dsc = baggageHeaderToDynamicSamplingContext(traceStateDsc);
34+
if (dsc) {
35+
return dsc;
36+
}
37+
}
38+
39+
const attributes = spanHasAttributes(span) ? span.attributes : {};
40+
41+
const sampleRate = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE];
42+
if (sampleRate != null) {
43+
dsc.sample_rate = `${sampleRate}`;
44+
}
45+
46+
// We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII
47+
const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
48+
const name = spanHasName(span) ? span.name : '';
49+
50+
if (source !== 'url' && name) {
51+
dsc.transaction = name;
52+
}
53+
54+
// eslint-disable-next-line no-bitwise
55+
const sampled = Boolean(span.spanContext().traceFlags & TraceFlags.SAMPLED);
56+
dsc.sampled = String(sampled);
57+
58+
client.emit('createDsc', dsc);
59+
60+
return dsc;
61+
}

packages/opentelemetry/test/integration/scope.test.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,21 +64,33 @@ describe('Integration | Scope', () => {
6464
await client.flush();
6565

6666
expect(beforeSend).toHaveBeenCalledTimes(1);
67+
68+
if (spanId) {
69+
expect(beforeSend).toHaveBeenCalledWith(
70+
expect.objectContaining({
71+
contexts: {
72+
trace: {
73+
span_id: spanId,
74+
trace_id: traceId,
75+
},
76+
},
77+
}),
78+
{
79+
event_id: expect.any(String),
80+
originalException: error,
81+
syntheticException: expect.any(Error),
82+
},
83+
);
84+
}
85+
6786
expect(beforeSend).toHaveBeenCalledWith(
6887
expect.objectContaining({
69-
contexts: expect.objectContaining({
70-
trace: spanId
71-
? {
72-
span_id: spanId,
73-
trace_id: traceId,
74-
}
75-
: expect.any(Object),
76-
}),
7788
tags: {
7889
tag1: 'val1',
7990
tag2: 'val2',
8091
tag3: 'val3',
8192
tag4: 'val4',
93+
...(enableTracing ? { transaction: 'outer' } : {}),
8294
},
8395
}),
8496
{
@@ -99,21 +111,22 @@ describe('Integration | Scope', () => {
99111
'otel.kind': 'INTERNAL',
100112
'sentry.origin': 'manual',
101113
'sentry.source': 'custom',
114+
'sentry.sample_rate': 1,
102115
},
103116
span_id: spanId,
104117
status: 'ok',
105118
trace_id: traceId,
106119
origin: 'manual',
107120
},
108121
}),
109-
110122
spans: [],
111123
start_timestamp: expect.any(Number),
112124
tags: {
113125
tag1: 'val1',
114126
tag2: 'val2',
115127
tag3: 'val3',
116128
tag4: 'val4',
129+
transaction: 'outer',
117130
},
118131
timestamp: expect.any(Number),
119132
transaction: 'outer',
@@ -207,6 +220,7 @@ describe('Integration | Scope', () => {
207220
tag2: 'val2a',
208221
tag3: 'val3a',
209222
tag4: 'val4a',
223+
...(enableTracing ? { transaction: 'outer' } : {}),
210224
},
211225
}),
212226
{
@@ -232,6 +246,7 @@ describe('Integration | Scope', () => {
232246
tag2: 'val2b',
233247
tag3: 'val3b',
234248
tag4: 'val4b',
249+
...(enableTracing ? { transaction: 'outer' } : {}),
235250
},
236251
}),
237252
{
@@ -331,6 +346,7 @@ describe('Integration | Scope', () => {
331346
tag4: 'val4a',
332347
isolationTag1: 'val1',
333348
isolationTag2: 'val2',
349+
...(enableTracing ? { transaction: 'outer' } : {}),
334350
},
335351
}),
336352
{
@@ -358,6 +374,7 @@ describe('Integration | Scope', () => {
358374
tag4: 'val4b',
359375
isolationTag1: 'val1',
360376
isolationTag2: 'val2b',
377+
...(enableTracing ? { transaction: 'outer' } : {}),
361378
},
362379
}),
363380
{

0 commit comments

Comments
 (0)