Skip to main content

Command Palette

Search for a command to run...

Why React Native OTA Source Maps Need Their Own Artifact Identity

Once JavaScript can change without a new native binary, app version alone is no longer enough to select the correct source map. The deployed bundle, uploaded map, and runtime event need to agree on one immutable OTA identity.

Updated
•8 min read•View as Markdown
Why React Native OTA Source Maps Need Their Own Artifact Identity
G
I'm a React Native engineer and the creator of Bundle Drop, an OTA update platform for React Native and Expo. I write about React Native, OTA updates, mobile release engineering, deployment reliability, Expo, and the engineering decisions behind developer infrastructure. Most of my writing focuses on practical problems: safe rollouts, runtime compatibility, rollback and recovery, patch delivery, release channels, and the lessons learned while building and operating mobile tooling. Bundle Drop: bundledrop.app

Why React Native OTA Source Maps Need Their Own Artifact Identity

A normal mobile release has a convenient debugging assumption:

one application version usually points to one JavaScript build.

React Native OTA updates break that assumption. Two users can both be running native version 4.2.0, but one may still be on the embedded JavaScript bundle, another may be running OTA release A, and another may already be on OTA release B.

If all three crash, app version alone does not identify the JavaScript that produced the stack.

That changes how source maps should be treated. They are not just files generated by CI. They are part of the identity of the exact JavaScript artifact running on the device.

Three records need to agree

For reliable symbolication, three things have to point at the same artifact:

production bundle + composed source map
                 ↓
        immutable OTA identity
                 ↓
 source-map upload metadata
                 =
   runtime event metadata

The first record is the actual production bundle and source map. The second is whatever metadata the error tracker uses to identify those files. The third is the identity attached to the runtime error.

If any of those describe different artifacts, the source map may exist in Sentry and still fail to symbolicate the event correctly. Bundle Drop’s current source-map guide describes this as a single build-to-runtime identity chain rather than a “generate a map later if we need one” workflow.

“Same commit” is not the same artifact

A Git commit is useful provenance. It is not proof that two generated bundles are byte-for-byte equivalent.

The final output can depend on things such as:

  • Metro configuration
  • lockfile and dependency resolution
  • platform
  • minification
  • environment substitution
  • Babel transforms
  • Sentry transforms
  • Hermes compilation
  • the exact build-tool versions used

A later rebuild from the same source revision can therefore produce different generated positions. Source maps are positional. That means rebuilding a map later is weaker than retaining the one generated next to the deployed bundle.

The safer release artifact is:

main.jsbundle
main.jsbundle.map
release metadata
immutable OTA identity

Build them once. Keep them together. Do not recreate one member of that set during an incident.

App version is useful, but too broad

Sentry has multiple concepts involved in artifact matching. A release can identify the broader application release. A distribution, or dist, can subdivide that release. Sentry’s release-file API allows an uploaded file to be associated with a dist value.

That maps naturally to OTA. For example:

release = native app release
dist    = immutable OTA update identity

Now several JavaScript releases can exist under the same native version without becoming indistinguishable. Bundle Drop’s documented Sentry path uses exactly that shape: the native release remains the Sentry release, while the immutable OTA hash becomes dist. Its CI/CD documentation retains the exact bundle and source-map pair, then uses the returned update hash when uploading them to the error tracker.

The important part is not the specific field names. The important part is using one matching contract consistently between CI and runtime.

Report what is executing, not what is downloaded

OTA clients have more state than:

updated
not updated

A device can have a new release downloaded but not activated. It can have an update staged for the next cold launch. It can recover to a previous known-good bundle. It can fall all the way back to the embedded bundle.

That creates a subtle observability rule:

error metadata must describe the bundle executing now, not the newest bundle known to the device.

Suppose bundle A is active. Bundle B has downloaded successfully and is waiting for the next launch. An error happens before that restart. The error belongs to A.

If your crash integration reports B because “B is the newest downloaded update,” Sentry may try to match the event against the wrong artifact. The current Bundle Drop observability model deliberately avoids that: getObservabilityContext() reports the active running state and does not treat a pending update as active.

