From 19c0fa0e37c4be060ecf11af3ce69d8259b0eef3 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Thu, 5 Feb 2026 09:50:08 +0100 Subject: [PATCH] fix(core): Avoid blocking the process for weightBasedFlushing --- packages/core/src/client.ts | 14 +++++---- packages/core/test/lib/client.test.ts | 42 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 1ed447ab802d..588c8a62e97e 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -49,6 +49,7 @@ import { safeMathRandom } from './utils/randomSafeContext'; import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span'; import { showSpanDropWarning } from './utils/spanUtils'; import { rejectedSyncPromise } from './utils/syncpromise'; +import { safeUnref } from './utils/timer'; import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; @@ -137,11 +138,14 @@ function setupWeightBasedFlushing< // This prevents flushing being delayed by items that arrive close to the timeout limit // and thus resetting the flushing timeout and delaying items being flushed. isTimerActive = true; - flushTimeout = setTimeout(() => { - flushFn(client); - // Note: isTimerActive is reset by the flushHook handler above, not here, - // to avoid race conditions when new items arrive during the flush. - }, DEFAULT_FLUSH_INTERVAL); + // Use safeUnref so the timer doesn't prevent the process from exiting + flushTimeout = safeUnref( + setTimeout(() => { + flushFn(client); + // Note: isTimerActive is reset by the flushHook handler above, not here, + // to avoid race conditions when new items arrive during the flush. + }, DEFAULT_FLUSH_INTERVAL), + ); } }); diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 21aab0ea609b..014ffe8ebbe1 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -23,6 +23,7 @@ import type { SpanJSON } from '../../src/types-hoist/span'; import * as debugLoggerModule from '../../src/utils/debug-logger'; import * as miscModule from '../../src/utils/misc'; import * as timeModule from '../../src/utils/time'; +import * as timerModule from '../../src/utils/timer'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { AdHocIntegration, AsyncTestIntegration, TestIntegration } from '../mocks/integration'; import { makeFakeTransport } from '../mocks/transport'; @@ -3076,6 +3077,27 @@ describe('Client', () => { expect(sendEnvelopeSpy).not.toHaveBeenCalled(); }); + + it('uses safeUnref on flush timer to not block process exit', () => { + const safeUnrefSpy = vi.spyOn(timerModule, 'safeUnref'); + + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Capture a log which will start the flush timer + _INTERNAL_captureLog({ message: 'test log', level: 'info' }, scope); + + // Verify safeUnref was called on the timer + expect(safeUnrefSpy).toHaveBeenCalledTimes(1); + expect(safeUnrefSpy).toHaveBeenCalledWith(expect.anything()); + + safeUnrefSpy.mockRestore(); + }); }); describe('metric weight-based flushing', () => { @@ -3142,6 +3164,26 @@ describe('Client', () => { expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); }); + + it('uses safeUnref on flush timer to not block process exit', () => { + const safeUnrefSpy = vi.spyOn(timerModule, 'safeUnref'); + + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Capture a metric which will start the flush timer + _INTERNAL_captureMetric({ name: 'test_metric', value: 42, type: 'counter', attributes: {} }, { scope }); + + // Verify safeUnref was called on the timer + expect(safeUnrefSpy).toHaveBeenCalledTimes(1); + expect(safeUnrefSpy).toHaveBeenCalledWith(expect.anything()); + + safeUnrefSpy.mockRestore(); + }); }); describe('promise buffer usage', () => {