From 29a93e1f3c31b3f140029dd49a97d0cedae225c2 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 28 Sep 2026 19:13:05 +0200 Subject: [PATCH] flush metrics --- packages/node/src/sdk/client.ts | 11 ++++++++ packages/node/test/sdk/client.test.ts | 38 +++++++++++++++------------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index 01de1b58fd82..4936a92f0ef6 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -5,6 +5,7 @@ import type { ServerRuntimeClientOptions } from '@sentry/core/server'; import { _INTERNAL_clearAiProviderSkips, _INTERNAL_flushLogsBuffer, + _INTERNAL_flushMetricsBuffer, _INTERNAL_setDeferSegmentSpanCapture, applySdkMetadata, debug, @@ -38,6 +39,7 @@ export class NodeClient extends ServerRuntimeClient { private _clientReportInterval: NodeJS.Timeout | undefined; private _clientReportOnExitFlushListener: (() => void) | undefined; private _logOnExitFlushListener: (() => void) | undefined; + private _metricsOnExitFlushListener: (() => void) | undefined; public constructor(options: NodeClientOptions) { const serverName = @@ -63,6 +65,10 @@ export class NodeClient extends ServerRuntimeClient { _INTERNAL_flushLogsBuffer(this); }; + this._metricsOnExitFlushListener = () => { + _INTERNAL_flushMetricsBuffer(this); + }; + if (serverName) { this.on('beforeCaptureLog', log => { log.attributes = { @@ -73,6 +79,7 @@ export class NodeClient extends ServerRuntimeClient { } process.on('beforeExit', this._logOnExitFlushListener); + process.on('beforeExit', this._metricsOnExitFlushListener); // Enable deferred segment-span transaction capture here, in the constructor, rather than in // `initOtel`. Every client runs its constructor exactly once, whereas `initOtel` only runs on @@ -158,6 +165,10 @@ export class NodeClient extends ServerRuntimeClient { process.off('beforeExit', this._logOnExitFlushListener); } + if (this._metricsOnExitFlushListener) { + process.off('beforeExit', this._metricsOnExitFlushListener); + } + const allEventsSent = await super.close(timeout); if (this.traceProvider) { await this.traceProvider.shutdown(); diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 1792dd6da0eb..03994b43c73d 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -397,35 +397,39 @@ describe('NodeClient', () => { expect(forceFlushSpy).toHaveBeenCalledTimes(1); }); - it('stops client report tracking if it was started', async () => { - const processOffSpy = vi.spyOn(process, 'off'); - const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval'); - + it('stops client report tracking when closed', async () => { + const originalListeners = process.listeners('beforeExit'); const client = new NodeClient(getDefaultNodeClientOptions({ sendClientReports: true })); + const listenersBeforeTracking = process.listeners('beforeExit'); + const setIntervalSpy = vi.spyOn(globalThis, 'setInterval'); + const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval'); client.startClientReportTracking(); - const result = await client.close(); + const addedListeners = process + .listeners('beforeExit') + .filter(listener => !listenersBeforeTracking.includes(listener)); + const [intervalResult] = setIntervalSpy.mock.results; - expect(result).toBe(true); + await client.close(); + expect(addedListeners).toHaveLength(1); + expect(process.listeners('beforeExit')).toEqual(originalListeners); + expect(setIntervalSpy).toHaveBeenCalledTimes(1); expect(clearIntervalSpy).toHaveBeenCalledTimes(1); - - // removes `_clientReportOnExitFlushListener` - expect(processOffSpy).toHaveBeenNthCalledWith(1, 'beforeExit', expect.any(Function)); + expect(clearIntervalSpy).toHaveBeenCalledWith(intervalResult?.value); }); - it('stops log capture if it was started', async () => { - const processOffSpy = vi.spyOn(process, 'off'); + it('removes log and metric exit listeners when closed', async () => { + const originalListeners = process.listeners('beforeExit'); + const client = new NodeClient(getDefaultNodeClientOptions({ sendClientReports: false })); - const client = new NodeClient(getDefaultNodeClientOptions()); + const addedListeners = process.listeners('beforeExit').filter(listener => !originalListeners.includes(listener)); - const result = await client.close(); - - expect(result).toBe(true); + await client.close(); - // removes `_logOnExitFlushListener` - expect(processOffSpy).toHaveBeenNthCalledWith(1, 'beforeExit', expect.any(Function)); + expect(addedListeners).toHaveLength(2); + expect(process.listeners('beforeExit')).toEqual(originalListeners); }); });