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 index 6f1e4a7d6339..e7359f552664 100644 --- a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -52,7 +52,11 @@ setTimeout(() => { setTimeout(() => { anr.startWorker(); - setTimeout(() => { + // Wait for the restarted worker to reconnect its debugger session before blocking the event + // loop. The main-thread inspector stays open across restarts, so there is no main-thread signal + // that the new worker is ready; without this, on slow CI `longWork` can run before the worker + // is sampling and the ANR is missed entirely. + anr.waitUntilWorkerReady().then(() => { longWork(); }); }, 2000); diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 1c5b00fe9d4e..0dcb64c288da 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -60,12 +60,17 @@ async function getContexts(client: NodeClient): Promise { const INTEGRATION_NAME = 'Anr' as const; -type AnrInternal = { startWorker: () => void; stopWorker: () => void }; +type AnrInternal = { + startWorker: () => void; + stopWorker: () => void; + waitUntilWorkerReady: () => Promise; +}; // eslint-disable-next-line typescript/no-deprecated const _anrIntegration = ((options: Partial = {}) => { let worker: Promise<() => void> | undefined; let client: NodeClient | undefined; + let workerReady: Promise | undefined; // Hookup the scope fetch function to the global object so that it can be called from the worker thread via the // debugger when it pauses @@ -79,12 +84,16 @@ const _anrIntegration = ((options: Partial = {}) => { return; } - if (client) { - worker = _startWorker(client, options); + const initializedClient = client; + if (initializedClient) { + workerReady = new Promise(resolve => { + worker = _startWorker(initializedClient, options, resolve); + }); } }, stopWorker: () => { if (worker) { + workerReady = undefined; // eslint-disable-next-line @typescript-eslint/no-floating-promises worker.then(stop => { stop(); @@ -92,6 +101,7 @@ const _anrIntegration = ((options: Partial = {}) => { }); } }, + waitUntilWorkerReady: () => workerReady ?? Promise.resolve(), async setup(initClient: NodeClient) { client = initClient; @@ -157,6 +167,7 @@ async function _startWorker( client: NodeClient, // eslint-disable-next-line typescript/no-deprecated integrationOptions: Partial, + onReady?: () => void, ): Promise<() => void> { const dsn = client.getDsn(); @@ -234,6 +245,8 @@ async function _startWorker( if (msg === 'session-ended') { log('ANR event sent from ANR worker. Clearing session in this thread.'); getIsolationScope().setSession(undefined); + } else if (msg === 'worker-ready') { + onReady?.(); } }); diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index 3ae9e009625c..7ef6c22d7c1a 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -328,3 +328,9 @@ parentPort?.on('message', (msg: { session: Session | undefined; debugImages?: Re poll(); }); + +// Signal that the worker is fully set up: the inspector session (when capturing stack traces) is +// connected to the main thread and the watchdog is armed. Consumers that restart the worker can wait +// for this before blocking the event loop, since the main-thread inspector stays open across restarts +// and gives no signal that the new worker has reconnected. +parentPort?.postMessage('worker-ready');