Skip to content

Commit 0248545

Browse files
mydeabillyvg
authored andcommitted
feat(node): Implement Sentry-specific http instrumentation (#13763)
This PR is a pretty big change, but it should help us to make custom OTEL support way better/easier to understand in the future. The fundamental problem this PR is trying to change is the restriction that we rely on our instance of `HttpInstrumentation` for Sentry-specific (read: not span-related) things. This made it tricky/annoying for users with a custom OTEL setup that may include `HttpInstrumentation` to get things working, because they may inadvertedly overwrite our instance of the instrumentation (because there can only be a single monkey-patch per module in the regular instrumentations), leading to hard-to-debug and often subtle problems. This PR fixes this by splitting out the non-span related http instrumentation code into a new, dedicated `SentryHttpInstrumentation`, which can be run side-by-side with the OTEL instrumentation (which emits spans, ...). We make this work by basically implementing our own custom, minimal `wrap` method instead of using shimmer. This way, OTEL instrumentation cannot identify the wrapped module as wrapped, and allow to wrap it again. While this is _slightly_ hacky and also means you cannot unwrap the http module, we do not generally support this for the Sentry SDK anyhow. This new Instrumentation does two things: 1. Handles automatic forking of the isolation scope per incoming request. By using our own code, we can actually make this much nicer, as we do not need to retrospectively update the isolation scope anymore, but instead we can do this properly now. 2. Emit breadcrumbs for outgoing requests. With this change, in errors only mode you really do not need our instance of the default `HttpInstrumentation` anymore at all, you can/should just provide your own if you want to capture http spans in a non-Sentry environment. However, this is sadly a bit tricky, because up to now we forced users in this scenario to still use our Http instance and avoid adding their own (instead we allowed users to pass their Http instrumentation config to our Http integration). This means that if we'd simply stop adding our http instrumentation instance when tracing is disabled, these users would stop getting otel spans as well :/ so we sadly can't change this without a major. Instead, I re-introduced the `spans: false` for `httpIntegration({ spans: false })`. When this is set (which for now is opt-in, but probably should be opt-out in v9) we will only register SentryHttpInstrumentation, not HttpInstrumentation, thus not emitting any spans. Users can add their own instance of HttpInstrumentation if they care. One semi-related thing that I noticed while looking into this is that we incorrectly emitted node-fetch spans in errors-only mode. This apparently sneaked in when we migrated to the new undici instrumentation. I extracted this out into a dedicated PR too, but the changes are in this too because tests were a bit fucked up otherwise. On top of #13765 This also includes a bump of import-in-the-middle to 1.11.2, as this includes a fix to properly allow double-wrapping ESM modules.
1 parent 89d748b commit 0248545

File tree

16 files changed

+557
-409
lines changed

16 files changed

+557
-409
lines changed

dev-packages/e2e-tests/test-applications/aws-lambda-layer-cjs/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
},
1515
"devDependencies": {
1616
"@sentry-internal/test-utils": "link:../../../test-utils",
17-
"@playwright/test": "^1.44.1",
18-
"wait-port": "1.0.4"
17+
"@playwright/test": "^1.44.1"
1918
},
2019
"volta": {
2120
"extends": "../../package.json"

dev-packages/e2e-tests/test-applications/aws-serverless-esm/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
},
1515
"devDependencies": {
1616
"@sentry-internal/test-utils": "link:../../../test-utils",
17-
"@playwright/test": "^1.41.1",
18-
"wait-port": "1.0.4"
17+
"@playwright/test": "^1.41.1"
1918
},
2019
"volta": {
2120
"extends": "../../package.json"

dev-packages/e2e-tests/test-applications/aws-serverless-esm/tests/basic.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ test('AWS Serverless SDK sends events in ESM mode', async ({ request }) => {
1818
);
1919

2020
child_process.execSync('pnpm start', {
21-
stdio: 'ignore',
21+
stdio: 'inherit',
2222
});
2323

2424
const transactionEvent = await transactionEventPromise;

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

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@opentelemetry/sdk-trace-node": "1.26.0",
1515
"@opentelemetry/exporter-trace-otlp-http": "0.53.0",
1616
"@opentelemetry/instrumentation-undici": "0.6.0",
17+
"@opentelemetry/instrumentation-http": "0.53.0",
1718
"@opentelemetry/instrumentation": "0.53.0",
1819
"@sentry/core": "latest || *",
1920
"@sentry/node": "latest || *",

dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/app.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import express from 'express';
77
const app = express();
88
const port = 3030;
99

10+
Sentry.setTag('root-level-tag', 'yes');
11+
1012
app.get('/test-success', function (req, res) {
1113
res.send({ version: 'v1' });
1214
});
@@ -23,8 +25,6 @@ app.get('/test-transaction', function (req, res) {
2325

2426
await fetch('http://localhost:3030/test-success');
2527

26-
await Sentry.flush();
27-
2828
res.send({});
2929
});
3030
});
@@ -38,7 +38,10 @@ app.get('/test-error', async function (req, res) {
3838
});
3939

4040
app.get('/test-exception/:id', function (req, _res) {
41-
throw new Error(`This is an exception with id ${req.params.id}`);
41+
const id = req.params.id;
42+
Sentry.setTag(`param-${id}`, id);
43+
44+
throw new Error(`This is an exception with id ${id}`);
4245
});
4346

4447
Sentry.setupExpressErrorHandler(app);

dev-packages/e2e-tests/test-applications/node-otel-without-tracing/src/instrument.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
12
const { NodeTracerProvider, BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node');
23
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
34
const Sentry = require('@sentry/node');
4-
const { SentrySpanProcessor, SentryPropagator } = require('@sentry/opentelemetry');
5+
const { SentryPropagator } = require('@sentry/opentelemetry');
56
const { UndiciInstrumentation } = require('@opentelemetry/instrumentation-undici');
67
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
78

@@ -15,6 +16,8 @@ Sentry.init({
1516
tunnel: `http://localhost:3031/`, // proxy server
1617
// Tracing is completely disabled
1718

19+
integrations: [Sentry.httpIntegration({ spans: false })],
20+
1821
// Custom OTEL setup
1922
skipOpenTelemetrySetup: true,
2023
});
@@ -37,5 +40,5 @@ provider.register({
3740
});
3841

3942
registerInstrumentations({
40-
instrumentations: [new UndiciInstrumentation()],
43+
instrumentations: [new UndiciInstrumentation(), new HttpInstrumentation()],
4144
});

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

+21
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,24 @@ test('Sends correct error event', async ({ baseURL }) => {
2828
span_id: expect.any(String),
2929
});
3030
});
31+
32+
test('Isolates requests correctly', async ({ baseURL }) => {
33+
const errorEventPromise1 = waitForError('node-otel-without-tracing', event => {
34+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 555-a';
35+
});
36+
const errorEventPromise2 = waitForError('node-otel-without-tracing', event => {
37+
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 555-b';
38+
});
39+
40+
fetch(`${baseURL}/test-exception/555-a`);
41+
fetch(`${baseURL}/test-exception/555-b`);
42+
43+
const errorEvent1 = await errorEventPromise1;
44+
const errorEvent2 = await errorEventPromise2;
45+
46+
expect(errorEvent1.transaction).toEqual('GET /test-exception/555-a');
47+
expect(errorEvent1.tags).toEqual({ 'root-level-tag': 'yes', 'param-555-a': '555-a' });
48+
49+
expect(errorEvent2.transaction).toEqual('GET /test-exception/555-b');
50+
expect(errorEvent2.tags).toEqual({ 'root-level-tag': 'yes', 'param-555-b': '555-b' });
51+
});

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

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
1212

1313
const scopeSpans = json.resourceSpans?.[0]?.scopeSpans;
1414

15-
const httpScope = scopeSpans?.find(
16-
scopeSpan => scopeSpan.scope.name === '@opentelemetry_sentry-patched/instrumentation-http',
17-
);
15+
const httpScope = scopeSpans?.find(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http');
1816

1917
return (
2018
httpScope &&
@@ -40,9 +38,7 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
4038
// But our default node-fetch spans are not emitted
4139
expect(scopeSpans.length).toEqual(2);
4240

43-
const httpScopes = scopeSpans?.filter(
44-
scopeSpan => scopeSpan.scope.name === '@opentelemetry_sentry-patched/instrumentation-http',
45-
);
41+
const httpScopes = scopeSpans?.filter(scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-http');
4642
const undiciScopes = scopeSpans?.filter(
4743
scopeSpan => scopeSpan.scope.name === '@opentelemetry/instrumentation-undici',
4844
);
@@ -114,7 +110,6 @@ test('Sends an API route transaction to OTLP', async ({ baseURL }) => {
114110
{ key: 'net.peer.port', value: { intValue: expect.any(Number) } },
115111
{ key: 'http.status_code', value: { intValue: 200 } },
116112
{ key: 'http.status_text', value: { stringValue: 'OK' } },
117-
{ key: 'sentry.origin', value: { stringValue: 'auto.http.otel.http' } },
118113
]),
119114
droppedAttributesCount: 0,
120115
events: [],

packages/node/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"@sentry/opentelemetry": "8.34.0",
100100
"@sentry/types": "8.34.0",
101101
"@sentry/utils": "8.34.0",
102-
"import-in-the-middle": "^1.11.0"
102+
"import-in-the-middle": "^1.11.2"
103103
},
104104
"devDependencies": {
105105
"@types/node": "^14.18.0"

0 commit comments

Comments
 (0)