Skip to content

@sentry/nuxt: responseHook flushes the client on every HTTP response, so every request ships its own client_report envelope #23306

Description

@maxmaxme

Is there an existing issue for this?

How do you use Sentry?

Self-hosted/on-premise

Which SDK are you using?

@sentry/nuxt

SDK Version

10.69.0

Framework Version

Nuxt 4.5.1

Link to Sentry event

No response

Reproduction Example/SDK Setup

Minimal Nuxt 4 app, @sentry/nuxt module enabled, one page + one API route.
A plain node:http server on :7877 acts as the tunnel and counts envelopes by type.

// sentry.server.config.ts
Sentry.init({
  dsn: 'https://<key>@example.net/4',
  environment: 'demo',
  release: 'demo-1',
  enableLogs: true,
  tunnel: 'http://localhost:7877/sentry',   // local sink that counts envelopes
  tracesSampleRate: 0.001,
  debug: false,
})

Built with the node-server preset and started with
node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs.

Steps to Reproduce

  1. Build the app above and start it with the --import flag.
  2. Point tunnel at a local HTTP server that logs the type of each envelope
    (parse the second line of the envelope body) and answers 200.
  3. Issue 30 plain GET requests to the page route, one after another.
  4. Wait ~5s and count the envelopes received by the sink.

Expected Result

Client reports are aggregated and flushed on an interval
(clientReportFlushInterval, 60s by default), so 30 requests should produce
roughly one client_report envelope, not one per request.

Actual Result

30 requests produce 60 envelopes — one client_report and one sessions per request:

SSR page requests:      30
envelopes total:        60
by type:                {"client_report":30,"sessions":30}
client_report per page: 1.00
client_report sizes:    212..214 B

The cause is in packages/nuxt/src/server/sdk.ts:

httpIntegration({
  instrumentation: {
    responseHook: () => {
      vercelWaitUntil(flushSafelyWithTimeout())   // argument is evaluated unconditionally
    },
  },
})

vercelWaitUntil is a no-op outside Vercel, but flushSafelyWithTimeout() is
called before it, so flush(2000) runs on every HTTP response on every
platform. flush() calls _flushOutcomes(), so any pending outcome is
immediately shipped as its own envelope instead of being aggregated.

Additional Context

Since 10.65.0, outgoing requests without a parent span record a no_parent_span
outcome (#22113, #20350). Combined with the per-response flush, each outcome
ships as its own envelope instead of being batched. Bisected on a
background-only workload (zero inbound requests, 100 outbound calls made
outside any request transaction):

@sentry/nuxt envelopes
10.52 – 10.64 0
10.65.0+ 100 (no_parent_span:span ×100)
10.69.0 + sendClientReports: false 0

Worst case: if another OpenTelemetry setup calls NodeTracerProvider.register()
after Sentry.init() (our own misconfiguration, fixed separately), the
SentryContextManager is replaced and no request ever has a parent span. The
SDK logs four errors about the missing setup, but keeps recording outcomes — so
every outbound request produces one, including the transport's own envelope
delivery. Each envelope response triggers the next flush, so the rate is bound
only by ingest RTT: 1166 envelopes in 90s from an idle pod (~500/s in-cluster at
RTT 2ms, ~13/s locally at RTT 60ms), with zero user traffic.

Suggestions:

  1. Call flushSafelyWithTimeout() lazily, inside the branch where waitUntil
    semantics apply, so non-Vercel deployments don't flush per response.
  2. Skip no_parent_span outcomes once the SDK has detected its context manager
    was replaced — they carry no signal in that state.

Priority

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it.

Metadata

Metadata

Assignees

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions