Skip to content

fix(cli): Recognize debug ID snippets from Oxc and React Native Metro - #2268

Merged
BYK merged 4 commits into
getsentry:mainfrom
esetnik:fix/debug-id-registration-template-literals
Nov 14, 2026
Merged

BYK merged 4 commits into
getsentry:mainfrom
esetnik:fix/debug-id-registration-template-literals

Conversation

@esetnik

@esetnik esetnik commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #2267.

sentry sourcemap inject and sentry sourcemap upload check whether a file already registers its debug ID by matching the registry write with DEBUGID_REGISTRATION_RE. Three forms that Sentry's own tools produce were not matched, so each got a second snippet on a new first line, with ; prepended to the map's mappings:

  1. A template literal ID. Oxc, the minifier in Vite 8, rewrites a bundler plugin's e._sentryDebugIds[n]="<id>" to e._sentryDebugIds[t]=`<id>` .
  2. A registry initialized with ||=. Oxc folds Turbopack's (e._debugIds||(e._debugIds={}))[n] into (e._debugIds||={})[t].
  3. A registry that is a bare global. The React Native Metro plugin emits _sentryDebugIds[stack]="<id>", with no object in front. This one needs no minifier.

Through @sentry/vite-plugin, the first shifts every uploaded source map one line ahead of the deployed chunk: the plugin uploads prepared copies, so the deployed files never get the extra line. Frames then resolve to the wrong function or fail with js_invalid_sourcemap_location.

Changes

One commit per form, all in DEBUGID_REGISTRATION_RE:

  • Accept backticks wherever a quote was accepted: around the ID, and around the registry name in the bracket form (e[`_sentryDebugIds`]).
  • Accept the inline registry initialization written as a logical assignment, ||= or ??=.
  • Accept the registry name as a bare identifier. It must not be the tail of a longer identifier, so my_sentryDebugIds[t]="<id>" still gets a snippet.

Tests, in the existing tables in inject.test.ts (each runs for external and inline maps):

  • Preserved registrations: templateLiterals and turbopackLogicalAssignment as Oxc emits them, and reactNativeMetro as the Metro plugin emits it.
  • Near miss that must still be injected: longerIdentifier.
  • 3b36a4a, pushed by sentry-junior, pins ??= and the backticked bracket form and adds three more near misses.

Verification

Project checks on the branch head (db3441c), run locally on macOS:

  • pnpm run tsc: passes.
  • pnpm run lint at the repo root (oxlint, oxfmt, ast-grep, workspace dependency check): passes.
  • pnpm run test:e2e in packages/cli: 206 passed, 1 failed, 3 skipped. The failure is telemetry-exit, a timing assertion (3.08 s against a 2 s limit) while the suite was running; it passes on its own on this branch and on main.
  • inject.test.ts: 72 passed.
  • Each new case fails when its part of the regex is reverted: templateLiterals and turbopackLogicalAssignment without the first two commits, reactNativeMetro without the third, and longerIdentifier if the identifier boundary is dropped.
  • pnpm run test in packages/cli: 13,170 passed, 28 failed, 61 skipped. None of the failures is in sourcemap code, and they are not caused by this change: the same 28, in the same six files, fail on unmodified main (f8464f8) on this machine: debug-files/upload, build/car-extract.integration, build/zip-writer, delta-upgrade.mocked, install-script and time-range.

A wider matrix, run locally through injectDirectory on the branch head and not committed (150 cases):

  • Snippet form x minifier, 120 cases. Six snippet forms (bundler plugin, this CLI, legacy CLI, formatted bracket form, Turbopack, React Native Metro) through 20 minifier setups: unminified, terser (default, ES2022 with 3 passes and unsafe, single quotes, beautify, no mangle), swc (default, ES2022 with 2 passes, no compress), esbuild (default, esnext, es2015, syntax only) and Oxc via rolldown 1.2.19 (default, no mangle, no compress, es2015, es2020, esnext, whitespace kept). Each file must be left unchanged and register its ID once at runtime.
    • 114 are left unchanged.
    • The other 6 are the Metro snippet under Oxc with mangling on. Oxc renames the top-level var _sentryDebugIds to e, so the file no longer registers under the registry name and injecting a snippet is the correct result. With mangling off the Metro snippet is left unchanged.
    • On the first commit alone, the five non-Metro forms gave 96 of 100; the 4 failures were Turbopack's snippet under Oxc, fixed by the second commit.
    • Only Oxc changes the quoting or the initialization. terser, swc and esbuild leave both as written.
  • File layout, 30 cases, all left unchanged: 15 layouts each for the Oxc-minified bundler plugin snippet and the unminified Metro snippet. Debug ID comment before or after sourceMappingURL, no trailing newline, CRLF, a hashbang first, a "use strict" directive first, the snippet after other code, an uppercase ID, a 32-hex ID, the ID only in the map (debug_id or debugId), the ID only in the comment, a legacy //@ sourceMappingURL, and an inline map with and without the comment.

