Skip to content

ref(browser): Extract browser-specific normalize code out of core - #21172

Merged
mydea merged 19 commits into
developfrom
fn/move-convertToPlainObject
May 28, 2026
Merged

ref(browser): Extract browser-specific normalize code out of core#21172
mydea merged 19 commits into
developfrom
fn/move-convertToPlainObject

Conversation

@mydea

@mydeamydea commented May 26, 2026

Copy link
Copy Markdown
Member

Summary

Today, @sentry/core carries browser-specific code that runs (or just ships) in every runtime: window / document / HTMLElement collapsing inside normalize(), Vue ViewModel detection, React SyntheticEvent detection, and htmlTreeAsString (DOM walk). This PR moves all of that into the packages that actually need it, leaving @sentry/core more runtime-agnostic (there are still a bunch of other things, but step by step...), and exposes a single new hook for SDKs to plug runtime-specific rendering into normalize().

Mechanism

packages/core/src/utils/normalize.ts now exposes:

exportfunctionsetNormalizeStringifier(fn: ((value)=>string|undefined)|undefined): void

normalize()'s internal stringifyValue consults the registered function before its runtime-agnostic fallbacks (NaN / function / symbol / bigint / [object ConstructorName]). Returning a string short-circuits; returning undefined falls through. Default state is no stringifier — server-only consumers never reach this code path.

What moved

SymbolNew homeCore export
htmlTreeAsString@sentry-internal/browser-utils@deprecated
isElement@sentry-internal/browser-utils@deprecated
isSyntheticEvent@sentry/react (internal)@deprecated
isVueViewModel@sentry/vue (internal)@deprecated
getVueInternalName@sentry/vue (internal)@deprecated

@sentry-internal/browser-utils also gains a new normalizeStringifyValue that handles window[Window], document[Document], and HTMLElement instances → [HTMLElement: <css-selector-path>] (via htmlTreeAsString). It explicitly does not handle Vue or React values — those are added by their respective SDKs.

SDK wiring

  • @sentry/browserinit() calls setNormalizeStringifier(normalizeStringifyValue) (the browser-utils variant).
  • @sentry/vueinit() runs after browserInit, then registers a wrapper that checks isVueViewModel and otherwise delegates to browser-utils normalizeStringifyValue.
  • @sentry/reactinit() does the same for isSyntheticEvent.

Other adjustments

  • safeJoin in core now uses stringifyValue from normalize instead of having special handling for vue, this should streamline things a bit. as a sideeffect, we also stringify certain things better now, e.g. html elements etc. get the nice tree format in browser

Tests

  • packages/core/test/lib/utils/normalize.test.ts — covers registry semantics: stub stringifier is consulted for every visited node, falls back to defaults when it returns undefined, behaves correctly with no stringifier registered.
  • packages/core/test/lib/utils/string.test.ts — new safeJoin() block covers primitive/non-primitive routing, default delimiter, non-array input, Error rendering, and interaction with setNormalizeStringifier.
  • packages/browser-utils/test/normalizeStringifyValue.test.ts — direct and integration tests for window / document / HTMLElement, plus confirmation that Vue/React values are not intercepted.
  • packages/vue/test/integration/normalize.test.ts — verifies Vue init's wrapper collapses Vue 2/3 ViewModels and VNodes while still delegating HTMLElement / document to the browser variant underneath.
  • packages/react/test/normalize.test.ts — same for React's SyntheticEvent wrap.

🤖 Generated with Claude Code

@mydeamydea self-assigned this May 26, 2026
Comment threadpackages/browser-utils/src/domEventNormalizer.ts Outdated
@github-actions

