Skip to content

Commit 9c2ebf3

Browse files
committed
feat(node): Support propagateTraceparent
1 parent d4301fd commit 9c2ebf3

File tree

10 files changed

+123
-36
lines changed

10 files changed

+123
-36
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import { setupOtel } from '../../../../utils/setupOtel.js';
4+
5+
const client = Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0',
8+
tracesSampleRate: 1,
9+
propagateTraceparent: true,
10+
transport: loggingTransport,
11+
});
12+
13+
setupOtel(client);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node-core';
2+
3+
async function run() {
4+
// Wrap in span that is not sampled
5+
await Sentry.startSpan({ name: 'outer' }, async () => {
6+
await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text());
7+
});
8+
}
9+
10+
run();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect } from 'vitest';
2+
import { createEsmAndCjsTests } from '../../../../utils/runner';
3+
import { createTestServer } from '../../../../utils/server';
4+
5+
describe('outgoing fetch traceparent', () => {
6+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
7+
test('outgoing fetch requests are correctly instrumented when not sampled', async () => {
8+
expect.assertions(5);
9+
10+
const [SERVER_URL, closeTestServer] = await createTestServer()
11+
.get('/api/v1', headers => {
12+
expect(headers['baggage']).toEqual(expect.any(String));
13+
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/));
14+
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
15+
expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-01$/));
16+
})
17+
.start();
18+
19+
await createRunner()
20+
.withEnv({ SERVER_URL })
21+
.expect({
22+
transaction: {},
23+
})
24+
.start()
25+
.completed();
26+
closeTestServer();
27+
});
28+
});
29+
});

packages/browser/src/client.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,6 @@ type BrowserSpecificOptions = BrowserClientReplayOptions &
5050
*/
5151
skipBrowserExtensionCheck?: boolean;
5252

53-
/**
54-
* If set to `true`, the SDK propagates the W3C `traceparent` header to any outgoing requests,
55-
* in addition to the `sentry-trace` and `baggage` headers. Use the {@link CoreOptions.tracePropagationTargets}
56-
* option to control to which outgoing requests the header will be attached.
57-
*
58-
* **Important:** If you set this option to `true`, make sure that you configured your servers'
59-
* CORS settings to allow the `traceparent` header. Otherwise, requests might get blocked.
60-
*
61-
* @see https://www.w3.org/TR/trace-context/
62-
*
63-
* @default false
64-
*/
65-
propagateTraceparent?: boolean;
66-
6753
/**
6854
* If you use Spotlight by Sentry during development, use
6955
* this option to forward captured Sentry events to Spotlight.

packages/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export { addAutoIpAddressToSession } from './utils/ipAddress';
7474
export { addAutoIpAddressToUser } from './utils/ipAddress';
7575
export {
7676
convertSpanLinksForEnvelope,
77+
spanToTraceparentHeader,
7778
spanToTraceHeader,
7879
spanToJSON,
7980
spanIsSampled,
@@ -89,7 +90,7 @@ export {
8990
export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope';
9091
export { parseSampleRate } from './utils/parseSampleRate';
9192
export { applySdkMetadata } from './utils/sdkMetadata';
92-
export { getTraceData } from './utils/traceData';
93+
export { getTraceData, scopeToTraceparentHeader } from './utils/traceData';
9394
export { getTraceMetaTags } from './utils/meta';
9495
export { debounce } from './utils/debounce';
9596
export {

packages/core/src/types-hoist/options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,20 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
369369
*/
370370
tracePropagationTargets?: TracePropagationTargets;
371371

372+
/**
373+
* If set to `true`, the SDK propagates the W3C `traceparent` header to any outgoing requests,
374+
* in addition to the `sentry-trace` and `baggage` headers. Use the {@link CoreOptions.tracePropagationTargets}
375+
* option to control to which outgoing requests the header will be attached.
376+
*
377+
* **Important:** If you set this option to `true`, make sure that you configured your servers'
378+
* CORS settings to allow the `traceparent` header. Otherwise, requests might get blocked.
379+
*
380+
* @see https://www.w3.org/TR/trace-context/
381+
*
382+
* @default false
383+
*/
384+
propagateTraceparent?: boolean;
385+
372386
/**
373387
* If set to `true`, the SDK will only continue a trace if the `organization ID` of the incoming trace found in the
374388
* `baggage` header matches the `organization ID` of the current Sentry client.

packages/core/src/utils/traceData.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ export function getTraceData(
5858
};
5959

6060
if (options.propagateTraceparent) {
61-
const traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope);
62-
if (traceparent) {
63-
traceData.traceparent = traceparent;
64-
}
61+
traceData.traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope);
6562
}
6663

6764
return traceData;
@@ -75,7 +72,10 @@ function scopeToTraceHeader(scope: Scope): string {
7572
return generateSentryTraceHeader(traceId, propagationSpanId, sampled);
7673
}
7774

78-
function scopeToTraceparentHeader(scope: Scope): string {
75+
/**
76+
* Get a traceparent header value for the given scope.
77+
*/
78+
export function scopeToTraceparentHeader(scope: Scope): string {
7979
const { traceId, sampled, propagationSpanId } = scope.getPropagationContext();
8080
return generateTraceparentHeader(traceId, propagationSpanId, sampled);
8181
}

packages/node-core/src/integrations/http/outgoing-requests.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function addRequestBreadcrumb(request: ClientRequest, response: IncomingM
4444
* Add trace propagation headers to an outgoing request.
4545
* This must be called _before_ the request is sent!
4646
*/
47+
// eslint-disable-next-line complexity
4748
export function addTracePropagationHeadersToOutgoingRequest(
4849
request: ClientRequest,
4950
propagationDecisionMap: LRUMap<string, boolean>,
@@ -53,16 +54,16 @@ export function addTracePropagationHeadersToOutgoingRequest(
5354
// Manually add the trace headers, if it applies
5455
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
5556
// Which we do not have in this case
56-
const tracePropagationTargets = getClient()?.getOptions().tracePropagationTargets;
57+
const { tracePropagationTargets, propagateTraceparent } = getClient()?.getOptions() || {};
5758
const headersToAdd = shouldPropagateTraceForUrl(url, tracePropagationTargets, propagationDecisionMap)
58-
? getTraceData()
59+
? getTraceData({ propagateTraceparent })
5960
: undefined;
6061

6162
if (!headersToAdd) {
6263
return;
6364
}
6465

65-
const { 'sentry-trace': sentryTrace, baggage } = headersToAdd;
66+
const { 'sentry-trace': sentryTrace, baggage, traceparent } = headersToAdd;
6667

6768
// We do not want to overwrite existing header here, if it was already set
6869
if (sentryTrace && !request.getHeader('sentry-trace')) {
@@ -79,6 +80,20 @@ export function addTracePropagationHeadersToOutgoingRequest(
7980
}
8081
}
8182

83+
if (traceparent && !request.getHeader('traceparent')) {
84+
try {
85+
request.setHeader('traceparent', traceparent);
86+
DEBUG_BUILD && debug.log(INSTRUMENTATION_NAME, 'Added traceparent header to outgoing request');
87+
} catch (error) {
88+
DEBUG_BUILD &&
89+
debug.error(
90+
INSTRUMENTATION_NAME,
91+
'Failed to add traceparent header to outgoing request:',
92+
isError(error) ? error.message : 'Unknown error',
93+
);
94+
}
95+
}
96+
8297
if (baggage) {
8398
// For baggage, we make sure to merge this into a possibly existing header
8499
const newBaggage = mergeBaggageHeaders(request.getHeader('baggage'), baggage);

packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
114114
* This method is called when a request is created.
115115
* You can still mutate the request here before it is sent.
116116
*/
117+
// eslint-disable-next-line complexity
117118
private _onRequestCreated({ request }: { request: UndiciRequest }): void {
118119
const config = this.getConfig();
119120
const enabled = config.enabled !== false;
@@ -137,16 +138,16 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
137138
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
138139
// Which we do not have in this case
139140
// The propagator _may_ overwrite this, but this should be fine as it is the same data
140-
const tracePropagationTargets = getClient()?.getOptions().tracePropagationTargets;
141+
const { tracePropagationTargets, propagateTraceparent } = getClient()?.getOptions() || {};
141142
const addedHeaders = shouldPropagateTraceForUrl(url, tracePropagationTargets, this._propagationDecisionMap)
142-
? getTraceData()
143+
? getTraceData({ propagateTraceparent })
143144
: undefined;
144145

145146
if (!addedHeaders) {
146147
return;
147148
}
148149

149-
const { 'sentry-trace': sentryTrace, baggage } = addedHeaders;
150+
const { 'sentry-trace': sentryTrace, baggage, traceparent } = addedHeaders;
150151

151152
// We do not want to overwrite existing headers here
152153
// If the core UndiciInstrumentation is registered, it will already have set the headers
@@ -159,6 +160,10 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
159160
requestHeaders.push(SENTRY_TRACE_HEADER, sentryTrace);
160161
}
161162

163+
if (traceparent && !requestHeaders.includes('traceparent')) {
164+
requestHeaders.push('traceparent', traceparent);
165+
}
166+
162167
// For baggage, we make sure to merge this into a possibly existing header
163168
const existingBaggagePos = requestHeaders.findIndex(header => header === SENTRY_BAGGAGE_HEADER);
164169
if (baggage && existingBaggagePos === -1) {
@@ -177,6 +182,10 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
177182
request.headers += `${SENTRY_TRACE_HEADER}: ${sentryTrace}\r\n`;
178183
}
179184

185+
if (traceparent && !requestHeaders.includes('traceparent:')) {
186+
request.headers += `traceparent: ${traceparent}\r\n`;
187+
}
188+
180189
const existingBaggage = request.headers.match(BAGGAGE_HEADER_REGEX)?.[1];
181190
if (baggage && !existingBaggage) {
182191
request.headers += `${SENTRY_BAGGAGE_HEADER}: ${baggage}\r\n`;

packages/opentelemetry/src/utils/getTraceData.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type { Client, Scope, SerializedTraceData, Span } from '@sentry/core';
33
import {
44
dynamicSamplingContextToSentryBaggageHeader,
55
generateSentryTraceHeader,
6+
getActiveSpan,
67
getCapturedScopesOnSpan,
8+
getCurrentScope,
9+
scopeToTraceparentHeader,
10+
spanToTraceparentHeader,
711
} from '@sentry/core';
812
import { getInjectionData } from '../propagator';
913
import { getContextFromScope } from './contextData';
@@ -12,23 +16,29 @@ import { getContextFromScope } from './contextData';
1216
* Otel-specific implementation of `getTraceData`.
1317
* @see `@sentry/core` version of `getTraceData` for more information
1418
*/
15-
export function getTraceData({
16-
span,
17-
scope,
18-
client,
19-
}: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData {
20-
let ctx = (scope && getContextFromScope(scope)) ?? api.context.active();
19+
export function getTraceData(
20+
options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {},
21+
): SerializedTraceData {
22+
const span = options.span || getActiveSpan();
23+
const scope = options.scope || (span && getCapturedScopesOnSpan(span).scope) || getCurrentScope();
24+
25+
let ctx = getContextFromScope(scope) ?? api.context.active();
2126

2227
if (span) {
23-
const { scope } = getCapturedScopesOnSpan(span);
2428
// fall back to current context if for whatever reason we can't find the one of the span
25-
ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span);
29+
ctx = getContextFromScope(scope) || api.trace.setSpan(api.context.active(), span);
2630
}
2731

28-
const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client });
32+
const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx, { scope, client: options.client });
2933

30-
return {
34+
const traceData: SerializedTraceData = {
3135
'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled),
3236
baggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext),
3337
};
38+
39+
if (options.propagateTraceparent) {
40+
traceData.traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope);
41+
}
42+
43+
return traceData;
3444
}

0 commit comments

Comments
 (0)