From 1aac4d0069eb3487404b382822c90743dd6197ba Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 18 Jun 2025 10:31:05 +0200 Subject: [PATCH] feat(core): Allow to pass `scope` & `client` to `getTraceData` In addition to `span`, this allows to use this with custom client setups as well. --- packages/core/src/utils/traceData.ts | 7 +++-- .../core/test/lib/utils/traceData.test.ts | 30 +++++++++++++++++++ packages/opentelemetry/src/propagator.ts | 12 +++++--- .../opentelemetry/src/utils/getTraceData.ts | 12 +++++--- .../test/utils/getTraceData.test.ts | 29 +++++++++++++++++- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index e9d284e5a2c5..ada865ed396d 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -1,5 +1,6 @@ import { getAsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../carrier'; +import type { Client } from '../client'; import { getClient, getCurrentScope } from '../currentScopes'; import { isEnabled } from '../exports'; import type { Scope } from '../scope'; @@ -22,8 +23,8 @@ import { getActiveSpan, spanToTraceHeader } from './spanUtils'; * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header * or meta tag name. */ -export function getTraceData(options: { span?: Span } = {}): SerializedTraceData { - const client = getClient(); +export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { + const client = options.client || getClient(); if (!isEnabled() || !client) { return {}; } @@ -34,7 +35,7 @@ export function getTraceData(options: { span?: Span } = {}): SerializedTraceData return acs.getTraceData(options); } - const scope = getCurrentScope(); + const scope = options.scope || getCurrentScope(); const span = options.span || getActiveSpan(); const sentryTrace = span ? spanToTraceHeader(span) : scopeToTraceHeader(scope); const dsc = span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromScope(client, scope); diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index 8d38bdb062d4..d10bf1e3b592 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -6,6 +6,7 @@ import { getIsolationScope, getMainCarrier, getTraceData, + Scope, SentrySpan, setAsyncContextStrategy, setCurrentClient, @@ -158,6 +159,35 @@ describe('getTraceData', () => { }); }); + it('allows to pass a scope & client directly', () => { + // this default client & scope should not be used! + setupClient(); + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampleRand: 0.44, + }); + + const options = getDefaultTestClientOptions({ + dsn: 'https://567@sentry.io/42', + tracesSampleRate: 1, + }); + const customClient = new TestClient(options); + + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + scope.setClient(customClient); + + const traceData = getTraceData({ client: customClient, scope }); + + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=production,sentry-public_key=567,sentry-trace_id=12345678901234567890123456789012', + ); + }); + it('returns propagationContext DSC data if no span is available', () => { setupClient(); diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index b5e493b31fd8..30429a490b14 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -2,7 +2,7 @@ import type { Baggage, Context, Span, SpanContext, TextMapGetter, TextMapSetter import { context, INVALID_TRACEID, propagation, trace, TraceFlags } from '@opentelemetry/api'; import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; -import type { continueTrace, DynamicSamplingContext, Options } from '@sentry/core'; +import type { Client, continueTrace, DynamicSamplingContext, Options, Scope } from '@sentry/core'; import { generateSentryTraceHeader, getClient, @@ -152,8 +152,12 @@ export function shouldPropagateTraceForUrl( /** * Get propagation injection data for the given context. + * The additional options can be passed to override the scope and client that is otherwise derived from the context. */ -export function getInjectionData(context: Context): { +export function getInjectionData( + context: Context, + options: { scope?: Scope; client?: Client } = {}, +): { dynamicSamplingContext: Partial | undefined; traceId: string | undefined; spanId: string | undefined; @@ -190,8 +194,8 @@ export function getInjectionData(context: Context): { // Else we try to use the propagation context from the scope // The only scenario where this should happen is when we neither have a span, nor an incoming trace - const scope = getScopesFromContext(context)?.scope || getCurrentScope(); - const client = getClient(); + const scope = options.scope || getScopesFromContext(context)?.scope || getCurrentScope(); + const client = options.client || getClient(); const propagationContext = scope.getPropagationContext(); const dynamicSamplingContext = client ? getDynamicSamplingContextFromScope(client, scope) : undefined; diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 88ab5a349cdf..3c41c4f949de 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -1,5 +1,5 @@ import * as api from '@opentelemetry/api'; -import type { SerializedTraceData, Span } from '@sentry/core'; +import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, @@ -12,8 +12,12 @@ import { getContextFromScope } from './contextData'; * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceData { - let ctx = api.context.active(); +export function getTraceData({ + span, + scope, + client, +}: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData { + let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { const { scope } = getCapturedScopesOnSpan(span); @@ -21,7 +25,7 @@ export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceDat ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); } - const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx); + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); return { 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), diff --git a/packages/opentelemetry/test/utils/getTraceData.test.ts b/packages/opentelemetry/test/utils/getTraceData.test.ts index a4fc5919e1b6..d94e86930619 100644 --- a/packages/opentelemetry/test/utils/getTraceData.test.ts +++ b/packages/opentelemetry/test/utils/getTraceData.test.ts @@ -1,9 +1,10 @@ import { context, trace } from '@opentelemetry/api'; -import { getCurrentScope, setAsyncContextStrategy } from '@sentry/core'; +import { getCurrentScope, Scope, setAsyncContextStrategy } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { getTraceData } from '../../src/utils/getTraceData'; import { makeTraceState } from '../../src/utils/makeTraceState'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import { getDefaultTestClientOptions, TestClient } from '../helpers/TestClient'; describe('getTraceData', () => { beforeEach(() => { @@ -52,6 +53,32 @@ describe('getTraceData', () => { }); }); + it('allows to pass a scope & client directly', () => { + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789099', + sampleRand: 0.44, + }); + + const customClient = new TestClient( + getDefaultTestClientOptions({ tracesSampleRate: 1, dsn: 'https://123@sentry.io/42' }), + ); + + // note: Right now, this only works properly if the scope is linked to a context + const scope = new Scope(); + scope.setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampleRand: 0.42, + }); + scope.setClient(customClient); + + const traceData = getTraceData({ client: customClient, scope }); + + expect(traceData['sentry-trace']).toMatch(/^12345678901234567890123456789012-[a-f0-9]{16}$/); + expect(traceData.baggage).toEqual( + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012', + ); + }); + it('returns propagationContext DSC data if no span is available', () => { getCurrentScope().setPropagationContext({ traceId: '12345678901234567890123456789012',