Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(node): Ensure isolation scope is correctly cloned for non-recording spans #11503

Merged
merged 4 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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',
transport: loggingTransport,
tracesSampleRate: 1,
});

import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
import express from 'express';

const app = express();

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

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

afterAll(() => {
cleanupChildProcesses();
});

test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => {
const runner = createRunner(__dirname, 'server.ts')
.ignore('session', 'sessions', 'transaction')
.expect({
event: {
exception: {
values: [
{
mechanism: {
type: 'middleware',
handled: false,
},
type: 'Error',
value: 'test_error with id 123',
stacktrace: {
frames: expect.arrayContaining([
expect.objectContaining({
function: expect.any(String),
lineno: expect.any(Number),
colno: expect.any(Number),
}),
]),
},
},
],
},
transaction: 'GET /test/express/:id',
},
})
.start(done);

expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow();
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import express from 'express';

const app = express();

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

Sentry.setupExpressErrorHandler(app);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ afterAll(() => {
cleanupChildProcesses();
});

test('should capture and send Express controller error.', done => {
test('should capture and send Express controller error if tracesSampleRate is not set.', done => {
const runner = createRunner(__dirname, 'server.ts')
.ignore('session', 'sessions')
.ignore('session', 'sessions', 'transaction')
.expect({
event: {
exception: {
Expand All @@ -17,7 +17,7 @@ test('should capture and send Express controller error.', done => {
handled: false,
},
type: 'Error',
value: 'test_error',
value: 'test_error with id 123',
stacktrace: {
frames: expect.arrayContaining([
expect.objectContaining({
Expand All @@ -34,5 +34,5 @@ test('should capture and send Express controller error.', done => {
})
.start(done);

expect(() => runner.makeRequest('get', '/test/express')).rejects.toThrow();
expect(() => runner.makeRequest('get', '/test/express/123')).rejects.toThrow();
});
15 changes: 13 additions & 2 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ServerResponse } from 'http';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';
import type { Span } from '@opentelemetry/api';
import { SpanKind } from '@opentelemetry/api';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
Expand Down Expand Up @@ -93,7 +93,9 @@ const _httpIntegration = ((options: HttpOptions = {}) => {
requestHook: (span, req) => {
addOriginToSpan(span, 'auto.http.otel.http');

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

Expand Down Expand Up @@ -179,3 +181,12 @@ function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMe
},
);
}

/**
* Determines if @param req is a ClientRequest, meaning the request was created within the express app
* and it's an outgoing request.
* Checking for properties instead of using `instanceOf` to avoid importing the request classes.
*/
function _isClientRequest(req: ClientRequest | IncomingMessage): req is ClientRequest {
return 'outputData' in req && 'outputSize' in req && !('client' in req) && !('statusCode' in req);
}
9 changes: 8 additions & 1 deletion packages/node/src/integrations/tracing/express.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type * as http from 'http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, getDefaultIsolationScope } from '@sentry/core';
import { captureException, getClient, getIsolationScope } from '@sentry/core';
import type { IntegrationFn } from '@sentry/types';

import { logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../../debug-build';
import type { NodeClient } from '../../sdk/client';
import { addOriginToSpan } from '../../utils/addOriginToSpan';

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