# Observability — Bundle Drop Docs

> Monitor OTA release health in Bundle Drop with install, download, apply, and rollback signals.

Canonical: https://bundledrop.app/docs/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.

**observability.ts**

```ts
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)

**observability.ts**

```ts
import { useBundleDrop } from "@gfean/react-native-bundle-drop";

const { installedInfo } = useBundleDrop();
const updateHash = installedInfo?.hash;
```

### Imperative

**observability.ts**

```ts
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 correlation
- `bundleVersion` — monotonic Bundle Drop bundle version number
- `version` — app version supplied during upload
- `channelName` — the channel this bundle was resolved from
- `platform` — `ios` or `android`
- `runtimeVersion` — the runtime version the bundle targets
- `installedAt` — ISO timestamp of when this bundle's metadata was last written locally (at download, apply, or rollback)
- `pendingApply` — 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:

**Terminal**

```bash
npx bundle-drop upload android \
  --version 1.2.3 \
  --channel develop \
  --token $BUNDLE_DROP_PAT \
  --sourcemap \
  --artifact-dir ./build/bundledrop
```

After upload, the artifact directory contains:

- `main.jsbundle` — the exact bundle that was uploaded
- `main.jsbundle.map` — the Metro source map matching that bundle
- `bundle-drop-result.json` — upload result with `hash`, `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:

**upload-sourcemaps.sh**

```bash
RESULT="./build/bundledrop/bundle-drop-result.json"
BUNDLE_HASH=$(jq -r '.hash' "$RESULT")
BUNDLE=$(jq -r '.bundlePath' "$RESULT")
SOURCE_MAP=$(jq -r '.sourceMapPath' "$RESULT")

sentry-cli sourcemaps upload \
  --release "$RELEASE" \
  --dist "$BUNDLE_HASH" \
  --bundle "$BUNDLE" \
  --bundle-sourcemap "$SOURCE_MAP" \
  --validate
```

### Runtime Configuration

**sentry.ts**

```ts
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);
}
```

Use the exact same `release` and `dist` values for the runtime event and the
corresponding CI source-map upload. When either value differs, Sentry cannot
reliably select the source map for that OTA bundle.

## Sentry and Hermes OTA Builds

Sentry's full Metro wrapper adds a content-derived Debug ID to the JavaScript
bundle and source map. That identifier is useful for automatic source-map
matching, but changing it can shift Hermes bytecode enough to produce a much
larger binary patch even when the application change is small. This is a
Sentry-and-Hermes interaction rather than an iOS-only issue.

The following composition is validated for `@sentry/react-native` 7.8.x. For
applications on that version that use both Sentry and Hermes, make this
one-time change in `metro.config.js`. Use your existing merged Metro `config`
in the example:

**metro.config.js**

```js
const {
  withSentryBabelTransformer,
  withSentryConfig,
  withSentryFramesCollapsed,
} = require("@sentry/react-native/metro");

const isBundleDropOtaBuild = process.env.BUNDLE_DROP_OTA_BUILD === "1";

module.exports = isBundleDropOtaBuild
  ? withSentryFramesCollapsed(withSentryBabelTransformer(config, true))
  : withSentryConfig(config, {
      annotateReactComponents: true,
    });
```

The Bundle Drop CLI sets `BUNDLE_DROP_OTA_BUILD=1` automatically and only for
the Metro subprocess used by `bundle-drop upload`. Local CLI uploads and CI/CD
therefore use the same path without a pipeline variable. Normal native builds
continue to use the full `withSentryConfig` integration, while OTA builds keep
Sentry component annotations and frame collapsing but omit Debug ID
serialization.

Do not copy this wrapper split unchanged to a newer Sentry major version.
`withSentryConfig` can gain additional resolvers, middleware, and build options
between releases. Check that version's Metro integration and preserve every
required behavior except Debug ID serialization.

This conditional does not disable source-map generation or composition.
Continue passing `--sourcemap`, uploading the generated map in CI, and using
matching `release` and `dist` values in the Sentry runtime event and source-map
upload. Because the OTA bundle has no Debug ID, upload the generated Hermes
bundle and its composed map together with `--bundle` and
`--bundle-sourcemap`, as shown above. The CLI warns without failing or
rewriting the bundle if a Sentry Debug ID is still present in a Hermes OTA
build.

Before production rollout, verify the integration on a staging release:

1. Confirm the OTA bundle log does not report a Sentry Bundle Debug ID.
2. Confirm `main.jsbundle.map` is generated, composed, and uploaded.
3. Capture an intentional JavaScript error from the staged OTA bundle and
   verify that Sentry symbolicates it under the expected `release` and `dist`.

## Bugsnag

**bugsnag.ts**

```ts
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:

1. Read the observability context at startup.
2. Set the dist/release identifier.
3. Attach tags and context.

**error-tracker.ts**

```ts
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 the embedded bundle is running, the executing code has no OTA hash and installed OTA metadata can be `null`, including after recovery to the embedded bundle. Prefer `getObservabilityContext()` for error reporting: it returns `source: "embedded"` and `dist: "embedded"` when no OTA bundle is active, and the running OTA hash when one is active.

If `pendingApply` is `true`, a downloaded update or rollback is waiting to apply. The app is still running the previous bundle (or the embedded bundle). Do not use staged metadata to identify executing code; `getObservabilityContext()` identifies the running bundle.

## 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](/docs/runtime-apis).
- For the update hook, see [useBundleDrop](/docs/use-bundle-drop).
- For CI upload workflows, see [CI/CD](/docs/ci-cd).
- For rollback behavior, see [Rollback](/docs/rollback).
