Uploading Source Maps to Bugsnag | Stallion OTA Updates

Learn how to upload React Native source maps from Stallion OTA updates to Bugsnag for accurate error monitoring. Complete guide for Hermes and JavaScript Core builds, including source map composition, code bundle ID configuration, and Bugsnag integration with Stallion for production debugging.

CLI Requirement:

Source map upload to Bugsnag requires Stallion CLI version 2.4.0 or above. Make sure you're running the latest version. Check the CLI Installation guide if you need to update.

Publish Bundle

When publishing with --keep-artifacts=true and --sourcemap=true, Stallion performs the standard OTA publish flow and additionally persists all bundle and sourcemap outputs on disk for Bugsnag integration.

Example command:

stallion publish-bundle --upload-path=orgname/project-name/bucket-name --platform=android/ios --release-note="notes" --keep-artifacts=true --sourcemap=true

This enables deterministic Bugsnag integration without re-running any React Native bundling step.

  • Normal OTA publish is executed

    Stallion generates the platform bundles, applies Hermes compilation when enabled, uploads the OTA payload to the Stallion backend, and completes the rollout exactly as in a standard publish.

  • Source maps are generated

    With --sourcemap=true, Stallion ensures both standard JS sourcemaps and Hermes sourcemaps are produced for the publish.

    Instead of treating the bundle and sourcemap as temporary files, Stallion keeps the final outputs in a stable directory structure so you can reliably pick them up in CI and upload to Bugsnag.

  • Artifacts are persisted on disk

    With --keep-artifacts=true, Stallion does not delete any intermediate build outputs and writes the final bundle + sourcemap pairs into a stable, CI-friendly directory.

    The resulting structure is:

Stallion Artifacts Structure

stallion-artifacts/ ├── android/ │ ├── normal/ │ │ ├── index.android.bundle │ │ └── index.android.bundle.map │ └── hermes/ │ ├── index.android.bundle │ └── index.android.bundle.hbc.map └── ios/ | ├── normal/ | | ├── main.jsbundle | | └── main.jsbundle.map | └── hermes/ | ├── main.jsbundle | └── main.jsbundle.hbc.map

Prepare sourcemaps for Bugsnag

Javascript Core

You can upload normal JS sourcemap directly to Bugsnag. No additional steps are required.

Hermes

For Hermes builds, you need to compose the packager and Hermes sourcemaps. Use the following commands:

mv stallion-artifacts/android/normal/index.android.bundle.map stallion-artifacts/android/normal/index.android.bundle.packager.map
node \
  node_modules/react-native/scripts/compose-source-maps.js \
  stallion-artifacts/android/normal/index.android.bundle.packager.map \
  stallion-artifacts/android/hermes/index.android.bundle.hbc.map \
  -o stallion-artifacts/android/normal/index.android.bundle.map
rm -f stallion-artifacts/android/normal/index.android.bundle.packager.map

Upload to Bugsnag

Installation

Install the Bugsnag source maps uploader:

npm install --save-dev @bugsnag/source-maps
# or
yarn add --dev @bugsnag/source-maps

Extract Bundle Hash

When publishing a bundle, extract the bundle hash from the output. In your CI pipeline:

OUTPUT=$(stallion publish-bundle \
  --upload-path=$UPLOAD_PATH \
  --platform=$PLATFORM \
  --release-note="$RELEASE_NOTE" \
  --keep-artifacts=true \
  --sourcemap=true)

BUNDLE_HASH=$(echo "$OUTPUT" | grep -oE '[a-f0-9]{64}')
CODE_BUNDLE_ID=${BUNDLE_HASH:0:32}  # First 32 characters

The CODE_BUNDLE_ID (first 32 characters of the bundle hash) is used to associate source maps with the running bundle, similar to how CodePush uses bundle identifiers.

Upload Source Maps

Upload the bundle and source map to Bugsnag using the code bundle ID:

Javascript Core

Upload source maps for React Native JavaScript Core application.

