Skip to content

feat(core): Warn on repeated Sentry.init() and unbind the client on close() - #24962

Merged
isaacs merged 7 commits into
developfrom
isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment
Oct 5, 2026
Merged

isaacs merged 7 commits into
developfrom
isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment

Conversation

@isaacs

@isaacs isaacs commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

A repeated Sentry.init() call was mostly undefined behavior, and each SDK handled it in its own way. Most SDKs built a new client and replaced the old one without a warning. Nothing closed the old client, so its buffers, timers, and hooks stayed alive, and setupOnce kept the settings of the first call.

The goal is one rule for all SDKs: the first init() wins, a later call returns the active client, and close() lets you start over. Most SDKs cannot switch to "first wins" before a major version, so this change adds the parts that are safe now:

  • initAndBind, Node's _init, and Vercel Edge's init print a warning (with or without debug) when a client is already bound. They still replace the client for now. Wrappers that expect a repeated call (Next.js server, Remix server, Nuxt server, Hono) keep their own guard and return early, so they do not warn. Cloudflare's cacheClient: false asks for a new client on each call, so it unbinds the old one first and does not warn. The Next.js client drops its own warning, which used a flag that never reset and so also fired after close(). Its config-file hint moves to the docs.
  • Sentry.close() unbinds the client after it closes it. Before, a guard based on getClient() treated the closed client as active, so close(); init() returned the closed client. Nuxt's server guard now also checks for a bound client, for the same reason. Cloudflare also caches its client for the isolate, and the cache handed the closed client back to every later init(), so the isolate sent nothing until it was recycled. Closing the cached client now clears the cache.
  • Next.js server and Remix server return the active client from a repeated call, not undefined. A caller could not tell "already initialized" from "failed".

The Next.js and Nuxt server init() added their event processors to the global scope. Since close() now unbinds the client, a later init() would run the full setup again, and each cycle would add another copy of each processor to the global scope. So instead, add them to the client, so that they go away with a closed client, and a later init() uses its own options. Client processors run before all scope processors, so these now also run before any global scope processor that user code added before init(). They only drop events or fix stack frames, so the earlier position does not change which events are sent.

docs/repeated-init.md records the rule, the current behavior of each SDK, and the plan for the next major. The warning text should point apps that share a page to the isolated client helper from #24883 once that helper has a final name.

Fixes #24960

@linear-code

linear-code Bot commented Oct 1, 2026

Copy link
Copy Markdown

JS-3867

@isaacs

