Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
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://public@dsn.ingest.sentry.io/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,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`);
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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,
});
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -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`);
});
Original file line number Diff line number Diff line change
@@ -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$/));

Check failure on line 41 in dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) (TS 3.8) Integration Tests

suites/tracing/requests/traceparent/test.ts > outgoing traceparent > esm/cjs > esm > outgoing http requests should get traceparent headers

AssertionError: expected undefined to deeply equal StringMatching{…} - Expected: StringMatching /^00-([a-f\d]{32})-([a-f\d]{16})-01$/ + Received: undefined ❯ suites/tracing/requests/traceparent/test.ts:41:42 ❯ utils/server.ts:23:13 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ next ../../node_modules/express/lib/router/route.js:149:13 ❯ Route.dispatch ../../node_modules/express/lib/router/route.js:119:3 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ ../../node_modules/express/lib/router/index.js:284:15 ❯ router.process_params ../../node_modules/express/lib/router/index.js:346:12 ❯ next ../../node_modules/express/lib/router/index.js:280:10 ❯ expressInit ../../node_modules/express/lib/middleware/init.js:40:5

Check failure on line 41 in dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts

View workflow job for this annotation

GitHub Actions / Node (20) Integration Tests

suites/tracing/requests/traceparent/test.ts > outgoing traceparent > esm/cjs > esm > outgoing http requests should get traceparent headers

AssertionError: expected undefined to deeply equal StringMatching{…} - Expected: StringMatching /^00-([a-f\d]{32})-([a-f\d]{16})-01$/ + Received: undefined ❯ suites/tracing/requests/traceparent/test.ts:41:42 ❯ utils/server.ts:23:13 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ next ../../node_modules/express/lib/router/route.js:149:13 ❯ Route.dispatch ../../node_modules/express/lib/router/route.js:119:3 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ ../../node_modules/express/lib/router/index.js:284:15 ❯ Function.process_params ../../node_modules/express/lib/router/index.js:346:12 ❯ next ../../node_modules/express/lib/router/index.js:280:10 ❯ expressInit ../../node_modules/express/lib/middleware/init.js:40:5

Check failure on line 41 in dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts

View workflow job for this annotation

GitHub Actions / Node (18) Integration Tests

suites/tracing/requests/traceparent/test.ts > outgoing traceparent > esm/cjs > esm > outgoing http requests should get traceparent headers

AssertionError: expected undefined to deeply equal StringMatching{…} - Expected: StringMatching /^00-([a-f\d]{32})-([a-f\d]{16})-01$/ + Received: undefined ❯ suites/tracing/requests/traceparent/test.ts:41:42 ❯ utils/server.ts:23:13 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ next ../../node_modules/express/lib/router/route.js:149:13 ❯ Route.dispatch ../../node_modules/express/lib/router/route.js:119:3 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ ../../node_modules/express/lib/router/index.js:284:15 ❯ Function.process_params ../../node_modules/express/lib/router/index.js:346:12 ❯ next ../../node_modules/express/lib/router/index.js:280:10 ❯ expressInit ../../node_modules/express/lib/middleware/init.js:40:5

Check failure on line 41 in dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts

View workflow job for this annotation

GitHub Actions / Node (24) Integration Tests

suites/tracing/requests/traceparent/test.ts > outgoing traceparent > esm/cjs > esm > outgoing http requests should get traceparent headers

AssertionError: expected undefined to deeply equal StringMatching{…} - Expected: StringMatching /^00-([a-f\d]{32})-([a-f\d]{16})-01$/ + Received: undefined ❯ suites/tracing/requests/traceparent/test.ts:41:42 ❯ utils/server.ts:23:13 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ next ../../node_modules/express/lib/router/route.js:149:13 ❯ Route.dispatch ../../node_modules/express/lib/router/route.js:119:3 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ ../../node_modules/express/lib/router/index.js:284:15 ❯ router.process_params ../../node_modules/express/lib/router/index.js:346:12 ❯ next ../../node_modules/express/lib/router/index.js:280:10 ❯ expressInit ../../node_modules/express/lib/middleware/init.js:40:5

Check failure on line 41 in dev-packages/node-integration-tests/suites/tracing/requests/traceparent/test.ts

View workflow job for this annotation

GitHub Actions / Node (22) Integration Tests

suites/tracing/requests/traceparent/test.ts > outgoing traceparent > esm/cjs > esm > outgoing http requests should get traceparent headers

AssertionError: expected undefined to deeply equal StringMatching{…} - Expected: StringMatching /^00-([a-f\d]{32})-([a-f\d]{16})-01$/ + Received: undefined ❯ suites/tracing/requests/traceparent/test.ts:41:42 ❯ utils/server.ts:23:13 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ next ../../node_modules/express/lib/router/route.js:149:13 ❯ Route.dispatch ../../node_modules/express/lib/router/route.js:119:3 ❯ Layer.handle [as handle_request] ../../node_modules/express/lib/router/layer.js:95:5 ❯ ../../node_modules/express/lib/router/index.js:284:15 ❯ Function.process_params ../../node_modules/express/lib/router/index.js:346:12 ❯ next ../../node_modules/express/lib/router/index.js:280:10 ❯ expressInit ../../node_modules/express/lib/middleware/init.js:40:5
})
.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();
});
});
});
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
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export {
generateSentryTraceHeader,
propagationContextFromHeaders,
shouldContinueTrace,
generateTraceparentHeader,
} from './utils/tracing';
export { getSDKSource, isBrowserBundle } from './utils/env';
export type { SdkSource } from './utils/env';
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
5 changes: 1 addition & 4 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 Down
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
Loading
Loading