Skip to content

Commit 76a2869

Browse files
authored
fix(node): Ensure isolation scope is correctly cloned for non-recording spans (#11503)
Our Http instrumentation didn't clone the isolation scope correctly when the request span is not recording. We early returned if the span was not a server kind span. However, non-recording spans apparently have no span kind, so we wrongfully early returned in the check for non-recording spans.
1 parent f46b29e commit 76a2869

File tree

6 files changed

+88
-9
lines changed

6 files changed

+88
-9
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
tracesSampleRate: 1,
9+
});
10+
11+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
12+
import express from 'express';
13+
14+
const app = express();
15+
16+
app.get('/test/express/:id', req => {
17+
throw new Error(`test_error with id ${req.params.id}`);
18+
});
19+
20+
Sentry.setupExpressErrorHandler(app);
21+
22+
startExpressServerAndSendPortToRunner(app);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => {
8+
const runner = createRunner(__dirname, 'server.ts')
9+
.ignore('session', 'sessions', 'transaction')
10+
.expect({
11+
event: {
12+
exception: {
13+
values: [
14+
{
15+
mechanism: {
16+
type: 'middleware',
17+
handled: false,
18+
},
19+
type: 'Error',
20+
value: 'test_error with id 123',
21+
stacktrace: {
22+
frames: expect.arrayContaining([
23+
expect.objectContaining({
24+
function: expect.any(String),
25+
lineno: expect.any(Number),
26+
colno: expect.any(Number),
27+
}),
28+
]),
29+
},
30+
},
31+
],
32+
},
33+
transaction: 'GET /test/express/:id',
34+
},
35+
})
36+
.start(done);
37+
38+
expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow();
39+
});

dev-packages/node-integration-tests/suites/express/handle-error/server.ts renamed to dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import express from 'express';
1212

1313
const app = express();
1414

15-
app.get('/test/express', () => {
16-
throw new Error('test_error');
15+
app.get('/test/express/:id', req => {
16+
throw new Error(`test_error with id ${req.params.id}`);
1717
});
1818

1919
Sentry.setupExpressErrorHandler(app);

dev-packages/node-integration-tests/suites/express/handle-error/test.ts renamed to dev-packages/node-integration-tests/suites/express/handle-error-tracesSampleRate-unset/test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ afterAll(() => {
44
cleanupChildProcesses();
55
});
66

7-
test('should capture and send Express controller error.', done => {
7+
test('should capture and send Express controller error if tracesSampleRate is not set.', done => {
88
const runner = createRunner(__dirname, 'server.ts')
9-
.ignore('session', 'sessions')
9+
.ignore('session', 'sessions', 'transaction')
1010
.expect({
1111
event: {
1212
exception: {
@@ -17,7 +17,7 @@ test('should capture and send Express controller error.', done => {
1717
handled: false,
1818
},
1919
type: 'Error',
20-
value: 'test_error',
20+
value: 'test_error with id 123',
2121
stacktrace: {
2222
frames: expect.arrayContaining([
2323
expect.objectContaining({
@@ -34,5 +34,5 @@ test('should capture and send Express controller error.', done => {
3434
})
3535
.start(done);
3636

37-
expect(() => runner.makeRequest('get', '/test/express')).rejects.toThrow();
37+
expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow();
3838
});

packages/node/src/integrations/http.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ServerResponse } from 'http';
1+
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';
22
import type { Span } from '@opentelemetry/api';
33
import { SpanKind } from '@opentelemetry/api';
44
import { registerInstrumentations } from '@opentelemetry/instrumentation';
@@ -93,7 +93,9 @@ const _httpIntegration = ((options: HttpOptions = {}) => {
9393
requestHook: (span, req) => {
9494
addOriginToSpan(span, 'auto.http.otel.http');
9595

96-
if (getSpanKind(span) !== SpanKind.SERVER) {
96+
// both, incoming requests and "client" requests made within the app trigger the requestHook
97+
// we only want to isolate and further annotate incoming requests (IncomingMessage)
98+
if (_isClientRequest(req)) {
9799
return;
98100
}
99101

@@ -179,3 +181,12 @@ function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMe
179181
},
180182
);
181183
}
184+
185+
/**
186+
* Determines if @param req is a ClientRequest, meaning the request was created within the express app
187+
* and it's an outgoing request.
188+
* Checking for properties instead of using `instanceOf` to avoid importing the request classes.
189+
*/
190+
function _isClientRequest(req: ClientRequest | IncomingMessage): req is ClientRequest {
191+
return 'outputData' in req && 'outputSize' in req && !('client' in req) && !('statusCode' in req);
192+
}

packages/node/src/integrations/tracing/express.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type * as http from 'http';
22
import { registerInstrumentations } from '@opentelemetry/instrumentation';
33
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
4-
import { defineIntegration } from '@sentry/core';
4+
import { defineIntegration, getDefaultIsolationScope } from '@sentry/core';
55
import { captureException, getClient, getIsolationScope } from '@sentry/core';
66
import type { IntegrationFn } from '@sentry/types';
77

8+
import { logger } from '@sentry/utils';
9+
import { DEBUG_BUILD } from '../../debug-build';
810
import type { NodeClient } from '../../sdk/client';
911
import { addOriginToSpan } from '../../utils/addOriginToSpan';
1012

@@ -19,6 +21,11 @@ const _expressIntegration = (() => {
1921
addOriginToSpan(span, 'auto.http.otel.express');
2022
},
2123
spanNameHook(info, defaultName) {
24+
if (getIsolationScope() === getDefaultIsolationScope()) {
25+
DEBUG_BUILD &&
26+
logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName');
27+
return defaultName;
28+
}
2229
if (info.layerType === 'request_handler') {
2330
// type cast b/c Otel unfortunately types info.request as any :(
2431
const req = info.request as { method?: string };

0 commit comments

Comments
 (0)