npx bugsnag-source-maps upload-react-native \
  --api-key YOUR_API_KEY_HERE \
  --code-bundle-id $CODE_BUNDLE_ID \
  --platform android \
  --source-map stallion-artifacts/android/normal/index.android.bundle.map \
  --bundle stallion-artifacts/android/normal/index.android.bundle

Hermes

Upload source maps for React Native Hermes applications.

npx bugsnag-source-maps upload-react-native \
  --api-key YOUR_API_KEY_HERE \
  --code-bundle-id $CODE_BUNDLE_ID \
  --platform android \
  --source-map stallion-artifacts/android/normal/index.android.bundle.map \
  --bundle stallion-artifacts/android/hermes/index.android.bundle

Configure Bugsnag in Your App

In your React Native app, import ACTIVE_RELEASE_HASH from react-native-stallion and use it as Bugsnag’s codeBundleId for the currently running Stallion bundle:

ACTIVE_RELEASE_HASH requirement:

ACTIVE_RELEASE_HASH is available in react-native-stallion 2.4.0-alpha.5 and above. On older SDK versions it may be missing/undefined.

import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import Bugsnag from '@bugsnag/react-native';
import { ACTIVE_RELEASE_HASH } from 'react-native-stallion';
Bugsnag.start({
  codeBundleId: ACTIVE_RELEASE_HASH ?? '',
});

AppRegistry.registerComponent(appName, () => App);

ACTIVE_RELEASE_HASH from the Stallion SDK matches the first 32 characters of the bundle hash, which is the same CODE_BUNDLE_ID used when uploading source maps. This ensures Bugsnag can correctly associate stack traces with the uploaded source maps.

Complete CI Example

Here's a complete GitHub Actions example that publishes, extracts the hash, and uploads to Bugsnag:

- name: Publish Bundle and Extract Hash
  id: publish
  run: |
    echo "Publishing bundle..."
    OUTPUT=$(stallion publish-bundle \
      --upload-path=$UPLOAD_PATH \
      --platform=$PLATFORM \
      --release-note="$RELEASE_NOTE" \
      --keep-artifacts=true \
      --sourcemap=true)

    echo "$OUTPUT"

    BUNDLE_HASH=$(echo "$OUTPUT" | grep -oE '[a-f0-9]{64}')
    CODE_BUNDLE_ID=${BUNDLE_HASH:0:32}
    
    echo "Bundle hash: $BUNDLE_HASH"
    echo "Code bundle ID: $CODE_BUNDLE_ID"
    
    echo "BUNDLE_HASH=$BUNDLE_HASH" >> $GITHUB_ENV
    echo "CODE_BUNDLE_ID=$CODE_BUNDLE_ID" >> $GITHUB_ENV

- name: Prepare Hermes Source Maps (if using Hermes)
  run: |
    if [ "$PLATFORM" = "android" ]; then
      mv stallion-artifacts/android/normal/index.android.bundle.map stallion-artifacts/android/normal/index.android.bundle.packager.map
      node node_modules/react-native/scripts/compose-source-maps.js \
        stallion-artifacts/android/normal/index.android.bundle.packager.map \
        stallion-artifacts/android/hermes/index.android.bundle.hbc.map \
        -o stallion-artifacts/android/normal/index.android.bundle.map
      rm -f stallion-artifacts/android/normal/index.android.bundle.packager.map
    else
      mv stallion-artifacts/ios/normal/main.jsbundle.map stallion-artifacts/ios/normal/main.jsbundle.packager.map
      node node_modules/react-native/scripts/compose-source-maps.js \
        stallion-artifacts/ios/normal/main.jsbundle.packager.map \
        stallion-artifacts/ios/hermes/main.jsbundle.hbc.map \
        -o stallion-artifacts/ios/normal/main.jsbundle.map
      rm -f stallion-artifacts/ios/normal/main.jsbundle.packager.map
    fi

- name: Upload Source Maps to Bugsnag
  run: |
    if [ "$PLATFORM" = "android" ]; then
      npx bugsnag-source-maps upload-react-native \
        --api-key $BUGSNAG_API_KEY \
        --code-bundle-id $CODE_BUNDLE_ID \
        --platform android \
        --source-map stallion-artifacts/android/normal/index.android.bundle.map \
        --bundle stallion-artifacts/android/hermes/index.android.bundle
    else
      npx bugsnag-source-maps upload-react-native \
        --api-key $BUGSNAG_API_KEY \
        --code-bundle-id $CODE_BUNDLE_ID \
        --platform ios \
        --source-map stallion-artifacts/ios/normal/main.jsbundle.map \
        --bundle stallion-artifacts/ios/hermes/main.jsbundle
    fi

- name: Release Bundle
  run: |
    stallion release-bundle \
      --project-id=$PROJECT_ID \
      --hash=$BUNDLE_HASH \
      --app-version=$APP_VERSION \
      --release-note="$RELEASE_NOTE" \
      --ci-token=$CI_TOKEN

Script example (publish + compose + upload)

If you prefer a single, reusable script (instead of wiring the steps directly into your CI YAML), you can use the following.

Save as stallion_publish_and_upload_bugsnag.sh, then:

chmod +x ./stallion_publish_and_upload_bugsnag.sh
./stallion_publish_and_upload_bugsnag.sh \
  --upload-path "orgname/project-name/bucket-name" \
  --platform "android" \
  --release-note "notes" \
  --hermes-disabled false \
  --bugsnag-api-key "YOUR_API_KEY_HERE"
#!/usr/bin/env bash
set -euo pipefail

# -----------------------------------------------------------------------------
# stallion_publish_and_upload_bugsnag.sh
#
# What it does:
#  1) Runs: stallion publish-bundle ... --sourcemap=true
#  2) Composes RN packager sourcemap + Hermes sourcemap into one .map
#  3) Uploads bundle + composed sourcemap to Bugsnag using CODE_BUNDLE_ID = hash
#
# Requirements:
#  - stallion CLI installed and authenticated
#  - react-native dependency present (compose-source-maps.js exists)
#  - Bugsnag CLI package installed (npx bugsnag-source-maps ...)
#
# Usage:
#  ./stallion_publish_and_upload_bugsnag.sh \
#    --upload-path "orgname/project-name/bucket-name" \
#    --platform "android" \
#    --release-note "notes" \
#    --hermes-disabled false \
#    --bugsnag-api-key "YOUR_API_KEY_HERE"
#
# Options (these are always passed to Stallion; defaults shown):
#  --keep-artifacts true|false          (default: true)
#  --sourcemap true|false               (default: true)
#  --hermes-disabled true|false         (default: false)
#  --stallion-cmd "stallion"            (default: stallion)
#  --artifacts-dir "stallion-artifacts" (default: stallion-artifacts)
# -----------------------------------------------------------------------------

log() { printf "\n[%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
die() { printf "\n[ERROR] %s\n" "$*" >&2; exit 1; }

UPLOAD_PATH=""
PLATFORM=""
RELEASE_NOTE=""
KEEP_ARTIFACTS="true"
SOURCEMAP="true"
HERMES_DISABLED="false"
BUGSNAG_API_KEY=""
STALLION_CMD="stallion"
ARTIFACTS_DIR="stallion-artifacts"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --upload-path) UPLOAD_PATH="${2:-}"; shift 2 ;;
    --platform) PLATFORM="${2:-}"; shift 2 ;;
    --release-note) RELEASE_NOTE="${2:-}"; shift 2 ;;
    --keep-artifacts) KEEP_ARTIFACTS="${2:-}"; shift 2 ;;
    --sourcemap) SOURCEMAP="${2:-}"; shift 2 ;;
    --hermes-disabled) HERMES_DISABLED="${2:-}"; shift 2 ;;
    --bugsnag-api-key) BUGSNAG_API_KEY="${2:-}"; shift 2 ;;
    --stallion-cmd) STALLION_CMD="${2:-}"; shift 2 ;;
    --artifacts-dir) ARTIFACTS_DIR="${2:-}"; shift 2 ;;
    -h|--help)
      cat <<'HELP'
Usage:
  ./stallion_publish_and_upload_bugsnag.sh --upload-path "org/project/bucket" --platform "android|ios" --release-note "notes" --hermes-disabled true|false --bugsnag-api-key "KEY"

Options (these are always passed to Stallion; defaults shown):
  --keep-artifacts true|false          (default: true)
  --sourcemap true|false               (default: true)
  --hermes-disabled true|false         (default: false)
  --stallion-cmd "stallion"            (default: stallion)
  --artifacts-dir "stallion-artifacts" (default: stallion-artifacts)
HELP
      exit 0
      ;;
    *)
      die "Unknown argument: $1"
      ;;
  esac
done

[[ -n "$UPLOAD_PATH" ]] || die "--upload-path is required"
[[ -n "$PLATFORM" ]] || die "--platform is required (android|ios)"
[[ -n "$RELEASE_NOTE" ]] || die "--release-note is required"
[[ -n "$BUGSNAG_API_KEY" ]] || die "--bugsnag-api-key is required"

if [[ "$PLATFORM" != "android" && "$PLATFORM" != "ios" ]]; then
  die "--platform must be 'android' or 'ios'"
fi

if [[ "$HERMES_DISABLED" != "true" && "$HERMES_DISABLED" != "false" ]]; then
  die "--hermes-disabled must be 'true' or 'false'"
fi

command -v "$STALLION_CMD" >/dev/null 2>&1 || die "Cannot find '$STALLION_CMD' in PATH"
command -v node >/dev/null 2>&1 || die "node is required"
command -v npx >/dev/null 2>&1 || die "npx is required"

# --- 1) Publish bundle and capture output ------------------------------------
log "Running Stallion publish-bundle..."
PUBLISH_OUTPUT="$(
  "$STALLION_CMD" publish-bundle \
    --upload-path="$UPLOAD_PATH" \
    --platform="$PLATFORM" \
    --release-note="$RELEASE_NOTE" \
    --keep-artifacts="$KEEP_ARTIFACTS" \
    --sourcemap="$SOURCEMAP" \
  2>&1 | tee /dev/stderr
)"

# Extract a likely hash from the output.
# This is intentionally flexible because different CLI versions print differently.
CODE_BUNDLE_ID="$(
  printf "%s\n" "$PUBLISH_OUTPUT" \
  | grep -Eo '([a-f0-9]{32,64})' \
  | head -n 1 \
  || true
)"

[[ -n "$CODE_BUNDLE_ID" ]] || die "Could not auto-detect bundle hash from publish output. Please ensure publish prints the hash."

#
# Bugsnag `--code-bundle-id` should be 32 chars in our setup.
# If Stallion prints 64-char hashes, trim them consistently.
#
CODE_BUNDLE_ID="${CODE_BUNDLE_ID:0:32}"

log "Detected CODE_BUNDLE_ID: $CODE_BUNDLE_ID"

# --- 2) Compose source maps (RN packager + Hermes) ----------------------------
# Stallion artifacts follow these patterns:
# - Android: stallion-artifacts/android/{normal,hermes}/index.android.bundle(.map|.hbc.map)
# - iOS:     stallion-artifacts/ios/{normal,hermes}/main.jsbundle(.map|.hbc.map)
NORMAL_DIR=""
HERMES_DIR=""
BUNDLE_BASENAME=""

if [[ "$PLATFORM" == "android" ]]; then
  NORMAL_DIR="$ARTIFACTS_DIR/android/normal"
  HERMES_DIR="$ARTIFACTS_DIR/android/hermes"
  BUNDLE_BASENAME="index.android.bundle"
elif [[ "$PLATFORM" == "ios" ]]; then
  NORMAL_DIR="$ARTIFACTS_DIR/ios/normal"
  HERMES_DIR="$ARTIFACTS_DIR/ios/hermes"
  BUNDLE_BASENAME="main.jsbundle"
