Skip to content

Commit 5fe6aab

Browse files
authored
ref(node): Stop using registerSpanErrorInstrumentation() on server (#21169)
I think this does nothing on node, because the implementation of this calls this under the hood: ```js addGlobalErrorInstrumentationHandler(errorCallback); addGlobalUnhandledRejectionInstrumentationHandler(errorCallback); ``` basically browser-only, using `window.onerror`. I believe this is handled specifically in node anyhow so should not be used/needed and allows us to streamline this a bit and potentially move this to a browser-only place eventually. This also means we can remove the tag we put here, because this is actually not needed. I adjusted a node-integration-test to verify this works there, both with using our own startSpan implementations (express uses this). With otel-native spans, it does not work, but it did not work before either - IMHO this is fine for the time being.
1 parent 96f96cb commit 5fe6aab

7 files changed

Lines changed: 74 additions & 25 deletions

File tree

dev-packages/node-integration-tests/suites/express/handle-error/test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ test('should capture and send Express controller error with txn name if tracesSa
1010
.expect({
1111
transaction: {
1212
transaction: 'GET /test/express/:id',
13+
contexts: {
14+
trace: {
15+
op: 'http.server',
16+
status: 'internal_error',
17+
data: expect.objectContaining({
18+
'http.response.status_code': 500,
19+
}),
20+
},
21+
},
1322
},
1423
})
1524
.expect({
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
tracesSampleRate: 1.0,
7+
transport: loggingTransport,
8+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/node';
2+
3+
const tracer = Sentry.getClient().tracer;
4+
5+
async function run() {
6+
await tracer.startActiveSpan('test span name', async span => {
7+
try {
8+
throw new Error('Test error from tracer.startActiveSpan');
9+
} finally {
10+
span.end();
11+
}
12+
});
13+
}
14+
15+
run();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
3+
4+
describe('tracer.startActiveSpan errors', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
10+
// When a callback to raw OTel `tracer.startActiveSpan` throws and the error propagates
11+
// (uncaught), the span status is NOT automatically marked as errored. The user's `finally`
12+
// calls `span.end()` before the error propagates, and an OTel span becomes immutable on end.
13+
//
14+
// Users who want auto-status-on-error should use `Sentry.startSpan` instead, or follow the
15+
// OTel-idiomatic pattern: `span.recordException(err); span.setStatus({ code: ERROR })` in a
16+
// `catch` inside the callback.
17+
test('does NOT mark span errored when uncaught error escapes raw tracer.startActiveSpan callback', async () => {
18+
await createRunner()
19+
.expect({
20+
transaction: {
21+
transaction: 'test span name',
22+
contexts: {
23+
trace: {
24+
status: 'ok',
25+
},
26+
},
27+
},
28+
})
29+
.start()
30+
.completed();
31+
});
32+
});
33+
});

packages/core/src/server-runtime-client.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Client } from './client';
33
import { getIsolationScope } from './currentScopes';
44
import { DEBUG_BUILD } from './debug-build';
55
import type { Scope } from './scope';
6-
import { registerSpanErrorInstrumentation } from './tracing';
76
import { DEFAULT_TRANSPORT_BUFFER_SIZE } from './transports/base';
87
import { addUserAgentToTransportHeaders } from './transports/userAgent';
98
import type { CheckIn, MonitorConfig, SerializedCheckIn } from './types/checkin';
@@ -38,9 +37,6 @@ export class ServerRuntimeClient<
3837
* @param options Configuration options for this SDK.
3938
*/
4039
public constructor(options: O) {
41-
// Server clients always support tracing
42-
registerSpanErrorInstrumentation();
43-
4440
addUserAgentToTransportHeaders(options);
4541

4642
super(options);

packages/core/src/tracing/errors.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ export function registerSpanErrorInstrumentation(): void {
3333
}
3434
}
3535

36-
// The function name will be lost when bundling but we need to be able to identify this listener later to maintain the
37-
// node.js default exit behaviour
38-
errorCallback.tag = 'sentry_tracingErrorCallback';
39-
4036
errorsInstrumented = true;
4137
addGlobalErrorInstrumentationHandler(errorCallback);
4238
addGlobalUnhandledRejectionInstrumentationHandler(errorCallback);

packages/node-core/src/integrations/onuncaughtexception.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import { logAndExitProcess } from '../utils/errorhandling';
66

77
type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void;
88

9-
type TaggedListener = NodeJS.UncaughtExceptionListener & {
10-
tag?: string;
11-
};
12-
139
interface OnUncaughtExceptionOptions {
1410
/**
1511
* Controls if the SDK should register a handler to exit the process on uncaught errors:
@@ -83,19 +79,15 @@ export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptio
8379
// exit behaviour of the SDK accordingly:
8480
// - If other listeners are attached, do not exit.
8581
// - If the only listener attached is ours, exit.
86-
const userProvidedListenersCount = (global.process.listeners('uncaughtException') as TaggedListener[]).filter(
87-
listener => {
88-
// There are 3 listeners we ignore:
89-
return (
90-
// as soon as we're using domains this listener is attached by node itself
91-
listener.name !== 'domainUncaughtExceptionClear' &&
92-
// the handler we register for tracing
93-
listener.tag !== 'sentry_tracingErrorCallback' &&
94-
// the handler we register in this integration
95-
(listener as ErrorHandler)._errorHandler !== true
96-
);
97-
},
98-
).length;
82+
const userProvidedListenersCount = global.process.listeners('uncaughtException').filter(listener => {
83+
// There are 3 listeners we ignore:
84+
return (
85+
// as soon as we're using domains this listener is attached by node itself
86+
listener.name !== 'domainUncaughtExceptionClear' &&
87+
// the handler we register in this integration
88+
(listener as ErrorHandler)._errorHandler !== true
89+
);
90+
}).length;
9991

10092
const processWouldExit = userProvidedListenersCount === 0;
10193
const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;

0 commit comments

Comments
 (0)