getsentry / getsentry/sentry-javascript

vercelWaitUntil() is a no-op on Vercel's Node.js runtime, silently dropping events flushed by the Next.js wrappers

オープン
#23,087 コメント 4 件 リアクション 0 件 担当者 1 名 @chargome が担当を希望しています GitHub で見る
Next.js Node.js post-hackweek
主要言語
TypeScript
スター
8.7k
フォーク
1.8k
平均マージ
1日 17時間
マージ済み PR(30日)
515

説明

## 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.

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。