else
  die "--platform must be 'android' or 'ios'"
fi

PACKAGER_MAP="$NORMAL_DIR/$BUNDLE_BASENAME.packager.map"
NORMAL_MAP="$NORMAL_DIR/$BUNDLE_BASENAME.map"
BUNDLE_FILE_NORMAL="$NORMAL_DIR/$BUNDLE_BASENAME"
BUNDLE_FILE_HERMES="$HERMES_DIR/$BUNDLE_BASENAME"
HERMES_MAP="$HERMES_DIR/$BUNDLE_BASENAME.hbc.map"

[[ -f "$NORMAL_MAP" ]] || die "Expected sourcemap not found: $NORMAL_MAP"
[[ -f "$BUNDLE_FILE_NORMAL" ]] || die "Expected bundle not found: $BUNDLE_FILE_NORMAL"

SOURCE_MAP_TO_UPLOAD="$NORMAL_MAP"
BUNDLE_FILE_TO_UPLOAD="$BUNDLE_FILE_NORMAL"

if [[ "$HERMES_DISABLED" == "false" ]]; then
  [[ -f "$HERMES_MAP" ]] || die "Expected Hermes sourcemap not found: $HERMES_MAP"

  log "Hermes enabled: composing source maps (packager + Hermes) for $PLATFORM..."
  mv "$NORMAL_MAP" "$PACKAGER_MAP"

  COMPOSE_SCRIPT="./node_modules/react-native/scripts/compose-source-maps.js"
  [[ -f "$COMPOSE_SCRIPT" ]] || die "compose-source-maps.js not found at: $COMPOSE_SCRIPT (is react-native installed?)"

  node "$COMPOSE_SCRIPT" \
    "$PACKAGER_MAP" \
    "$HERMES_MAP" \
    -o "$NORMAL_MAP"

  rm -f "$PACKAGER_MAP"
  log "Composed map written to: $NORMAL_MAP"

  SOURCE_MAP_TO_UPLOAD="$NORMAL_MAP"

  # Prefer the Hermes bundle artifact when present (Android), otherwise fall back
  # to the normal bundle path (iOS artifacts may not include a separate Hermes bundle).
  if [[ -f "$BUNDLE_FILE_HERMES" ]]; then
    BUNDLE_FILE_TO_UPLOAD="$BUNDLE_FILE_HERMES"
  else
    BUNDLE_FILE_TO_UPLOAD="$BUNDLE_FILE_NORMAL"
  fi
else
  log "Hermes disabled: uploading normal bundle + packager sourcemap for $PLATFORM..."
  SOURCE_MAP_TO_UPLOAD="$NORMAL_MAP"
  BUNDLE_FILE_TO_UPLOAD="$BUNDLE_FILE_NORMAL"
fi

[[ -f "$SOURCE_MAP_TO_UPLOAD" ]] || die "Expected sourcemap not found: $SOURCE_MAP_TO_UPLOAD"
[[ -f "$BUNDLE_FILE_TO_UPLOAD" ]] || die "Expected bundle not found: $BUNDLE_FILE_TO_UPLOAD"
log "Using bundle for upload: $BUNDLE_FILE_TO_UPLOAD"
log "Using sourcemap for upload: $SOURCE_MAP_TO_UPLOAD"

# --- 3) Upload to Bugsnag ----------------------------------------------------
log "Uploading bundle + sourcemap to Bugsnag..."
npx bugsnag-source-maps upload-react-native \
  --api-key "$BUGSNAG_API_KEY" \
  --code-bundle-id "$CODE_BUNDLE_ID" \
  --platform "$PLATFORM" \
  --source-map "$SOURCE_MAP_TO_UPLOAD" \
  --bundle "$BUNDLE_FILE_TO_UPLOAD"

log "Done. Bugsnag upload completed for CODE_BUNDLE_ID=$CODE_BUNDLE_ID"

Reference