From 572bffd85374405b1c8fd2a491809ae025ecf0c9 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 4 Mar 2024 13:56:48 +0000 Subject: [PATCH] fix(opentelemetry): Ensure DSC propagation works correctly This updates the propagation handling in OTEL to the following: 1. If there is an active (local) span, we always pick up propagation context/DSC data from it. 2. Else, we try to pick it from the current scope 3. If we can't find this (e.g. a detached otel context is used), we try to pick it from the remote span context 4. Finally, if that also fails, we have no DSC --- .../test/integration/transactions.test.ts | 44 +- packages/opentelemetry/src/constants.ts | 4 +- packages/opentelemetry/src/index.ts | 6 +- packages/opentelemetry/src/propagator.ts | 188 ++++-- packages/opentelemetry/src/sampler.ts | 10 +- .../opentelemetry/src/utils/contextData.ts | 24 +- .../test/integration/transactions.test.ts | 60 +- .../opentelemetry/test/propagator.test.ts | 623 ++++++++++++------ packages/opentelemetry/test/trace.test.ts | 76 +-- 9 files changed, 607 insertions(+), 428 deletions(-) diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 73d536c06cc1..fee0ed9b9072 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,8 +1,8 @@ import { TraceFlags, context, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; -import { SentrySpanProcessor, setPropagationContextOnContext } from '@sentry/opentelemetry'; -import type { PropagationContext, TransactionEvent } from '@sentry/types'; +import { SentrySpanProcessor } from '@sentry/opentelemetry'; +import type { TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import * as Sentry from '../../src'; @@ -488,37 +488,27 @@ describe('Integration | Transactions', () => { traceFlags: TraceFlags.SAMPLED, }; - const propagationContext: PropagationContext = { - traceId, - parentSpanId, - spanId: '6e0c63257de34c93', - sampled: true, - }; - mockSdkInit({ enableTracing: true, beforeSendTransaction }); const client = Sentry.getClient()!; // We simulate the correct context we'd normally get from the SentryPropagator - context.with( - trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), - () => { - Sentry.startSpan( - { - op: 'test op', - name: 'test name', - origin: 'auto.test', - attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task' }, - }, - () => { - const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); + context.with(trace.setSpanContext(context.active(), spanContext), () => { + Sentry.startSpan( + { + op: 'test op', + name: 'test name', + origin: 'auto.test', + attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task' }, + }, + () => { + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); - Sentry.startSpan({ name: 'inner span 2' }, () => {}); - }, - ); - }, - ); + Sentry.startSpan({ name: 'inner span 2' }, () => {}); + }, + ); + }); await client.flush(); diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts index 64b611af8c1c..0826ee6cb653 100644 --- a/packages/opentelemetry/src/constants.ts +++ b/packages/opentelemetry/src/constants.ts @@ -3,9 +3,7 @@ import { createContextKey } from '@opentelemetry/api'; export const SENTRY_TRACE_HEADER = 'sentry-trace'; export const SENTRY_BAGGAGE_HEADER = 'baggage'; export const SENTRY_TRACE_STATE_DSC = 'sentry.trace'; - -/** Context Key to hold a PropagationContext. */ -export const SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY = createContextKey('SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY'); +export const SENTRY_TRACE_STATE_PARENT_SPAN_ID = 'sentry.parent_span_id'; /** Context Key to hold a Hub. */ export const SENTRY_HUB_CONTEXT_KEY = createContextKey('sentry_hub'); diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index f5ffb08199c8..08112f68d6dc 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -14,11 +14,7 @@ export { getSpanScopes, } from './utils/spanData'; -export { - getPropagationContextFromContext, - setPropagationContextOnContext, - getScopesFromContext, -} from './utils/contextData'; +export { getScopesFromContext } from './utils/contextData'; export { spanHasAttributes, diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 903f9cc0585f..942cedc19121 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -1,7 +1,7 @@ import type { Baggage, Context, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; import { TraceFlags, propagation, trace } from '@opentelemetry/api'; import { TraceState, W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; -import { getClient, getDynamicSamplingContextFromClient } from '@sentry/core'; +import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, getIsolationScope } from '@sentry/core'; import type { DynamicSamplingContext, PropagationContext } from '@sentry/types'; import { SENTRY_BAGGAGE_KEY_PREFIX, @@ -11,28 +11,30 @@ import { propagationContextFromHeaders, } from '@sentry/utils'; -import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC } from './constants'; -import { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData'; - -function getDynamicSamplingContextFromContext(context: Context): Partial | undefined { - // If possible, we want to take the DSC from the active span - // That should take precedence over the DSC from the propagation context - const activeSpan = trace.getSpan(context); - const traceStateDsc = activeSpan?.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC); - const dscOnSpan = traceStateDsc ? baggageHeaderToDynamicSamplingContext(traceStateDsc) : undefined; - - if (dscOnSpan) { - return dscOnSpan; - } - - const propagationContext = getPropagationContextFromContext(context); - - if (propagationContext) { - const { traceId } = getSentryTraceData(context, propagationContext); - return getDynamicSamplingContext(propagationContext, traceId); - } - - return undefined; +import { + SENTRY_BAGGAGE_HEADER, + SENTRY_TRACE_HEADER, + SENTRY_TRACE_STATE_DSC, + SENTRY_TRACE_STATE_PARENT_SPAN_ID, +} from './constants'; +import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; + +/** Get the Sentry propagation context from a span context. */ +export function getPropagationContextFromSpanContext(spanContext: SpanContext): PropagationContext { + const { traceId, spanId, traceFlags, traceState } = spanContext; + + const dscString = traceState ? traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; + const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; + const parentSpanId = traceState ? traceState.get(SENTRY_TRACE_STATE_PARENT_SPAN_ID) : undefined; + const sampled = traceFlags === TraceFlags.SAMPLED; + + return { + traceId, + spanId, + sampled, + parentSpanId, + dsc, + }; } /** @@ -49,10 +51,7 @@ export class SentryPropagator extends W3CBaggagePropagator { let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); - const propagationContext = getPropagationContextFromContext(context); - const { spanId, traceId, sampled } = getSentryTraceData(context, propagationContext); - - const dynamicSamplingContext = getDynamicSamplingContextFromContext(context); + const { dynamicSamplingContext, traceId, spanId, sampled } = getInjectionData(context); if (dynamicSamplingContext) { baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { @@ -83,15 +82,11 @@ export class SentryPropagator extends W3CBaggagePropagator { const propagationContext = propagationContextFromHeaders(sentryTraceHeader, maybeBaggageHeader); - // Add propagation context to context - const contextWithPropagationContext = setPropagationContextOnContext(context, propagationContext); - // We store the DSC as OTEL trace state on the span context - const dscString = propagationContext.dsc - ? dynamicSamplingContextToSentryBaggageHeader(propagationContext.dsc) - : undefined; - - const traceState = dscString ? new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString) : undefined; + const traceState = makeTraceState({ + parentSpanId: propagationContext.parentSpanId, + dsc: propagationContext.dsc, + }); const spanContext: SpanContext = { traceId: propagationContext.traceId, @@ -101,8 +96,18 @@ export class SentryPropagator extends W3CBaggagePropagator { traceState, }; - // Add remote parent span context - return trace.setSpanContext(contextWithPropagationContext, spanContext); + // Add remote parent span context, + const ctxWithSpanContext = trace.setSpanContext(context, spanContext); + + // Also update the scope on the context (to be sure this is picked up everywhere) + const scopes = getScopesFromContext(ctxWithSpanContext); + const newScopes = { + scope: scopes ? scopes.scope.clone() : getCurrentScope().clone(), + isolationScope: scopes ? scopes.isolationScope : getIsolationScope(), + }; + newScopes.scope.setPropagationContext(propagationContext); + + return setScopesOnContext(ctxWithSpanContext, newScopes); } /** @@ -113,13 +118,91 @@ export class SentryPropagator extends W3CBaggagePropagator { } } -/** Get the DSC. */ +/** Exported for tests. */ +export function makeTraceState({ + parentSpanId, + dsc, +}: { parentSpanId?: string; dsc?: Partial }): TraceState | undefined { + if (!parentSpanId && !dsc) { + return undefined; + } + + // We store the DSC as OTEL trace state on the span context + const dscString = dsc ? dynamicSamplingContextToSentryBaggageHeader(dsc) : undefined; + + const traceStateBase = parentSpanId + ? new TraceState().set(SENTRY_TRACE_STATE_PARENT_SPAN_ID, parentSpanId) + : new TraceState(); + + return dscString ? traceStateBase.set(SENTRY_TRACE_STATE_DSC, dscString) : traceStateBase; +} + +function getInjectionData(context: Context): { + dynamicSamplingContext: Partial | undefined; + traceId: string | undefined; + spanId: string | undefined; + sampled: boolean | undefined; +} { + const span = trace.getSpan(context); + const spanIsRemote = span?.spanContext().isRemote; + + // If we have a local span, we can just pick everything from it + if (span && !spanIsRemote) { + const spanContext = span.spanContext(); + const propagationContext = getPropagationContextFromSpanContext(spanContext); + const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); + return { + dynamicSamplingContext, + traceId: spanContext.traceId, + spanId: spanContext.spanId, + sampled: spanContext.traceFlags === TraceFlags.SAMPLED, + }; + } + + // Else we try to use the propagation context from the scope + const scope = getScopesFromContext(context)?.scope; + if (scope) { + const propagationContext = scope.getPropagationContext(); + const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, propagationContext.traceId); + return { + dynamicSamplingContext, + traceId: propagationContext.traceId, + spanId: propagationContext.spanId, + sampled: propagationContext.sampled, + }; + } + + // Else, we look at the remote span context + const spanContext = trace.getSpanContext(context); + if (spanContext) { + const propagationContext = getPropagationContextFromSpanContext(spanContext); + const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); + + return { + dynamicSamplingContext, + traceId: spanContext.traceId, + spanId: spanContext.spanId, + sampled: spanContext.traceFlags === TraceFlags.SAMPLED, + }; + } + + // If we have neither, there is nothing much we can do, but that should not happen usually + // Unless there is a detached OTEL context being passed around + return { + dynamicSamplingContext: undefined, + traceId: undefined, + spanId: undefined, + sampled: undefined, + }; +} + +/** Get the DSC from a context, or fall back to use the one from the client. */ function getDynamicSamplingContext( propagationContext: PropagationContext, traceId: string | undefined, ): Partial | undefined { // If we have a DSC on the propagation context, we just use it - if (propagationContext.dsc) { + if (propagationContext?.dsc) { return propagationContext.dsc; } @@ -132,30 +215,3 @@ function getDynamicSamplingContext( return undefined; } - -/** Get the trace data for propagation. */ -function getSentryTraceData( - context: Context, - propagationContext: PropagationContext | undefined, -): { - spanId: string | undefined; - traceId: string | undefined; - sampled: boolean | undefined; -} { - const span = trace.getSpan(context); - const spanContext = span && span.spanContext(); - - const traceId = spanContext ? spanContext.traceId : propagationContext?.traceId; - - // We have a few scenarios here: - // If we have an active span, and it is _not_ remote, we just use the span's ID - // If we have an active span that is remote, we do not want to use the spanId, as we don't want to attach it to the parent span - // If `isRemote === true`, the span is bascially virtual - // If we don't have a local active span, we use the generated spanId from the propagationContext - const spanId = spanContext && !spanContext.isRemote ? spanContext.spanId : propagationContext?.spanId; - - // eslint-disable-next-line no-bitwise - const sampled = spanContext ? Boolean(spanContext.traceFlags & TraceFlags.SAMPLED) : propagationContext?.sampled; - - return { traceId, spanId, sampled }; -} diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index 820c35ce371d..1a7c019bda85 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -8,8 +8,8 @@ import type { Client, ClientOptions, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; +import { getPropagationContextFromSpanContext } from './propagator'; import { InternalSentrySemanticAttributes } from './semanticAttributes'; -import { getPropagationContextFromContext } from './utils/contextData'; /** * A custom OTEL sampler that uses Sentry sampling rates to make it's decision @@ -44,7 +44,7 @@ export class SentrySampler implements Sampler { // Note for testing: `isSpanContextValid()` checks the format of the traceId/spanId, so we need to pass valid ones if (parentContext && isSpanContextValid(parentContext) && parentContext.traceId === traceId) { if (parentContext.isRemote) { - parentSampled = getParentRemoteSampled(parentContext, context); + parentSampled = getParentRemoteSampled(parentContext); DEBUG_BUILD && logger.log(`[Tracing] Inheriting remote parent's sampled decision for ${spanName}: ${parentSampled}`); } else { @@ -178,10 +178,10 @@ function isValidSampleRate(rate: unknown): boolean { return true; } -function getParentRemoteSampled(spanContext: SpanContext, context: Context): boolean | undefined { +function getParentRemoteSampled(spanContext: SpanContext): boolean | undefined { const traceId = spanContext.traceId; - const traceparentData = getPropagationContextFromContext(context); + const traceparentData = getPropagationContextFromSpanContext(spanContext); - // Only inherit sample rate if `traceId` is the same + // Only inherit sampled if `traceId` is the same return traceparentData && traceId === traceparentData.traceId ? traceparentData.sampled : undefined; } diff --git a/packages/opentelemetry/src/utils/contextData.ts b/packages/opentelemetry/src/utils/contextData.ts index 85c31ee822bf..5cb5cac79e3e 100644 --- a/packages/opentelemetry/src/utils/contextData.ts +++ b/packages/opentelemetry/src/utils/contextData.ts @@ -1,31 +1,11 @@ import type { Context } from '@opentelemetry/api'; -import type { Hub, PropagationContext, Scope } from '@sentry/types'; +import type { Hub, Scope } from '@sentry/types'; -import { - SENTRY_HUB_CONTEXT_KEY, - SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, - SENTRY_SCOPES_CONTEXT_KEY, -} from '../constants'; +import { SENTRY_HUB_CONTEXT_KEY, SENTRY_SCOPES_CONTEXT_KEY } from '../constants'; import type { CurrentScopes } from '../types'; const SCOPE_CONTEXT_MAP = new WeakMap(); -/** - * Try to get the Propagation Context from the given OTEL context. - * This requires the SentryPropagator to be registered. - */ -export function getPropagationContextFromContext(context: Context): PropagationContext | undefined { - return context.getValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY) as PropagationContext | undefined; -} - -/** - * Set a Propagation Context on an OTEL context.. - * This will return a forked context with the Propagation Context set. - */ -export function setPropagationContextOnContext(context: Context, propagationContext: PropagationContext): Context { - return context.setValue(SENTRY_PROPAGATION_CONTEXT_CONTEXT_KEY, propagationContext); -} - /** * Try to get the Hub from the given OTEL context. * This requires a Context Manager that was wrapped with getWrappedContextManager. diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 86f16f994217..bbcbf7afa35e 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -1,8 +1,9 @@ import type { SpanContext } from '@opentelemetry/api'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; import { TraceFlags, context, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addBreadcrumb, getClient, setTag, withIsolationScope } from '@sentry/core'; -import type { Event, PropagationContext, TransactionEvent } from '@sentry/types'; +import type { Event, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; import { TraceState } from '@opentelemetry/core'; @@ -10,7 +11,6 @@ import { spanToJSON } from '@sentry/core'; import { SENTRY_TRACE_STATE_DSC } from '../../src/constants'; import { SentrySpanProcessor } from '../../src/spanProcessor'; import { startInactiveSpan, startSpan } from '../../src/trace'; -import { setPropagationContextOnContext } from '../../src/utils/contextData'; import type { TestClientInterface } from '../helpers/TestClient'; import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; @@ -343,29 +343,19 @@ describe('Integration | Transactions', () => { traceFlags: TraceFlags.SAMPLED, }; - const propagationContext: PropagationContext = { - traceId, - parentSpanId, - spanId: '6e0c63257de34c93', - sampled: true, - }; - mockSdkInit({ enableTracing: true, beforeSendTransaction }); const client = getClient() as TestClientInterface; // We simulate the correct context we'd normally get from the SentryPropagator - context.with( - trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), - () => { - startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, () => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); - - startSpan({ name: 'inner span 2' }, () => {}); - }); - }, - ); + context.with(trace.setSpanContext(ROOT_CONTEXT, spanContext), () => { + startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, () => { + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); + + startSpan({ name: 'inner span 2' }, () => {}); + }); + }); await client.flush(); @@ -554,36 +544,26 @@ describe('Integration | Transactions', () => { traceState: new TraceState().set(SENTRY_TRACE_STATE_DSC, dscString), }; - const propagationContext: PropagationContext = { - traceId, - parentSpanId, - spanId: '6e0c63257de34c93', - sampled: true, - }; - mockSdkInit({ enableTracing: true, beforeSendTransaction }); const client = getClient() as TestClientInterface; // We simulate the correct context we'd normally get from the SentryPropagator - context.with( - trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), - () => { - startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { - expect(span.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); + context.with(trace.setSpanContext(ROOT_CONTEXT, spanContext), () => { + startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + expect(span.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); - const subSpan = startInactiveSpan({ name: 'inner span 1' }); + const subSpan = startInactiveSpan({ name: 'inner span 1' }); - expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); + expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); - subSpan.end(); + subSpan.end(); - startSpan({ name: 'inner span 2' }, subSpan => { - expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); - }); + startSpan({ name: 'inner span 2' }, subSpan => { + expect(subSpan.spanContext().traceState?.get(SENTRY_TRACE_STATE_DSC)).toEqual(dscString); }); - }, - ); + }); + }); await client.flush(); diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index 27436117d885..50629e965713 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -1,18 +1,19 @@ import { ROOT_CONTEXT, TraceFlags, + context, defaultTextMapGetter, defaultTextMapSetter, propagation, trace, } from '@opentelemetry/api'; -import { TraceState, suppressTracing } from '@opentelemetry/core'; -import { addTracingExtensions, setCurrentClient } from '@sentry/core'; -import type { Client, PropagationContext } from '@sentry/types'; +import { suppressTracing } from '@opentelemetry/core'; +import { addTracingExtensions, withScope } from '@sentry/core'; -import { SENTRY_BAGGAGE_HEADER, SENTRY_TRACE_HEADER, SENTRY_TRACE_STATE_DSC } from '../src/constants'; -import { SentryPropagator } from '../src/propagator'; -import { getPropagationContextFromContext, setPropagationContextOnContext } from '../src/utils/contextData'; +import { SENTRY_BAGGAGE_HEADER, SENTRY_SCOPES_CONTEXT_KEY, SENTRY_TRACE_HEADER } from '../src/constants'; +import { SentryPropagator, makeTraceState } from '../src/propagator'; +import { getScopesFromContext } from '../src/utils/contextData'; +import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; beforeAll(() => { addTracingExtensions(); @@ -24,6 +25,16 @@ describe('SentryPropagator', () => { beforeEach(() => { carrier = {}; + mockSdkInit({ + environment: 'production', + release: '1.0.0', + enableTracing: true, + dsn: 'https://abc@domain/123', + }); + }); + + afterEach(() => { + cleanupOtel(); }); it('returns fields set', () => { @@ -31,289 +42,454 @@ describe('SentryPropagator', () => { }); describe('inject', () => { - const client = { - getOptions: () => ({ - environment: 'production', - release: '1.0.0', - }), - getDsn: () => ({ - publicKey: 'abc', - }), - emit: () => {}, - } as unknown as Client; - - setCurrentClient(client); - - describe('with active span', () => { + describe('without active local span', () => { it.each([ [ - 'works with a sampled propagation context', + 'uses remote spanContext without DSC for sampled remote span', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', spanId: '6e0c63257de34c92', traceFlags: TraceFlags.SAMPLED, - }, - { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c94', - parentSpanId: '6e0c63257de34c93', - sampled: true, - dsc: { - transaction: 'sampled-transaction', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', - sampled: 'true', - public_key: 'abc', - environment: 'production', - release: '1.0.0', - }, + isRemote: true, }, [ 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - 'sentry-transaction=sampled-transaction', - 'sentry-sampled=true', ], 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', ], [ - 'works with a DSC on the span trace state', + 'uses remote spanContext without DSC for unsampled remote span', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - traceState: new TraceState().set( - SENTRY_TRACE_STATE_DSC, - 'sentry-transaction=other-transaction,sentry-environment=other,sentry-release=8.0.0,sentry-public_key=public,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-sampled=true', - ), + traceFlags: TraceFlags.NONE, + isRemote: true, }, + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', + ], + [ + 'uses remote spanContext with DSC for sampled remote span', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c94', - parentSpanId: '6e0c63257de34c93', - sampled: true, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceState: makeTraceState({ + parentSpanId: '6e0c63257de34c92', + dsc: { + transaction: 'sampled-transaction', + sampled: 'true', + trace_id: 'dsc_trace_id', + public_key: 'dsc_public_key', + environment: 'dsc_environment', + release: 'dsc_release', + sample_rate: '0.5', + replay_id: 'dsc_replay_id', + }, + }), + isRemote: true, }, [ - 'sentry-environment=other', - 'sentry-release=8.0.0', - 'sentry-public_key=public', - 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - 'sentry-transaction=other-transaction', + 'sentry-environment=dsc_environment', + 'sentry-release=dsc_release', + 'sentry-public_key=dsc_public_key', + 'sentry-trace_id=dsc_trace_id', + 'sentry-transaction=sampled-transaction', 'sentry-sampled=true', + 'sentry-sample_rate=0.5', + 'sentry-replay_id=dsc_replay_id', ], 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', ], [ - 'works with an unsampled propagation context', + 'uses remote spanContext with DSC for unsampled remote span', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', spanId: '6e0c63257de34c92', traceFlags: TraceFlags.NONE, + traceState: makeTraceState({ + parentSpanId: '6e0c63257de34c92', + dsc: { + transaction: 'sampled-transaction', + sampled: 'false', + trace_id: 'dsc_trace_id', + public_key: 'dsc_public_key', + environment: 'dsc_environment', + release: 'dsc_release', + sample_rate: '0.5', + replay_id: 'dsc_replay_id', + }, + }), + isRemote: true, }, - { + [ + 'sentry-environment=dsc_environment', + 'sentry-release=dsc_release', + 'sentry-public_key=dsc_public_key', + 'sentry-trace_id=dsc_trace_id', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=false', + 'sentry-sample_rate=0.5', + 'sentry-replay_id=dsc_replay_id', + ], + 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', + ], + ])('%s', (_name, spanContext, baggage, sentryTrace) => { + const ctx = trace.setSpanContext(ROOT_CONTEXT, spanContext); + propagator.inject(ctx, carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); + }); + + it('uses scope propagation context without DSC if no span is found', () => { + withScope(scope => { + scope.setPropagationContext({ traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c94', parentSpanId: '6e0c63257de34c93', - sampled: false, + spanId: '6e0c63257de34c92', + sampled: true, + }); + + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'); + }); + }); + + it('uses scope propagation context with DSC if no span is found', () => { + withScope(scope => { + scope.setPropagationContext({ + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + parentSpanId: '6e0c63257de34c93', + spanId: '6e0c63257de34c92', + sampled: true, dsc: { - transaction: 'not-sampled-transaction', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + transaction: 'sampled-transaction', sampled: 'false', - public_key: 'abc', - environment: 'production', - release: '1.0.0', + trace_id: 'dsc_trace_id', + public_key: 'dsc_public_key', + environment: 'dsc_environment', + release: 'dsc_release', + sample_rate: '0.5', + replay_id: 'dsc_replay_id', }, + }); + + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-environment=dsc_environment', + 'sentry-release=dsc_release', + 'sentry-public_key=dsc_public_key', + 'sentry-trace_id=dsc_trace_id', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=false', + 'sentry-sample_rate=0.5', + 'sentry-replay_id=dsc_replay_id', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'); + }); + }); + + it('uses scope propagation context over remote spanContext', () => { + context.with( + trace.setSpanContext(ROOT_CONTEXT, { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + isRemote: true, + }), + () => { + withScope(scope => { + scope.setPropagationContext({ + traceId: 'TRACE_ID', + parentSpanId: 'PARENT_SPAN_ID', + spanId: 'SPAN_ID', + sampled: true, + }); + + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=TRACE_ID', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe('TRACE_ID-SPAN_ID-1'); + }); + }, + ); + }); + + it('creates random traceId & spanId if no scope & span is found', () => { + const ctx = trace.deleteSpan(ROOT_CONTEXT).deleteValue(SENTRY_SCOPES_CONTEXT_KEY); + propagator.inject(ctx, carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual([]); + expect(carrier[SENTRY_TRACE_HEADER]).toMatch(/^\w{32}-\w{16}$/); + }); + }); + + describe('with active span', () => { + it.each([ + [ + 'continues a remote trace without dsc', + { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + isRemote: true, }, [ 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - 'sentry-transaction=not-sampled-transaction', - 'sentry-sampled=false', ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', + 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-1', ], [ - 'creates a new DSC if none exists yet', + 'continues a remote trace with dsc', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', spanId: '6e0c63257de34c92', traceFlags: TraceFlags.SAMPLED, + isRemote: true, + traceState: makeTraceState({ + parentSpanId: '6e0c63257de34c92', + dsc: { + transaction: 'sampled-transaction', + sampled: 'true', + trace_id: 'dsc_trace_id', + public_key: 'dsc_public_key', + environment: 'dsc_environment', + release: 'dsc_release', + sample_rate: '0.5', + replay_id: 'dsc_replay_id', + }, + }), }, + [ + 'sentry-environment=dsc_environment', + 'sentry-release=dsc_release', + 'sentry-public_key=dsc_public_key', + 'sentry-trace_id=dsc_trace_id', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=true', + 'sentry-sample_rate=0.5', + 'sentry-replay_id=dsc_replay_id', + ], + 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-1', + ], + [ + 'continues an unsampled remote trace without dsc', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c94', - parentSpanId: '6e0c63257de34c93', - sampled: true, - dsc: undefined, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + isRemote: true, }, [ 'sentry-environment=production', - 'sentry-public_key=abc', 'sentry-release=1.0.0', + 'sentry-public_key=abc', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', + 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-0', ], [ - 'works with a remote parent span', + 'continues an unsampled remote trace with dsc', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, + traceFlags: TraceFlags.NONE, isRemote: true, + traceState: makeTraceState({ + parentSpanId: '6e0c63257de34c92', + dsc: { + transaction: 'sampled-transaction', + sampled: 'false', + trace_id: 'dsc_trace_id', + public_key: 'dsc_public_key', + environment: 'dsc_environment', + release: 'dsc_release', + sample_rate: '0.5', + replay_id: 'dsc_replay_id', + }, + }), }, + [ + 'sentry-environment=dsc_environment', + 'sentry-release=dsc_release', + 'sentry-public_key=dsc_public_key', + 'sentry-trace_id=dsc_trace_id', + 'sentry-transaction=sampled-transaction', + 'sentry-sampled=false', + 'sentry-sample_rate=0.5', + 'sentry-replay_id=dsc_replay_id', + ], + 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-0', + ], + [ + 'starts a new trace without existing dsc', { traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c94', - parentSpanId: '6e0c63257de34c93', - sampled: true, - dsc: { - transaction: 'sampled-transaction', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', - sampled: 'true', - public_key: 'abc', - environment: 'production', - release: '1.0.0', - }, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, }, [ 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - 'sentry-transaction=sampled-transaction', - 'sentry-sampled=true', ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c94-1', + 'd4cda95b652f4a1592b449d5929fda1b-{{spanId}}-1', ], - ])('%s', (_name, spanContext, propagationContext, baggage, sentryTrace) => { - const context = trace.setSpanContext( - setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), - spanContext, - ); - propagator.inject(context, carrier, defaultTextMapSetter); - expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); - expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); + ])('%s', (_name, spanContext, baggage, sentryTrace) => { + context.with(trace.setSpanContext(ROOT_CONTEXT, spanContext), () => { + trace.getTracer('test').startActiveSpan('test', span => { + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace.replace('{{spanId}}', span.spanContext().spanId)); + }); + }); }); - it('should include existing baggage', () => { - const propagationContext: PropagationContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - parentSpanId: '6e0c63257de34c93', - sampled: true, - dsc: { - transaction: 'sampled-transaction', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', - sampled: 'true', - public_key: 'abc', - environment: 'production', - release: '1.0.0', + it('uses local span over propagation context', () => { + context.with( + trace.setSpanContext(ROOT_CONTEXT, { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + isRemote: true, + }), + () => { + trace.getTracer('test').startActiveSpan('test', span => { + withScope(scope => { + scope.setPropagationContext({ + traceId: 'TRACE_ID', + parentSpanId: 'PARENT_SPAN_ID', + spanId: 'SPAN_ID', + sampled: true, + }); + + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe( + `d4cda95b652f4a1592b449d5929fda1b-${span.spanContext().spanId}-1`, + ); + }); + }); }, - }; - - const spanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - const context = trace.setSpanContext( - setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), - spanContext, ); - const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); - propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); - expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( - [ - 'foo=bar', - 'sentry-transaction=sampled-transaction', - 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - 'sentry-sampled=true', - 'sentry-public_key=abc', - 'sentry-environment=production', - 'sentry-release=1.0.0', - ].sort(), - ); - }); - it('should create baggage without propagation context', () => { - const spanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); - const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); - propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); - expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe('foo=bar'); - }); + context.with( + trace.setSpanContext(ROOT_CONTEXT, { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + isRemote: true, + }), + () => { + withScope(scope => { + scope.setPropagationContext({ + traceId: 'TRACE_ID', + parentSpanId: 'PARENT_SPAN_ID', + spanId: 'SPAN_ID', + sampled: true, + }); - it('should NOT set baggage and sentry-trace header if instrumentation is supressed', () => { - const spanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - const propagationContext: PropagationContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - parentSpanId: '6e0c63257de34c93', - sampled: true, - dsc: { - transaction: 'sampled-transaction', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', - sampled: 'true', - public_key: 'abc', - environment: 'production', - release: '1.0.0', + propagator.inject(context.active(), carrier, defaultTextMapSetter); + + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( + [ + 'sentry-environment=production', + 'sentry-release=1.0.0', + 'sentry-public_key=abc', + 'sentry-trace_id=TRACE_ID', + ].sort(), + ); + expect(carrier[SENTRY_TRACE_HEADER]).toBe('TRACE_ID-SPAN_ID-1'); + }); }, - }; - const context = suppressTracing( - trace.setSpanContext(setPropagationContextOnContext(ROOT_CONTEXT, propagationContext), spanContext), ); - propagator.inject(context, carrier, defaultTextMapSetter); - expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); - expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(undefined); }); }); - it('should take span from propagationContext id if no active span is found', () => { - const propagationContext: PropagationContext = { + it('should include existing baggage', () => { + const spanContext = { traceId: 'd4cda95b652f4a1592b449d5929fda1b', - parentSpanId: '6e0c63257de34c93', spanId: '6e0c63257de34c92', - sampled: true, - dsc: { - transaction: 'sampled-transaction', - trace_id: 'd4cda95b652f4a1592b449d5929fda1b', - sampled: 'true', - public_key: 'abc', - environment: 'production', - release: '1.0.0', - }, + traceFlags: TraceFlags.SAMPLED, }; - - const context = setPropagationContextOnContext(ROOT_CONTEXT, propagationContext); - propagator.inject(context, carrier, defaultTextMapSetter); + const context = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual( [ - 'sentry-transaction=sampled-transaction', + 'foo=bar', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - 'sentry-sampled=true', 'sentry-public_key=abc', 'sentry-environment=production', 'sentry-release=1.0.0', ].sort(), ); - expect(carrier[SENTRY_TRACE_HEADER]).toBe('d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'); + }); + + it('should create baggage without propagation context', () => { + const context = ROOT_CONTEXT; + const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); + propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe('foo=bar'); + }); + + it('should NOT set baggage and sentry-trace header if instrumentation is supressed', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + const context = suppressTracing(trace.setSpanContext(ROOT_CONTEXT, spanContext)); + propagator.inject(context, carrier, defaultTextMapSetter); + expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(undefined); }); }); describe('extract', () => { - it('sets sentry span context on the context', () => { + it('sets data from sentry trace header on span context', () => { const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); @@ -322,66 +498,98 @@ describe('SentryPropagator', () => { spanId: '6e0c63257de34c92', traceFlags: TraceFlags.SAMPLED, traceId: 'd4cda95b652f4a1592b449d5929fda1b', + traceState: makeTraceState({ parentSpanId: '6e0c63257de34c92' }), }); }); - it('sets defined sentry trace header on context', () => { + it('sets data from sentry trace header on scope', () => { const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - const propagationContext = getPropagationContextFromContext(context); - expect(propagationContext).toEqual({ - sampled: true, - parentSpanId: '6e0c63257de34c92', + const scopes = getScopesFromContext(context); + + expect(scopes).toBeDefined(); + expect(scopes?.scope.getPropagationContext()).toEqual({ spanId: expect.any(String), + sampled: true, traceId: 'd4cda95b652f4a1592b449d5929fda1b', - dsc: {}, // Frozen DSC + parentSpanId: '6e0c63257de34c92', + dsc: {}, }); - - // Ensure spanId !== parentSpanId - it should be a new random ID - expect(propagationContext?.spanId).not.toBe('6e0c63257de34c92'); }); - it('sets undefined sentry trace header on context', () => { + it('handles undefined sentry trace header', () => { const sentryTraceHeader = undefined; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(getPropagationContextFromContext(context)).toEqual({ + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, spanId: expect.any(String), + traceFlags: TraceFlags.NONE, traceId: expect.any(String), }); }); - it('sets defined dynamic sampling context on context', () => { + it('sets data from baggage header on span context', () => { const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; const baggage = 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(getPropagationContextFromContext(context)).toEqual({ - sampled: true, - parentSpanId: expect.any(String), + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + traceState: makeTraceState({ + parentSpanId: '6e0c63257de34c92', + dsc: { + environment: 'production', + release: '1.0.0', + public_key: 'abc', + trace_id: 'd4cda95b652f4a1592b449d5929fda1b', + transaction: 'dsc-transaction', + }, + }), + }); + }); + + it('sets data from baggage header on scope', () => { + const sentryTraceHeader = 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1'; + const baggage = + 'sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b,sentry-transaction=dsc-transaction'; + carrier[SENTRY_TRACE_HEADER] = sentryTraceHeader; + carrier[SENTRY_BAGGAGE_HEADER] = baggage; + const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); + + const scopes = getScopesFromContext(context); + + expect(scopes).toBeDefined(); + expect(scopes?.scope.getPropagationContext()).toEqual({ spanId: expect.any(String), - traceId: expect.any(String), // Note: This is not automatically taken from the DSC (in reality, this should be aligned) + sampled: true, + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + parentSpanId: '6e0c63257de34c92', dsc: { environment: 'production', - public_key: 'abc', release: '1.0.0', + public_key: 'abc', trace_id: 'd4cda95b652f4a1592b449d5929fda1b', transaction: 'dsc-transaction', }, }); }); - it('sets undefined dynamic sampling context on context', () => { + it('handles empty dsc baggage header', () => { const baggage = ''; carrier[SENTRY_BAGGAGE_HEADER] = baggage; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(getPropagationContextFromContext(context)).toEqual({ - sampled: undefined, + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, spanId: expect.any(String), + traceFlags: TraceFlags.NONE, traceId: expect.any(String), }); }); @@ -389,9 +597,10 @@ describe('SentryPropagator', () => { it('handles when sentry-trace is an empty array', () => { carrier[SENTRY_TRACE_HEADER] = []; const context = propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter); - expect(getPropagationContextFromContext(context)).toEqual({ - sampled: undefined, + expect(trace.getSpanContext(context)).toEqual({ + isRemote: true, spanId: expect.any(String), + traceFlags: TraceFlags.NONE, traceId: expect.any(String), }); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index b6967d654127..b71a5a15064e 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1,4 +1,5 @@ import type { Span, TimeInput } from '@opentelemetry/api'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { TraceFlags, context, trace } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; @@ -13,11 +14,10 @@ import { spanToJSON, withScope, } from '@sentry/core'; -import type { Event, PropagationContext, Scope } from '@sentry/types'; +import type { Event, Scope } from '@sentry/types'; import { startInactiveSpan, startSpan, startSpanManual } from '../src/trace'; import type { AbstractSpan } from '../src/types'; -import { setPropagationContextOnContext } from '../src/utils/contextData'; import { getActiveSpan, getRootSpan } from '../src/utils/getActiveSpan'; import { getSpanKind } from '../src/utils/getSpanKind'; import { getSpanMetadata } from '../src/utils/spanData'; @@ -1077,25 +1077,15 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.SAMPLED, }; - const propagationContext: PropagationContext = { - traceId, - sampled: true, - parentSpanId, - spanId: '6e0c63257de34c93', - }; - // We simulate the correct context we'd normally get from the SentryPropagator - context.with( - trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), - () => { - // This will def. be sampled because of the tracesSampleRate - startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeDefined(); - expect(outerSpan.isRecording()).toBe(true); - expect(getSpanName(outerSpan)).toBe('outer'); - }); - }, - ); + context.with(trace.setSpanContext(ROOT_CONTEXT, spanContext), () => { + // This will def. be sampled because of the tracesSampleRate + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(true); + expect(getSpanName(outerSpan)).toBe('outer'); + }); + }); }); it('negative remote parent sampling takes precedence over tracesSampleRate', () => { @@ -1114,24 +1104,14 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.NONE, }; - const propagationContext: PropagationContext = { - traceId, - sampled: false, - parentSpanId, - spanId: '6e0c63257de34c93', - }; - // We simulate the correct context we'd normally get from the SentryPropagator - context.with( - trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), - () => { - // This will def. be sampled because of the tracesSampleRate - startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan).toBeDefined(); - expect(outerSpan.isRecording()).toBe(false); - }); - }, - ); + context.with(trace.setSpanContext(ROOT_CONTEXT, spanContext), () => { + // This will def. be sampled because of the tracesSampleRate + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(outerSpan.isRecording()).toBe(false); + }); + }); }); it('samples with a tracesSampler returning a boolean', () => { @@ -1254,23 +1234,13 @@ describe('trace (sampling)', () => { traceFlags: TraceFlags.SAMPLED, }; - const propagationContext: PropagationContext = { - traceId, - sampled: true, - parentSpanId, - spanId: '6e0c63257de34c93', - }; - // We simulate the correct context we'd normally get from the SentryPropagator - context.with( - trace.setSpanContext(setPropagationContextOnContext(context.active(), propagationContext), spanContext), - () => { - // This will def. be sampled because of the tracesSampleRate - startSpan({ name: 'outer' }, outerSpan => { - expect(outerSpan.isRecording()).toBe(false); - }); - }, - ); + context.with(trace.setSpanContext(ROOT_CONTEXT, spanContext), () => { + // This will def. be sampled because of the tracesSampleRate + startSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan.isRecording()).toBe(false); + }); + }); expect(tracesSampler).toBeCalledTimes(1); expect(tracesSampler).toHaveBeenLastCalledWith({