diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts new file mode 100644 index 000000000000..f1393c3cfc5b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/server.ts @@ -0,0 +1,35 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + integrations: [ + // TODO: This used to use the Express integration + ], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http.get('http://somewhere.not.sentry/').getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts index 2870f0a8b51c..071f02f83647 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/trace-header-assign/test.ts @@ -7,7 +7,7 @@ afterAll(() => { }); test('Should assign `sentry-trace` header which sets parent trace id of an outgoing request.', async () => { - const runner = createRunner(__dirname, '..', 'server.ts').start(); + const runner = createRunner(__dirname, 'server.ts').start(); const response = await runner.makeRequest('get', '/test/express', { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/scenario.ts new file mode 100644 index 000000000000..55b0a04c6784 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/scenario.ts @@ -0,0 +1,24 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +async function run(): Promise { + // Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented + await new Promise(resolve => setTimeout(resolve, 100)); + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts new file mode 100644 index 000000000000..2de3dcfea66b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-noSampleRate/test.ts @@ -0,0 +1,53 @@ +import { conditionalTest } from '../../../../utils'; +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +conditionalTest({ min: 18 })('outgoing fetch', () => { + test('outgoing fetch requests are correctly instrumented without tracesSampleRate', done => { + expect.assertions(15); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .ensureNoErrorOutput() + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start(done); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts new file mode 100644 index 000000000000..191797a10c15 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.ts @@ -0,0 +1,25 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +async function run(): Promise { + // Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented + await new Promise(resolve => setTimeout(resolve, 100)); + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts new file mode 100644 index 000000000000..40e14fe648f8 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts @@ -0,0 +1,48 @@ +import { conditionalTest } from '../../../../utils'; +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +conditionalTest({ min: 18 })('outgoing fetch', () => { + test('outgoing sampled fetch requests without active span are correctly instrumented', done => { + expect.assertions(11); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start(done); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled/scenario.ts new file mode 100644 index 000000000000..4d47e16fd42f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled/scenario.ts @@ -0,0 +1,25 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +async function run(): Promise { + await Sentry.startSpan({ name: 'test_span' }, async () => { + // Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented + await new Promise(resolve => setTimeout(resolve, 100)); + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled/test.ts new file mode 100644 index 000000000000..40d05d2131eb --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-sampled/test.ts @@ -0,0 +1,40 @@ +import { conditionalTest } from '../../../../utils'; +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +conditionalTest({ min: 18 })('outgoing fetch', () => { + test('outgoing sampled fetch requests are correctly instrumented', done => { + expect.assertions(11); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start(done); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts new file mode 100644 index 000000000000..91c38bf2b23a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.ts @@ -0,0 +1,28 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 0, + integrations: [], + transport: loggingTransport, +}); + +async function run(): Promise { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + // Since fetch is lazy loaded, we need to wait a bit until it's fully instrumented + await new Promise(resolve => setTimeout(resolve, 100)); + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + }); + + Sentry.captureException(new Error('foo')); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts new file mode 100644 index 000000000000..0b0ceeaa499c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts @@ -0,0 +1,48 @@ +import { conditionalTest } from '../../../../utils'; +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +conditionalTest({ min: 18 })('outgoing fetch', () => { + test('outgoing fetch requests are correctly instrumented when not sampled', done => { + expect.assertions(11); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start(done); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/scenario.ts new file mode 100644 index 000000000000..8213ddf7034e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/scenario.ts @@ -0,0 +1,39 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +import * as http from 'http'; + +async function run(): Promise { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + + Sentry.captureException(new Error('foo')); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts new file mode 100644 index 000000000000..570d9f4865a6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-noSampleRate/test.ts @@ -0,0 +1,50 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('outgoing http requests are correctly instrumented without tracesSampleRate', done => { + expect.assertions(15); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .ensureNoErrorOutput() + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.ts new file mode 100644 index 000000000000..e98a9e9aea80 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.ts @@ -0,0 +1,40 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +import * as http from 'http'; + +async function run(): Promise { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + + Sentry.captureException(new Error('foo')); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts new file mode 100644 index 000000000000..ee0151cd6297 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts @@ -0,0 +1,49 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('outgoing sampled http requests without active span are correctly instrumented', done => { + expect.assertions(15); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/scenario.ts new file mode 100644 index 000000000000..c346b617b9e6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/scenario.ts @@ -0,0 +1,20 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +import * as http from 'http'; + +Sentry.startSpan({ name: 'test_span' }, () => { + http.get(`${process.env.SERVER_URL}/api/v0`); + http.get(`${process.env.SERVER_URL}/api/v1`); + http.get(`${process.env.SERVER_URL}/api/v2`); + http.get(`${process.env.SERVER_URL}/api/v3`); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts new file mode 100644 index 000000000000..f3ad8bc5728e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-sampled/test.ts @@ -0,0 +1,41 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('outgoing sampled http requests are correctly instrumented', done => { + expect.assertions(15); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/scenario.ts new file mode 100644 index 000000000000..6b66b11b8ffb --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/scenario.ts @@ -0,0 +1,43 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 0, + integrations: [], + transport: loggingTransport, +}); + +import * as http from 'http'; + +async function run(): Promise { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + }); + + Sentry.captureException(new Error('foo')); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts new file mode 100644 index 000000000000..c860958622fa --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-unsampled/test.ts @@ -0,0 +1,49 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('outgoing http requests are correctly instrumented when not sampled', done => { + expect.assertions(15); + + createTestServer(done) + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + expect(headers['__requestUrl']).toBeUndefined(); + }) + .start() + .then(SERVER_URL => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .ignore('session', 'sessions') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts index e87e9f3df1bc..c43b5607ef52 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/test.ts @@ -2,16 +2,18 @@ import { createRunner } from '../../../utils/runner'; import { createTestServer } from '../../../utils/server'; test('HttpIntegration should instrument correct requests when tracePropagationTargets option is provided', done => { - expect.assertions(9); + expect.assertions(11); createTestServer(done) .get('/api/v0', headers => { - expect(typeof headers['baggage']).toBe('string'); - expect(typeof headers['sentry-trace']).toBe('string'); + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); }) .get('/api/v1', headers => { - expect(typeof headers['baggage']).toBe('string'); - expect(typeof headers['sentry-trace']).toBe('string'); + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); }) .get('/api/v2', headers => { expect(headers['baggage']).toBeUndefined(); diff --git a/packages/browser/test/unit/utils/lazyLoadIntegration.test.ts b/packages/browser/test/unit/utils/lazyLoadIntegration.test.ts index a9a86cbf4374..f2afefcbe9a2 100644 --- a/packages/browser/test/unit/utils/lazyLoadIntegration.test.ts +++ b/packages/browser/test/unit/utils/lazyLoadIntegration.test.ts @@ -64,7 +64,7 @@ describe('lazyLoadIntegration', () => { expect(global.document.querySelectorAll('script')).toHaveLength(0); }); - test('it injects a script tag if integration is not yet loaded xxx', async () => { + test('it injects a script tag if integration is not yet loaded', async () => { // @ts-expect-error For testing sake global.Sentry = { ...Sentry, diff --git a/packages/node/package.json b/packages/node/package.json index c711fc35b620..e4019a00e6e5 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -82,7 +82,7 @@ "@types/node": "^14.18.0" }, "optionalDependencies": { - "opentelemetry-instrumentation-fetch-node": "1.1.2" + "opentelemetry-instrumentation-fetch-node": "1.2.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 633f27c7b64c..89aa8d90b997 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -88,7 +88,7 @@ const _httpIntegration = ((options: HttpOptions = {}) => { return false; }, - requireParentforOutgoingSpans: true, + requireParentforOutgoingSpans: false, requireParentforIncomingSpans: false, requestHook: (span, req) => { addOriginToSpan(span, 'auto.http.otel.http'); @@ -101,10 +101,10 @@ const _httpIntegration = ((options: HttpOptions = {}) => { const scopes = getCapturedScopesOnSpan(span); - // Update the isolation scope, isolate this request const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); const scope = scopes.scope || getCurrentScope(); + // Update the isolation scope, isolate this request isolationScope.setSDKProcessingMetadata({ request: req }); const client = getClient(); diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts index e153d50b0180..0f056d09a4ea 100644 --- a/packages/opentelemetry/src/constants.ts +++ b/packages/opentelemetry/src/constants.ts @@ -6,6 +6,7 @@ export const SENTRY_BAGGAGE_HEADER = 'baggage'; export const SENTRY_TRACE_STATE_DSC = 'sentry.dsc'; export const SENTRY_TRACE_STATE_PARENT_SPAN_ID = 'sentry.parent_span_id'; export const SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING = 'sentry.sampled_not_recording'; +export const SENTRY_TRACE_STATE_URL = 'sentry.url'; export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index d96f19c16b7c..6640a0f4e0f1 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -4,6 +4,7 @@ import { TraceFlags, propagation, trace } from '@opentelemetry/api'; import { TraceState, W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { continueTrace } from '@sentry/core'; +import { hasTracingEnabled } from '@sentry/core'; import { getRootSpan } from '@sentry/core'; import { spanToJSON } from '@sentry/core'; import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, getIsolationScope } from '@sentry/core'; @@ -26,6 +27,7 @@ import { SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_PARENT_SPAN_ID, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, + SENTRY_TRACE_STATE_URL, } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; @@ -84,7 +86,8 @@ export class SentryPropagator extends W3CBaggagePropagator { } const activeSpan = trace.getSpan(context); - const url = activeSpan && spanToJSON(activeSpan).data?.[SemanticAttributes.HTTP_URL]; + const url = activeSpan && getCurrentURL(activeSpan); + const tracePropagationTargets = getClient()?.getOptions()?.tracePropagationTargets; if ( typeof url === 'string' && @@ -92,7 +95,10 @@ export class SentryPropagator extends W3CBaggagePropagator { !this._shouldInjectTraceData(tracePropagationTargets, url) ) { DEBUG_BUILD && - logger.log('[Tracing] Not injecting trace data for url because it does not matchTracePropagationTargets:', url); + logger.log( + '[Tracing] Not injecting trace data for url because it does not match tracePropagationTargets:', + url, + ); return; } @@ -120,7 +126,10 @@ export class SentryPropagator extends W3CBaggagePropagator { }, baggage); } - setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled)); + // We also want to avoid setting the default OTEL trace ID, if we get that for whatever reason + if (traceId && traceId !== '00000000000000000000000000000000') { + setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled)); + } super.inject(propagation.setBaggage(context, baggage), carrier, setter); } @@ -212,7 +221,7 @@ function getInjectionData(context: Context): { spanId: string | undefined; sampled: boolean | undefined; } { - const span = trace.getSpan(context); + const span = hasTracingEnabled() ? trace.getSpan(context) : undefined; const spanIsRemote = span?.spanContext().isRemote; // If we have a local span, we can just pick everything from it @@ -230,39 +239,15 @@ function getInjectionData(context: Context): { } // Else we try to use the propagation context from the scope - const scope = getScopesFromContext(context)?.scope; - if (scope) { - const propagationContext = scope.getPropagationContext(); - const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, propagationContext.traceId); - return { - dynamicSamplingContext, - traceId: propagationContext.traceId, - spanId: propagationContext.spanId, - sampled: propagationContext.sampled, - }; - } + const scope = getScopesFromContext(context)?.scope || getCurrentScope(); - // Else, we look at the remote span context - if (span) { - const spanContext = span.spanContext(); - const propagationContext = getPropagationContextFromSpan(span); - const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); - - return { - dynamicSamplingContext, - traceId: spanContext.traceId, - spanId: spanContext.spanId, - sampled: getSamplingDecision(spanContext), - }; - } - - // If we have neither, there is nothing much we can do, but that should not happen usually - // Unless there is a detached OTEL context being passed around + const propagationContext = scope.getPropagationContext(); + const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, propagationContext.traceId); return { - dynamicSamplingContext: undefined, - traceId: undefined, - spanId: undefined, - sampled: undefined, + dynamicSamplingContext, + traceId: propagationContext.traceId, + spanId: propagationContext.spanId, + sampled: propagationContext.sampled, }; } @@ -333,3 +318,27 @@ function getExistingBaggage(carrier: unknown): string | undefined { return undefined; } } + +/** + * It is pretty tricky to get access to the outgoing request URL of a request in the propagator. + * As we only have access to the context of the span to be sent and the carrier (=headers), + * but the span may be unsampled and thus have no attributes. + * + * So we use the following logic: + * 1. If we have an active span, we check if it has a URL attribute. + * 2. Else, if the active span has no URL attribute (e.g. it is unsampled), we check a special trace state (which we set in our sampler). + */ +function getCurrentURL(span: Span): string | undefined { + const urlAttribute = spanToJSON(span).data?.[SemanticAttributes.HTTP_URL]; + if (urlAttribute) { + return urlAttribute; + } + + // Also look at the traceState, which we may set in the sampler even for unsampled spans + const urlTraceState = span.spanContext().traceState?.get(SENTRY_TRACE_STATE_URL); + if (urlTraceState) { + return urlTraceState; + } + + return undefined; +} diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index e611d6a43395..024f802c5ae6 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -1,4 +1,5 @@ import type { Attributes, Context, Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; import { isSpanContextValid, trace } from '@opentelemetry/api'; import { TraceState } from '@opentelemetry/core'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; @@ -6,7 +7,7 @@ import { SamplingDecision } from '@opentelemetry/sdk-trace-base'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, hasTracingEnabled, sampleSpan } from '@sentry/core'; import type { Client, SpanAttributes } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from './constants'; +import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, SENTRY_TRACE_STATE_URL } from './constants'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { DEBUG_BUILD } from './debug-build'; @@ -30,19 +31,36 @@ export class SentrySampler implements Sampler { context: Context, traceId: string, spanName: string, - _spanKind: unknown, + spanKind: SpanKind, spanAttributes: SpanAttributes, _links: unknown, ): SamplingResult { const options = this._client.getOptions(); + const parentSpan = trace.getSpan(context); + const parentContext = parentSpan?.spanContext(); + + let traceState = parentContext?.traceState || new TraceState(); + + // We always keep the URL on the trace state, so we can access it in the propagator + const url = spanAttributes[SemanticAttributes.HTTP_URL]; + if (url && typeof url === 'string') { + traceState = traceState.set(SENTRY_TRACE_STATE_URL, url); + } + if (!hasTracingEnabled(options)) { - return { decision: SamplingDecision.NOT_RECORD }; + return { decision: SamplingDecision.NOT_RECORD, traceState }; } - const parentSpan = trace.getSpan(context); - const parentContext = parentSpan?.spanContext(); - const traceState = parentContext?.traceState || new TraceState(); + // If we have a http.client span that has no local parent, we never want to sample it + // but we want to leave downstream sampling decisions up to the server + if ( + spanKind === SpanKind.CLIENT && + spanAttributes[SemanticAttributes.HTTP_METHOD] && + (!parentSpan || parentContext?.isRemote) + ) { + return { decision: SamplingDecision.NOT_RECORD, traceState }; + } let parentSampled: boolean | undefined = undefined; @@ -94,6 +112,7 @@ export class SentrySampler implements Sampler { return { decision: SamplingDecision.RECORD_AND_SAMPLED, attributes, + traceState, }; } diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index da9f2d30fc98..0d84da98e4f6 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -8,7 +8,7 @@ import { trace, } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; -import { withScope } from '@sentry/core'; +import { getCurrentScope, withScope } from '@sentry/core'; import { SENTRY_BAGGAGE_HEADER, SENTRY_SCOPES_CONTEXT_KEY, SENTRY_TRACE_HEADER } from '../src/constants'; import { SentryPropagator, makeTraceState } from '../src/propagator'; @@ -40,134 +40,6 @@ describe('SentryPropagator', () => { describe('inject', () => { describe('without active local span', () => { - it.each([ - [ - 'uses remote spanContext without DSC for sampled remote span', - { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - isRemote: true, - }, - [ - 'sentry-environment=production', - 'sentry-release=1.0.0', - 'sentry-public_key=abc', - 'sentry-sampled=true', - 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', - ], - [ - 'uses remote spanContext without DSC for unsampled remote span', - { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.NONE, - isRemote: true, - }, - [ - 'sentry-environment=production', - 'sentry-release=1.0.0', - 'sentry-public_key=abc', - 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92', - ], - [ - 'uses remote spanContext with trace state & without DSC for unsampled remote span', - { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.NONE, - isRemote: true, - traceState: makeTraceState({ - sampled: false, - }), - }, - [ - 'sentry-environment=production', - 'sentry-release=1.0.0', - 'sentry-public_key=abc', - 'sentry-sampled=false', - 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', - ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', - ], - [ - 'uses remote spanContext with DSC for sampled remote span', - { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - traceState: makeTraceState({ - parentSpanId: '6e0c63257de34c92', - dsc: { - transaction: 'sampled-transaction', - sampled: 'true', - trace_id: 'dsc_trace_id', - public_key: 'dsc_public_key', - environment: 'dsc_environment', - release: 'dsc_release', - sample_rate: '0.5', - replay_id: 'dsc_replay_id', - }, - }), - isRemote: true, - }, - [ - 'sentry-environment=dsc_environment', - 'sentry-release=dsc_release', - 'sentry-public_key=dsc_public_key', - 'sentry-trace_id=dsc_trace_id', - 'sentry-transaction=sampled-transaction', - 'sentry-sampled=true', - 'sentry-sample_rate=0.5', - 'sentry-replay_id=dsc_replay_id', - ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1', - ], - [ - 'uses remote spanContext with DSC for unsampled remote span', - { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.NONE, - traceState: makeTraceState({ - parentSpanId: '6e0c63257de34c92', - dsc: { - transaction: 'sampled-transaction', - sampled: 'false', - trace_id: 'dsc_trace_id', - public_key: 'dsc_public_key', - environment: 'dsc_environment', - release: 'dsc_release', - sample_rate: '0.5', - replay_id: 'dsc_replay_id', - }, - }), - isRemote: true, - }, - [ - 'sentry-environment=dsc_environment', - 'sentry-release=dsc_release', - 'sentry-public_key=dsc_public_key', - 'sentry-trace_id=dsc_trace_id', - 'sentry-transaction=sampled-transaction', - 'sentry-sampled=false', - 'sentry-sample_rate=0.5', - 'sentry-replay_id=dsc_replay_id', - ], - 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-0', - ], - ])('%s', (_name, spanContext, baggage, sentryTrace) => { - const ctx = trace.setSpanContext(ROOT_CONTEXT, spanContext); - propagator.inject(ctx, carrier, defaultTextMapSetter); - - expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual(baggage.sort()); - expect(carrier[SENTRY_TRACE_HEADER]).toBe(sentryTrace); - }); - it('uses scope propagation context without DSC if no span is found', () => { withScope(scope => { scope.setPropagationContext({ @@ -261,12 +133,20 @@ describe('SentryPropagator', () => { ); }); - it('creates random traceId & spanId if no scope & span is found', () => { + it('uses propagation data from current scope if no scope & span is found', () => { + const scope = getCurrentScope(); + const traceId = scope.getPropagationContext().traceId; + const ctx = trace.deleteSpan(ROOT_CONTEXT).deleteValue(SENTRY_SCOPES_CONTEXT_KEY); propagator.inject(ctx, carrier, defaultTextMapSetter); - expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual([]); - expect(carrier[SENTRY_TRACE_HEADER]).toMatch(/^\w{32}-\w{16}$/); + expect(baggageToArray(carrier[SENTRY_BAGGAGE_HEADER])).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=abc', + 'sentry-release=1.0.0', + `sentry-trace_id=${traceId}`, + ]); + expect(carrier[SENTRY_TRACE_HEADER]).toMatch(traceId); }); }); @@ -641,10 +521,15 @@ describe('SentryPropagator', () => { }); it('should create baggage without propagation context', () => { + const scope = getCurrentScope(); + const traceId = scope.getPropagationContext().traceId; + const context = ROOT_CONTEXT; const baggage = propagation.createBaggage({ foo: { value: 'bar' } }); propagator.inject(propagation.setBaggage(context, baggage), carrier, defaultTextMapSetter); - expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe('foo=bar'); + expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe( + `foo=bar,sentry-environment=production,sentry-release=1.0.0,sentry-public_key=abc,sentry-trace_id=${traceId}`, + ); }); it('should NOT set baggage and sentry-trace header if instrumentation is supressed', () => { diff --git a/yarn.lock b/yarn.lock index 64bc3a06b8bb..2db9b7009ea8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23178,10 +23178,10 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -opentelemetry-instrumentation-fetch-node@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.1.2.tgz#ba18648b8e1273c5e801a1d9d7a5e4c6f1daf6df" - integrity sha512-w5KYAw/X/F0smj1v67VQUhnWusS6+0y/v7W/H5iY6s1Zf7uOCCm0fiffY8t2H+4wXui2AOjcfEM77S6AvSTAJg== +opentelemetry-instrumentation-fetch-node@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-fetch-node/-/opentelemetry-instrumentation-fetch-node-1.2.0.tgz#5beaad33b622f7021c61733af864fb505cd35626" + integrity sha512-aiSt/4ubOTyb1N5C2ZbGrBvaJOXIZhZvpRPYuUVxQJe27wJZqf/o65iPrqgLcgfeOLaQ8cS2Q+762jrYvniTrA== dependencies: "@opentelemetry/api" "^1.6.0" "@opentelemetry/instrumentation" "^0.43.0"