Skip to content

JS exceptions often lack debug_meta on Hermes; profiling path already has a robust Debug ID lookup #6480

Description

@deadlinecode

Summary

On React Native + Hermes (including Expo OTA / expo export Hermes .hbc bundles), plain JS exception events often ship without debug_meta.images, even when:

  1. Metro has injected the Debug ID premodule (_sentryDebugIds / _sentryDebugIdIdentifier)
  2. The matching artifact bundle was uploaded with that Debug ID (sentry-expo-upload-sourcemaps / @sentry/expo-upload-sourcemaps, Release: None)

Profiling already solves this correctly via getDebugMetadata() in dist/js/profiling/debugid.js, which reads GLOBAL_OBJ._sentryDebugIds directly and stamps code_file as app:///index.android.bundle / app:///main.jsbundle. That helper is not wired into the exception event pipeline.

Why it matters

Without debug_meta, backends fall back to release + filename matching. Expo/RN rewrites every JS frame to the same canonical filename (app:///index.android.bundle / app:///main.jsbundle) via RewriteFrames. OTA updates keep the native release, so symbolication can apply the native build’s Hermes map to OTA bytecode → wrong files/lines.

This is especially painful on Sentry-compatible self-hosts, but the missing debug_meta is an SDK-side gap regardless of backend.

Observed with @sentry/react-native 8.19.0, Expo Router, Hermes, OTA via artifact bundles.

Root cause (SDK)

Exception events rely on @sentry/core prepareEventapplyDebugIdsgetFilenameToDebugIdMap:

  1. Take the single key in _sentryDebugIds (the Error().stack string captured when the Debug ID premodule ran)
  2. Re-parse that stack with the generic stack parser
  3. Require an exact filename string match against exception frames

That path is fragile for Hermes top-level/premodule stacks. If the reparse yields no usable filename (or a different shape than real exception frames), filenameDebugIdMap is empty → applyDebugMeta no-ops → no debug_meta on the event.

By contrast, profiling:

// profiling/debugid.ts (conceptually)
const debugId = Object.values(GLOBAL_OBJ._sentryDebugIds)[0]
return [{ type: 'sourcemap', code_file: DEFAULT_BUNDLE_NAME, debug_id: debugId }]

No stack reparse. One bundle, one Debug ID, fixed code_file.

Suggested fix

Reuse the profiling approach for all events:

  • Extract / share getDebugMetadata() outside profiling
  • Add a default integration (or extend an existing one) that stamps event.debug_meta.images from _sentryDebugIds (+ _sentryDebugIdIdentifier fallback) using ANDROID_DEFAULT_BUNDLE_NAME / IOS_DEFAULT_BUNDLE_NAME
  • Keep it idempotent if core applyDebugIds already succeeded

This matches how RN actually works (single Hermes bundle per platform) and makes Expo OTA Debug ID matching reliable without release/dist workarounds.

Reproduction sketch

  1. Expo app with getSentryExpoConfig + @sentry/react-native/expo plugin
  2. Publish an OTA / expo export Hermes bundle; upload with npx @sentry/expo-upload-sourcemaps dist (artifact bundle, Debug ID in upload report)
  3. On a device running that OTA (Updates.isEmbeddedLaunch === false), Sentry.captureException(new Error('probe'))
  4. Inspect event JSON: debug_meta often missing; release still native (*.androidapp@x.y.z+build); frames use app:///index.android.bundle

Workaround we use today

App-level Sentry.addEventProcessor that mirrors profiling’s getDebugMetadata() and attaches debug_meta.images before send. Happy to upstream that if maintainers agree on the approach.

Related

  • Expo docs assume Debug ID path “just works” after sentry-expo-upload-sourcemaps (Using Sentry)
  • Metro Debug ID injection is fine; upload Debug IDs match; the break is event emission

Metadata

Metadata

Assignees

Labels

BugErrorsExpoIssues related to Sentry React Native Expo supportReact-Native

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions