From f7d93b219f6a0b52ddc31e2a5164ce5c21445f4e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 24 Aug 2026 13:19:28 +0200 Subject: [PATCH] test(node): Fix flaky ANR restart test with deterministic worker-ready signal The `stop-and-start` ANR test restarts the worker and then blocks the event loop, expecting the restarted worker to sample and report the ANR. It previously ran `longWork` almost immediately after `startWorker()`. `waitForDebuggerReady` (polling `inspector.url()`) cannot gate this: the ANR integration opens the main-thread inspector once and never closes it, so `inspector.url()` stays truthy across restarts and the poll returns immediately. `_startWorker` is also async, so the new worker may not even be spawned yet. On slow CI the event loop can block before the new worker reconnects its inspector session, and the ANR is missed. The worker now posts a `worker-ready` message once it is fully set up, and the integration exposes `waitUntilWorkerReady()` so the restart path can await the new worker's actual readiness instead of a fixed delay. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../suites/anr/stop-and-start.js | 6 +++++- packages/node/src/integrations/anr/index.ts | 19 ++++++++++++++++--- packages/node/src/integrations/anr/worker.ts | 6 ++++++ 3 files changed, 27 insertions(+), 4 deletions(-) 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');