Skip to content

Commit f127479

Browse files
authored
ref(node): Refactor node integrations to functional syntax (#9959)
Getting there...
1 parent 35205b4 commit f127479

File tree

12 files changed

+346
-446
lines changed

12 files changed

+346
-446
lines changed

packages/core/src/integration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ function findIndex<T>(arr: T[], callback: (item: T) => boolean): number {
165165
export function convertIntegrationFnToClass<Fn extends IntegrationFn>(
166166
name: string,
167167
fn: Fn,
168-
): {
168+
): Integration & {
169169
id: string;
170170
new (...args: Parameters<Fn>): Integration &
171171
ReturnType<Fn> & {
@@ -182,7 +182,7 @@ export function convertIntegrationFnToClass<Fn extends IntegrationFn>(
182182
};
183183
},
184184
{ id: name },
185-
) as unknown as {
185+
) as unknown as Integration & {
186186
id: string;
187187
new (...args: Parameters<Fn>): Integration &
188188
ReturnType<Fn> & {

packages/node/src/index.ts

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

8888
import { Integrations as CoreIntegrations } from '@sentry/core';
89+
import type { Integration, IntegrationClass } from '@sentry/types';
8990

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

9495
const INTEGRATIONS = {
9596
...CoreIntegrations,
96-
...NodeIntegrations,
97+
// This typecast is somehow needed for now, probably because of the convertIntegrationFnToClass TS shenanigans
98+
// This is OK for now but should be resolved in v8 when we just pass the functional integrations directly
99+
...(NodeIntegrations as {
100+
Console: IntegrationClass<Integration>;
101+
Http: typeof NodeIntegrations.Http;
102+
OnUncaughtException: IntegrationClass<Integration>;
103+
OnUnhandledRejection: IntegrationClass<Integration>;
104+
Modules: IntegrationClass<Integration>;
105+
ContextLines: IntegrationClass<Integration>;
106+
Context: IntegrationClass<Integration>;
107+
RequestData: IntegrationClass<Integration>;
108+
LocalVariables: IntegrationClass<Integration>;
109+
Undici: typeof NodeIntegrations.Undici;
110+
Spotlight: IntegrationClass<Integration>;
111+
Anr: IntegrationClass<Integration>;
112+
Hapi: IntegrationClass<Integration>;
113+
}),
97114
...TracingIntegrations,
98115
};
99116

Lines changed: 87 additions & 89 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,108 +50,106 @@ 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 < 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';
72+
// eslint-disable-next-line deprecation/deprecation
73+
export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration);
5874

59-
public constructor(private readonly _options: Partial<Options> = {}) {}
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();
6081

61-
/** @inheritdoc */
62-
public setupOnce(): void {
63-
// Do nothing
82+
if (!dsn) {
83+
return;
6484
}
6585

66-
/** @inheritdoc */
67-
public setup(client: NodeClient): void {
68-
if (NODE_VERSION.major < 16) {
69-
throw new Error('ANR detection requires Node 16 or later');
70-
}
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;
7189

72-
// setImmediate is used to ensure that all other integrations have been setup
73-
setImmediate(() => this._startWorker(client));
74-
}
90+
const initOptions = client.getOptions();
7591

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();
92+
const sdkMetadata = client.getSdkMetadata() || {};
93+
if (sdkMetadata.sdk) {
94+
sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name);
95+
}
8296

83-
if (!dsn) {
84-
return;
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+
if (!inspector.url()) {
114+
inspector.open(0);
85115
}
116+
}
86117

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

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-
if (!inspector.url()) {
115-
inspector.open(0);
116-
}
139+
worker.on('message', (msg: string) => {
140+
if (msg === 'session-ended') {
141+
log('ANR event sent from ANR worker. Clearing session in this thread.');
142+
getCurrentScope().setSession(undefined);
117143
}
144+
});
118145

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

140-
worker.on('message', (msg: string) => {
141-
if (msg === 'session-ended') {
142-
log('ANR event sent from ANR worker. Clearing session in this thread.');
143-
getCurrentScope().setSession(undefined);
144-
}
145-
});
146-
147-
worker.once('error', (err: Error) => {
148-
clearInterval(timer);
149-
log('ANR worker error', err);
150-
});
151-
152-
worker.once('exit', (code: number) => {
153-
clearInterval(timer);
154-
log('ANR worker exit', code);
155-
});
156-
}
151+
worker.once('exit', (code: number) => {
152+
clearInterval(timer);
153+
log('ANR worker exit', code);
154+
});
157155
}
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)