From 813bbcb0d3b2023bbb4366876fb047d93dcc4a97 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:24:54 +0000 Subject: [PATCH] fix(v10/node-core): Register the Vercel SIGTERM listener once Both init() paths added a SIGTERM listener on every call when VERCEL is set, so repeated init() leaked one listener per call and flushed stale clients. Register a single shared listener that flushes the latest client, matching #25146 on v11. Fixes #25825 Co-Authored-By: Burak Yigit Kaya <126780+BYK@users.noreply.github.com> --- packages/node-core/src/light/sdk.ts | 8 ++------ packages/node-core/src/sdk/index.ts | 8 ++------ packages/node-core/src/utils/vercel.ts | 20 +++++++++++++++++++ packages/node-core/test/light/sdk.test.ts | 24 +++++++++++++++++++++++ packages/node-core/test/sdk/init.test.ts | 24 +++++++++++++++++++++++ 5 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 packages/node-core/src/utils/vercel.ts diff --git a/packages/node-core/src/light/sdk.ts b/packages/node-core/src/light/sdk.ts index ee45c322194b..3c5701b2fed3 100644 --- a/packages/node-core/src/light/sdk.ts +++ b/packages/node-core/src/light/sdk.ts @@ -29,6 +29,7 @@ import { defaultStackParser, getSentryRelease } from '../sdk/api'; import { makeNodeTransport } from '../transports'; import type { NodeClientOptions, NodeOptions } from '../types'; import { getSpotlightConfig } from '../utils/spotlight'; +import { setupVercelSigtermFlush } from '../utils/vercel'; import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageStrategy'; import { LightNodeClient } from './client'; import { httpIntegration } from './integrations/httpIntegration'; @@ -130,13 +131,8 @@ function _init( updateScopeFromEnvVariables(); - // Ensure we flush events when vercel functions are ended - // See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal if (process.env.VERCEL) { - process.on('SIGTERM', async () => { - // We have 500ms for processing here, so we try to make sure to have enough time to send the events - await client.flush(200); - }); + setupVercelSigtermFlush(client); } return client; diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index c7ce19122458..787a325acd70 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -38,6 +38,7 @@ import { systemErrorIntegration } from '../integrations/systemError'; import { makeNodeTransport } from '../transports'; import type { NodeClientOptions, NodeOptions } from '../types'; import { getSpotlightConfig } from '../utils/spotlight'; +import { setupVercelSigtermFlush } from '../utils/vercel'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initializeEsmLoader } from './esmLoader'; @@ -147,13 +148,8 @@ function _init( enhanceDscWithOpenTelemetryRootSpanName(client); setupEventContextTrace(client); - // Ensure we flush events when vercel functions are ended - // See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal if (process.env.VERCEL) { - process.on('SIGTERM', async () => { - // We have 500ms for processing here, so we try to make sure to have enough time to send the events - await client.flush(200); - }); + setupVercelSigtermFlush(client); } return client; diff --git a/packages/node-core/src/utils/vercel.ts b/packages/node-core/src/utils/vercel.ts new file mode 100644 index 000000000000..31e34c1df88a --- /dev/null +++ b/packages/node-core/src/utils/vercel.ts @@ -0,0 +1,20 @@ +import type { Client } from '@sentry/core'; + +// A repeated `init()` replaces the client, so a single listener flushes the latest one instead of adding one per init. +let sigtermClient: Client | undefined; + +async function flushOnSigterm(): Promise { + // We have 500ms for processing here, so we try to make sure to have enough time to send the events + await sigtermClient?.flush(200); +} + +/** + * Ensure we flush events when vercel functions are ended. + * See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal + */ +export function setupVercelSigtermFlush(client: Client): void { + sigtermClient = client; + if (!process.listeners('SIGTERM').includes(flushOnSigterm)) { + process.on('SIGTERM', flushOnSigterm); + } +} diff --git a/packages/node-core/test/light/sdk.test.ts b/packages/node-core/test/light/sdk.test.ts index 48cac52022b0..162c1ff756e1 100644 --- a/packages/node-core/test/light/sdk.test.ts +++ b/packages/node-core/test/light/sdk.test.ts @@ -62,6 +62,30 @@ describe('Light Mode | SDK', () => { expect(client?.getOptions().release).toBe('test-release@1.0.0'); }); + + it('registers one SIGTERM handler on Vercel that flushes the latest client across repeated inits', () => { + const originalVercelEnv = process.env.VERCEL; + process.env.VERCEL = '1'; + + const baselineListeners = process.listeners('SIGTERM'); + + const firstClient = mockLightSdkInit(); + const latestClient = mockLightSdkInit(); + const firstFlushSpy = vi.spyOn(firstClient!, 'flush').mockResolvedValue(true); + const latestFlushSpy = vi.spyOn(latestClient!, 'flush').mockResolvedValue(true); + + const postInitListeners = process.listeners('SIGTERM'); + const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l)); + expect(addedListeners).toHaveLength(1); + + process.emit('SIGTERM'); + + expect(firstFlushSpy).not.toHaveBeenCalled(); + expect(latestFlushSpy).toHaveBeenCalledWith(200); + + process.off('SIGTERM', addedListeners[0] as any); + process.env.VERCEL = originalVercelEnv; + }); }); describe('initWithoutDefaultIntegrations', () => { diff --git a/packages/node-core/test/sdk/init.test.ts b/packages/node-core/test/sdk/init.test.ts index 6ee986c3be75..f1ad3f5dcc33 100644 --- a/packages/node-core/test/sdk/init.test.ts +++ b/packages/node-core/test/sdk/init.test.ts @@ -186,6 +186,30 @@ describe('init()', () => { process.env.VERCEL = originalVercelEnv; }); + it('registers one SIGTERM handler on Vercel that flushes the latest client across repeated inits', () => { + const originalVercelEnv = process.env.VERCEL; + process.env.VERCEL = '1'; + + const baselineListeners = process.listeners('SIGTERM'); + + const firstClient = init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); + const latestClient = init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); + const firstFlushSpy = vi.spyOn(firstClient as NodeClient, 'flush').mockResolvedValue(true); + const latestFlushSpy = vi.spyOn(latestClient as NodeClient, 'flush').mockResolvedValue(true); + + const postInitListeners = process.listeners('SIGTERM'); + const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l)); + expect(addedListeners).toHaveLength(1); + + process.emit('SIGTERM'); + + expect(firstFlushSpy).not.toHaveBeenCalled(); + expect(latestFlushSpy).toHaveBeenCalledWith(200); + + process.off('SIGTERM', addedListeners[0] as any); + process.env.VERCEL = originalVercelEnv; + }); + it('does not register a SIGTERM handler when not running on Vercel', () => { const originalVercelEnv = process.env.VERCEL; delete process.env.VERCEL;