Skip to content
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
Expand Up @@ -10,6 +10,15 @@ test('should capture and send Express controller error with txn name if tracesSa
.expect({
transaction: {
transaction: 'GET /test/express/:id',
contexts: {
trace: {
op: 'http.server',
status: 'internal_error',
data: expect.objectContaining({
'http.response.status_code': 500,
}),
},
},
},
})
.expect({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
transport: loggingTransport,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/node';

const tracer = Sentry.getClient().tracer;

async function run() {
await tracer.startActiveSpan('test span name', async span => {
try {
throw new Error('Test error from tracer.startActiveSpan');
} finally {
span.end();
}
});
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { afterAll, describe, expect } from 'vitest';

Check warning on line 1 in dev-packages/node-integration-tests/suites/tracing/tracer-start-active-span-error/test.ts

View workflow job for this annotation

GitHub Actions / Lint

eslint(no-unused-vars)

Identifier 'expect' is imported but never used.
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('tracer.startActiveSpan errors', () => {
afterAll(() => {
cleanupChildProcesses();
});

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
// When a callback to raw OTel `tracer.startActiveSpan` throws and the error propagates
// (uncaught), the span status is NOT automatically marked as errored. The user's `finally`
// calls `span.end()` before the error propagates, and an OTel span becomes immutable on end.
//
// Users who want auto-status-on-error should use `Sentry.startSpan` instead, or follow the
// OTel-idiomatic pattern: `span.recordException(err); span.setStatus({ code: ERROR })` in a
// `catch` inside the callback.
test('does NOT mark span errored when uncaught error escapes raw tracer.startActiveSpan callback', async () => {
await createRunner()
.expect({
transaction: {
transaction: 'test span name',
contexts: {
trace: {
status: 'ok',
},
},
},
})
.start()
.completed();
});
});
});
4 changes: 0 additions & 4 deletions packages/core/src/server-runtime-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Client } from './client';
import { getIsolationScope } from './currentScopes';
import { DEBUG_BUILD } from './debug-build';
import type { Scope } from './scope';
import { registerSpanErrorInstrumentation } from './tracing';
import { DEFAULT_TRANSPORT_BUFFER_SIZE } from './transports/base';
import { addUserAgentToTransportHeaders } from './transports/userAgent';
import type { CheckIn, MonitorConfig, SerializedCheckIn } from './types/checkin';
Expand Down Expand Up @@ -38,9 +37,6 @@ export class ServerRuntimeClient<
* @param options Configuration options for this SDK.
*/
public constructor(options: O) {
// Server clients always support tracing
registerSpanErrorInstrumentation();

addUserAgentToTransportHeaders(options);

super(options);
Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/tracing/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ export function registerSpanErrorInstrumentation(): void {
}
}

// The function name will be lost when bundling but we need to be able to identify this listener later to maintain the
// node.js default exit behaviour
errorCallback.tag = 'sentry_tracingErrorCallback';

errorsInstrumented = true;
addGlobalErrorInstrumentationHandler(errorCallback);
addGlobalUnhandledRejectionInstrumentationHandler(errorCallback);
Expand Down
26 changes: 9 additions & 17 deletions packages/node-core/src/integrations/onuncaughtexception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { logAndExitProcess } from '../utils/errorhandling';

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

type TaggedListener = NodeJS.UncaughtExceptionListener & {
tag?: string;
};

interface OnUncaughtExceptionOptions {
/**
* Controls if the SDK should register a handler to exit the process on uncaught errors:
Expand Down Expand Up @@ -83,19 +79,15 @@ export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptio
// exit behaviour of the SDK accordingly:
// - If other listeners are attached, do not exit.
// - If the only listener attached is ours, exit.
const userProvidedListenersCount = (global.process.listeners('uncaughtException') as TaggedListener[]).filter(
listener => {
// There are 3 listeners we ignore:
return (
// as soon as we're using domains this listener is attached by node itself
listener.name !== 'domainUncaughtExceptionClear' &&
// the handler we register for tracing
Comment thread
cursor[bot] marked this conversation as resolved.
listener.tag !== 'sentry_tracingErrorCallback' &&
// the handler we register in this integration
(listener as ErrorHandler)._errorHandler !== true
);
},
).length;
const userProvidedListenersCount = global.process.listeners('uncaughtException').filter(listener => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm this change has me slightly suspicious because IIUC it implies that the registerSpanErrorInstrumentation() had an effect that we needed to skip here. Any idea why this might be? Could also just be a false flag and the tag condition just never made a difference...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah also not sure, the code was here to handle the theoretical case where this would have worked but it hasn't - either it used to work differently back in the day, or it never worked - there was also not really test coverage for this as far as I can tell 😬

// There are 3 listeners we ignore:
return (
// as soon as we're using domains this listener is attached by node itself
listener.name !== 'domainUncaughtExceptionClear' &&
// the handler we register in this integration
(listener as ErrorHandler)._errorHandler !== true
);
}).length;

const processWouldExit = userProvidedListenersCount === 0;
const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
Expand Down
Loading