Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/node/src/sdk/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ interface VercelRequestContextGlobal {
get?(): { waitUntil?: (task: Promise<unknown>) => void } | undefined;
}

// A repeated `init()` replaces the client, so the listeners are registered once and flush the latest client.
let keepAliveClient: Client | undefined;

/**
* Keeps Vercel Node.js functions alive until the SDK has sent the telemetry of each request.
*
Expand All @@ -23,11 +26,17 @@ interface VercelRequestContextGlobal {
* end (2 seconds at most), and then flushes the client.
*/
export function setupVercelKeepAlive(client: Client): void {
const isRegistered = !!keepAliveClient;
keepAliveClient = client;
if (isRegistered) {
return;
}

// Ensure we flush events when vercel functions are ended
// See: https://vercel.com/docs/functions/functions-api-reference#sigterm-signal
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);
await keepAliveClient?.flush(200);
});

subscribeDiagnosticsChannel(HTTP_ON_SERVER_RESPONSE_FINISH, message => {
Expand All @@ -36,7 +45,8 @@ export function setupVercelKeepAlive(client: Client): void {
// @ts-expect-error Vercel sets this global, so `GLOBAL_OBJ` does not type it
GLOBAL_OBJ[Symbol.for('@vercel/request-context')];
const requestContext = requestContextGlobal?.get?.();
if (!response || !requestContext?.waitUntil) {
const client = keepAliveClient;
if (!client || !response || !requestContext?.waitUntil) {
return;
}

Expand Down
86 changes: 49 additions & 37 deletions packages/node/test/sdk/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,62 +322,74 @@ describe('init()', () => {
});
});

it('registers a SIGTERM handler on Vercel', () => {
const originalVercelEnv = process.env.VERCEL;
process.env.VERCEL = '1';
describe('Vercel keep-alive', () => {
let vercelInit: typeof init;
let VercelNodeClient: typeof NodeClient;

beforeEach(async () => {
// The keep-alive listeners are registered once per module instance, so each test needs a fresh one.
vi.resetModules();
({ init: vercelInit } = await import('../../src/sdk'));
({ NodeClient: VercelNodeClient } = await import('../../src/sdk/client'));
});

const baselineListeners = process.listeners('SIGTERM');
it('registers a SIGTERM handler on Vercel', () => {
const originalVercelEnv = process.env.VERCEL;
process.env.VERCEL = '1';

init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false });
const baselineListeners = process.listeners('SIGTERM');

const postInitListeners = process.listeners('SIGTERM');
const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l));
vercelInit({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false });

expect(addedListeners).toHaveLength(1);
const postInitListeners = process.listeners('SIGTERM');
const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l));

// Cleanup: remove the handler we added in this test.
process.off('SIGTERM', addedListeners[0] as any);
process.env.VERCEL = originalVercelEnv;
});
expect(addedListeners).toHaveLength(1);

it('flushes when SIGTERM is received on Vercel', () => {
const originalVercelEnv = process.env.VERCEL;
process.env.VERCEL = '1';
// Cleanup: remove the handler we added in this test.
process.off('SIGTERM', addedListeners[0] as any);
process.env.VERCEL = originalVercelEnv;
});

const baselineListeners = process.listeners('SIGTERM');
it('flushes when SIGTERM is received on Vercel', () => {
const originalVercelEnv = process.env.VERCEL;
process.env.VERCEL = '1';

const client = init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false });
expect(client).toBeInstanceOf(NodeClient);
const baselineListeners = process.listeners('SIGTERM');

const flushSpy = vi.spyOn(client as NodeClient, 'flush').mockResolvedValue(true);
const client = vercelInit({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false });
expect(client).toBeInstanceOf(VercelNodeClient);

const postInitListeners = process.listeners('SIGTERM');
const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l));
expect(addedListeners).toHaveLength(1);
const flushSpy = vi.spyOn(client as InstanceType<typeof VercelNodeClient>, 'flush').mockResolvedValue(true);

process.emit('SIGTERM');
const postInitListeners = process.listeners('SIGTERM');
const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l));
expect(addedListeners).toHaveLength(1);

expect(flushSpy).toHaveBeenCalledWith(200);
process.emit('SIGTERM');

// Cleanup: remove the handler we added in this test.
process.off('SIGTERM', addedListeners[0] as any);
process.env.VERCEL = originalVercelEnv;
});
expect(flushSpy).toHaveBeenCalledWith(200);

// Cleanup: remove the handler we added in this test.
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;
it('does not register a SIGTERM handler when not running on Vercel', () => {
const originalVercelEnv = process.env.VERCEL;
delete process.env.VERCEL;

const baselineListeners = process.listeners('SIGTERM');
const baselineListeners = process.listeners('SIGTERM');

init({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false });
vercelInit({ dsn: PUBLIC_DSN, enableOpenTelemetrySetup: false });

const postInitListeners = process.listeners('SIGTERM');
const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l));
const postInitListeners = process.listeners('SIGTERM');
const addedListeners = postInitListeners.filter(l => !baselineListeners.includes(l));

expect(addedListeners).toHaveLength(0);
expect(addedListeners).toHaveLength(0);

process.env.VERCEL = originalVercelEnv;
process.env.VERCEL = originalVercelEnv;
});
});

describe('environment variable options', () => {
Expand Down
41 changes: 39 additions & 2 deletions packages/node/test/sdk/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { getActiveSpan } from '@sentry/core';
import type * as SentryServerUtils from '@sentry/server-utils';
import { subscribeDiagnosticsChannel } from '@sentry/server-utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { setupVercelKeepAlive } from '../../src/sdk/vercel';

vi.mock('@sentry/core', async importOriginal => ({
...(await importOriginal<typeof SentryCore>()),
Expand Down Expand Up @@ -46,8 +45,12 @@ function finishResponse() {

describe('setupVercelKeepAlive', () => {
let waitUntil: ReturnType<typeof vi.fn>;
let setupVercelKeepAlive: (client: Client) => void;

beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.mocked(subscribeDiagnosticsChannel).mockClear();
({ setupVercelKeepAlive } = await import('../../src/sdk/vercel'));
vi.useFakeTimers();
waitUntil = vi.fn();
(globalThis as any)[REQUEST_CONTEXT] = { get: () => ({ waitUntil }) };
Expand Down Expand Up @@ -167,4 +170,38 @@ describe('setupVercelKeepAlive', () => {

expect(client.flush).toHaveBeenCalledTimes(1);
});

it('registers the listeners once and flushes the latest client on repeated calls', async () => {
const firstClient = createClient();
const secondClient = createClient();
setupVercelKeepAlive(firstClient as unknown as Client);
setupVercelKeepAlive(secondClient as unknown as Client);

expect(subscribeDiagnosticsChannel).toHaveBeenCalledTimes(1);
expect(process.on).toHaveBeenCalledTimes(1);

const response = finishResponse();
expect(waitUntil).toHaveBeenCalledTimes(1);
response.emit('close');
await (waitUntil.mock.calls[0]![0] as Promise<unknown>);

expect(firstClient.flush).not.toHaveBeenCalled();
expect(secondClient.flush).toHaveBeenCalledTimes(1);
});

it('flushes the latest client once on SIGTERM', async () => {
const firstClient = createClient();
const secondClient = createClient();
setupVercelKeepAlive(firstClient as unknown as Client);
setupVercelKeepAlive(secondClient as unknown as Client);

const sigtermHandlers = vi.mocked(process.on).mock.calls.filter(([event]) => event === 'SIGTERM');
expect(sigtermHandlers).toHaveLength(1);

await (sigtermHandlers[0]![1] as () => Promise<void>)();

expect(firstClient.flush).not.toHaveBeenCalled();
expect(secondClient.flush).toHaveBeenCalledTimes(1);
expect(secondClient.flush).toHaveBeenCalledWith(200);
});
});
Loading