Skip to content

Commit 4152d5d

Browse files
committed
feat(node-experimental): Move integrations from node
1 parent 8ba889f commit 4152d5d

File tree

32 files changed

+6663
-40
lines changed

32 files changed

+6663
-40
lines changed

dev-packages/e2e-tests/test-applications/node-hapi-app/src/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Sentry.init({
1010
environment: 'qa', // dynamic sampling bias to keep transactions
1111
dsn: process.env.E2E_TEST_DSN,
1212
includeLocalVariables: true,
13-
integrations: [new Sentry.Integrations.Hapi({ server })],
13+
integrations: [new Sentry.hapiIntegration({ server })],
1414
debug: true,
1515
tunnel: `http://localhost:3031/`, // proxy server
1616
tracesSampleRate: 1,

dev-packages/node-integration-tests/suites/tracing-experimental/hapi/scenario.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const init = async () => {
2626
},
2727
});
2828

29+
await Sentry.setupHapiErrorHandler(server);
2930
await server.start();
3031

3132
sendPortToRunner(port);

packages/node-experimental/src/index.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ export { errorHandler } from './sdk/handlers/errorHandler';
22

33
export { httpIntegration } from './integrations/http';
44
export { nativeNodeFetchIntegration } from './integrations/node-fetch';
5+
6+
export { consoleIntegration } from './integrations/console';
7+
export { nodeContextIntegration } from './integrations/context';
8+
export { contextLinesIntegration } from './integrations/contextlines';
9+
export { localVariablesIntegration } from './integrations/local-variables';
10+
export { modulesIntegration } from './integrations/modules';
11+
export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception';
12+
export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection';
13+
514
export { expressIntegration } from './integrations/tracing/express';
615
export { fastifyIntegration } from './integrations/tracing/fastify';
716
export { graphqlIntegration } from './integrations/tracing/graphql';
@@ -12,6 +21,7 @@ export { mysql2Integration } from './integrations/tracing/mysql2';
1221
export { nestIntegration } from './integrations/tracing/nest';
1322
export { postgresIntegration } from './integrations/tracing/postgres';
1423
export { prismaIntegration } from './integrations/tracing/prisma';
24+
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';
1525

