-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(node): Use process.on('SIGTERM') for flushing in Vercel functions
#17583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2fd044a
bcac2c0
29e1c80
6ad0b10
e20d4a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; | ||
| import * as Sentry from '@sentry/node'; | ||
|
|
||
| function bufferedLoggingTransport(_options: BaseTransportOptions): Transport { | ||
| const bufferedEnvelopes: Envelope[] = []; | ||
|
|
||
| return { | ||
| send(envelope: Envelope): Promise<TransportMakeRequestResponse> { | ||
| bufferedEnvelopes.push(envelope); | ||
| return Promise.resolve({ statusCode: 200 }); | ||
| }, | ||
| flush(_timeout?: number): PromiseLike<boolean> { | ||
| // Print envelopes once flushed to verify they were sent. | ||
| for (const envelope of bufferedEnvelopes.splice(0, bufferedEnvelopes.length)) { | ||
| // eslint-disable-next-line no-console | ||
| console.log(JSON.stringify(envelope)); | ||
| } | ||
|
|
||
| return Promise.resolve(true); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| Sentry.init({ | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| transport: bufferedLoggingTransport, | ||
| }); | ||
|
|
||
| Sentry.captureMessage('SIGTERM flush message'); | ||
|
|
||
| // Signal that we're ready to receive SIGTERM. | ||
| // eslint-disable-next-line no-console | ||
| console.log('READY'); | ||
|
|
||
| // Keep the process alive so the integration test can send SIGTERM. | ||
| setInterval(() => undefined, 1_000); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { afterAll, expect, test } from 'vitest'; | ||
| import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; | ||
|
|
||
| afterAll(() => { | ||
| cleanupChildProcesses(); | ||
| }); | ||
|
|
||
| test('flushes buffered events when SIGTERM is received on Vercel', async () => { | ||
| const runner = createRunner(__dirname, 'scenario.ts') | ||
| .withEnv({ VERCEL: '1' }) | ||
| .expect({ | ||
| event: { | ||
| message: 'SIGTERM flush message', | ||
| }, | ||
| }) | ||
| .start(); | ||
|
|
||
| // Wait for the scenario to signal it's ready (SIGTERM handler is registered). | ||
| const waitForReady = async (): Promise<void> => { | ||
| const maxWait = 10_000; | ||
| const start = Date.now(); | ||
| while (Date.now() - start < maxWait) { | ||
| if (runner.getLogs().some(line => line.includes('READY'))) { | ||
| return; | ||
| } | ||
| await new Promise<void>(resolve => setTimeout(resolve, 50)); | ||
| } | ||
| throw new Error('Timed out waiting for scenario to be ready'); | ||
| }; | ||
|
|
||
| await waitForReady(); | ||
|
|
||
| runner.sendSignal('SIGTERM'); | ||
|
|
||
| await runner.completed(); | ||
|
|
||
| // Check that the child didn't crash (it may be killed by the runner after completion). | ||
| expect(runner.getLogs().join('\n')).not.toMatch(/Error starting child process/i); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,8 @@ export async function flushIfServerless( | |
| return; | ||
| } | ||
|
|
||
| // Note: vercelWaitUntil only does something in Vercel Edge runtime | ||
| // In Node runtime, we use process.on('SIGTERM') instead | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. flushIfServerless returns early without flushing in Vercel NodeIn Vercel Node runtime, when Additional Locations (1) |
||
| // @ts-expect-error This is not typed | ||
| if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { | ||
| // Vercel has a waitUntil equivalent that works without execution context | ||
|
Comment on lines
+54
to
58
This comment was marked as outdated.
Sorry, something went wrong.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we instead moved the flushing this to node-core |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| import { GLOBAL_OBJ } from './worldwide'; | ||
|
|
||
| declare const EdgeRuntime: string | undefined; | ||
|
|
||
| interface VercelRequestContextGlobal { | ||
| get?(): | ||
| | { | ||
|
|
@@ -14,6 +16,11 @@ interface VercelRequestContextGlobal { | |
| * Vendored from https://www.npmjs.com/package/@vercel/functions | ||
| */ | ||
| export function vercelWaitUntil(task: Promise<unknown>): void { | ||
| // We only flush manually in Vercel Edge runtime | ||
| // In Node runtime, we use process.on('SIGTERM') instead | ||
|
Comment on lines
18
to
+20
This comment was marked as outdated.
Sorry, something went wrong.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, that's the whole point of this PR my friend |
||
| if (typeof EdgeRuntime !== 'string') { | ||
| return; | ||
| } | ||
| const vercelRequestContextGlobal: VercelRequestContextGlobal | undefined = | ||
| // @ts-expect-error This is not typed | ||
| GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,40 +1,63 @@ | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
| import { vercelWaitUntil } from '../../../src/utils/vercelWaitUntil'; | ||
| import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; | ||
|
|
||
| describe('vercelWaitUntil', () => { | ||
| const VERCEL_REQUEST_CONTEXT_SYMBOL = Symbol.for('@vercel/request-context'); | ||
| const globalWithEdgeRuntime = globalThis as typeof globalThis & { EdgeRuntime?: string }; | ||
| const globalWithVercelRequestContext = GLOBAL_OBJ as unknown as Record<symbol, unknown>; | ||
|
|
||
| // `vercelWaitUntil` only runs in Vercel Edge runtime, which is detected via the global `EdgeRuntime` variable. | ||
| // In tests we set it explicitly so the logic is actually exercised. | ||
| const originalEdgeRuntime = globalWithEdgeRuntime.EdgeRuntime; | ||
|
|
||
| beforeEach(() => { | ||
| globalWithEdgeRuntime.EdgeRuntime = 'edge-runtime'; | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests don't verify new non-Edge early return behavior (Bugbot Rules)Per the review rules requiring "tests actually test the newly added behaviour": The |
||
|
|
||
| afterEach(() => { | ||
| if (originalEdgeRuntime === undefined) { | ||
| delete globalWithEdgeRuntime.EdgeRuntime; | ||
| } else { | ||
| globalWithEdgeRuntime.EdgeRuntime = originalEdgeRuntime; | ||
| } | ||
| }); | ||
|
|
||
| it('should do nothing if GLOBAL_OBJ does not have the @vercel/request-context symbol', () => { | ||
| const task = Promise.resolve(); | ||
| vercelWaitUntil(task); | ||
| // No assertions needed, just ensuring no errors are thrown | ||
| }); | ||
|
|
||
| it('should do nothing if get method is not defined', () => { | ||
| // @ts-expect-error - Not typed | ||
| GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = {}; | ||
| const originalRequestContext = globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL]; | ||
| globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL] = {}; | ||
| const task = Promise.resolve(); | ||
| vercelWaitUntil(task); | ||
| // No assertions needed, just ensuring no errors are thrown | ||
| globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL] = originalRequestContext; | ||
| }); | ||
|
|
||
| it('should do nothing if waitUntil method is not defined', () => { | ||
| // @ts-expect-error - Not typed | ||
| GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = { | ||
| const originalRequestContext = globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL]; | ||
| globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL] = { | ||
| get: () => ({}), | ||
| }; | ||
| const task = Promise.resolve(); | ||
| vercelWaitUntil(task); | ||
| // No assertions needed, just ensuring no errors are thrown | ||
| globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL] = originalRequestContext; | ||
| }); | ||
|
|
||
| it('should call waitUntil method if it is defined', () => { | ||
| const originalRequestContext = globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL]; | ||
| const waitUntilMock = vi.fn(); | ||
| // @ts-expect-error - Not typed | ||
| GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = { | ||
| globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL] = { | ||
| get: () => ({ waitUntil: waitUntilMock }), | ||
| }; | ||
| const task = Promise.resolve(); | ||
| vercelWaitUntil(task); | ||
| expect(waitUntilMock).toHaveBeenCalledWith(task); | ||
| globalWithVercelRequestContext[VERCEL_REQUEST_CONTEXT_SYMBOL] = originalRequestContext; | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -143,6 +143,15 @@ 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you test this on a vercel function? Generally makes sense but we could bump this more up I'd say?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. did not test this so far at all, maybe you can take this PR over when you have some time and properly test it. The reason I went with 200 here is because this is just the timeout used for finishing events, we still need time to actually make the http request so there needs to be some wiggle room -to the max. 500ms - if we spend 500ms on processing events, we have no more time to actually send the events 😅
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah that makes sense I thought this time is included already, I'll take it over |
||
| }); | ||
| } | ||
|
Comment on lines
+148
to
+153
This comment was marked as outdated.
Sorry, something went wrong. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multiple SIGTERM handlers accumulate on repeated init callsEach call to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can safely ignore this case.
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
Comment on lines
+149
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The 🔍 Detailed AnalysisThe 💡 Suggested FixTo ensure the flush completes, the process exit should be explicitly managed. After the 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| return client; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this comment be in the other file
vercelWaitUntil?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a comment in both places 👍