Skip to content

Commit 5a709df

Browse files
authored
fix(node): Do not emit fetch spans when tracing is disabled (#13003)
Ensure that TWP works with fetch, but we do not emit a span. Otherwise, if a user does not use our tracing functionality but adds their own fetch instrumentation, they get spans twice. The benefit we have with fetch is that fetch is instrumented via diagnostics channel, which means that this can be instrumented multiple times. So if we instrument it, on the plus side this does not interfere with a user instrumenting it again themselves if they want. However, with the previous implementation our integration would always emit spans. If a user is using our own sampler, this is "OK" because it will sample it out. But if a user does not use our sampler, the spans we emit there get sent unintentionally. With this change, we ensure to ignore requests if tracing is disabled. In order to make TWP work, we instead manually inject the propagator headers into the fetch request in this scenario. Part of #12984 Fixes #12969
1 parent c48d87c commit 5a709df

File tree

35 files changed

+554
-155
lines changed

35 files changed

+554
-155
lines changed

dev-packages/e2e-tests/test-applications/node-otel-sdk-node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "node-otel-sdk-trace",
2+
"name": "node-otel-sdk-node",
33
"version": "1.0.0",
44
"private": true,
55
"scripts": {

dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-event-proxy.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils';
22

33
startEventProxyServer({
44
port: 3031,
5-
proxyServerName: 'node-otel-sdk-trace',
5+
proxyServerName: 'node-otel-sdk-node',
66
});

dev-packages/e2e-tests/test-applications/node-otel-sdk-node/start-otel-proxy.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { startProxyServer } from '@sentry-internal/test-utils';
22

33
startProxyServer({
44
port: 3032,
5-
proxyServerName: 'node-otel-sdk-trace-otel',
5+
proxyServerName: 'node-otel-sdk-node-otel',
66
});

dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/errors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
33

44
test('Sends correct error event', async ({ baseURL }) => {
5-
const errorEventPromise = waitForError('node-otel-sdk-trace', event => {
5+
const errorEventPromise = waitForError('node-otel-sdk-node', event => {
66
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
77
});
88

dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { expect, test } from '@playwright/test';
22
import { waitForPlainRequest, waitForTransaction } from '@sentry-internal/test-utils';
33

44
test('Sends an API route transaction', async ({ baseURL }) => {
5-
const pageloadTransactionEventPromise = waitForTransaction('node-otel-sdk-trace', transactionEvent => {
5+
const pageloadTransactionEventPromise = waitForTransaction('node-otel-sdk-node', transactionEvent => {
66
return (
77
transactionEvent?.contexts?.trace?.op === 'http.server' &&
88
transactionEvent?.transaction === 'GET /test-transaction'
99
);
1010
});
1111

1212
// Ensure we also send data to the OTLP endpoint
13-
const otelPromise = waitForPlainRequest('node-otel-sdk-trace-otel', data => {
13+
const otelPromise = waitForPlainRequest('node-otel-sdk-node-otel', data => {
1414
const json = JSON.parse(data) as any;
1515

1616
return json.resourceSpans.length > 0;
@@ -129,7 +129,7 @@ test('Sends an API route transaction', async ({ baseURL }) => {
129129
});
130130

131131
test('Sends an API route transaction for an errored route', async ({ baseURL }) => {
132-
const transactionEventPromise = waitForTransaction('node-otel-sdk-trace', transactionEvent => {
132+
const transactionEventPromise = waitForTransaction('node-otel-sdk-node', transactionEvent => {
133133
return (
134134
transactionEvent.contexts?.trace?.op === 'http.server' &&
135135
transactionEvent.transaction === 'GET /test-exception/:id' &&
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "node-otel-without-tracing",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "tsc",
7+
"start": "node dist/app.js",
8+
"test": "playwright test",
9+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
10+
"test:build": "pnpm install && pnpm build",
11+
"test:assert": "pnpm test"
12+
},
13+
"dependencies": {
14+
"@opentelemetry/sdk-trace-node": "1.25.1",
15+
"@opentelemetry/exporter-trace-otlp-http": "0.52.1",
16+
"@opentelemetry/instrumentation-undici": "0.4.0",
17+
"@opentelemetry/instrumentation": "0.52.1",
18+
"@sentry/core": "latest || *",
19+
"@sentry/node": "latest || *",
20+
"@sentry/opentelemetry": "latest || *",
21+
"@sentry/types": "latest || *",
22+
"@types/express": "4.17.17",
23+
"@types/node": "18.15.1",
24+
"express": "4.19.2",
25+
"typescript": "4.9.5"
26+
},
27+
"devDependencies": {
28+
"@playwright/test": "^1.44.1",
29+
"@sentry-internal/test-utils": "link:../../../test-utils"
30+
},
31+
"volta": {
32+
"extends": "../../package.json"
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig(
4+
{
5+
startCommand: `pnpm start`,
6+
},
7+
{
8+
webServer: [
9+
{
10+
command: `node ./start-event-proxy.mjs`,
11+
port: 3031,
12+
stdout: 'pipe',
13+
stderr: 'pipe',
14+
},
15+
{
16+
command: `node ./start-otel-proxy.mjs`,
17+
port: 3032,
18+
stdout: 'pipe',
19+
stderr: 'pipe',
20+
},
21+
{
22+
command: 'pnpm start',
23+
port: 3030,
24+
stdout: 'pipe',
25+
stderr: 'pipe',
26+
env: {
27+
PORT: 3030,
28+
},
29+
},
30+
],
31+
},
32+
);
33+
34+
export default config;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import './instrument';
2+
3+
// Other imports below
4+
import * as Sentry from '@sentry/node';
5+
import express from 'express';
6+
7+
const app = express();
8+
const port = 3030;
9+
10+
app.get('/test-success', function (req, res) {
11+
res.send({ version: 'v1' });
12+
});
13+
14+
app.get('/test-param/:param', function (req, res) {
15+
res.send({ paramWas: req.params.param });
16+
});
17+
18+
app.get('/test-transaction', function (req, res) {
19+
Sentry.withActiveSpan(null, async () => {
20+
Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => {
21+
Sentry.startSpan({ name: 'test-span' }, () => undefined);
22+
});
23+
24+
await fetch('http://localhost:3030/test-success');
25+
26+
await Sentry.flush();
27+
28+
res.send({});
29+
});
30+
});
31+
32+
app.get('/test-error', async function (req, res) {
33+
const exceptionId = Sentry.captureException(new Error('This is an error'));
34+
35+
await Sentry.flush(2000);
36+
37+
res.send({ exceptionId });
38+
});
39+
40+
app.get('/test-exception/:id', function (req, _res) {
41+
throw new Error(`This is an exception with id ${req.params.id}`);
42+
});
43+
44+
Sentry.setupExpressErrorHandler(app);
45+
46+
app.use(function onError(err: unknown, req: any, res: any, next: any) {
47+
// The error id is attached to `res.sentry` to be returned
48+
// and optionally displayed to the user for support.
49+
res.statusCode = 500;
50+
res.end(res.sentry + '\n');
51+
});
52+
53+
app.listen(port, () => {
54+
console.log(`Example app listening on port ${port}`);
55+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const { NodeTracerProvider, BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node');
2+
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
3+
const Sentry = require('@sentry/node');
4+
const { SentrySpanProcessor, SentryPropagator } = require('@sentry/opentelemetry');
5+
const { UndiciInstrumentation } = require('@opentelemetry/instrumentation-undici');
6+
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
7+
8+
const sentryClient = Sentry.init({
9+
environment: 'qa', // dynamic sampling bias to keep transactions
10+
dsn:
11+
process.env.E2E_TEST_DSN ||
12+
'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576',
13+
includeLocalVariables: true,
14+
debug: !!process.env.DEBUG,
15+
tunnel: `http://localhost:3031/`, // proxy server
16+
// Tracing is completely disabled
17+
18+
// Custom OTEL setup
19+
skipOpenTelemetrySetup: true,
20+
});
21+
22+
// Create and configure NodeTracerProvider
23+
const provider = new NodeTracerProvider({});
24+
25+
provider.addSpanProcessor(
26+
new BatchSpanProcessor(
27+
new OTLPTraceExporter({
28+
url: 'http://localhost:3032/',
29+
}),
30+
),
31+
);
32+
33+
// Initialize the provider
34+
provider.register({
35+
propagator: new SentryPropagator(),
36+
contextManager: new Sentry.SentryContextManager(),
37+
});
38+
39+
registerInstrumentations({
40+
instrumentations: [new UndiciInstrumentation()],
41+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'node-otel-without-tracing',
6+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startProxyServer } from '@sentry-internal/test-utils';
2+
3+
startProxyServer({
4+
port: 3032,
5+
proxyServerName: 'node-otel-without-tracing-otel',
6+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test('Sends correct error event', async ({ baseURL }) => {
5+
const errorEventPromise = waitForError('node-otel-without-tracing', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
7+
});
8+
9+
await fetch(`${baseURL}/test-exception/123`);
10+
11+
const errorEvent = await errorEventPromise;
12+
13+
expect(errorEvent.exception?.values).toHaveLength(1);
14+
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
15+
16+
expect(errorEvent.request).toEqual({
17+
method: 'GET',
18+
cookies: {},
19+
headers: expect.any(Object),
20+
url: 'http://localhost:3030/test-exception/123',
21+
});
22+
23+
// This is unparametrized here because we do not have the express instrumentation
24+
expect(errorEvent.transaction).toEqual('GET /test-exception/123');
25+
26+
expect(errorEvent.contexts?.trace).toEqual({
27+
trace_id: expect.any(String),
28+
span_id: expect.any(String),
29+
});
30+
});

0 commit comments

Comments
 (0)