diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js new file mode 100644 index 000000000000..9de453abf23d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -0,0 +1,50 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +const Sentry = require('@sentry/node'); + +setTimeout(() => { + process.exit(); +}, 10000); + +const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + autoSessionTracking: false, + integrations: [anr], +}); + +function longWorkIgnored() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + anr.stopWorker(); + + setTimeout(() => { + longWorkIgnored(); + + setTimeout(() => { + anr.startWorker(); + + setTimeout(() => { + longWork(); + }); + }, 2000); + }, 2000); +}, 2000); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index 966c47ad0370..9600978fb097 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -106,4 +106,8 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => test('from forked process', done => { createRunner(__dirname, 'forker.js').expect({ event: EXPECTED_ANR_EVENT }).start(done); }); + + test('worker can be stopped and restarted', done => { + createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done); + }); }); diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 91deb2259e72..bf59b719fb18 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,7 +1,16 @@ // TODO (v8): This import can be removed once we only support Node with global URL import { URL } from 'url'; import { convertIntegrationFnToClass, defineIntegration, getCurrentScope } from '@sentry/core'; -import type { Client, Contexts, Event, EventHint, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import type { + Client, + Contexts, + Event, + EventHint, + Integration, + IntegrationClass, + IntegrationFn, + IntegrationFnResult, +} from '@sentry/types'; import { dynamicRequire, logger } from '@sentry/utils'; import type { Worker, WorkerOptions } from 'worker_threads'; import type { NodeClient } from '../../client'; @@ -52,23 +61,51 @@ interface InspectorApi { const INTEGRATION_NAME = 'Anr'; +type AnrInternal = { startWorker: () => void; stopWorker: () => void }; + const _anrIntegration = ((options: Partial = {}) => { + let worker: Promise<() => void> | undefined; + let client: NodeClient | undefined; + return { name: INTEGRATION_NAME, // TODO v8: Remove this setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function - setup(client: NodeClient) { + startWorker: () => { + if (worker) { + return; + } + + if (client) { + worker = _startWorker(client, options); + } + }, + stopWorker: () => { + if (worker) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.then(stop => { + stop(); + worker = undefined; + }); + } + }, + setup(initClient: NodeClient) { if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { throw new Error('ANR detection requires Node 16.17.0 or later'); } - // setImmediate is used to ensure that all other integrations have been setup - setImmediate(() => _startWorker(client, options)); + client = initClient; + + // setImmediate is used to ensure that all other integrations have had their setup called first. + // This allows us to call into all integrations to fetch the full context + setImmediate(() => this.startWorker()); }, - }; + } as IntegrationFnResult & AnrInternal; }) satisfies IntegrationFn; -export const anrIntegration = defineIntegration(_anrIntegration); +type AnrReturn = (options?: Partial) => IntegrationFnResult & AnrInternal; + +export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; /** * Starts a thread to detect App Not Responding (ANR) events @@ -90,14 +127,20 @@ export type Anr = typeof Anr; /** * Starts the ANR worker thread */ -async function _startWorker(client: NodeClient, _options: Partial): Promise { - const contexts = await getContexts(client); +async function _startWorker( + client: NodeClient, + integrationOptions: Partial, +): Promise<() => void> { const dsn = client.getDsn(); if (!dsn) { - return; + return () => { + // + }; } + const contexts = await getContexts(client); + // These will not be accurate if sent later from the worker thread delete contexts.app?.app_memory; delete contexts.device?.free_memory; @@ -116,11 +159,11 @@ async function _startWorker(client: NodeClient, _options: Partial { + // eslint-disable-next-line @typescript-eslint/no-floating-promises worker.terminate(); }); @@ -176,4 +220,10 @@ async function _startWorker(client: NodeClient, _options: Partial { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + clearInterval(timer); + }; }