SIGN IN SIGN UP

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

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

```ts
export function setNormalizeStringifier(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

| Symbol | New home | Core 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/browser` `init()` calls
`setNormalizeStringifier(normalizeStringifyValue)` (the browser-utils
variant).
- `@sentry/vue` `init()` runs after `browserInit`, then registers a
wrapper that checks `isVueViewModel` and otherwise delegates to
browser-utils `normalizeStringifyValue`.
- `@sentry/react` `init()` 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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
F
Francesco Gringl-Novy committed
02cd482fa3c600426619ef9f0139e11610a1d154
Parent: 5bd7ea2
Committed by GitHub <noreply@github.com> on 5/28/2026, 7:38:29 AM