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-core-integration-tests/suites/tracing/requests/traceparent/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/instrument.mjs new file mode 100644 index 000000000000..bd16f7b0315c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/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/traceparent/scenario-fetch.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.mjs new file mode 100644 index 000000000000..cda662214b4d --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/traceparent/scenario-fetch.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/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(); + }); + }); +}); 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(); + }); + }); +}); 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..5e414f76c341 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -269,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/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