diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 8d964f033739..20991fe0132b 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -540,6 +540,13 @@ export class Scope implements ScopeInterface { return this; } + /** + * @inheritdoc + */ + public getPropagationContext(): PropagationContext { + return this._propagationContext; + } + /** * This will be called after {@link applyToEvent} is finished. */ diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index a0d2716fda49..79ef58f7be2a 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -1,6 +1,6 @@ export { startIdleTransaction, addTracingExtensions } from './hubextensions'; export { IdleTransaction, TRACING_DEFAULTS } from './idletransaction'; -export { Span, spanStatusfromHttpCode } from './span'; +export { Span, spanStatusfromHttpCode, spanContextToTraceparent } from './span'; export { Transaction } from './transaction'; export { extractTraceparentData, getActiveTransaction } from './utils'; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 59a8ed11d68a..62f41c8016fb 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -265,11 +265,7 @@ export class Span implements SpanInterface { * @inheritDoc */ public toTraceparent(): string { - let sampledString = ''; - if (this.sampled !== undefined) { - sampledString = this.sampled ? '-1' : '-0'; - } - return `${this.traceId}-${this.spanId}${sampledString}`; + return spanContextToTraceparent(this.traceId, this.spanId, this.sampled); } /** @@ -438,3 +434,13 @@ export function spanStatusfromHttpCode(httpStatus: number): SpanStatusType { return 'unknown_error'; } + +/** Generate sentry-trace header from span context */ +export function spanContextToTraceparent(traceId: string, maybeSpanId?: string, sampled?: boolean): string { + let sampledString = ''; + if (sampled !== undefined) { + sampledString = sampled ? '-1' : '-0'; + } + const spanId = maybeSpanId || uuid4().substring(16); + return `${traceId}-${spanId}${sampledString}`; +} diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 38f920283b7e..b033e4b7fa6e 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,5 +1,6 @@ import type { Hub } from '@sentry/core'; -import type { EventProcessor, Integration } from '@sentry/types'; +import { spanContextToTraceparent } from '@sentry/core'; +import type { EventProcessor, Integration, Span } from '@sentry/types'; import { dynamicRequire, dynamicSamplingContextToSentryBaggageHeader, @@ -12,7 +13,13 @@ import { LRUMap } from 'lru_map'; import type { NodeClient } from '../../client'; import { NODE_VERSION } from '../../nodeVersion'; import { isSentryRequest } from '../utils/http'; -import type { DiagnosticsChannel, RequestCreateMessage, RequestEndMessage, RequestErrorMessage } from './types'; +import type { + DiagnosticsChannel, + RequestCreateMessage, + RequestEndMessage, + RequestErrorMessage, + RequestWithSentry, +} from './types'; export enum ChannelName { // https://github.com/nodejs/undici/blob/e6fc80f809d1217814c044f52ed40ef13f21e43c/docs/api/DiagnosticsChannel.md#undicirequestcreate @@ -124,7 +131,6 @@ export class Undici implements Integration { const { request } = message as RequestCreateMessage; const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - const url = parseUrl(stringUrl); if (isSentryRequest(stringUrl) || request.__sentry__ !== undefined) { return; @@ -133,52 +139,45 @@ export class Undici implements Integration { const client = hub.getClient(); const scope = hub.getScope(); - const activeSpan = scope.getSpan(); + const parentSpan = scope.getSpan(); - if (activeSpan && client) { + if (client) { const clientOptions = client.getOptions(); - if (shouldCreateSpan(stringUrl)) { - const method = request.method || 'GET'; - const data: Record = { - 'http.method': method, - }; - if (url.search) { - data['http.query'] = url.search; + const shouldAttachTraceData = (url: string): boolean => { + if (clientOptions.tracePropagationTargets === undefined) { + return true; } - if (url.hash) { - data['http.fragment'] = url.hash; - } - const span = activeSpan.startChild({ - op: 'http.client', - description: `${method} ${getSanitizedUrlString(url)}`, - data, - }); - request.__sentry__ = span; - const shouldAttachTraceData = (url: string): boolean => { - if (clientOptions.tracePropagationTargets === undefined) { - return true; - } + const cachedDecision = this._headersUrlMap.get(url); + if (cachedDecision !== undefined) { + return cachedDecision; + } - const cachedDecision = this._headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } + const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); + this._headersUrlMap.set(url, decision); + return decision; + }; - const decision = stringMatchesSomePattern(url, clientOptions.tracePropagationTargets); - this._headersUrlMap.set(url, decision); - return decision; - }; + const span = shouldCreateSpan(stringUrl) ? createRequestSpan(request, stringUrl, parentSpan) : undefined; + if (span) { + request.__sentry__ = span; + } - if (shouldAttachTraceData(stringUrl)) { + if (shouldAttachTraceData(stringUrl)) { + if (span) { request.addHeader('sentry-trace', span.toTraceparent()); - if (span.transaction) { - const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - if (sentryBaggageHeader) { - request.addHeader('baggage', sentryBaggageHeader); - } + const dynamicSamplingContext = span.transaction?.getDynamicSamplingContext(); + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + if (sentryBaggageHeader) { + request.addHeader('baggage', sentryBaggageHeader); + } + } else { + const { traceId, sampled, dsc } = scope.getPropagationContext(); + request.addHeader('sentry-trace', spanContextToTraceparent(traceId, undefined, sampled)); + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dsc); + if (sentryBaggageHeader) { + request.addHeader('baggage', sentryBaggageHeader); } } } @@ -265,3 +264,24 @@ export class Undici implements Integration { }); } } + +/** */ +function createRequestSpan(request: RequestWithSentry, stringUrl: string, parentSpan?: Span): Span | undefined { + const url = parseUrl(stringUrl); + const method = request.method || 'GET'; + const data: Record = { + 'http.method': method, + }; + if (url.search) { + data['http.query'] = url.search; + } + if (url.hash) { + data['http.fragment'] = url.hash; + } + const span = parentSpan?.startChild({ + op: 'http.client', + description: `${method} ${getSanitizedUrlString(url)}`, + data, + }); + return span; +} diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 1351d2ce3161..2e3030b48415 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -188,6 +188,11 @@ export interface Scope { */ setSDKProcessingMetadata(newData: { [key: string]: unknown }): this; + /** + * Returns propagation context on the scope. + */ + getPropagationContext(): PropagationContext; + /** * Add propagation context to the scope, used for distributed tracing */ diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts index 406ee3adc819..7826b011744d 100644 --- a/packages/utils/src/baggage.ts +++ b/packages/utils/src/baggage.ts @@ -83,8 +83,11 @@ export function baggageHeaderToDynamicSamplingContext( */ export function dynamicSamplingContextToSentryBaggageHeader( // this also takes undefined for convenience and bundle size in other places - dynamicSamplingContext: Partial, + dynamicSamplingContext?: Partial, ): string | undefined { + if (!dynamicSamplingContext) { + return undefined; + } // Prefix all DSC keys with "sentry-" and put them into a new object const sentryPrefixedDSC = Object.entries(dynamicSamplingContext).reduce>( (acc, [dscKey, dscValue]) => {