Checked against the regex only, on the branch head:

  • uglify-js 3.19.3 and Bun 1.4.4 (--minify, --minify-syntax, --minify-whitespace) leave the bundler plugin, CLI, formatted, Turbopack and esbuild-plugin snippets recognisable.

  • Inputs it must reject: mismatched quotes, a registry read that is not a write, ||={a:1}, and &&=.

  • Closure Compiler v20240317: WHITESPACE_ONLY and SIMPLE leave all six snippet forms recognisable. ADVANCED renames the _sentryDebugIds property (to g), so the bundle no longer registers under the registry name and injecting is correct.

A real Metro bundle: metro 0.88.0 with createSentryMetroSerializer from @sentry/react-native 8.35.0, built with --minify true and --minify false. Both bundles carry the bare-global snippet and a //# debugId= comment.

CLI Minified bundle Unminified bundle
sentry@0.45.0 unchanged unchanged
sentry@0.49.0 snippet prepended snippet prepended
main (f8464f8), from source snippet prepended snippet prepended
this branch, from source unchanged unchanged

Not tested: a full React Native app build (the bundle above has a plain JS entry file, with no react-native runtime).

🤖 Generated with Claude Code

`sourcemap inject` and `sourcemap upload` decide whether a file already
registers its debug ID by matching the registry write, and only accepted
a quoted ID. Oxc, the minifier in Vite 8, rewrites a bundler plugin's
`e._sentryDebugIds[n]="<id>"` to a template literal, so the check missed
it and a second snippet was prepended on a new first line.

Through @sentry/vite-plugin this shifted every uploaded source map one
line ahead of the deployed chunk, because the plugin uploads prepared
copies and the deployed files are not touched.

Accept backticks wherever a quote is accepted.

Fixes getsentry#2267

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Nov 14, 2026 3:50pm UTC

Request Review

Oxc also folds Turbopack's inline registry initialization,
`(e._debugIds||(e._debugIds={}))[n]`, into a logical assignment,
`(e._debugIds||={})[n]`. Accept that form, and `??=`, so a Turbopack
chunk minified by Oxc is not given a second snippet either.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@github-actions github-actions Bot added risk: medium PR risk score: medium and removed risk: low PR risk score: low labels Nov 14, 2026
Each form the registration check accepts now has a test that fails when
that form is removed: `??=` and a bracketed registry name in backticks
had none. Add near misses (an interpolated template, `&&=`, and `==`)
that must still get a snippet, since this check gates skipping injection.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>

@sentry-junior sentry-junior Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good to me, and thanks for the thorough write-up @esetnik. The bug still reproduces on main, and both new cases fail there and pass here. I also minified the bundler plugin, CLI, legacy CLI and Turbopack snippets with oxc-minify, rolldown, terser, esbuild and swc, and every output is recognized and registers its ID at runtime. Timing the regex on 16 MB near-miss inputs showed it stays linear.

This check decides whether injection is skipped, so I pushed one test-only commit (3b36a4a):

  • ??= and a bracketed registry name in backticks were accepted, but removing either one left every test passing. Each now has a case in the existing table, and each new case fails when its branch is reverted.
  • Added three near misses that must still get a snippet: an interpolated template, &&=, and ==. A version of the regex that also accepted &&= fails these.

@esetnik esetnik changed the title fix(cli): Recognize debug ID snippets that use template literals fix(cli): Recognize debug ID snippets rewritten by Oxc Nov 14, 2026
The React Native Metro plugin registers on a global variable,
`_sentryDebugIds[stack]="<id>"`, not on a property of the global
object. The check required a `.` or a bracketed name before the
registry, so a Metro bundle was given a second snippet as well. This
one does not depend on a minifier.

Accept the registry name when it is not part of a longer identifier.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@esetnik esetnik changed the title fix(cli): Recognize debug ID snippets rewritten by Oxc fix(cli): Recognize debug ID snippets from Oxc and React Native Metro Nov 14, 2026
@BYK
BYK enabled auto-merge (squash) November 14, 2026 19:21
@BYK
BYK merged commit aa80347 into getsentry:main Nov 14, 2026
21 checks passed

This branch was previously deployed

1 inactive deployment
Preview — db3441c9 Deployed Nov 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

2 participants