isaacs commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.6 kB +0.53% +155 B 🔺
@sentry/browser - with treeshaking flags 27.75 kB +0.61% +167 B 🔺
@sentry/browser - with treeshaking flags tracing without tracing 27.65 kB +0.63% +171 B 🔺
@sentry/browser (incl. Tracing) 51.52 kB +0.39% +198 B 🔺
@sentry/browser (incl. Tracing + Span Streaming) 51.52 kB +0.37% +189 B 🔺
@sentry/browser (incl. Tracing, Profiling) 54.5 kB +0.35% +185 B 🔺
@sentry/browser (incl. Tracing, Replay) 91.14 kB +0.22% +193 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.12 kB +0.25% +193 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 95.83 kB +0.2% +186 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 108.83 kB +0.17% +182 B 🔺
@sentry/browser (incl. Feedback) 47.12 kB +0.34% +159 B 🔺
@sentry/browser (incl. sendFeedback) 34.65 kB +0.45% +152 B 🔺
@sentry/browser (incl. FeedbackAsync) 39.76 kB +0.39% +151 B 🔺
@sentry/browser (incl. Metrics) 30.61 kB +0.52% +157 B 🔺
@sentry/browser (incl. Logs) 30.89 kB +0.49% +148 B 🔺
@sentry/browser (incl. Metrics & Logs) 31.55 kB +0.51% +160 B 🔺
@sentry/react 31.43 kB +0.5% +155 B 🔺
@sentry/react (incl. Tracing) 53.84 kB +0.28% +145 B 🔺
@sentry/vue 37.55 kB +0.37% +137 B 🔺
@sentry/vue (incl. Tracing) 54.4 kB +0.3% +160 B 🔺
@sentry/svelte 29.63 kB +0.53% +154 B 🔺
@sentry/remix (Remix 3 client bundle) 55.85 kB +0.36% +199 B 🔺
CDN Bundle 31.33 kB +0.65% +202 B 🔺
CDN Bundle (incl. Tracing) 52.07 kB +0.4% +203 B 🔺
CDN Bundle (incl. Logs, Metrics) 33.56 kB +0.59% +194 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 54.03 kB +0.41% +218 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 74.3 kB +0.27% +199 B 🔺
CDN Bundle (incl. Tracing, Replay) 89.67 kB +0.25% +219 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 91.62 kB +0.22% +201 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 95.83 kB +0.21% +197 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 97.8 kB +0.22% +212 B 🔺
CDN Bundle - uncompressed 92.46 kB +0.56% +513 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 154.77 kB +0.34% +513 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.04 kB +0.53% +513 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 160.72 kB +0.33% +513 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 228.66 kB +0.23% +513 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 274.58 kB +0.19% +513 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 280.52 kB +0.19% +513 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 288.28 kB +0.18% +513 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 294.21 kB +0.18% +513 B 🔺
@sentry/nextjs (client) 56.19 kB +0.02% +6 B 🔺
@sentry/sveltekit (client) 51.9 kB +0.33% +166 B 🔺
@sentry/core/server 40.52 kB +0.06% +24 B 🔺
@sentry/core/browser 13.51 kB - -
@sentry/node 145.02 kB +0.07% +97 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 83.22 kB - -
@sentry/node - without tracing 93.46 kB +0.06% +52 B 🔺
@sentry/node - without channel injection 123.17 kB +0.04% +49 B 🔺
@sentry/aws-serverless 101.69 kB +0.04% +37 B 🔺
@sentry/cloudflare (withSentry) - minified 209.04 kB +0.28% +571 B 🔺
@sentry/cloudflare (withSentry) 517.75 kB +0.2% +1 kB 🔺

View base workflow run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/remix/src/server/sdk.ts
@isaacs
isaacs marked this pull request as ready for review October 1, 2026 22:00
@isaacs
isaacs requested review from a team as code owners October 1, 2026 22:00
@isaacs
isaacs requested review from JPeer264, logaretm, mydea, nicohrubec and s1gr1d and removed request for a team October 1, 2026 22:00
Comment thread packages/nuxt/src/server/sdk.ts

@JPeer264 JPeer264 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change. Got some questions throughout. The Sentry bot comment might be worth to check out

Comment thread packages/browser/test/profiling/integration.test.ts
Comment thread packages/cloudflare/src/baseSdk.ts Outdated
Comment thread AGENTS.md
Comment thread packages/cloudflare/src/baseSdk.ts
@isaacs
isaacs force-pushed the isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment branch 3 times, most recently from 1386927 to 2f4c9d9 Compare October 2, 2026 20:04

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/exports.ts
Comment thread packages/core/src/sdk.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/core/src/client.ts Outdated
@isaacs
isaacs force-pushed the isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment branch from 2df3d49 to f20a5b1 Compare October 3, 2026 04:51

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/cloudflare/src/baseSdk.ts Outdated
@isaacs
isaacs requested a review from JPeer264 October 3, 2026 17:29
@isaacs

isaacs commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

@JPeer264 Thanks for the review! Bugbot and SentryBot caught a few subtle interesting issues, which led to a bit of refactoring. If you want to take another look, it might be worth doing. Or just rubber-stamp if it still LGTY, that's fine too :)

Also: the size increase is 99% a result of just adding a new human-readable error message, so not much to be done for it.

@isaacs
isaacs force-pushed the isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment branch from a9c4fae to 429dbf9 Compare October 5, 2026 14:20
Comment thread packages/core/src/client.ts

@JPeer264 JPeer264 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice. Some tests fail though, but it doesn't seem related to this PR

isaacs and others added 5 commits October 5, 2026 13:35
… `close()`

A repeated `Sentry.init()` call was mostly undefined behavior, and
each SDK handled it in its own way. Most SDKs built a new client and
replaced the old one without a warning. Nothing closed the old client,
so its buffers, timers, and hooks stayed alive, and `setupOnce` kept
the settings of the first call.

