getsentry / getsentry/sentry-javascript
vercelWaitUntil() is a no-op on Vercel's Node.js runtime, silently dropping events flushed by the Next.js wrappers
- Lingua principale
- TypeScript
- Stelle
- 8.7k
- Fork
- 1.8k
- Merge medio
- 1g 17h
- PR unite (30g)
- 515
Descrizione
## Summary
`vercelWaitUntil()` returns early on anything that is not the Edge runtime. Because every Next.js SDK wrapper flushes through it, **server-side events captured by those wrappers are silently dropped on Vercel's Node.js runtime (Fluid Compute)** — the function freezes before the envelope is sent.
There is no error, no warning, and no debug output. `captureException` runs, the event is queued, and nothing ever leaves the process.
## Versions
* `@sentry/nextjs` 10.69.0
* `next` 16.2.11 (Turbopack build, the default in Next 16)
* Vercel Functions, Node.js runtime (Fluid Compute), `nodejs22.x`
## The code
`@sentry/core/build/cjs/utils/vercelWaitUntil.js`:
```js
function vercelWaitUntil(task) {
if (typeof EdgeRuntime !== "string") {
return; // <-- Node.js runtime returns here
}
const vercelRequestContextGlobal =
worldwide.GLOBAL_OBJ[Symbol.for("@vercel/request-context")];
const ctx = vercelRequestContextGlobal?.get?.();
if (ctx?.waitUntil) {
ctx.waitUntil(task);
}
}
```
`@sentry/nextjs/build/cjs/common/utils/responseEnd.js` routes every flush through it:
```js
function waitUntil(task) {
if (isCloudflareWaitUntilAvailable()) {
cloudflareWaitUntil(task);
return;
}
core.vercelWaitUntil(task);
}
```
And both App Router wrappers end with it — `wrapRouteHandlerWithSentry`:
```js
() => {
responseEnd.waitUntil(responseEnd.flushSafelyWithTimeout());
}
```
`wrapServerComponentWithSentry` does the same.
So on Vercel + Node.js runtime, the flush is a no-op and the queued envelope dies with the frozen instance.
## Reproduction
1. Next.js 16 app on Vercel, Node.js runtime (the default), Turbopack build.
2. Wrap an App Router route handler with `wrapRouteHandlerWithSentry` and throw from it.
3. The error never appears in Sentry.
Because Turbopack builds no longer apply build-time instrumentation, we were applying the wrappers manually, but the same code path runs when the SDK applies them itself under webpack.
## Evidence that the request is never made
The Vercel trace for the invocation reports **"External APIs: No outgoing requests"**, while the thrown error itself is present in the runtime logs. So the handler ran, the wrapper ran, and no HTTP request to Sentry was ever attempted.
Adding an explicit awaited flush around the same wrapper fixes it immediately and the event arrives with the correct `mechanism: auto.function.nextjs.route_handler`:
```ts
try {
return await wrapped(...args)
} catch (err) {
await flush(2000)
throw err
}
```
A probe on the same route that called `captureException` followed by `await flush()` always delivered (`afterSendEvent status=200`), which isolates the problem to the missing wait rather than to transport, DSN, or initialization.
## Why the guard looks wrong
`EdgeRuntime` is only defined on the Edge runtime, but Vercel now recommends the Node.js runtime (Fluid Compute) over Edge for most workloads, and it is the default for Next.js apps. The Node.js runtime also exposes `Symbol.for("@vercel/request-context")` with a working `waitUntil`, which is how `@vercel/functions`' own `waitUntil` operates there.
So the early return excludes the runtime where most Vercel users actually are.
## Suggested fix
Drop the `EdgeRuntime` check and attempt the request-context lookup unconditionally; it already degrades gracefully when the symbol or `waitUntil` is absent:
```js
function vercelWaitUntil(task) {
const vercelRequestContextGlobal =
worldwide.GLOBAL_OBJ[Symbol.for("@vercel/request-context")];
const ctx = vercelRequestContextGlobal?.get?.();
if (ctx?.waitUntil) {
ctx.waitUntil(task);
}
}
```
If the guard exists for a reason I am missing, it would help a lot if the no-op path emitted a debug log. Right now the failure is indistinguishable from a DSN problem, a network problem, or the wrapper not being applied at all, which is what made this take a while to find.
## Possibly related
* [#18871]() (`makeNodeTransport` silently dropping server-side events under Next 16 + Turbopack)
* getsentry/sentry-javascript#21713 (`proxy.ts`/middleware not instrumented under Turbopack builds)
All three share the same shape: the code runs, and the event silently never leaves.
Guida per i contributori
Apri la guida per i contributori
Valutazione
Questa issue non è ancora stata valutata.