1626
export { init, getDefaultIntegrations } from './sdk/init';
1727
export { getAutoPerformanceIntegrations } from './integrations/tracing';
@@ -27,17 +37,7 @@ export { startSpan, startSpanManual, startInactiveSpan, getActiveSpan, withActiv
2737

2838
export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils';
2939

30-
export {
31-
hapiErrorPlugin,
32-
consoleIntegration,
33-
onUncaughtExceptionIntegration,
34-
onUnhandledRejectionIntegration,
35-
modulesIntegration,
36-
contextLinesIntegration,
37-
nodeContextIntegration,
38-
localVariablesIntegration,
39-
cron,
40-
} from '@sentry/node';
40+
export { cron } from '@sentry/node';
4141

4242
export {
4343
addBreadcrumb,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Contexts, DsnComponents, Primitive, SdkMetadata } from '@sentry/types';
2+
3+
export interface AnrIntegrationOptions {
4+
/**
5+
* Interval to send heartbeat messages to the ANR worker.
6+
*
7+
* Defaults to 50ms.
8+
*/
9+
pollInterval: number;
10+
/**
11+
* Threshold in milliseconds to trigger an ANR event.
12+
*
13+
* Defaults to 5000ms.
14+
*/
15+
anrThreshold: number;
16+
/**
17+
* Whether to capture a stack trace when the ANR event is triggered.
18+
*
19+
* Defaults to `false`.
20+
*
21+
* This uses the node debugger which enables the inspector API and opens the required ports.
22+
*/
23+
captureStackTrace: boolean;
24+
/**
25+
* Tags to include with ANR events.
26+
*/
27+
staticTags: { [key: string]: Primitive };
28+
/**
29+
* @ignore Internal use only.
30+
*
31+
* If this is supplied, stack frame filenames will be rewritten to be relative to this path.
32+
*/
33+
appRootPath: string | undefined;
34+
}
35+
36+
export interface WorkerStartData extends AnrIntegrationOptions {
37+
debug: boolean;
38+
sdkMetadata: SdkMetadata;
39+
dsn: DsnComponents;
40+
release: string | undefined;
41+
environment: string;
42+
dist: string | undefined;
43+
contexts: Contexts;
44+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { URL } from 'url';
2+
import { defineIntegration, getCurrentScope } from '@sentry/core';
3+
import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/types';
4+
import { dynamicRequire, logger } from '@sentry/utils';
5+
import type { Worker, WorkerOptions } from 'worker_threads';
6+
import { NODE_MAJOR, NODE_VERSION } from '../../nodeVersion';
7+
import type { NodeClient } from '../../sdk/client';
8+
import type { AnrIntegrationOptions, WorkerStartData } from './common';
9+
import { base64WorkerScript } from './worker-script';
10+
11+
const DEFAULT_INTERVAL = 50;
12+
const DEFAULT_HANG_THRESHOLD = 5000;
13+
14+
type WorkerNodeV14 = Worker & { new (filename: string | URL, options?: WorkerOptions): Worker };
15+
16+
type WorkerThreads = {
17+
Worker: WorkerNodeV14;
18+
};
19+
20+
function log(message: string, ...args: unknown[]): void {
21+
logger.log(`[ANR] ${message}`, ...args);
22+
}
23+
24+
/**
25+
* We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when
26+
* targeting those versions
27+
*/
28+
function getWorkerThreads(): WorkerThreads {
29+
return dynamicRequire(module, 'worker_threads');
30+
}
31+
32+
/**
33+
* Gets contexts by calling all event processors. This relies on being called after all integrations are setup
34+
*/
35+
async function getContexts(client: NodeClient): Promise<Contexts> {
36+
let event: Event | null = { message: 'ANR' };
37+
const eventHint: EventHint = {};
38+
39+
for (const processor of client.getEventProcessors()) {
40+
if (event === null) break;
41+
event = await processor(event, eventHint);
42+
}
43+
44+
return event?.contexts || {};
45+
}
46+
47+
interface InspectorApi {
48+
open: (port: number) => void;
49+
url: () => string | undefined;
50+
}
51+
52+
const INTEGRATION_NAME = 'Anr';
53+
54+
const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
55+
return {
56+
name: INTEGRATION_NAME,
57+
setup(client: NodeClient) {
58+
if (NODE_MAJOR < 16 || (NODE_MAJOR === 16 && (NODE_VERSION.minor || 0) < 17)) {
59+
throw new Error('ANR detection requires Node 16.17.0 or later');
60+
}
61+
62+
// setImmediate is used to ensure that all other integrations have been setup
63+
setImmediate(() => _startWorker(client, options));
64+
},
65+
};
66+
}) satisfies IntegrationFn;
67+
68+
export const anrIntegration = defineIntegration(_anrIntegration);
69+
70+
/**
71+
* Starts the ANR worker thread
72+
*/
73+
async function _startWorker(client: NodeClient, _options: Partial<AnrIntegrationOptions>): Promise<void> {
74+
const contexts = await getContexts(client);
75+
const dsn = client.getDsn();
76+
77+
if (!dsn) {
78+
return;
79+
}
80+
81+
// These will not be accurate if sent later from the worker thread
82+
delete contexts.app?.app_memory;
83+
delete contexts.device?.free_memory;
84+
85+
const initOptions = client.getOptions();
86+
87+
const sdkMetadata = client.getSdkMetadata() || {};
88+
if (sdkMetadata.sdk) {
89+
sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name);
90+
}
91+
92+
const options: WorkerStartData = {
93+
debug: logger.isEnabled(),
94+
dsn,
95+
environment: initOptions.environment || 'production',
96+
release: initOptions.release,
97+
dist: initOptions.dist,
98+
sdkMetadata,
99+
appRootPath: _options.appRootPath,
100+
pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
101+
anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
102+
captureStackTrace: !!_options.captureStackTrace,
103+
staticTags: _options.staticTags || {},
104+
contexts,
105+
};
106+
107+
if (options.captureStackTrace) {
108+
// eslint-disable-next-line @typescript-eslint/no-var-requires
109+
const inspector: InspectorApi = require('inspector');
110+
if (!inspector.url()) {
111+
inspector.open(0);
112+
}
113+
}
114+
115+
const { Worker } = getWorkerThreads();
116+
117+
const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), {
118+
workerData: options,
119+
});
120+
121+
process.on('exit', () => {
122+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
123+
worker.terminate();
124+
});
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+
//
136+
}
137+
}, options.pollInterval);
138+
// Timer should not block exit
139+
timer.unref();
140+
141+
worker.on('message', (msg: string) => {
142+
if (msg === 'session-ended') {
143+
log('ANR event sent from ANR worker. Clearing session in this thread.');
144+
getCurrentScope().setSession(undefined);
145+
}
146+
});
147+
148+
worker.once('error', (err: Error) => {
149+
clearInterval(timer);
150+
log('ANR worker error', err);
151+
});
152+
153+
worker.once('exit', (code: number) => {
154+
clearInterval(timer);
155+
log('ANR worker exit', code);
156+
});
157+
158+
// Ensure this thread can't block app exit
159+
worker.unref();
160+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This file is a placeholder that gets overwritten in the build directory.
2+
export const base64WorkerScript = '';

0 commit comments

Comments
 (0)