npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@grafana/faro-metro-plugin

v0.2.0

Published

Upload React Native (Metro + Hermes) source maps to the Faro source map API and inject Faro bundle id preamble

Readme

Faro source maps — Metro (React Native)

This package configures Metro so release bundles work end-to-end with Grafana Frontend Observability:

  1. Bundle id preamble — Same idea as @grafana/faro-webpack-plugin: a small snippet at the top of the JS bundle sets meta.app.bundleId so it matches the source map record in @grafana/faro-react-native / @grafana/faro-web-sdk.

  2. Source map shape — Metro emits the packager map in the form Hermes precompile, Hermes interpreter, or JSC expects (see Hermes modes).

  3. Upload — Happens after the native pipeline produces the composed map (hermesc + compose-source-maps.js). Metro does not upload; the composed map is the one the collector uses for symbolication.


End-to-end flow

| Step | Where | |------|--------| | 1 | Install @grafana/faro-metro-plugin and wrap metro.config.js with withFaroConfig. | | 2 | Install and initialise @grafana/faro-react-native with the same app.name as appName in Metro options. | | 3 | For release builds, export FARO_BUNDLE_ID and FARO_SOURCEMAP_* (see Environment variables). | | 4 | Android: Upload runs from Gradle after the composed map is produced (see Android).iOS: Upload after the composed map exists via Xcode automation that calls this package’s bin/ helpers (Release-only), or manually with faro-cli metro upload (see iOS upload). |

Symbolication runs server-side in the Faro collector: it loads the map by bundleId from the source map API and resolves stack frames.


Installation

npm install --save-dev @grafana/faro-metro-plugin
# or
yarn add --dev @grafana/faro-metro-plugin

Also add @grafana/faro-react-native as a runtime dependency if you use the standard Android Gradle wiring.


Metro configuration

In metro.config.js:

const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const withFaroConfig = require('@grafana/faro-metro-plugin').default;

const faroOpts = {
  appName: 'MyApp',
  endpoint: 'https://your-collector.example.com/api/v1',
  appId: 'your-app-id',
  stackId: 'your-stack-id',
  apiKey: process.env.FARO_SOURCEMAP_API_KEY,
  bundleId: process.env.FARO_BUNDLE_ID,
  verbose: true,
};

module.exports = mergeConfig(getDefaultConfig(__dirname), withFaroConfig({}, faroOpts));

Metro uses: appName, bundleId (or FARO_BUNDLE_ID), optional hermes, sourceMapFile, skipUpload, verbose.
The shared option shape also requires endpoint, appId, stackId, apiKey; keep them aligned with Frontend Observability → Settings → Source Maps and with the same values you pass into faro-cli metro upload or your Gradle / Xcode automation.

When using @sentry/react-native, wrap Sentry outside and Faro inside so the preamble stays first, for example:

mergeConfig(getDefaultConfig(__dirname), withSentryConfig(withFaroConfig({}, faroOpts))) — check Sentry’s docs for your RN version.


Environment variables

| Variable | Purpose | |----------|---------| | FARO_SOURCEMAP_API_KEY | Bearer token for the upload HTTP request (Gradle, Xcode automation, or CLI). | | FARO_SOURCEMAP_ENDPOINT | Collector API base URL (Gradle, Xcode automation, or CLI). | | FARO_SOURCEMAP_APP_ID | App id segment in the upload URL (Gradle, Xcode automation, or CLI). | | FARO_SOURCEMAP_STACK_ID | Stack id for the upload (Gradle, Xcode automation, or CLI). | | FARO_BUNDLE_ID | Release: Stable build id (commit SHA, CI build number, …). Must match what Metro baked into the bundle. Omit for local dev. | | FARO_SKIP_SOURCEMAP_UPLOAD | If 1 or true, native upload steps skip while still building; preamble and map shaping unchanged. Also participates in dev bundle-id behaviour when no explicit id is set. | | FARO_DISABLE_HERMES_PRECOMPILE | Rare. Only if Metro runs with dev: false but the JS you ship never goes through the usual native hermesc + compose-source-maps.js step (see When Hermes skips native precompile). Dev does not need this—Metro dev: true is detected automatically. Normal Android/iOS release builds must leave this unset. |

skipUpload: true, NODE_ENV=development, Metro dev: true, and FARO_SKIP_SOURCEMAP_UPLOAD affect placeholder bundle ids and whether native upload steps run; none of them perform upload from Metro.

