Skip to content

Commit bbb7c84

Browse files
committed
ensure trace state is picked up form spans & add tests
1 parent a906b9a commit bbb7c84

File tree

4 files changed

+129
-11
lines changed

4 files changed

+129
-11
lines changed

packages/opentelemetry/src/propagator.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getClient, getDynamicSamplingContextFromClient } from '@sentry/core';
55
import type { DynamicSamplingContext, PropagationContext } from '@sentry/types';
66
import {
77
SENTRY_BAGGAGE_KEY_PREFIX,
8+
baggageHeaderToDynamicSamplingContext,
89
dynamicSamplingContextToSentryBaggageHeader,
910
generateSentryTraceHeader,
1011
propagationContextFromHeaders,
@@ -13,6 +14,27 @@ import {
1314
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC } from './constants';
1415
import { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData';
1516

17+
function getDynamicSamplingContextFromContext(context: Context): Partial<DynamicSamplingContext> | undefined {
18+
// If possible, we want to take the DSC from the active span
19+
// That should take precedence over the DSC from the propagation context
20+
const activeSpan = trace.getSpan(context);
21+
const traceStateDsc = activeSpan?.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC);
22+
const dscOnSpan = traceStateDsc ? baggageHeaderToDynamicSamplingContext(traceStateDsc) : undefined;
23+
24+
if (dscOnSpan) {
25+
return dscOnSpan;
26+
}
27+
28+
const propagationContext = getPropagationContextFromContext(context);
29+
30+
if (propagationContext) {
31+
const { traceId } = getSentryTraceData(context, propagationContext);
32+
return getDynamicSamplingContext(propagationContext, traceId);
33+
}
34+
35+
return undefined;
36+
}
37+
1638
/**
1739
* Injects and extracts `sentry-trace` and `baggage` headers from carriers.
1840
*/
@@ -29,9 +51,8 @@ export class SentryPropagator extends W3CBaggagePropagator {
2951

3052
const propagationContext = getPropagationContextFromContext(context);
3153
const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext);
32-
const dynamicSamplingContext = propagationContext
33-
? getDynamicSamplingContext(propagationContext, traceId)
34-
: undefined;
54+
55+
const dynamicSamplingContext = getDynamicSamplingContextFromContext(context);
3556

3657
if (dynamicSamplingContext) {
3758
baggage = Object.entries(dynamicSamplingContext).reduce<Baggage>((b, [dscKey, dscValue]) => {

packages/opentelemetry/src/spanExporter.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
55
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
66
import type { Transaction } from '@sentry/core';
77
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
8-
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, flush, getCurrentHub } from '@sentry/core';
8+
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, getCurrentHub } from '@sentry/core';
99
import type { Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types';
1010
import { addNonEnumerableProperty, dropUndefinedKeys, logger } from '@sentry/utils';
1111
import { startTransaction } from './custom/transaction';
@@ -74,14 +74,17 @@ export class SentrySpanExporter implements SpanExporter {
7474

7575
/** @inheritDoc */
7676
public shutdown(): Promise<void> {
77+
const forceFlush = this.forceFlush();
7778
this._stopped = true;
7879
this._finishedSpans = [];
79-
return this.forceFlush();
80+
return forceFlush;
8081
}
8182

8283
/** @inheritDoc */
83-
public async forceFlush(): Promise<void> {
84-
await flush();
84+
public forceFlush(): Promise<void> {
85+
return new Promise(resolve => {
86+
this.export(this._finishedSpans, () => resolve());
87+
});
8588
}
8689
}
8790

packages/opentelemetry/test/integration/transactions.test.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { TraceFlags, context, trace } from '@opentelemetry/api';
1+
import { SpanContext, TraceFlags, context, trace } from '@opentelemetry/api';
22
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
33
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addBreadcrumb, getClient, setTag, withIsolationScope } from '@sentry/core';
4-
import type { PropagationContext, TransactionEvent } from '@sentry/types';
4+
import type { Event, PropagationContext, TransactionEvent } from '@sentry/types';
55
import { logger } from '@sentry/utils';
66

7+
import { TraceState } from '@opentelemetry/core';
78
import { spanToJSON } from '@sentry/core';
9+
import { SENTRY_TRACE_STATE_DSC } from '../../src/constants';
810
import { SentrySpanProcessor } from '../../src/spanProcessor';
911
import { startInactiveSpan, startSpan } from '../../src/trace';
1012
import { setPropagationContextOnContext } from '../../src/utils/contextData';
@@ -14,6 +16,7 @@ import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit';
1416
describe('Integration | Transactions', () => {
1517
afterEach(() => {
1618
jest.restoreAllMocks();
19+
jest.useRealTimers();
1720
cleanupOtel();
1821
});
1922

@@ -515,4 +518,68 @@ describe('Integration | Transactions', () => {
515518
]),
516519
);
517520
});
521+
522+
it('uses & inherits DSC on span trace state', async () => {
523+
const transactionEvents: Event[] = [];
524+
const beforeSendTransaction = jest.fn(event => {
525+
transactionEvents.push(event);
526+
return null;
527+
});
528+
529+
const traceId = 'd4cda95b652f4a1592b449d5929fda1b';
530+
const parentSpanId = '6e0c63257de34c92';
531+
532+
const dscString = `sentry-transaction=other-transaction,sentry-environment=other,sentry-release=8.0.0,sentry-public_key=public,sentry-trace_id=${traceId},sentry-sampled=true`;
533+
534+
const spanContext: SpanContext = {
535+
traceId,
536+
spanId: parentSpanId,
537+
isRemote: true,
538+
traceFlags: TraceFlags.SAMPLED,
539+
traceState: new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString),
540+
};
541+
542+
const propagationContext: PropagationContext = {
543+
traceId,
544+
parentSpanId,
545+
spanId: '6e0c63257de34c93',
546+
sampled: true,
547+
};
548+
549+
mockSdkInit({ enableTracing: true, beforeSendTransaction });
550+
551+
const client = getClient() as TestClientInterface;
552+
553+
// We simulate the correct context we'd normally get from the SentryPropagator
554+
context.with(
555+
trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext),
556+
() => {
557+
startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => {
558+
expect(span.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString);
559+
560+
const subSpan = startInactiveSpan({ name: 'inner span 1' });
561+
562+
expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString);
563+
564+
subSpan.end();
565+
566+
startSpan({ name: 'inner span 2' }, subSpan => {
567+
expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString);
568+
});
569+
});
570+
},
571+
);
572+
573+
await client.flush();
574+
575+
expect(transactionEvents).toHaveLength(1);
576+
expect(transactionEvents[0]?.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({
577+
environment: 'other',
578+
public_key: 'public',
579+
release: '8.0.0',
580+
sampled: 'true',
581+
trace_id: traceId,
582+
transaction: 'other-transaction',
583+
});
584+
});
518585
});

