Skip to content

Commit 9b516cb

Browse files
committed
ref(node): Refactor node integrations to avoid setupOnce
1 parent cf773fc commit 9b516cb

File tree

5 files changed

+202
-129
lines changed

5 files changed

+202
-129
lines changed

packages/node/src/integrations/console.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as util from 'util';
2-
import { addBreadcrumb, getCurrentHub } from '@sentry/core';
3-
import type { Integration } from '@sentry/types';
2+
import { addBreadcrumb, getClient } from '@sentry/core';
3+
import type { Client, Integration } from '@sentry/types';
44
import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils';
55

66
/** Console module integration */
@@ -19,10 +19,13 @@ export class Console implements Integration {
1919
* @inheritDoc
2020
*/
2121
public setupOnce(): void {
22-
addConsoleInstrumentationHandler(({ args, level }) => {
23-
const hub = getCurrentHub();
22+
// noop
23+
}
2424

25-
if (!hub.getIntegration(Console)) {
25+
/** @inheritdoc */
26+
public setup(client: Client): void {
27+
addConsoleInstrumentationHandler(({ args, level }) => {
28+
if (getClient() !== client) {
2629
return;
2730
}
2831

packages/node/src/integrations/onuncaughtexception.ts

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Scope } from '@sentry/core';
2-
import { getClient, getCurrentHub } from '@sentry/core';
1+
import { captureException } from '@sentry/core';
2+
import { getClient } from '@sentry/core';
33
import type { Integration } from '@sentry/types';
44
import { logger } from '@sentry/utils';
55

@@ -50,11 +50,6 @@ export class OnUncaughtException implements Integration {
5050
*/
5151
public name: string = OnUncaughtException.id;
5252

53-
/**
54-
* @inheritDoc
55-
*/
56-
public readonly handler: (error: Error) => void = this._makeErrorHandler();
57-
5853
// CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts`
5954
private readonly _options: OnUncaughtExceptionOptions;
6055

@@ -68,31 +63,46 @@ export class OnUncaughtException implements Integration {
6863
};
6964
}
7065

66+
/**
67+
* @deprecated This does nothing anymore.
68+
*/
69+
public readonly handler: (error: Error) => void = () => {
70+
// noop
71+
};
72+
7173
/**
7274
* @inheritDoc
7375
*/
7476
public setupOnce(): void {
75-
global.process.on('uncaughtException', this.handler);
77+
// noop
7678
}
7779

78-
/**
79-
* @hidden
80-
*/
81-
private _makeErrorHandler(): (error: Error) => void {
82-
const timeout = 2000;
83-
let caughtFirstError: boolean = false;
84-
let caughtSecondError: boolean = false;
85-
let calledFatalError: boolean = false;
86-
let firstError: Error;
87-
88-
return (error: Error): void => {
80+
/** @inheritdoc */
81+
public setup(client: NodeClient): void {
82+
global.process.on('uncaughtException', makeErrorHandler(client, this._options));
83+
}
84+
}
85+
86+
type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void);
87+
88+
/** Exported only for tests */
89+
export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptionOptions): ErrorHandler {
90+
const timeout = 2000;
91+
let caughtFirstError: boolean = false;
92+
let caughtSecondError: boolean = false;
93+
let calledFatalError: boolean = false;
94+
let firstError: Error;
95+
96+
const clientOptions = client.getOptions();
97+
98+
return Object.assign(
99+
(error: Error): void => {
89100
let onFatalError: OnFatalErrorHandler = logAndExitProcess;
90-
const client = getClient<NodeClient>();
91101

92-
if (this._options.onFatalError) {
93-
onFatalError = this._options.onFatalError;
94-
} else if (client && client.getOptions().onFatalError) {
95-
onFatalError = client.getOptions().onFatalError as OnFatalErrorHandler;
102+
if (options.onFatalError) {
103+
onFatalError = options.onFatalError;
104+
} else if (clientOptions.onFatalError) {
105+
onFatalError = clientOptions.onFatalError as OnFatalErrorHandler;
96106
}
97107

98108
// Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not
@@ -107,7 +117,7 @@ export class OnUncaughtException implements Integration {
107117
// There are 3 listeners we ignore:
108118
listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself
109119
(listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing
110-
listener === this.handler // the handler we register in this integration
120+
(listener as ErrorHandler)._errorHandler // the handler we register in this integration
111121
) {
112122
return acc;
113123
} else {
@@ -116,34 +126,31 @@ export class OnUncaughtException implements Integration {
116126
}, 0);
117127

118128
const processWouldExit = userProvidedListenersCount === 0;
119-
const shouldApplyFatalHandlingLogic = this._options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
129+
const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
120130

121131
if (!caughtFirstError) {
122-
const hub = getCurrentHub();
123-
124132
// this is the first uncaught error and the ultimate reason for shutting down
125133
// we want to do absolutely everything possible to ensure it gets captured
126134
// also we want to make sure we don't go recursion crazy if more errors happen after this one
127135
firstError = error;
128136
caughtFirstError = true;
129137

130-
if (hub.getIntegration(OnUncaughtException)) {
131-
hub.withScope((scope: Scope) => {
132-
scope.setLevel('fatal');
133-
hub.captureException(error, {
134-
originalException: error,
135-
data: { mechanism: { handled: false, type: 'onuncaughtexception' } },
136-
});
137-
if (!calledFatalError && shouldApplyFatalHandlingLogic) {
138-
calledFatalError = true;
139-
onFatalError(error);
140-
}
138+
if (getClient() === client) {
139+
captureException(error, {
140+
originalException: error,
141+
captureContext: {
142+
level: 'fatal',
143+
},
144+
mechanism: {
145+
handled: false,
146+
type: 'onuncaughtexception',
147+
},
141148
});
142-
} else {
143-
if (!calledFatalError && shouldApplyFatalHandlingLogic) {
144-
calledFatalError = true;
145-
onFatalError(error);
146-
}
149+
}
150+
151+
if (!calledFatalError && shouldApplyFatalHandlingLogic) {
152+
calledFatalError = true;
153+
onFatalError(error);
147154
}
148155
} else {
149156
if (shouldApplyFatalHandlingLogic) {
@@ -182,6 +189,7 @@ export class OnUncaughtException implements Integration {
182189
}
183190
}
184191
}
185-
};
186-
}
192+
},
193+
{ _errorHandler: true },
194+
);
187195
}
Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
1-
import type { Scope } from '@sentry/core';
2-
import { getCurrentHub } from '@sentry/core';
3-
import type { Integration } from '@sentry/types';
1+
import { captureException, getClient } from '@sentry/core';
2+
import type { Client, Integration } from '@sentry/types';
43
import { consoleSandbox } from '@sentry/utils';
54

65
import { logAndExitProcess } from './utils/errorhandling';
76

87
type UnhandledRejectionMode = 'none' | 'warn' | 'strict';
98

9+
interface OnUnhandledRejectionOptions {
10+
/**
11+
* Option deciding what to do after capturing unhandledRejection,
12+
* that mimicks behavior of node's --unhandled-rejection flag.
13+
*/
14+
mode: UnhandledRejectionMode;
15+
}
16+
1017
/** Global Promise Rejection handler */
1118
export class OnUnhandledRejection implements Integration {
1219
/**
@@ -22,67 +29,80 @@ export class OnUnhandledRejection implements Integration {
2229
/**
2330
* @inheritDoc
2431
*/
25-
public constructor(
26-
private readonly _options: {
27-
/**
28-
* Option deciding what to do after capturing unhandledRejection,
29-
* that mimicks behavior of node's --unhandled-rejection flag.
30-
*/
31-
mode: UnhandledRejectionMode;
32-
} = { mode: 'warn' },
33-
) {}
32+
public constructor(private readonly _options: OnUnhandledRejectionOptions = { mode: 'warn' }) {}
3433

3534
/**
3635
* @inheritDoc
3736
*/
3837
public setupOnce(): void {
39-
global.process.on('unhandledRejection', this.sendUnhandledPromise.bind(this));
38+
// noop
4039
}
4140

42-
/**
43-
* Send an exception with reason
44-
* @param reason string
45-
* @param promise promise
46-
*/
47-
public sendUnhandledPromise(reason: unknown, promise: unknown): void {
48-
const hub = getCurrentHub();
49-
if (hub.getIntegration(OnUnhandledRejection)) {
50-
hub.withScope((scope: Scope) => {
51-
scope.setExtra('unhandledPromiseRejection', true);
52-
hub.captureException(reason, {
53-
originalException: promise,
54-
data: { mechanism: { handled: false, type: 'onunhandledrejection' } },
55-
});
56-
});
57-
}
58-
this._handleRejection(reason);
41+
/** @inheritdoc */
42+
public setup(client: Client): void {
43+
global.process.on('unhandledRejection', makeUnhandledPromiseHandler(client, this._options));
5944
}
45+
}
6046

61-
/**
62-
* Handler for `mode` option
63-
*/
47+
/**
48+
* Send an exception with reason
49+
* @param reason string
50+
* @param promise promise
51+
*
52+
* Exported only for tests.
53+
*/
54+
export function makeUnhandledPromiseHandler(
55+
client: Client,
56+
options: OnUnhandledRejectionOptions,
57+
): (reason: unknown, promise: unknown) => void {
58+
return function sendUnhandledPromise(reason: unknown, promise: unknown): void {
59+
if (getClient() !== client) {
60+
return;
61+
}
62+
63+
captureException(reason, {
64+
originalException: promise,
65+
captureContext: {
66+
extra: { unhandledPromiseRejection: true },
67+
},
68+
mechanism: {
69+
handled: false,
70+
type: 'onunhandledrejection',
71+
},
72+
});
73+
74+
handleRejection(reason, options);
75+
};
76+
}
77+
78+
/**
79+
* Handler for `mode` option
80+
81+
*/
82+
function handleRejection(
6483
// eslint-disable-next-line @typescript-eslint/no-explicit-any
65-
private _handleRejection(reason: any): void {
66-
// https://github.com/nodejs/node/blob/7cf6f9e964aa00772965391c23acda6d71972a9a/lib/internal/process/promises.js#L234-L240
67-
const rejectionWarning =
68-
'This error originated either by ' +
69-
'throwing inside of an async function without a catch block, ' +
70-
'or by rejecting a promise which was not handled with .catch().' +
71-
' The promise rejected with the reason:';
84+
reason: any,
85+
options: OnUnhandledRejectionOptions,
86+
): void {
87+
// https://github.com/nodejs/node/blob/7cf6f9e964aa00772965391c23acda6d71972a9a/lib/internal/process/promises.js#L234-L240
88+
const rejectionWarning =
89+
'This error originated either by ' +
90+
'throwing inside of an async function without a catch block, ' +
91+
'or by rejecting a promise which was not handled with .catch().' +
92+
' The promise rejected with the reason:';
7293

73-
/* eslint-disable no-console */
74-
if (this._options.mode === 'warn') {
75-
consoleSandbox(() => {
76-
console.warn(rejectionWarning);
77-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
78-
console.error(reason && reason.stack ? reason.stack : reason);
79-
});
80-
} else if (this._options.mode === 'strict') {
81-
consoleSandbox(() => {
82-
console.warn(rejectionWarning);
83-
});
84-
logAndExitProcess(reason);
85-
}
86-
/* eslint-enable no-console */
94+
/* eslint-disable no-console */
95+
if (options.mode === 'warn') {
96+
consoleSandbox(() => {
97+
console.warn(rejectionWarning);
98+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
99+
console.error(reason && reason.stack ? reason.stack : reason);
100+
});
101+
} else if (options.mode === 'strict') {
102+
consoleSandbox(() => {
103+
console.warn(rejectionWarning);
104+
});
105+
logAndExitProcess(reason);
87106
}
107+
/* eslint-enable no-console */
88108
}
Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,50 @@
1-
import { Hub } from '@sentry/core';
1+
import * as SentryCore from '@sentry/core';
2+
import type { NodeClient } from '../src/client';
23

3-
import { OnUncaughtException } from '../src/integrations/onuncaughtexception';
4+
import { OnUncaughtException, makeErrorHandler } from '../src/integrations/onuncaughtexception';
5+
6+
const client = {
7+
getOptions: () => ({}),
8+
close: () => Promise.resolve(true),
9+
} as unknown as NodeClient;
410

511
jest.mock('@sentry/core', () => {
612
// we just want to short-circuit it, so dont worry about types
713
const original = jest.requireActual('@sentry/core');
8-
original.Hub.prototype.getIntegration = () => true;
914
return {
1015
...original,
11-
getCurrentHub: () => new Hub(),
16+
getClient: () => client,
1217
};
1318
});
1419

1520
describe('uncaught exceptions', () => {
1621
test('install global listener', () => {
1722
const integration = new OnUncaughtException();
18-
integration.setupOnce();
23+
integration.setup(client);
1924
expect(process.listeners('uncaughtException')).toHaveLength(1);
2025
});
2126

22-
test('sendUncaughtException', () => {
23-
const integration = new OnUncaughtException({ onFatalError: jest.fn() });
24-
integration.setupOnce();
25-
26-
const captureException = jest.spyOn(Hub.prototype, 'captureException');
27+
test('makeErrorHandler', () => {
28+
const captureExceptionMock = jest.spyOn(SentryCore, 'captureException');
29+
const handler = makeErrorHandler(client, {
30+
exitEvenIfOtherHandlersAreRegistered: true,
31+
onFatalError: () => {},
32+
});
2733

28-
integration.handler({ message: 'message', name: 'name' });
34+
handler({ message: 'message', name: 'name' });
2935

30-
expect(captureException.mock.calls[0][1]?.data).toEqual({
31-
mechanism: { handled: false, type: 'onuncaughtexception' },
36+
expect(captureExceptionMock.mock.calls[0][1]).toEqual({
37+
originalException: {
38+
message: 'message',
39+
name: 'name',
40+
},
41+
captureContext: {
42+
level: 'fatal',
43+
},
44+
mechanism: {
45+
handled: false,
46+
type: 'onuncaughtexception',
47+
},
3248
});
3349
});
3450
});

0 commit comments

Comments
 (0)