When Hermes skips native precompile

Set FARO_DISABLE_HERMES_PRECOMPILE only in the situations below. Everyone else should omit it.

Default: leave it unset. That matches stock React Native: e.g. Android ./gradlew assembleRelease (or your project’s equivalent that runs bundleReleaseJsAndAssets and the RN Hermes/map steps) and iOS Release archive—those pipelines always precompile Hermes bytecode and compose maps after Metro.

Metro already chooses the right map shape for development (dev: true → Hermes “runtime” / flattened map). You do not need this variable for npx react-native start or day-to-day debugging.

Set the variable (usually only in CI or a dedicated script) when all of the following are true:

  1. You produce a production bundle with Metro dev: false (same as a release JS bundle).
  2. That bundle is loaded in Hermes in a way where stack traces look like dev Hermes (line 1 + UTF-8 byte column in the JS source text)—not like a map meant for the composed Hermes bytecode pipeline.
  3. Your pipeline does not run the standard post-Metro hermesc + compose-source-maps.js flow that assembleRelease / a typical Xcode Release build performs.

Concrete-style example (illustrative, not a single official command): a team runs only npx react-native bundle --platform android --dev false … in CI, ships index.android.bundle + .map through a custom native shell or distribution path, and never invokes Gradle’s release Hermes steps that merge Metro’s map with the bytecode map. Production crashes then behave like Hermes-on-raw-bundle symbolication; setting FARO_DISABLE_HERMES_PRECOMPILE=1 for that Metro job makes the emitted map match those stacks. The same app built with a normal ./gradlew assembleRelease must not set this—doing so can break compose-source-maps (multi-line packager map is required there).

Who sets it: not every developer—only the maintainer of the non-standard build defines it once (e.g. export in the CI job or script that runs the odd Metro-only release bundle).


Hermes modes

How Metro output is shaped depends on the scenario. The plugin always injects the Faro bundle id line at the top of the JS; it reshapes the source map only when the engine reports positions in a different coordinate system.

| Scenario | What the plugin is doing (plain language) | What you end up with | |----------|---------------------------------------------|----------------------| | Release, Hermes (normal RN) | Tags the bundle with a Faro id; lightly fixes the map so that extra line doesn’t break positions; leaves a normal multi-line map so the native build can still merge Metro + Hermes maps. | JS with id + map ready for the native Hermes/compose step; production symbolication usually uses the final map from that pipeline. | | Dev, Metro + Hermes | Tags the bundle; rewrites the map so Hermes dev errors (one line + byte offset) can still be mapped back to source. | JS with id + flattened map that matches live dev stacks. | | JSC only | Tags the bundle; only shifts line numbers for that id line—no Hermes-specific map rewrite. | JS with id + classic line/column map. |

We always label the bundle; we only “reshape” the source map when the JavaScript engine reports crashes in a different coordinate system (Hermes in dev).

Implementation (reference): internal mode names and triggers:

| Mode | When | Map shape from Metro | |------|------|----------------------| | precompiled | dev: false, Hermes enabled (hermes !== false), FARO_DISABLE_HERMES_PRECOMPILE unset — typical Android/iOS release | Multi-line map with +1 line shift on generated lines | | runtime | dev: true or FARO_DISABLE_HERMES_PRECOMPILE set | Single-line mappings with UTF-8 byte offsets on line 1 | | jsc | hermes: false in plugin options | Multi-line map with +1 line shift |

For precompiled, Xcode/Gradle run compose-source-maps.js after Metro. That step needs the multi-line packager map; flattening it at Metro would break the composed map (sources empty) and symbolication.

Upload always happens after compose: Gradle on Android, Xcode automation calling this package’s bin/ on iOS (Release-only), or faro-cli metro upload where you drive uploads yourself.


Android

With @grafana/faro-react-native

  1. Dependencies: @grafana/faro-react-native (app) and @grafana/faro-metro-plugin (dev).
  2. Configure Metro as in Metro configuration.
  3. Export FARO_BUNDLE_ID and all FARO_SOURCEMAP_* vars before release builds.
  4. Run a normal release workflow (yarn android --mode=release, installRelease, assembleRelease, bundleRelease, …).

React Native autolinks the SDK’s Android library. Its android/build.gradle registers faroUploadComposedSourceMapAndroidRelease on your :app project and attaches it as a finaliser of bundleReleaseJsAndAssets / createBundleReleaseJsAndAssets after gradle.projectsEvaluated.