packages/opentelemetry/test/propagator.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import {
66
propagation,
77
trace,
88
} from '@opentelemetry/api';
9-
import { suppressTracing } from '@opentelemetry/core';
9+
import { TraceState, suppressTracing } from '@opentelemetry/core';
1010
import { addTracingExtensions, setCurrentClient } from '@sentry/core';
1111
import type { Client, PropagationContext } from '@sentry/types';
1212

13-
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER } from '../src/constants';
13+
import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC } from '../src/constants';
1414
import { SentryPropagator } from '../src/propagator';
1515
import { getPropagationContextFromContext, setPropagationContextOnContext } from '../src/utils/contextData';
1616

@@ -77,6 +77,33 @@ describe('SentryPropagator', () => {
7777
],
7878
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
7979
],
80+
[
81+
'works with a DSC on the span trace state',
82+
{
83+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
84+
spanId: '6e0c63257de34c92',
85+
traceFlags: TraceFlags.SAMPLED,
86+
traceState: new TraceState().set(
87+
SENTRY_TRACE_STATE_DSC,
88+
'sentry-transaction=other-transaction,sentry-environment=other,sentry-release=8.0.0,sentry-public_key=public,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-sampled=true',
89+
),
90+
},
91+
{
92+
traceId: 'd4cda95b652f4a1592b449d5929fda1b',
93+
spanId: '6e0c63257de34c94',
94+
parentSpanId: '6e0c63257de34c93',
95+
sampled: true,
96+
},
97+
[
98+
'sentry-environment=other',
99+
'sentry-release=8.0.0',
100+
'sentry-public_key=public',
101+
'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b',
102+
'sentry-transaction=other-transaction',
103+
'sentry-sampled=true',
104+
],
105+
'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
106+
],
80107
[
81108
'works with an unsampled propagation context',
82109
{

0 commit comments

Comments
 (0)