This principle applies to any OTA implementation. Derive debugging identity from the active bundle resolver, not from “last downloaded release.”

Hermes makes exact pairing even more important

With Hermes, the debugging path includes another generated-artifact boundary. Metro produces the JavaScript output. Hermes compiles that output. The final source-map chain must describe the generated code that actually became the production artifact.

That is why the final composed map matters. Using an earlier intermediate map can leave the stack technically “mapped” but wrong. The correct pair is the production Hermes artifact and the map generated for that exact artifact.

This is also why platform artifacts should remain separate. An Android bundle/map pair and an iOS bundle/map pair should not be treated as interchangeable simply because they came from the same Git commit.

Debug IDs are useful, but they change the build

Modern Sentry tooling can inject content-derived Debug IDs into generated JavaScript and source maps. That can improve artifact matching. It also means the Sentry integration participates in the bytes produced by the build pipeline.

For an OTA system that uses binary patch delivery, even a useful build-time transformation can affect patch characteristics. That does not make Debug IDs a problem. It means they belong in the release-artifact conversation.

Bundle Drop currently documents a narrow Sentry/Hermes OTA Metro path for @sentry/react-native 7.8.x. That setup is intentionally version-specific and should not be copied unchanged into a newer Sentry major without revalidating how that version’s Metro integration works.

That caveat is more important than the snippet itself. Build integrations evolve. Treat them like versioned release infrastructure.

A boring CI contract is a good CI contract

A source-map pipeline becomes easier to trust when it is intentionally mechanical. A useful shape is:

build production bundle + final map
        ↓
retain both as one CI artifact
        ↓
publish that exact OTA artifact
        ↓
capture immutable OTA identity
        ↓
upload retained bundle + map
        ↓
attach same identity to runtime errors

The CI job should not rebuild the bundle for the error tracker. It should upload the files that were actually published. The release job should keep enough metadata to prove which pair belongs to which update.

Bundle Drop’s current CLI supports retaining that evidence with --sourcemap and --artifact-dir, including the result metadata needed by the next CI step.

Test symbolication with a real staged error

A successful source-map upload does not prove the whole chain. It proves that files reached the error tracker.

The useful test is stronger:

  1. Publish a staging OTA release.
  2. Confirm the exact update identity.
  3. Trigger a deliberate JavaScript error from that release.
  4. Inspect the resulting Sentry event.
  5. Verify that the original source file, function, and line resolve correctly.

If your pipeline uses release + dist, inspect both values. If it uses another supported matching mechanism, verify that path instead.

That staged error proves the important relationship:

running bytes
    ↓
event identity
    ↓
uploaded artifact
    ↓
correct original source

Keep source maps private

Source maps can contain more than line mappings. They may expose internal paths. They can also include original source content through sourcesContent.

That makes them production engineering artifacts, not harmless metadata. Retain them in controlled CI storage and upload them directly to the error tracker. Do not put them on a public delivery path unless you made that decision intentionally.

Source maps and bundle analysis ask different questions

There is some overlap between source-map workflows and bundle analysis. Both may use a production JavaScript bundle and its matching map. But the purpose is different.

Bundle analysis asks:

Which source modules account for these generated bytes?

Crash symbolication asks:

Which original source position produced this runtime frame?

The exact bundle/map relationship matters in both cases. But the operational workflows should stay separate. A size-analysis tool can be local developer tooling. A production source-map workflow is part of release observability.

Where Bundle Drop fits

Disclosure: I build Bundle Drop. Bundle Drop gives each OTA artifact an immutable bundle identity, supports retaining the exact uploaded bundle and source map in CI, and exposes active runtime identity for error-tracker metadata. The documented Sentry path uses the native application release together with the OTA hash as dist.

But the broader rule is provider-independent:

once JavaScript can change independently of the native binary, app version is no longer enough to identify the JavaScript for debugging.

Build the bundle and map once. Keep the exact pair. Give the artifact a stable identity. Use that identity when uploading debugging artifacts. Report the same identity from the code that is actually executing. Then prove the whole chain with a staged error before a production incident depends on it.


Originally published on Bundle Drop Resources.