From 9c2ebf32f24e4b47d3d905a97abfe3bcb351a7e1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 11 Dec 2025 15:02:42 +0100 Subject: [PATCH 1/5] feat(node): Support `propagateTraceparent` --- .../requests/fetch-sampled/instrument.mjs | 13 ++++++++ .../requests/fetch-sampled/scenario.mjs | 10 +++++++ .../tracing/requests/fetch-sampled/test.ts | 29 ++++++++++++++++++ packages/browser/src/client.ts | 14 --------- packages/core/src/index.ts | 3 +- packages/core/src/types-hoist/options.ts | 14 +++++++++ packages/core/src/utils/traceData.ts | 10 +++---- .../integrations/http/outgoing-requests.ts | 21 +++++++++++-- .../SentryNodeFetchInstrumentation.ts | 15 ++++++++-- .../opentelemetry/src/utils/getTraceData.ts | 30 ++++++++++++------- 10 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs new file mode 100644 index 000000000000..bd16f7b0315c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + propagateTraceparent: true, + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs new file mode 100644 index 000000000000..cda662214b4d --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + }); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts new file mode 100644 index 000000000000..ec8b6a0b1290 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts @@ -0,0 +1,29 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch traceparent', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests are correctly instrumented when not sampled', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: {}, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 65fcdf24734a..4ffc85b07762 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -50,20 +50,6 @@ type BrowserSpecificOptions = BrowserClientReplayOptions & */ skipBrowserExtensionCheck?: boolean; - /** - * If set to `true`, the SDK propagates the W3C `traceparent` header to any outgoing requests, - * in addition to the `sentry-trace` and `baggage` headers. Use the {@link CoreOptions.tracePropagationTargets} - * option to control to which outgoing requests the header will be attached. - * - * **Important:** If you set this option to `true`, make sure that you configured your servers' - * CORS settings to allow the `traceparent` header. Otherwise, requests might get blocked. - * - * @see https://www.w3.org/TR/trace-context/ - * - * @default false - */ - propagateTraceparent?: boolean; - /** * If you use Spotlight by Sentry during development, use * this option to forward captured Sentry events to Spotlight. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 387ba0aba4a2..a38e841da6c1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -74,6 +74,7 @@ export { addAutoIpAddressToSession } from './utils/ipAddress'; export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, + spanToTraceparentHeader, spanToTraceHeader, spanToJSON, spanIsSampled, @@ -89,7 +90,7 @@ export { export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; -export { getTraceData } from './utils/traceData'; +export { getTraceData, scopeToTraceparentHeader } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; export { debounce } from './utils/debounce'; export { diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index c33d0107df5f..3d4ad7b67ea5 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -369,6 +369,20 @@ export interface ClientOptions, @@ -53,16 +54,16 @@ export function addTracePropagationHeadersToOutgoingRequest( // Manually add the trace headers, if it applies // Note: We do not use `propagation.inject()` here, because our propagator relies on an active span // Which we do not have in this case - const tracePropagationTargets = getClient()?.getOptions().tracePropagationTargets; + const { tracePropagationTargets, propagateTraceparent } = getClient()?.getOptions() || {}; const headersToAdd = shouldPropagateTraceForUrl(url, tracePropagationTargets, propagationDecisionMap) - ? getTraceData() + ? getTraceData({ propagateTraceparent }) : undefined; if (!headersToAdd) { return; } - const { 'sentry-trace': sentryTrace, baggage } = headersToAdd; + const { 'sentry-trace': sentryTrace, baggage, traceparent } = headersToAdd; // We do not want to overwrite existing header here, if it was already set if (sentryTrace && !request.getHeader('sentry-trace')) { @@ -79,6 +80,20 @@ export function addTracePropagationHeadersToOutgoingRequest( } } + if (traceparent && !request.getHeader('traceparent')) { + try { + request.setHeader('traceparent', traceparent); + DEBUG_BUILD && debug.log(INSTRUMENTATION_NAME, 'Added traceparent header to outgoing request'); + } catch (error) { + DEBUG_BUILD && + debug.error( + INSTRUMENTATION_NAME, + 'Failed to add traceparent header to outgoing request:', + isError(error) ? error.message : 'Unknown error', + ); + } + } + if (baggage) { // For baggage, we make sure to merge this into a possibly existing header const newBaggage = mergeBaggageHeaders(request.getHeader('baggage'), baggage); diff --git a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 3b7b745077be..f3bb8ca1e15a 100644 --- a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -114,6 +114,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase header === SENTRY_BAGGAGE_HEADER); if (baggage && existingBaggagePos === -1) { @@ -177,6 +182,10 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase Date: Fri, 12 Dec 2025 10:43:57 +0100 Subject: [PATCH 2/5] less otel changes --- .../opentelemetry/src/utils/getTraceData.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 4de14aab1bb3..7c25b6b11a6b 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -3,41 +3,51 @@ import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, - getActiveSpan, getCapturedScopesOnSpan, - getCurrentScope, scopeToTraceparentHeader, spanToTraceparentHeader, } from '@sentry/core'; import { getInjectionData } from '../propagator'; -import { getContextFromScope } from './contextData'; +import { getContextFromScope, getScopesFromContext } from './contextData'; /** * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData( - options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}, -): SerializedTraceData { - const span = options.span || getActiveSpan(); - const scope = options.scope || (span && getCapturedScopesOnSpan(span).scope) || getCurrentScope(); +export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData { + const { client, propagateTraceparent } = options; + let { span, scope } = options; - let ctx = getContextFromScope(scope) ?? api.context.active(); + let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { + const { scope } = getCapturedScopesOnSpan(span); // fall back to current context if for whatever reason we can't find the one of the span - ctx = getContextFromScope(scope) || api.trace.setSpan(api.context.active(), span); + ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); + } else { + span = api.trace.getSpan(ctx); } - const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client: options.client }); + if (!scope) { + const scopes = getScopesFromContext(ctx); + if (scopes) { + scope = scopes.scope; + } + } + + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); const traceData: SerializedTraceData = { 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), baggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), }; - if (options.propagateTraceparent) { - traceData.traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope); + if (propagateTraceparent) { + if (span) { + traceData.traceparent = spanToTraceparentHeader(span); + } else if (scope) { + traceData.traceparent = scopeToTraceparentHeader(scope); + } } return traceData; From c448386b6ed23029097aaf10f80878b2168ed0c1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 11:02:10 +0100 Subject: [PATCH 3/5] Revert --- packages/core/src/index.ts | 4 +-- packages/core/src/utils/traceData.ts | 5 +--- .../opentelemetry/src/utils/getTraceData.ts | 30 ++++++------------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a38e841da6c1..5e414f76c341 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -74,7 +74,6 @@ export { addAutoIpAddressToSession } from './utils/ipAddress'; export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, - spanToTraceparentHeader, spanToTraceHeader, spanToJSON, spanIsSampled, @@ -90,7 +89,7 @@ export { export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; -export { getTraceData, scopeToTraceparentHeader } from './utils/traceData'; +export { getTraceData } from './utils/traceData'; export { getTraceMetaTags } from './utils/meta'; export { debounce } from './utils/debounce'; export { @@ -270,6 +269,7 @@ export { generateSentryTraceHeader, propagationContextFromHeaders, shouldContinueTrace, + generateTraceparentHeader, } from './utils/tracing'; export { getSDKSource, isBrowserBundle } from './utils/env'; export type { SdkSource } from './utils/env'; diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index 22358d750f14..9958e2761960 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -72,10 +72,7 @@ function scopeToTraceHeader(scope: Scope): string { return generateSentryTraceHeader(traceId, propagationSpanId, sampled); } -/** - * Get a traceparent header value for the given scope. - */ -export function scopeToTraceparentHeader(scope: Scope): string { +function scopeToTraceparentHeader(scope: Scope): string { const { traceId, sampled, propagationSpanId } = scope.getPropagationContext(); return generateTraceparentHeader(traceId, propagationSpanId, sampled); } diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 7c25b6b11a6b..cae4059cf9e1 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -3,36 +3,28 @@ import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, + generateTraceparentHeader, getCapturedScopesOnSpan, - scopeToTraceparentHeader, - spanToTraceparentHeader, } from '@sentry/core'; import { getInjectionData } from '../propagator'; -import { getContextFromScope, getScopesFromContext } from './contextData'; +import { getContextFromScope } from './contextData'; /** * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData { - const { client, propagateTraceparent } = options; - let { span, scope } = options; - +export function getTraceData({ + span, + scope, + client, + propagateTraceparent, +}: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData { let ctx = (scope && getContextFromScope(scope)) ?? api.context.active(); if (span) { const { scope } = getCapturedScopesOnSpan(span); // fall back to current context if for whatever reason we can't find the one of the span ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); - } else { - span = api.trace.getSpan(ctx); - } - - if (!scope) { - const scopes = getScopesFromContext(ctx); - if (scopes) { - scope = scopes.scope; - } } const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client }); @@ -43,11 +35,7 @@ export function getTraceData(options: { span?: Span; scope?: Scope; client?: Cli }; if (propagateTraceparent) { - if (span) { - traceData.traceparent = spanToTraceparentHeader(span); - } else if (scope) { - traceData.traceparent = scopeToTraceparentHeader(scope); - } + traceData.traceparent = generateTraceparentHeader(traceId, spanId, sampled); } return traceData; From 89f1ee2b3d94be466c8f2e7075727638ac0e3650 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 11:38:43 +0100 Subject: [PATCH 4/5] Test http --- .../tracing/requests/fetch-sampled/test.ts | 29 ---------- .../instrument.mjs | 0 .../scenario-fetch.mjs} | 0 .../requests/traceparent/scenario-http.mjs | 21 +++++++ .../tracing/requests/traceparent/test.ts | 57 +++++++++++++++++++ 5 files changed, 78 insertions(+), 29 deletions(-) delete mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts rename dev-packages/node-core-integration-tests/suites/tracing/requests/{fetch-sampled => traceparent}/instrument.mjs (100%) rename dev-packages/node-core-integration-tests/suites/tracing/requests/{fetch-sampled/scenario.mjs => traceparent/scenario-fetch.mjs} (100%) create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts deleted file mode 100644 index ec8b6a0b1290..000000000000 --- a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect } from 'vitest'; -import { createEsmAndCjsTests } from '../../../../utils/runner'; -import { createTestServer } from '../../../../utils/server'; - -describe('outgoing fetch traceparent', () => { - createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { - test('outgoing fetch requests are correctly instrumented when not sampled', async () => { - expect.assertions(5); - - const [SERVER_URL, closeTestServer] = await createTestServer() - .get('/api/v1', headers => { - expect(headers['baggage']).toEqual(expect.any(String)); - expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); - expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); - expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); - }) - .start(); - - await createRunner() - .withEnv({ SERVER_URL }) - .expect({ - transaction: {}, - }) - .start() - .completed(); - closeTestServer(); - }); - }); -}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/instrument.mjs similarity index 100% rename from dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/instrument.mjs rename to dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/instrument.mjs diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs similarity index 100% rename from dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled/scenario.mjs rename to dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs new file mode 100644 index 000000000000..4ddeb450f76d --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +await Sentry.startSpan({ name: 'outer' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts new file mode 100644 index 000000000000..2cdb4cfd1aa7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/test.ts @@ -0,0 +1,57 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing traceparent', () => { + createEsmAndCjsTests(__dirname, 'scenario-fetch.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-http.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing http requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); From d9fa8ba63478ce0c6b99570fba8e6ad17ec14a88 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 12 Dec 2025 11:57:16 +0100 Subject: [PATCH 5/5] More tests and bump size limit --- .size-limit.js | 2 +- .../requests/traceparent/instrument.mjs | 10 ++++ .../requests/traceparent/scenario-fetch.mjs | 10 ++++ .../requests/traceparent/scenario-http.mjs | 21 +++++++ .../tracing/requests/traceparent/test.ts | 57 +++++++++++++++++++ 5 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs create mode 100644 dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts diff --git a/.size-limit.js b/.size-limit.js index 6e6ee0f68303..00b4bdbfd4d8 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -240,7 +240,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '160 KB', + limit: '161 KB', }, { name: '@sentry/node - without tracing', diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs new file mode 100644 index 000000000000..886377ef59e7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/instrument.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + propagateTraceparent: true, + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs new file mode 100644 index 000000000000..84203bb8843e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; + +async function run() { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs new file mode 100644 index 000000000000..35805293a797 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/scenario-http.mjs @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/node'; +import * as http from 'http'; + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +await Sentry.startSpan({ name: 'outer' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts new file mode 100644 index 000000000000..2cdb4cfd1aa7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts @@ -0,0 +1,57 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing traceparent', () => { + createEsmAndCjsTests(__dirname, 'scenario-fetch.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + + createEsmAndCjsTests(__dirname, 'scenario-http.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing http requests should get traceparent headers', async () => { + expect.assertions(5); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/)); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +});