Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1,
propagateTraceparent: true,
transport: loggingTransport,
});

setupOtel(client);
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
14 changes: 0 additions & 14 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export { addAutoIpAddressToSession } from './utils/ipAddress';
export { addAutoIpAddressToUser } from './utils/ipAddress';
export {
convertSpanLinksForEnvelope,
spanToTraceparentHeader,
spanToTraceHeader,
spanToJSON,
spanIsSampled,
Expand All @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/types-hoist/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,20 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*/
tracePropagationTargets?: TracePropagationTargets;

/**
* 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 set to `true`, the SDK will only continue a trace if the `organization ID` of the incoming trace found in the
* `baggage` header matches the `organization ID` of the current Sentry client.
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/utils/traceData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ export function getTraceData(
};

if (options.propagateTraceparent) {
const traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope);
if (traceparent) {
traceData.traceparent = traceparent;
}
traceData.traceparent = span ? spanToTraceparentHeader(span) : scopeToTraceparentHeader(scope);
}

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

function scopeToTraceparentHeader(scope: Scope): string {
/**
* Get a traceparent header value for the given scope.
*/
export function scopeToTraceparentHeader(scope: Scope): string {
const { traceId, sampled, propagationSpanId } = scope.getPropagationContext();
return generateTraceparentHeader(traceId, propagationSpanId, sampled);
}
21 changes: 18 additions & 3 deletions packages/node-core/src/integrations/http/outgoing-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function addRequestBreadcrumb(request: ClientRequest, response: IncomingM
* Add trace propagation headers to an outgoing request.
* This must be called _before_ the request is sent!
*/
// eslint-disable-next-line complexity
export function addTracePropagationHeadersToOutgoingRequest(
request: ClientRequest,
propagationDecisionMap: LRUMap<string, boolean>,
Expand All @@ -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')) {
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
* This method is called when a request is created.
* You can still mutate the request here before it is sent.
*/
// eslint-disable-next-line complexity
private _onRequestCreated({ request }: { request: UndiciRequest }): void {
const config = this.getConfig();
const enabled = config.enabled !== false;
Expand All @@ -137,16 +138,16 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
// Which we do not have in this case
// The propagator _may_ overwrite this, but this should be fine as it is the same data
const tracePropagationTargets = getClient()?.getOptions().tracePropagationTargets;
const { tracePropagationTargets, propagateTraceparent } = getClient()?.getOptions() || {};
const addedHeaders = shouldPropagateTraceForUrl(url, tracePropagationTargets, this._propagationDecisionMap)
? getTraceData()
? getTraceData({ propagateTraceparent })
: undefined;

if (!addedHeaders) {
return;
}

const { 'sentry-trace': sentryTrace, baggage } = addedHeaders;
const { 'sentry-trace': sentryTrace, baggage, traceparent } = addedHeaders;

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

if (traceparent && !requestHeaders.includes('traceparent')) {
requestHeaders.push('traceparent', traceparent);
}

// For baggage, we make sure to merge this into a possibly existing header
const existingBaggagePos = requestHeaders.findIndex(header => header === SENTRY_BAGGAGE_HEADER);
if (baggage && existingBaggagePos === -1) {
Expand All @@ -177,6 +182,10 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase<SentryNo
request.headers += `${SENTRY_TRACE_HEADER}: ${sentryTrace}\r\n`;
}

if (traceparent && !requestHeaders.includes('traceparent:')) {
request.headers += `traceparent: ${traceparent}\r\n`;
}

const existingBaggage = request.headers.match(BAGGAGE_HEADER_REGEX)?.[1];
if (baggage && !existingBaggage) {
request.headers += `${SENTRY_BAGGAGE_HEADER}: ${baggage}\r\n`;
Expand Down
34 changes: 27 additions & 7 deletions packages/opentelemetry/src/utils/getTraceData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,51 @@ import {
dynamicSamplingContextToSentryBaggageHeader,
generateSentryTraceHeader,
getCapturedScopesOnSpan,
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({
span,
scope,
client,
}: { span?: Span; scope?: Scope; client?: Client } = {}): SerializedTraceData {
export function getTraceData(options: { span?: Span; scope?: Scope; client?: Client; propagateTraceparent?: boolean } = {}): SerializedTraceData {
const { client, propagateTraceparent } = options;
let { span, scope } = options;

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 });

return {
const traceData: SerializedTraceData = {
'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled),
baggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext),
};

if (propagateTraceparent) {
if (span) {
traceData.traceparent = spanToTraceparentHeader(span);
} else if (scope) {
traceData.traceparent = scopeToTraceparentHeader(scope);
}
}

return traceData;
}
Loading