# Sourcemaps


When React Native builds your app for release, Metro minifies your JavaScript, and the default engine (Hermes) compiles it further to bytecode. By the time an error is reported, the stack frames point at a single minified line in `index.android.bundle` or `main.jsbundle`, not at your original source. A frame ends up looking like this:

```
address at index.android.bundle:1:428913
```

Flare can turn that back into the real file, line, and code snippet using a **sourcemap**. The [`@flareapp/react-native-sourcemaps`](https://www.npmjs.com/package/@flareapp/react-native-sourcemaps) package uploads the sourcemap your release build produces and ties it to your errors.

> This is only needed for **release** builds. In development your app runs the unminified bundle from the Metro dev server, so stack traces are already readable and there is nothing to upload.

## How it works

There is no native module and no Metro bundler plugin involved. The package has two halves that share a single **version** string:

1. A **Babel plugin** stamps a version into your app bundle at build time, so every error your app reports carries that version.
2. A **CLI** (`flare-rn-sourcemaps upload`) uploads your sourcemap to Flare under the **same** version.

When an error arrives, Flare looks up the sourcemap whose version matches the report, and uses it to resolve the frames. The whole job is to make sure the version baked into your app and the version you upload the sourcemap under are identical. The package handles that for you, as long as you set the version in one place.

## Installing the package

Install it as a dev dependency. It only runs at build time and is never bundled into your app. The CLI runs on Node 18 or newer, Node 22 recommended.

```bash
npm install --save-dev @flareapp/react-native-sourcemaps
# or
yarn add --dev @flareapp/react-native-sourcemaps
# or
pnpm add --save-dev @flareapp/react-native-sourcemaps
```

This assumes you already have the [React Native SDK installed](/docs/react-native/getting-started/quick-start).

## Step 1: Add the Babel plugin

Add the plugin to your `babel.config.js`:

```js
module.exports = {
    presets: ['module:@react-native/babel-preset'],
    plugins: ['@flareapp/react-native-sourcemaps/babel'],
};
```

The only thing the plugin does, is it inlines the sourcemap version into your bundle at build time.

## Step 2: Pass the version to Flare

Import `flareSourcemapVersion` and pass it to `flare.configure()` where you set up Flare (see [Installation](/docs/react-native/getting-started/quick-start)):

```ts
// App.tsx
import { flare } from '@flareapp/react-native';
import { flareSourcemapVersion } from '@flareapp/react-native-sourcemaps/runtime';

flare.light('YOUR PROJECT KEY').configure({ sourcemapVersionId: flareSourcemapVersion });
```

`flareSourcemapVersion` is a normal typed `string`, so there is nothing to add to your `tsconfig.json` and no `@types/node` to install. At build time the Babel plugin replaces it with the actual version and removes the import. If you build without the plugin, it is an empty string and Flare simply skips sourcemap resolution.

> Unlike the web plugins (Vite, Webpack), this package does **not** pass your project key to the client. You still call `flare.light('YOUR PROJECT KEY')` yourself.

## Step 3: Choose a version

The version is read from the `FLARE_SOURCEMAP_VERSION` environment variable. Set it in the shell that runs your release build, to anything project-unique. A Git commit SHA is a good choice:

```bash
export FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)"
```

Both the Babel plugin (step 1) and the upload CLI (step 4) read this same variable, so they always agree. If you leave it unset it falls back to your app's `package.json` version, with a warning. That is fine for a quick test, but prefer setting it explicitly, so a rebuild that does not change the app version still gets a fresh version.

## Step 4: Build and upload the sourcemap

Your release build already generates a sourcemap. With Hermes, it is the **composed** sourcemap (the Metro sourcemap merged with the Hermes bytecode sourcemap). That is the one to upload. Build with `FLARE_SOURCEMAP_VERSION` set, then point the CLI at the generated `.map` file under the same version.

> **Prefer not to upload by hand?** You can have the upload run automatically as part of your release build, so it happens on every release without a separate command, through native build hooks in a **bare** project, or an **Expo config plugin** in a managed (CNG) one. See [Automatic upload for bare React Native](#automatic-upload-for-bare-react-native) and [Automatic upload for Expo (CNG)](#automatic-upload-for-expo-cng) below.

### Android

A release build writes the composed sourcemap into the Gradle build output:

```bash
export FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)"

# Build the release app (this also produces the sourcemap)
npx react-native run-android --mode release
# or: cd android && ./gradlew assembleRelease

# Upload it
npx flare-rn-sourcemaps upload \
  --api-key "YOUR PROJECT KEY" \
  --sourcemap android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
  --bundle-filename index.android.bundle \
  --version "$FLARE_SOURCEMAP_VERSION"
```

### iOS

Tell the iOS build where to write the sourcemap by setting `SOURCEMAP_FILE` (React Native's build script reads it), then upload that file:

```bash
export FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)"
export SOURCEMAP_FILE="$(pwd)/main.jsbundle.map"

# Build the release app in Xcode (Product > Archive) or:
npx react-native run-ios --mode Release

npx flare-rn-sourcemaps upload \
  --api-key "YOUR PROJECT KEY" \
  --sourcemap main.jsbundle.map \
  --bundle-filename main.jsbundle \
  --version "$FLARE_SOURCEMAP_VERSION"
```

### Expo

If you use Expo, export your bundle and sourcemaps, then upload the generated map:

```bash
FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)" npx expo export

npx flare-rn-sourcemaps upload \
  --api-key "YOUR PROJECT KEY" \
  --sourcemap dist/_expo/static/js/ios/<your-bundle>.hbc.map \
  --bundle-filename main.jsbundle \
  --version "$FLARE_SOURCEMAP_VERSION"
```

On success you will see:

```
@flareapp/react-native-sourcemaps: Uploading sourcemap "index.android.bundle" (version a1b2c3d) to Flare.
@flareapp/react-native-sourcemaps: Successfully uploaded sourcemap to Flare.
```

## Automatic upload for bare React Native

Step 4 uploads the sourcemap by hand. For a **bare** React Native project you can instead wire the upload into your native release build, so it happens automatically every time you build a release. There is no separate command to remember, and no way to forget it in CI.

> This covers **bare** React Native (a project with its own `android/` and `ios/` directories). For Expo managed (CNG) projects, see [Automatic upload for Expo (CNG)](#automatic-upload-for-expo-cng) below.

You still need the Babel plugin (step 1), the `flareSourcemapVersion` wiring (step 2), and `FLARE_SOURCEMAP_VERSION` set in your build environment (step 3). The automatic hooks replace only the upload in step 4.

### Configure your key

The native hooks need your project's API key. There are two ways to pass the key, depending on whether you are fine having the key in your repository or not.

**Option 1: `flare.json` (simplest).** If you don't mind committing the key, create a `flare.json` file at the root of your project (next to `package.json`):

```json
{
    "apiKey": "YOUR PROJECT KEY"
}
```

The key is now in version control along with the rest of your config. That's fine for many teams, and there's nothing else to set up.

**Option 2: `FLARE_API_KEY` environment variable (keeps the key out of git).** If you'd rather not commit the key, set `FLARE_API_KEY` in the environment that runs the build. Most teams will have CI setup for their release builds, so they have it as a secret in their CI/EAS build environment (`build.<profile>.env` in `eas.json`, repository secrets in GitHub Actions, etc.).

To test the upload locally, which is rare since you don't normally create a production build on your laptop, drop the key in a gitignored `.env.local` (or `.env`) next to `package.json`. The hooks will auto-load `FLARE_API_KEY` from there at build time, so a local release build picks it up without exporting anything by hand:

```bash
# .env.local (gitignored)
FLARE_API_KEY=YOUR PROJECT KEY
```

If both options are present, the environment wins. The full precedence is: shell env > `.env.local` > `.env` > `flare.json`.

> In the automatic flow the version comes **only** from `FLARE_SOURCEMAP_VERSION` (the same variable the Babel plugin reads), so the version baked into your app and the version on the sourcemap always match. There is no fallback to the `package.json` version here: if `FLARE_SOURCEMAP_VERSION` is unset, the upload is skipped with a warning log in your build output, rather than guessing a version that would not resolve.

### Android

Add this line to your `android/app/build.gradle`. It can go anywhere in the file. The script hooks the build lazily, so the order doesn't matter:

```gradle
apply from: "../../node_modules/@flareapp/react-native-sourcemaps/flare.gradle"
```

That is all. After each release JS bundle is built, the composed Hermes sourcemap is uploaded automatically.

### iOS

First, tell React Native's bundle step to emit the composed sourcemap by adding this line to `ios/.xcode.env`:

```bash
export SOURCEMAP_FILE="$TARGET_TEMP_DIR/main.jsbundle.map"
```

Use `$TARGET_TEMP_DIR`, not `$CONFIGURATION_BUILD_DIR`. With Hermes, `react-native-xcode.sh` writes an intermediate sourcemap to `$CONFIGURATION_BUILD_DIR/main.jsbundle.map` and deletes it after composing the final map, so pointing `SOURCEMAP_FILE` there would have the composed map deleted before it can be uploaded.

Then, in Xcode, add a new **Run Script** build phase (Build Phases > + > New Run Script Phase), placed **after** the existing "Bundle React Native code and images" phase. Name it "Upload Flare sourcemaps" and paste:

```bash
set -e
WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
FLARE_XCODE="../node_modules/@flareapp/react-native-sourcemaps/scripts/flare-xcode.sh"
/bin/sh "$WITH_ENVIRONMENT" "$FLARE_XCODE"
```

The `with-environment.sh` wrapper is the same one React Native uses for its own bundle phase; it makes sure the Flare phase sees `SOURCEMAP_FILE` (and any `FLARE_*` variables) from your `.xcode.env`.

In that build phase, **uncheck "Based on Dependency Analysis"**. The script has no input or output files, so otherwise Xcode warns that it has ambiguous dependencies and runs on every build. You want it to run on every release build anyway. (The Expo config plugin sets this for you.)

> The key comes from `flare.json` or the auto-loaded `.env.local`, so it's there even for a GUI build. The **version** (`FLARE_SOURCEMAP_VERSION`) is the one variable the phase still reads from the **build's** environment, inherited from whatever launched the build. A `react-native run-ios`, `xcodebuild`, or Fastlane run from a terminal that exported it works. A build started from the Xcode GUI (including Product > Archive) doesn't have it, so the upload skips with the banner. Archive from the command line or in CI for releases. The build still succeeds either way.

### Custom build configurations (bare / brownfield)

Your build configuration doesn't have to be called `Release`.

The hooks upload whenever a build makes a JavaScript bundle. They skip a build only when its name contains `debug` (any casing).

- Uploads: `Release`, `Staging`, `Production`, `AppStore`, your own Android build type
- Skipped: `Debug`, `debug`, `StagingDebug`

A debug build runs from Metro and makes no bundle, so there's nothing to upload. The hook skips it and your dev builds stay fast.

Want a bundling config to skip the upload anyway? Put `debug` in its name. Or remove the Flare setup you added above: the `apply from "…/flare.gradle"` line in `android/app/build.gradle` on Android, or the **Upload Flare sourcemaps** build phase in Xcode on iOS. Either one turns the automatic upload off completely.

### Builds never fail because of sourcemaps

A sourcemap problem never breaks your build: if the key or `FLARE_SOURCEMAP_VERSION` is missing, or the upload itself fails, the build still succeeds and a clear, boxed warning is printed with a command you can run to upload the map by hand.

## Automatic upload for Expo (CNG)

For an Expo project that uses prebuild (managed / CNG), a config plugin wires the
upload into the generated native projects on every `expo prebuild`, so it survives
regeneration. You pass the plugin your key the same two ways as the bare project. Pick one depending on whether you mind the key living in your repository.

**Option 1: inline in `app.json` (simplest).** If you don't mind committing the key, add the plugin with the key inline:

```json
{
    "expo": {
        "plugins": [
            ["@flareapp/react-native-sourcemaps/expo", { "apiKey": "YOUR PROJECT KEY" }]
        ]
    }
}
```

The key is now in version control along with the rest of your Expo config.

**Option 2: `app.config.ts` + environment variable (keeps the key out of git).** If you'd rather not commit the key, switch from the static `app.json` to a dynamic `app.config.ts` (or `app.config.js`) and read the key from the environment. The Expo CLI auto-loads `.env` / `.env.local`, so a local build picks the key up without exporting anything by hand, and in CI/EAS you set it as a secret (`build.<profile>.env` in `eas.json`):

```ts
// app.config.ts
import type { ExpoConfig } from 'expo/config';

// Resolved by the Expo CLI, which auto-loads .env / .env.local, so the Flare key
// never lives in a committed file. The same key feeds two consumers:
//   - the sourcemaps config plugin -> build-time flare.json (sourcemap upload)
//   - `extra` -> read at runtime via expo-constants to boot the SDK
// The report ingest URL is independent of the plugin's sourcemap-upload endpoint.
const flareApiKey = process.env.FLARE_API_KEY ?? '';
const flareIngestUrl = process.env.FLARE_INGEST_URL ?? 'https://ingress.flareapp.io/v1/errors';

const config: ExpoConfig = {
    name: 'your-app',
    slug: 'your-app',
    // ...your existing icon, ios, android, web config
    plugins: [['@flareapp/react-native-sourcemaps/expo', { apiKey: flareApiKey }]],
    extra: {
        flareApiKey,
        flareIngestUrl,
    },
};

export default config;
```

Reading the key from `extra` at runtime (via [`expo-constants`](https://docs.expo.dev/versions/latest/sdk/constants/)) lets you keep it out of your source there too, instead of hardcoding it in `flare.light('YOUR PROJECT KEY')`. If you only care about the sourcemap upload, the `plugins` line alone is enough. The `extra` block is optional.

The plugin only does the **native wiring**. You still:

- add the Babel plugin (step 1) and pass `flareSourcemapVersion` to `flare.configure()`
  (step 2), and
- set `FLARE_SOURCEMAP_VERSION` in the build environment (step 3): a shell `export`
  locally, or `eas.json`'s `"build": { "<profile>": { "env": { "FLARE_SOURCEMAP_VERSION": "..." } } }`
  for EAS Build.

It runs only for **release** native builds. The upload happens inside the native build. It needs `FLARE_SOURCEMAP_VERSION` in that build's environment, plus a key, either the `apiKey` you passed the plugin (written into `flare.json`) or a gitignored `.env.local` the hooks auto-load. If the version or key is missing, the upload is skipped with a warning and the build still succeeds.

### Building for distribution

You don't have to use EAS. The build phase inherits the environment of whatever **launches** the build, so any release build started from a shell with the variables set will upload:

- **EAS Build**: set them in `eas.json` (`build.<profile>.env`).
- **`eas build --local`**: the same `eas.json`, run on your machine.
- **A command-line archive**: `xcodebuild archive` or Fastlane on iOS, `./gradlew bundleRelease` (the App Store `.aab`) on Android, run from a shell where you exported the variables:

```bash
export FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)"
# then your normal release build, e.g.
xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Release archive   # iOS
./gradlew :app:assembleRelease                                                                  # Android
```

> **OTA updates (EAS Update) are not covered.** The plugin's hooks fire only during a
> native build. `eas update` ships a new JS bundle via `expo export` with no Gradle or
> Xcode phase, so no sourcemap is uploaded and those frames stay minified. For an OTA
> release, upload the map yourself:
>
> ```bash
> FLARE_SOURCEMAP_VERSION="$(git rev-parse --short HEAD)" npx expo export
> npx flare-rn-sourcemaps upload \
>   --api-key "YOUR PROJECT KEY" \
>   --sourcemap dist/_expo/static/js/ios/<your-bundle>.hbc.map \
>   --bundle-filename main.jsbundle \
>   --version "$FLARE_SOURCEMAP_VERSION"
> ```
>
> Use the **same** `FLARE_SOURCEMAP_VERSION` for the export and the upload, or the map
> will not match.

## CLI options

```bash
npx flare-rn-sourcemaps upload [options]
```

| Option              | Description                                                                                                                              |
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| `--sourcemap`       | **(required)** Path to the composed `.map` file to upload.                                                                              |
| `--api-key`         | **(required)** Your Flare project's public API key. Falls back to the `FLARE_API_KEY` environment variable.                             |
| `--bundle-filename` | The name the bundle appears under in stack frames. Defaults to the sourcemap's filename without `.map` (e.g. `index.android.bundle`).   |
| `--version`         | The sourcemap version. Defaults to `FLARE_SOURCEMAP_VERSION`, then your `package.json` version.                                         |
| `--api-endpoint`    | The upload endpoint. Defaults to `https://flareapp.io/api/sourcemaps`.                                                                  |

## Verifying it worked

Build a release app with the steps above and upload the sourcemap. Then send a test report from somewhere in your app, for example a button that calls `flare.test()`:

```ts
import { flare } from '@flareapp/react-native';

// e.g. in a button's onPress
flare.test();
```

Open that report in Flare. The stack frames should show your real files (`App.tsx`, your components) with code snippets, instead of a minified `index.android.bundle:1` (or `main.jsbundle:1`) line.

If a frame is still unresolved, the cause is almost always one of:

- **The version does not match.** The app reported one version and the sourcemap was uploaded under another. Make sure `FLARE_SOURCEMAP_VERSION` is set to the same value for both the build and the upload, and that you rebuilt the app after adding the Babel plugin (step 1).
- **The sourcemap was uploaded after the error.** Flare resolves frames when the error is received, so upload the sourcemap as part of your release, before users hit errors. Re-uploading does not re-resolve errors that already came in.
- **The wrong sourcemap.** With Hermes you must upload the **composed** map your release build produced, not the raw Metro packager map (`*.packager.map`).

## Manually uploading sourcemaps

The CLI is a thin wrapper around Flare's sourcemap API. If you need to upload from a custom script, see [Manually uploading sourcemaps](/docs/javascript/errors/sourcemaps#manually-uploading-sourcemaps). The `version_id` you send must equal the `flareSourcemapVersion` baked into your app, and `relative_filename` must match the bundle name in your stack frames (`index.android.bundle` on Android, `main.jsbundle` on iOS).
