Observability
The Problem
OTA updates mean multiple JavaScript bundles can run under the same native app version. Error tracking tools like Sentry, Bugsnag, and Datadog assume one version equals one bundle. Without bundle identity, stack traces from OTA bundles will not match the correct source maps, and crash reports become difficult to attribute.
Bundle Identity
Every OTA bundle produced by Bundle Drop has a unique update hash. This hash is available at runtime via installedInfo.hash and is the key to connecting runtime errors with the correct source maps.
Bundle Drop does not reuse the same hash for different bundles. That makes it a reliable identifier for source map uploads and error context.
Accessing Bundle Info
There are three ways to read the active bundle's identity at runtime.
Observability Context (Recommended)
getObservabilityContext() returns a pre-formatted object ready for any error tracking provider. It handles the embedded fallback and pending-apply safety automatically.
import { getObservabilityContext } from '@gfean/react-native-bundle-drop';
const ctx = await getObservabilityContext();
// ctx.source → 'ota' or 'embedded'
// ctx.dist → hash or 'embedded'
// ctx.tags → { bundle_drop_hash, bundle_drop_channel, ... }
// ctx.context → full BundleInfo or null
Reactive (Hook)
import { useBundleDrop } from '@gfean/react-native-bundle-drop';
const { installedInfo } = useBundleDrop();
const updateHash = installedInfo?.hash;
Imperative
import { getInstalledBundleInfo } from '@gfean/react-native-bundle-drop';
const info = await getInstalledBundleInfo();
const updateHash = info?.hash;
Use getObservabilityContext() when integrating with error tracking — it handles fallbacks and shapes the data for you. Use the hook when you need the raw value to stay in sync with React rendering. Use the imperative call for one-off reads.
Available Fields
The BundleInfo object returned by both approaches includes:
hash— unique update hash for the bundle; use this as the primary identifier for source map correlationbundleVersion— monotonic Bundle Drop bundle version numberversion— app version supplied during uploadchannelName— the channel this bundle was resolved fromplatform—iosorandroidruntimeVersion— the runtime version the bundle targetsinstalledAt— ISO timestamp of when the bundle was installedpendingApply— whether the bundle is staged but not yet active
Generating Source Maps
To get readable stack traces from OTA bundles, pass --sourcemap during upload. Pair it with --artifact-dir to retain the source map and bundle in a CI-accessible directory:
npx bundle-drop upload android \
--version 1.2.3 \
--channel General \
--token $BUNDLE_DROP_PAT \
--sourcemap \
--artifact-dir ./build/bundledrop
After upload, the artifact directory contains:
main.jsbundle— the exact bundle that was uploadedmain.jsbundle.map— the Metro source map matching that bundlebundle-drop-result.json— upload result withhash,bundlePath,sourceMapPath, and other metadata
Without --sourcemap, no source map is generated and the upload flow works exactly as before.
Sentry
Source Map Upload (CI)
Read the hash from the result file and upload the source map with Sentry CLI:
RESULT="./build/bundledrop/bundle-drop-result.json"
BUNDLE_HASH=$(jq -r '.hash' "$RESULT")
SOURCE_MAP=$(jq -r '.sourceMapPath' "$RESULT")
sentry-cli releases files "$RELEASE" upload-sourcemaps \
--dist "$BUNDLE_HASH" \
"$SOURCE_MAP"
Runtime Configuration
import * as Sentry from '@sentry/react-native';
import { getObservabilityContext } from '@gfean/react-native-bundle-drop';
const ctx = await getObservabilityContext();
Sentry.init({
dsn: 'https://your-dsn@sentry.io/project',
release: 'com.yourapp@1.0.0',
dist: ctx.dist,
});
Sentry.setTags(ctx.tags);
if (ctx.context) {
Sentry.setContext('bundleDrop', ctx.context);
}
Bugsnag
import Bugsnag from '@bugsnag/react-native';
import { getObservabilityContext } from '@gfean/react-native-bundle-drop';
const ctx = await getObservabilityContext();
Bugsnag.start({
codeBundleId: ctx.dist,
});
if (ctx.context) {
Bugsnag.addMetadata('bundleDrop', ctx.context);
}
Generic Integration
For any error tracking tool, the pattern is the same:
- Read the observability context at startup.
- Set the dist/release identifier.
- Attach tags and context.
import { getObservabilityContext } from '@gfean/react-native-bundle-drop';
const ctx = await getObservabilityContext();
errorTracker.setDist(ctx.dist);
errorTracker.setTags(ctx.tags);
if (ctx.context) {
errorTracker.setContext('bundleDrop', ctx.context);
}
You can still use getInstalledBundleInfo() directly if you prefer to shape the data yourself.
Embedded Bundle Fallback
When no OTA bundle is installed, getInstalledBundleInfo() returns null. In that case, use 'embedded' as the fallback identifier so your error tracker still has a meaningful dist value.
If pendingApply is true, the bundle has been downloaded and staged but is not yet active. The app is still running the previous bundle (or the native bundle). Do not use the pending bundle's hash for error reporting — it does not match the code that is actually executing.
Best Practices
- Upload source maps for every OTA bundle as part of your CI pipeline.
- Use the Bundle Drop update hash as the unique identifier in both CI uploads and runtime configuration.
- Keep CI upload identifiers and runtime identifiers in sync so stack traces resolve correctly.
- Include the full bundle info object in error context for richer debugging when triaging issues.
Related Docs
- For runtime APIs, see Runtime APIs.
- For the update hook, see useBundleDrop.
- For CI upload workflows, see CI/CD.
- For rollback behavior, see Rollback.
