diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index a235b6a16b83..1084643584d6 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -55,6 +55,7 @@ export { getSentryRelease, getSpanDescendants, getSpanStatusFromHttpCode, + getTraceData, graphqlIntegration, hapiIntegration, httpIntegration, diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts deleted file mode 100644 index 42d50c9d865d..000000000000 --- a/packages/astro/src/server/meta.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getRootSpan, - spanToTraceHeader, -} from '@sentry/core'; -import type { Client, Scope, Span } from '@sentry/types'; -import { - TRACEPARENT_REGEXP, - dynamicSamplingContextToSentryBaggageHeader, - generateSentryTraceHeader, - logger, -} from '@sentry/utils'; - -/** - * Extracts the tracing data from the current span or from the client's scope - * (via transaction or propagation context) and renders the data to tags. - * - * This function creates two serialized tags: - * - `` - * - `` - * - * TODO: Extract this later on and export it from the Core or Node SDK - * - * @param span the currently active span - * @param client the SDK's client - * - * @returns an object with the two serialized tags - */ -export function getTracingMetaTags( - span: Span | undefined, - scope: Scope, - client: Client | undefined, -): { sentryTrace: string; baggage?: string } { - const { dsc, sampled, traceId } = scope.getPropagationContext(); - const rootSpan = span && getRootSpan(span); - - const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); - - const dynamicSamplingContext = rootSpan - ? getDynamicSamplingContextFromSpan(rootSpan) - : dsc - ? dsc - : client - ? getDynamicSamplingContextFromClient(traceId, client) - : undefined; - - const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - - const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); - if (!isValidSentryTraceHeader) { - logger.warn('Invalid sentry-trace data. Returning empty tag'); - } - - const validBaggage = isValidBaggageString(baggage); - if (!validBaggage) { - logger.warn('Invalid baggage data. Returning empty tag'); - } - - return { - sentryTrace: ``, - baggage: baggage && ``, - }; -} - -/** - * Tests string against baggage spec as defined in: - * - * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition - * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - * - * exported for testing - */ -export function isValidBaggageString(baggage?: string): boolean { - if (!baggage || !baggage.length) { - return false; - } - const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; - const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; - const spaces = '\\s*'; - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input - const baggageRegex = new RegExp( - `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, - ); - return baggageRegex.test(baggage); -} diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index adfac32843f8..6b668f462489 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -14,7 +14,7 @@ import type { Client, Scope, Span, SpanAttributes } from '@sentry/types'; import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; -import { getTracingMetaTags } from './meta'; +import { getTraceData } from '@sentry/node'; type MiddlewareOptions = { /** @@ -189,9 +189,17 @@ function addMetaTagToHead(htmlChunk: string, scope: Scope, client: Client, span? if (typeof htmlChunk !== 'string') { return htmlChunk; } + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(span, scope, client); + + if (!sentryTrace) { + return htmlChunk; + } + + const sentryTraceMeta = ``; + const baggageMeta = baggage && ``; + + const content = `
\n${sentryTraceMeta}`.concat(baggageMeta ? `\n${baggageMeta}` : '', '\n'); - const { sentryTrace, baggage } = getTracingMetaTags(span, scope, client); - const content = `\n${sentryTrace}\n${baggage}\n`; return htmlChunk.replace('', content); } diff --git a/packages/astro/test/integration/middleware/index.test.ts b/packages/astro/test/integration/middleware/index.test.ts index 3b12508feaa7..3c48086a2ee2 100644 --- a/packages/astro/test/integration/middleware/index.test.ts +++ b/packages/astro/test/integration/middleware/index.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { onRequest } from '../../../src/integration/middleware'; vi.mock('../../../src/server/meta', () => ({ - getTracingMetaTags: () => ({ + getTracingMetaTagValues: () => ({ sentryTrace: '', baggage: '', }), diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index a678fcceaee6..58405c8d1c12 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,12 +1,13 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Client, Span } from '@sentry/types'; -import { vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; vi.mock('../../src/server/meta', () => ({ - getTracingMetaTags: () => ({ + getTracingMetaTagValues: () => ({ sentryTrace: '', baggage: '', }), @@ -28,10 +29,18 @@ describe('sentryMiddleware', () => { setPropagationContext: vi.fn(), getSpan: getSpanMock, setSDKProcessingMetadata: setSDKProcessingMetadataMock, + getPropagationContext: () => ({}), } as any; }); vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock); vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); + vi.spyOn(SentryNode, 'getTraceData').mockImplementation(() => ({ + 'sentry-trace': '123', + baggage: 'abc', + })); + vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockImplementation(() => ({ + transaction: 'test', + })); }); const nextResult = Promise.resolve(new Response(null, { status: 200, headers: new Headers() })); diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index eee24075bdf8..95b2d553f2d4 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -20,6 +20,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 199013b959ff..287dbc26eeee 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -40,6 +40,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index a4a466fa5bb5..867abd8e4a6e 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1971bb8c94bd..5c21c8e484ed 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,6 +82,7 @@ export { } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; +export { getTraceData } from './utils/traceData'; export { DEFAULT_ENVIRONMENT } from './constants'; export { addBreadcrumb } from './breadcrumbs'; export { functionToStringIntegration } from './integrations/functiontostring'; diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts new file mode 100644 index 000000000000..abc05f449365 --- /dev/null +++ b/packages/core/src/utils/traceData.ts @@ -0,0 +1,89 @@ +import type { Client, Scope, Span } from '@sentry/types'; +import { + TRACEPARENT_REGEXP, + dynamicSamplingContextToSentryBaggageHeader, + generateSentryTraceHeader, + logger, +} from '@sentry/utils'; +import { getClient, getCurrentScope } from '../currentScopes'; +import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing'; +import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; + +type TraceData = { + 'sentry-trace'?: string; + baggage?: string; +}; + +/** + * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation + * context) and serializes it to `sentry-trace` and `baggage` values to strings. These values can be used to propagate + * a trace via our tracing Http headers or Html `` tags. + * + * This function also applies some validation to the generated sentry-trace and baggage values to ensure that + * only valid strings are returned. + * + * @param span a span to take the trace data from. By default, the currently active span is used. + * @param scope the scope to take trace data from By default, the active current scope is used. + * @param client the SDK's client to take trace data from. By default, the current client is used. + * + * @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(span?: Span, scope?: Scope, client?: Client): TraceData { + const clientToUse = client || getClient(); + const scopeToUse = scope || getCurrentScope(); + const spanToUse = span || getActiveSpan(); + + const { dsc, sampled, traceId } = scopeToUse.getPropagationContext(); + const rootSpan = spanToUse && getRootSpan(spanToUse); + + const sentryTrace = spanToUse ? spanToTraceHeader(spanToUse) : generateSentryTraceHeader(traceId, undefined, sampled); + + const dynamicSamplingContext = rootSpan + ? getDynamicSamplingContextFromSpan(rootSpan) + : dsc + ? dsc + : clientToUse + ? getDynamicSamplingContextFromClient(traceId, clientToUse) + : undefined; + + const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + + const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); + if (!isValidSentryTraceHeader) { + logger.warn('Invalid sentry-trace data. Cannot generate trace data'); + return {}; + } + + const validBaggage = isValidBaggageString(baggage); + if (!validBaggage) { + logger.warn('Invalid baggage data. Not returning "baggage" value'); + } + + return { + 'sentry-trace': sentryTrace, + ...(validBaggage && { baggage }), + }; +} + +/** + * Tests string against baggage spec as defined in: + * + * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition + * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 + * + * exported for testing + */ +export function isValidBaggageString(baggage?: string): boolean { + if (!baggage || !baggage.length) { + return false; + } + const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; + const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; + const spaces = '\\s*'; + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input + const baggageRegex = new RegExp( + `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, + ); + return baggageRegex.test(baggage); +} diff --git a/packages/astro/test/server/meta.test.ts b/packages/core/test/lib/utils/traceData.test.ts similarity index 65% rename from packages/astro/test/server/meta.test.ts rename to packages/core/test/lib/utils/traceData.test.ts index 8b65beaa4eaf..e757926ca30d 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -1,9 +1,7 @@ -import * as SentryCore from '@sentry/core'; -import { SentrySpan } from '@sentry/core'; -import type { Transaction } from '@sentry/types'; -import { vi } from 'vitest'; +import { SentrySpan, getTraceData } from '../../../src/'; +import * as SentryCoreTracing from '../../../src/tracing'; -import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; +import { isValidBaggageString } from '../../../src/utils/traceData'; const TRACE_FLAG_SAMPLED = 1; @@ -12,12 +10,6 @@ const mockedSpan = new SentrySpan({ spanId: '1234567890123456', sampled: true, }); -// eslint-disable-next-line deprecation/deprecation -mockedSpan.transaction = { - getDynamicSamplingContext: () => ({ - environment: 'production', - }), -} as Transaction; const mockedClient = {} as any; @@ -27,24 +19,24 @@ const mockedScope = { }), } as any; -describe('getTracingMetaTags', () => { - it('returns the tracing tags from the span, if it is provided', () => { +describe('getTraceData', () => { + it('returns the tracing data from the span, if a span is available', () => { { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ environment: 'production', }); - const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); + const tags = getTraceData(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ - sentryTrace: '', - baggage: '', + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=production', }); } }); it('returns propagationContext DSC data if no span is available', () => { - const tags = getTracingMetaTags( + const traceData = getTraceData( undefined, { getPropagationContext: () => ({ @@ -61,23 +53,20 @@ describe('getTracingMetaTags', () => { mockedClient, ); - expect(tags).toEqual({ - sentryTrace: expect.stringMatching( - //, - ), - baggage: - '', + expect(traceData).toEqual({ + 'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/), + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', }); }); - it('returns only the `sentry-trace` tag if no DSC is available', () => { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ + it('returns only the `sentry-trace` value if no DSC is available', () => { + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, }); - const tags = getTracingMetaTags( - // @ts-expect-error - only passing a partial span object + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties { isRecording: () => true, spanContext: () => { @@ -87,25 +76,24 @@ describe('getTracingMetaTags', () => { traceFlags: TRACE_FLAG_SAMPLED, }; }, - transaction: undefined, }, mockedScope, mockedClient, ); - expect(tags).toEqual({ - sentryTrace: '', + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', }); }); it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, }); - const tags = getTracingMetaTags( - // @ts-expect-error - only passing a partial span object + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties { isRecording: () => true, spanContext: () => { @@ -115,15 +103,35 @@ describe('getTracingMetaTags', () => { traceFlags: TRACE_FLAG_SAMPLED, }; }, - transaction: undefined, }, mockedScope, undefined, ); - expect(tags).toEqual({ - sentryTrace: '', + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', }); + expect('baggage' in traceData).toBe(false); + }); + + it('returns an empty object if the `sentry-trace` value is invalid', () => { + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties + { + isRecording: () => true, + spanContext: () => { + return { + traceId: '1234567890123456789012345678901+', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, + }, + mockedScope, + mockedClient, + ); + + expect(traceData).toEqual({}); }); }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index aa30c762d624..69b26bb1729a 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 73e94aa5f271..351f843d2c2d 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -20,6 +20,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 85d001b465e5..3aa519c055d1 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -95,6 +95,7 @@ export { getCurrentHub, getCurrentScope, getIsolationScope, + getTraceData, withScope, withIsolationScope, captureException, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 3a14771218e4..a74e5bb89dc0 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -51,6 +51,7 @@ export { getSentryRelease, getSpanDescendants, getSpanStatusFromHttpCode, + getTraceData, graphqlIntegration, hapiIntegration, httpIntegration, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 6a768627b5d2..a96fc15e35d2 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual,