Skip to content

Commit 0b188b4

Browse files
committed
ref(node): Refactor node integrations to functional syntax
1 parent b6a7cef commit 0b188b4

File tree

14 files changed

+447
-546
lines changed

14 files changed

+447
-546
lines changed

packages/core/src/integration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ function findIndex<T>(arr: T[], callback: (item: T) => boolean): number {
175175
export function convertIntegrationFnToClass<Fn extends IntegrationFn>(
176176
name: string,
177177
fn: Fn,
178-
): {
178+
): Integration & {
179179
id: string;
180180
new (...args: Parameters<Fn>): Integration &
181181
ReturnType<Fn> & {
@@ -192,7 +192,7 @@ export function convertIntegrationFnToClass<Fn extends IntegrationFn>(
192192
};
193193
},
194194
{ id: name },
195-
) as unknown as {
195+
) as unknown as Integration & {
196196
id: string;
197197
new (...args: Parameters<Fn>): Integration &
198198
ReturnType<Fn> & {

packages/node/src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,29 @@ export { getModuleFromFilename } from './module';
8585
export { enableAnrDetection } from './integrations/anr/legacy';
8686

8787
import { Integrations as CoreIntegrations } from '@sentry/core';
88+
import type { Integration } from '@sentry/types';
8889

8990
import * as Handlers from './handlers';
9091
import * as NodeIntegrations from './integrations';
9192
import * as TracingIntegrations from './tracing/integrations';
9293

9394
const INTEGRATIONS = {
9495
...CoreIntegrations,
95-
...NodeIntegrations,
96+
...(NodeIntegrations as {
97+
Console: Integration;
98+
Http: typeof NodeIntegrations.Http;
99+
OnUncaughtException: Integration;
100+
OnUnhandledRejection: Integration;
101+
Modules: Integration;
102+
ContextLines: Integration;
103+
Context: Integration;
104+
RequestData: Integration;
105+
LocalVariables: Integration;
106+
Undici: typeof NodeIntegrations.Undici;
107+
Spotlight: Integration;
108+
Anr: Integration;
109+
Hapi: Integration;
110+
}),
96111
...TracingIntegrations,
97112
};
98113

Lines changed: 86 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// TODO (v8): This import can be removed once we only support Node with global URL
22
import { URL } from 'url';
3-
import { getCurrentScope } from '@sentry/core';
4-
import type { Contexts, Event, EventHint, Integration } from '@sentry/types';
3+
import { convertIntegrationFnToClass, getCurrentScope } from '@sentry/core';
4+
import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/types';
55
import { dynamicRequire, logger } from '@sentry/utils';
66
import type { Worker, WorkerOptions } from 'worker_threads';
77
import type { NodeClient } from '../../client';
@@ -50,106 +50,104 @@ interface InspectorApi {
5050
url: () => string | undefined;
5151
}
5252

53+
const INTEGRATION_NAME = 'Anr';
54+
55+
const anrIntegration = ((options: Partial<Options> = {}) => {
56+
return {
57+
name: INTEGRATION_NAME,
58+
setup(client: NodeClient) {
59+
if ((NODE_VERSION.major || 0) < 16) {
60+
throw new Error('ANR detection requires Node 16 or later');
61+
}
62+
63+
// setImmediate is used to ensure that all other integrations have been setup
64+
setImmediate(() => _startWorker(client, options));
65+
},
66+
};
67+
}) satisfies IntegrationFn;
68+
5369
/**
5470
* Starts a thread to detect App Not Responding (ANR) events
5571
*/
56-
export class Anr implements Integration {
57-
public name: string = 'Anr';
58-
59-
public constructor(private readonly _options: Partial<Options> = {}) {}
72+
// eslint-disable-next-line deprecation/deprecation
73+
export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration);
6074

61-
/** @inheritdoc */
62-
public setupOnce(): void {
63-
// Do nothing
64-
}
65-
66-
/** @inheritdoc */
67-
public setup(client: NodeClient): void {
68-
if ((NODE_VERSION.major || 0) < 16) {
69-
throw new Error('ANR detection requires Node 16 or later');
70-
}
75+
/**
76+
* Starts the ANR worker thread
77+
*/
78+
async function _startWorker(client: NodeClient, _options: Partial<Options>): Promise<void> {
79+
const contexts = await getContexts(client);
80+
const dsn = client.getDsn();
7181

72-
// setImmediate is used to ensure that all other integrations have been setup
73-
setImmediate(() => this._startWorker(client));
82+
if (!dsn) {
83+
return;
7484
}
7585

76-
/**
77-
* Starts the ANR worker thread
78-
*/
79-
private async _startWorker(client: NodeClient): Promise<void> {
80-
const contexts = await getContexts(client);
81-
const dsn = client.getDsn();
86+
// These will not be accurate if sent later from the worker thread
87+
delete contexts.app?.app_memory;
88+
delete contexts.device?.free_memory;
8289

83-
if (!dsn) {
84-
return;
85-
}
90+
const initOptions = client.getOptions();
8691

87-
// These will not be accurate if sent later from the worker thread
88-
delete contexts.app?.app_memory;
89-
delete contexts.device?.free_memory;
92+
const sdkMetadata = client.getSdkMetadata() || {};
93+
if (sdkMetadata.sdk) {
94+
sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name);
95+
}
9096

91-
const initOptions = client.getOptions();
97+
const options: WorkerStartData = {
98+
debug: logger.isEnabled(),
99+
dsn,
100+
environment: initOptions.environment || 'production',
101+
release: initOptions.release,
102+
dist: initOptions.dist,
103+
sdkMetadata,
104+
pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
105+
anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
106+
captureStackTrace: !!_options.captureStackTrace,
107+
contexts,
108+
};
109+
110+
if (options.captureStackTrace) {
111+
// eslint-disable-next-line @typescript-eslint/no-var-requires
112+
const inspector: InspectorApi = require('inspector');
113+
inspector.open(0);
114+
}
92115

93-
const sdkMetadata = client.getSdkMetadata() || {};
94-
if (sdkMetadata.sdk) {
95-
sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name);
116+
const { Worker } = getWorkerThreads();
117+
118+
const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), {
119+
workerData: options,
120+
});
121+
// Ensure this thread can't block app exit
122+
worker.unref();
123+
124+
const timer = setInterval(() => {
125+
try {
126+
const currentSession = getCurrentScope().getSession();
127+
// We need to copy the session object and remove the toJSON method so it can be sent to the worker
128+
// serialized without making it a SerializedSession
129+
const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined;
130+
// message the worker to tell it the main event loop is still running
131+
worker.postMessage({ session });
132+
} catch (_) {
133+
//
96134
}
135+
}, options.pollInterval);
97136

98-
const options: WorkerStartData = {
99-
debug: logger.isEnabled(),
100-
dsn,
101-
environment: initOptions.environment || 'production',
102-
release: initOptions.release,
103-
dist: initOptions.dist,
104-
sdkMetadata,
105-
pollInterval: this._options.pollInterval || DEFAULT_INTERVAL,
106-
anrThreshold: this._options.anrThreshold || DEFAULT_HANG_THRESHOLD,
107-
captureStackTrace: !!this._options.captureStackTrace,
108-
contexts,
109-
};
110-
111-
if (options.captureStackTrace) {
112-
// eslint-disable-next-line @typescript-eslint/no-var-requires
113-
const inspector: InspectorApi = require('inspector');
114-
inspector.open(0);
137+
worker.on('message', (msg: string) => {
138+
if (msg === 'session-ended') {
139+
log('ANR event sent from ANR worker. Clearing session in this thread.');
140+
getCurrentScope().setSession(undefined);
115141
}
142+
});
116143

117-
const { Worker } = getWorkerThreads();
118-
119-
const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), {
120-
workerData: options,
121-
});
122-
// Ensure this thread can't block app exit
123-
worker.unref();
124-
125-
const timer = setInterval(() => {
126-
try {
127-
const currentSession = getCurrentScope().getSession();
128-
// We need to copy the session object and remove the toJSON method so it can be sent to the worker
129-
// serialized without making it a SerializedSession
130-
const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined;
131-
// message the worker to tell it the main event loop is still running
132-
worker.postMessage({ session });
133-
} catch (_) {
134-
//
135-
}
136-
}, options.pollInterval);
144+
worker.once('error', (err: Error) => {
145+
clearInterval(timer);
146+
log('ANR worker error', err);
147+
});
137148

138-
worker.on('message', (msg: string) => {
139-
if (msg === 'session-ended') {
140-
log('ANR event sent from ANR worker. Clearing session in this thread.');
141-
getCurrentScope().setSession(undefined);
142-
}
143-
});
144-
145-
worker.once('error', (err: Error) => {
146-
clearInterval(timer);
147-
log('ANR worker error', err);
148-
});
149-
150-
worker.once('exit', (code: number) => {
151-
clearInterval(timer);
152-
log('ANR worker exit', code);
153-
});
154-
}
149+
worker.once('exit', (code: number) => {
150+
clearInterval(timer);
151+
log('ANR worker exit', code);
152+
});
155153
}
Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,35 @@
11
import * as util from 'util';
2-
import { addBreadcrumb, getClient } from '@sentry/core';
3-
import type { Client, Integration } from '@sentry/types';
2+
import { addBreadcrumb, convertIntegrationFnToClass, getClient } from '@sentry/core';
3+
import type { IntegrationFn } from '@sentry/types';
44
import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils';
55

6-
/** Console module integration */
7-
export class Console implements Integration {
8-
/**
9-
* @inheritDoc
10-
*/
11-
public static id: string = 'Console';
12-
13-
/**
14-
* @inheritDoc
15-
*/
16-
public name: string = Console.id;
6+
const INTEGRATION_NAME = 'Console';
177

18-
/**
19-
* @inheritDoc
20-
*/
21-
public setupOnce(): void {
22-
// noop
23-
}
8+
const consoleIntegration = (() => {
9+
return {
10+
name: INTEGRATION_NAME,
11+
setup(client) {
12+
addConsoleInstrumentationHandler(({ args, level }) => {
13+
if (getClient() !== client) {
14+
return;
15+
}
2416

25-
/** @inheritdoc */
26-
public setup(client: Client): void {
27-
addConsoleInstrumentationHandler(({ args, level }) => {
28-
if (getClient() !== client) {
29-
return;
30-
}
17+
addBreadcrumb(
18+
{
19+
category: 'console',
20+
level: severityLevelFromString(level),
21+
message: util.format.apply(undefined, args),
22+
},
23+
{
24+
input: [...args],
25+
level,
26+
},
27+
);
28+
});
29+
},
30+
};
31+
}) satisfies IntegrationFn;
3132

32-
addBreadcrumb(
33-
{
34-
category: 'console',
35-
level: severityLevelFromString(level),
36-
message: util.format.apply(undefined, args),
37-
},
38-
{
39-
input: [...args],
40-
level,
41-
},
42-
);
43-
});
44-
}
45-
}
33+
/** Console module integration */
34+
// eslint-disable-next-line deprecation/deprecation
35+
export const Console = convertIntegrationFnToClass(INTEGRATION_NAME, consoleIntegration);

0 commit comments

Comments
 (0)