github-actionsBot commented May 26, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.16 kB-0.43%-117 B 🔽
@sentry/browser - with treeshaking flags25.61 kB-0.28%-71 B 🔽
@sentry/browser (incl. Tracing)45.23 kB-0.06%-23 B 🔽
@sentry/browser (incl. Tracing + Span Streaming)47.47 kB-0.06%-24 B 🔽
@sentry/browser (incl. Tracing, Profiling)50.21 kB-0.04%-18 B 🔽
@sentry/browser (incl. Tracing, Replay)84.81 kB-0.06%-44 B 🔽
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags74.38 kB+0.03%+20 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas)89.52 kB-0.07%-54 B 🔽
@sentry/browser (incl. Tracing, Replay, Feedback)102.14 kB-0.04%-38 B 🔽
@sentry/browser (incl. Feedback)44.33 kB-0.29%-128 B 🔽
@sentry/browser (incl. sendFeedback)31.98 kB-0.33%-104 B 🔽
@sentry/browser (incl. FeedbackAsync)37.07 kB-0.37%-135 B 🔽
@sentry/browser (incl. Metrics)28.24 kB-0.46%-128 B 🔽
@sentry/browser (incl. Logs)28.48 kB-0.4%-114 B 🔽
@sentry/browser (incl. Metrics & Logs)29.18 kB-0.37%-108 B 🔽
@sentry/react28.99 kB-0.09%-24 B 🔽
@sentry/react (incl. Tracing)47.5 kB+0.03%+11 B 🔺
@sentry/vue32.2 kB+0.04%+10 B 🔺
@sentry/vue (incl. Tracing)47.15 kB+0.08%+35 B 🔺
@sentry/svelte27.18 kB-0.43%-117 B 🔽
CDN Bundle29.55 kB-0.45%-133 B 🔽
CDN Bundle (incl. Tracing)47.81 kB+0.07%+32 B 🔺
CDN Bundle (incl. Logs, Metrics)31.05 kB-0.38%-118 B 🔽
CDN Bundle (incl. Tracing, Logs, Metrics)49.02 kB-0.03%-14 B 🔽
CDN Bundle (incl. Replay, Logs, Metrics)70.31 kB-0.25%-174 B 🔽
CDN Bundle (incl. Tracing, Replay)85.17 kB-0.13%-108 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)86.34 kB-0.12%-97 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback)91.05 kB-0.11%-100 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)92.21 kB-0.12%-107 B 🔽
CDN Bundle - uncompressed87.56 kB-0.16%-136 B 🔽
CDN Bundle (incl. Tracing) - uncompressed144.03 kB-0.09%-120 B 🔽
CDN Bundle (incl. Logs, Metrics) - uncompressed92.04 kB-0.15%-138 B 🔽
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed147.78 kB-0.09%-120 B 🔽
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed216.77 kB-0.07%-138 B 🔽
CDN Bundle (incl. Tracing, Replay) - uncompressed262.8 kB-0.05%-121 B 🔽
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed266.55 kB-0.05%-121 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed276.5 kB-0.05%-121 B 🔽
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed280.24 kB-0.05%-121 B 🔽
@sentry/nextjs (client)50.01 kB+0.08%+36 B 🔺
@sentry/sveltekit (client)45.66 kB-0.16%-72 B 🔽
@sentry/core/server75.9 kB-0.68%-518 B 🔽
@sentry/core/browser63.06 kB-0.17%-103 B 🔽
@sentry/node-core61.69 kB-0.92%-568 B 🔽
@sentry/node130.14 kB-0.46%-600 B 🔽
@sentry/node - without tracing74.11 kB-0.8%-596 B 🔽
@sentry/aws-serverless86.34 kB-0.66%-568 B 🔽
@sentry/cloudflare (withSentry) - minified170.59 kB-1.06%-1.83 kB 🔽
@sentry/cloudflare (withSentry)426.95 kB-0.87%-3.73 kB 🔽

View base workflow run

@mydea
mydeaforce-pushed the fn/move-convertToPlainObject branch 2 times, most recently from 9729b8a to fbe2a21CompareMay 27, 2026 09:03
Comment threadpackages/core/src/utils/string.ts Outdated
Comment threadpackages/core/src/utils/object.ts
Comment threadpackages/core/test/lib/utils/normalize.test.ts Outdated
@mydeamydea changed the title ref(browser): Extract browser-specific normalize code into registerable utilref(browser): Extract browser-specific normalize code out of coreMay 27, 2026

@cursorcursorBot 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.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 02da5dd. Configure here.

Comment threadpackages/core/src/asyncContext/types.ts Outdated
@mydea
mydeaforce-pushed the fn/move-convertToPlainObject branch from 87b679e to 36cca78CompareMay 27, 2026 12:23

@Lms24Lms24 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! I think this makes things more targeted and cleaner.

One concern, but I think this is something we can address if it comes up: Us pulling out the Vue/React specific stuff into the respective framework SDKs is cleaner (fully agree!) but also means anyone using the default browser SDK (loader/CDN especially) on React/Vue apps won't get the specific functionality. Again, probably fine to fix if it becomes a real issue. The fix would be that we simply pull the added functionality into the browser-utils core version. We might need to eat the bundle size/minimal perf overhead increase at this point.

* Vue ViewModels and React SyntheticEvents are not handled here — the Vue and React
* SDKs wrap this function in their `init` and add their own checks on top.
*/
export function normalizeStringifyValue(value: Exclude<unknown, string | number | boolean | null>): string | undefined {

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.

l: I might be missing something obvious but can we hoist this up into @sentry/browser? I don't see it being imported by packages that don't depend on browser (as opposed to htmlTreeAsString)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

good point, this makes sense 👍

@mydea

Copy link
Copy Markdown
MemberAuthor

One concern, but I think this is something we can address if it comes up: Us pulling out the Vue/React specific stuff into the respective framework SDKs is cleaner (fully agree!) but also means anyone using the default browser SDK (loader/CDN especially) on React/Vue apps won't get the specific functionality. Again, probably fine to fix if it becomes a real issue. The fix would be that we simply pull the added functionality into the browser-utils core version. We might need to eat the bundle size/minimal perf overhead increase at this point.

I agree, but I think in this case it is fine because mostly this is additive stuff (and a fix for a weird vue bug that may/may not be around still, sadly we don't really have integration/e2e tests where we know this is covered...), so it feels fine to say you only get this if using the proper SDK. E.g. for react, this will just be serialized a bit differently which is also not the end of the world I'd say :)

Relocates `normalizeStringifyValue` (and its test) out of
`@sentry-internal/browser-utils` into `@sentry/browser`, exports it from
the browser package's public surface, and switches `@sentry/vue` and
`@sentry/react` to import from `@sentry/browser` instead.
Both framework packages no longer need a direct dependency on
`@sentry-internal/browser-utils`, so that entry is dropped from their
`package.json` files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mydea
mydea marked this pull request as ready for review May 27, 2026 14:59
@mydea
mydea requested review from a team as code ownersMay 27, 2026 14:59
@mydea
mydea requested review from logaretm, nicohrubec and s1gr1d and removed request for a teamMay 27, 2026 14:59
@mydea
mydea merged commit 02cd482 into developMay 28, 2026
265 of 266 checks passed
@mydea
mydea deleted the fn/move-convertToPlainObject branch May 28, 2026 07:38
Sign up for freeto 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.

2 participants

@mydea@Lms24