You do not edit android/app/build.gradle for this path.

The task runs node_modules/@grafana/faro-metro-plugin/bin/faro-upload-source-map.js, which invokes faro-cli metro upload with --map pointing at:

android/app/build/generated/sourcemaps/react/release/index.android.bundle.map

and passes --bundle-id, --endpoint, --app-id, --stack-id, --api-key from the Gradle environment.

If the shim is missing, the composed map does not exist, any required env var is missing, or FARO_SKIP_SOURCEMAP_UPLOAD is set, the task logs and skips — it does not fail the build.

Without @grafana/faro-react-native

Use the same Gradle behaviour by applying this package’s script from android/app/build.gradle:

apply from: file("../../node_modules/@grafana/faro-metro-plugin/android/source-map-upload.gradle")

Adjust the relative path if your node_modules layout differs. Export the same env vars as above before release builds.


iOS upload

Layout (same idea as Android): Anything executable lives in @grafana/faro-metro-plugin under bin/ (for example the existing faro-upload-source-map entry that forwards to faro-cli metro upload). @grafana/faro-react-native should only wire Xcode — for example a React Native scriptPhases hook — that execs those paths under node_modules/@grafana/faro-metro-plugin/bin/, not ship duplicate scripts inside the SDK.

Release-only: That Xcode step must no-op on non-Release configurations (inspect Xcode’s CONFIGURATION / equivalent). Debug and simulator-oriented builds stay quiet and never call the upload CLI.

Manual / CI (when nothing uploads the map for you): Use this if the automatic path does not run—for example you have not wired the Android Gradle task or an Xcode Release script phase that calls node_modules/@grafana/faro-metro-plugin/bin/, upload steps are skipped (FARO_SKIP_SOURCEMAP_UPLOAD, missing env vars), or CI builds the composed map in a job without those hooks. Invoke faro-cli metro upload after Release produces the composed map:

npx faro-cli metro upload \
  --map "$BUILD_DIR/main.jsbundle.map" \
  --endpoint "$FARO_SOURCEMAP_ENDPOINT" \
  --app-id "$FARO_SOURCEMAP_APP_ID" \
  --stack-id "$FARO_SOURCEMAP_STACK_ID" \
  --api-key "$FARO_SOURCEMAP_API_KEY" \
  --bundle-id "$FARO_BUNDLE_ID"

Android the same way—only when Gradle is not already running the upload finaliser (or you need a standalone CI step using the same flags as Gradle):

npx faro-cli metro upload \
  --map android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
  --endpoint "$FARO_SOURCEMAP_ENDPOINT" \
  --app-id "$FARO_SOURCEMAP_APP_ID" \
  --stack-id "$FARO_SOURCEMAP_STACK_ID" \
  --api-key "$FARO_SOURCEMAP_API_KEY" \
  --bundle-id "$FARO_BUNDLE_ID"

Flags accept the matching FARO_* env fallbacks where documented in @grafana/faro-cli.

The CLI rejects composed maps with empty sources (usually wrong Metro map shape for compose-source-maps.js) and exits with code 3.


Optional: sourceMapFile

Metro writes the map’s file field from this basename (default bundle.js). Align with releaseBundleFilename in @grafana/faro-react-native if you change it.


Sanity check: react-native bundle

Match appName, FARO_BUNDLE_ID, and upload settings with CI.

Android

npx react-native bundle \
  --platform android \
  --dev false \
  --minify true \
  --entry-file index.js \
  --bundle-output dist/android-release/index.android.bundle \
  --sourcemap-output dist/android-release/index.android.bundle.map \
  --assets-dest dist/android-release/res

iOS — Set sourceMapFile to main.jsbundle for this run so the map’s file matches stacks and releaseBundleFilename:

FARO_PLATFORM=ios npx react-native bundle \
  --platform ios \
  --dev false \
  --minify true \
  --entry-file index.js \
  --bundle-output dist/ios-release/main.jsbundle \
  --sourcemap-output dist/ios-release/main.jsbundle.map \
  --assets-dest dist/ios-release/assets

Check: Bundle starts with __faroBundleId_<appName>; .map is JSON version 3, non-empty sources, file matching your bundle basename.


Grafana UI

In Frontend Observability → your app → Settings, open Source Maps for endpoint, appId, stackId, and Metro-oriented snippets.