The goal is one rule for all SDKs: the first `init()` wins, a later
call returns the active client, and `close()` lets you start over.
Most SDKs cannot switch to "first wins" before a major version, so
this change adds the parts that are safe now:

- `initAndBind`, Node's `_init`, and Vercel Edge's `init` print a
  warning (with or without `debug`) when a client is already bound.
  They still replace the client for now. Wrappers that expect a
  repeated call (Next.js server, Remix server, Nuxt server, Hono) keep
  their own guard and return early, so they do not warn. Cloudflare's
  `cacheClient: false` asks for a new client on each call, so it
  unbinds the old one first and does not warn. The Next.js client drops
  its own warning, which used a flag that never reset and so also fired
  after `close()`. Its config-file hint moves to the docs.
- `Sentry.close()` unbinds the client after it closes it. Before, a
  guard based on `getClient()` treated the closed client as active, so
  `close(); init()` returned the closed client. Nuxt's server guard
  now also checks for a bound client, for the same reason. Cloudflare
  also caches its client for the isolate, and the cache handed the
  closed client back to every later `init()`, so the isolate sent
  nothing until it was recycled. Closing the cached client now clears
  the cache.
- Next.js server and Remix server return the active client from a
  repeated call, not `undefined`. A caller could not tell "already
  initialized" from "failed".

`docs/repeated-init.md` records the rule, the current behavior of
each SDK, and the plan for the next major. The warning text should
point apps that share a page to the isolated client helper from
PR #24883 once that helper has a final name.

The Next.js and Nuxt server `init()` added their event processors to the
global scope. Since `close()` now unbinds the client, a later `init()`
would run the full setup again, and each cycle would add another copy of
each processor to the global scope. So instead, add them to the client,
so that they go away with a closed client, and a later `init()` uses its
own options. Client processors run before all scope processors, so these
now also run before any global scope processor that user code added
before `init()`. They only drop events or fix stack frames, so the
earlier position does not change which events are sent.

Update the write-tests skill to reset a bound client between tests that
call `init()`.

Fixes #24960

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`close()` unbinds the client only from the scope that is current when it
runs. Other scopes can still hold the closed client: the root scope when
`close()` runs inside a request or `withScope`, scopes forked before
`close()` ran, and every scope when code calls `client.close()`
directly. The "already initialized" guards in Next.js server, Remix
server, Nuxt server, and Hono, and the repeated-init warning, used
`getClient()`, so they returned the closed, disabled client or warned
for no reason.

Core now records closed clients, and a new internal
`getActiveClient()` returns the bound client only if it is not closed.
The guards and the warning use it. A client created with
`enabled: false` still counts as active, because only `close()` marks a
client as closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`isClientClosed()` used a `WeakSet` local to the `client` module. When
two copies of `@sentry/core` at the same version load (for example the
CJS and ESM builds in one Node process), they share the carrier and so
the bound client, but not that set. A client closed through one copy
still looked active to the other, so its init guards returned the
closed client.

Store the mark on the client under a `Symbol.for` key instead, so every
copy reads the same mark.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@isaacs
isaacs force-pushed the isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment branch from 429dbf9 to ec00267 Compare October 5, 2026 20:36
@isaacs
isaacs enabled auto-merge (squash) October 5, 2026 22:57

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 60932ab. Configure here.

Comment thread packages/nextjs/src/client/index.ts
The Next.js client `init()` added `NextRedirectErrorFilter`, and
`devErrorSymbolicationEventProcessor` in development, to the isolation
scope. Each repeated `init()`, including one after `close()`, added
another copy that outlived the old client.

Add them to the client instead, as the server `init()` already does.
Client processors run before scope processors. These only drop redirect
errors or fix stack frames, so the earlier position does not change
which events are sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@isaacs
isaacs merged commit 6805115 into develop Oct 5, 2026
354 checks passed
@isaacs
isaacs deleted the isaacschlueter/js-3867-bring-repeated-sentryinit-behavior-into-alignment branch October 5, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bring repeated Sentry.init() behavior into alignment

2 participants