Most React Native analytics work is not analysis. It is instrumentation: adding a tracking call to every button, remembering to fire a screen view on every route, keeping event names consistent across a team, and discovering six months later that the screen you most wanted to measure was never wired up.
Nohmo removes that work with a Babel plugin that runs at compile time, wrapping every onPress and onLongPress and instrumenting NavigationContainer — so taps and screen views arrive with no call sites in your code. On top of it sit the things a mobile team actually asks for: funnels, retention, install attribution, and JavaScript and native crash reporting in the same session model.
This page covers all of it — the analytics SDK, crash reporting, what works under Expo, and where we stand on session replay, which is a section that ends by recommending somebody else.
React Native analytics SDK
The Babel plugin is the part that makes this different from every SDK that asks you to instrument by hand. Added once to babel.config.js, it wraps onPress and onLongPress props at compile time and injects navigation listeners onto NavigationContainer. There is no runtime wrapping cost and nothing to remember at each call site.
Everything else follows from a single provider at the root of the app. Installs, opens, backgrounds, screens, taps, rage taps, crashes and install attribution are all captured from there, and the same npm package covers your React, Next.js or plain HTML surfaces into the same dashboard and the same identity model.
Tap autocapture with no call sites
Every PRESS carries the component name, the static label if there is one, and the file and line it came from — so you can find the button in your editor straight from the report.
Screen views from your navigator
The plugin instruments NavigationContainer directly, so every navigation state change becomes a SCREEN_VIEW with time spent. Prefer explicit control? useScreenView("Home") does the same job per screen.
Install attribution built in
Android reads the Play Store referrer, iOS checks the system pasteboard for a click token written when the ad was tapped. No MMP and no extra package — campaign parameters land on the install itself.
Identity that works backwards
Call linkUser() after login and every event that device ever fired, including sessions before the account existed, is attached to that user on the backend rather than orphaned.
Funnels, journeys and retention
Build a funnel in the dashboard and read it immediately. Session journeys show each step in order with time spent, rather than only an aggregate you have to interpret.
Batched, persisted, non-blocking
Events are queued in memory, persisted to disk and flushed as a batch on an interval. Nothing blocks the JS thread, and queued events survive the app being killed mid-session.
Conversions with a value
trackConversion() against a goal defined in Settings → Conversions, optionally with an amount and currency — which is what later lets errors be ranked by revenue lost rather than by frequency.
The same SDK on web
One npm package covers React, Next.js, plain HTML and React Native. One dashboard, one identity model, one bill — instead of reconciling two vendors that disagree about what a user is.
React Native crash reporting
React Native crashes in two places and most tooling only watches one. A JavaScript exception is caught by a global handler and reported normally. A native crash — an uncaught Kotlin exception, a Swift force-unwrap on nil, a signal on iOS — kills the process before any JavaScript runs, so the report has to survive the death of the app that was writing it.
Nohmo installs handlers on both sides, and the native ones ship inside the SDK with no extra packages to add. A native crash is written to disk as it happens and reported as APP_CRASH on the next launch, attributed back to the session it actually happened in rather than the one that reported it.
What changes how you triage is what comes attached. Because a crash is just another event in the same session model, every one carries the journey that produced it — the screens, taps and funnel step immediately before the app died.
Native crashes on both platforms
Android catches uncaught Java and Kotlin exceptions on any thread. iOS catches Objective-C exceptions plus Swift fatalError, force-unwraps and memory crashes through signal handlers.
JavaScript errors, on by default
Uncaught exceptions and unhandled promise rejections are captured as JS_ERROR with no setup at all. autoErrors: false turns the whole thing off in one prop if you would rather wire it yourself.
The journey to the crash
Each occurrence opens onto the session timeline up to the moment it broke. This is usually the difference between a bug you can reproduce and one you close as unreproducible.
Grouped by signature
Errors are grouped with occurrence counts, affected users and devices, and a sample stack trace — then drill into every individual occurrence rather than an aggregate.
Crash-free rate per release
Each app version compared against the build it replaced, so a regression reads as a comparison rather than something you infer from a rising count.
The failures that never throw
A Pressable wired to a handler that returns early. A form that submits into the void. Nothing raises, so no crash reporter sees it — while the user taps three times and leaves. Nohmo detects these behaviourally.
How Nohmo detects themAlerts the moment it happens
An Event Match webhook on JS_ERROR or APP_CRASH under Settings → Webhooks fires in real time, so a bad release reaches you before it reaches your reviews.
Bounded by default
Messages are truncated, query strings are stripped from request URLs, and Nohmo never reports failures of its own tracking endpoint.
Stack traces are raw and unsymbolicated today — dSYM and ProGuard mapping are planned but not shipped. If readable native traces are what you need right now, Crashlytics and Sentry both do this and we do not.
Expo analytics
Start with the constraint, because most vendors bury it and you will find out at the worst moment. Nohmo ships native modules — crash handlers on both platforms, the Play Store install referrer on Android, pasteboard attribution on iOS. Expo Go bundles a fixed set of native modules and cannot load arbitrary ones, so the SDK will not run inside Expo Go. No configuration fixes this; it is what Expo Go is.
It does work with Expo development builds and EAS Build, which is where most Expo apps past the prototype stage already live. The package ships an autolinking config, so once you have run a prebuild or created a dev client the native side is picked up without editing an Xcode project or a Gradle file. Expo Router is supported too — it is built on React Navigation, which the Babel plugin already instruments.
Autolinked into your dev build
The package ships a react-native.config.js, so Expo autolinking picks up the iOS and Android modules during prebuild. Nothing to edit by hand.
Composes with babel-preset-expo
Add the plugin alongside Expo’s preset in babel.config.js, then run expo start --clear. The Metro cache is the usual reason a first attempt captures nothing.
Expo Router out of the box
Expo Router sits on React Navigation, so screen views are captured with no extra configuration. Mount the provider in app/_layout.tsx wrapping Slot.
If Expo Go is non-negotiable for your team, this is not your tool — and a JS-only analytics SDK is the honest answer, accepting that you lose native crash capture and install attribution with it, because both need native code.
React Native session replay
Nohmo does not have session replay, it is not on our roadmap, and if replay is the reason you are here then one of the tools below is what you want. We would rather say that plainly than waste your evaluation.
What Nohmo keeps instead is the journey: every screen, tap, rage tap and funnel step in order, with time spent, attached to the session and to any error that occurred in it. That answers "what did this user do before it broke" without recording anything — no frame capture cost, no sampling, and a far smaller privacy surface. It will never show you what the screen looked like, so for a visual bug, a broken layout or an off-screen control it is not a substitute and pretending otherwise would not help you.
Replay on React Native is also genuinely harder than on the web. There is no DOM to serialise, so tools snapshot the native view hierarchy or capture frames, which costs more in CPU, payload and battery — which is why mobile replay is usually sampled, and why the session you most want to watch may not have been recorded.
LogRocket
compareThe most replay-centric product, with network calls and state alongside the recording. Start here if replay is the primary workflow.
Sentry
compareReplay attached to an error you were already monitoring, with symbolicated traces. Replays meter separately from errors.
PostHog
Replay bundled with analytics, feature flags and experiments on a generous free tier. Mobile replay is newer than the web product.
UXCam
Mobile-only and built for UX research — recordings with heatmaps, gesture analysis and well-considered privacy masking.
Instabug
Recording paired with in-app bug reporting and shake-to-report, so a report arrives with what the user said and what they did.
Embrace
Complete session telemetry rather than video — every event, network call and ANR. Often more useful than a recording, and cheaper to store.
If you adopt replay, treat masking as a first-class task. A recording of a real user is a far larger privacy surface than an event stream, and the defaults are rarely sufficient for a screen showing personal data.
Captured without writing code
Captured once the provider is mounted and the Babel plugin is added — no per-component code.
| Event | Trigger |
|---|---|
| APP_INSTALL | First ever app open — platform, appVersion, osVersion |
| APP_OPEN / APP_BACKGROUND | Every foreground and background, with session duration and screen |
| INSTALL_ATTRIBUTED | Play Store referrer on Android, pasteboard click token on iOS |
| SCREEN_VIEW / TIME_SPENT | Every NavigationContainer state change, Expo Router included |
| PRESS / LONG_PRESS | Any onPress or onLongPress — with component, text, file and line |
| RAGE_CLICK | Three taps on the same control inside a second |
| JS_ERROR | Uncaught exceptions and unhandled promise rejections |
| APP_CRASH | Fatal JS crashes and native crashes — reported on next launch |
Setting it up
- 1
Install
The core package plus AsyncStorage, which is what makes a device recognisable across restarts. Native crash handlers and install attribution are inside the SDK — there is nothing else to add.
bashnpm install nohmonpm install @react-native-async-storage/async-storagecd ios && pod install - 2
Add one line to babel.config.js
The step that removes the instrumentation work. On Expo, swap the preset for babel-preset-expo and run expo start --clear afterwards — modules cached before the config changed are not re-transformed.
jsmodule.exports = {presets: ['module:@react-native/babel-preset'], // 'babel-preset-expo' on Expoplugins: ['nohmo/babel-plugin'], // ← taps + screen views, no code changes} - 3
Mount the provider
The whole setup. On Expo Router this goes in app/_layout.tsx wrapping Slot; otherwise App.tsx. Pass appVersion so crashes can be attributed to a release and compared against the previous one.
tsx// App.tsximport { NohmoProvider } from 'nohmo/react-native'import AsyncStorage from '@react-native-async-storage/async-storage'export default function App() {return (<NohmoProviderprojectId="proj_xxxx" // Settings → General → SDK credentialsapiKey="pk_xxxx"options={{ appVersion: '1.0.0', debug: __DEV__, storage: AsyncStorage }}><YourApp /></NohmoProvider>)} - 4
Add your own business events
Autocapture covers interaction; these are the events worth naming by hand. Events are unlimited on a flat plan, so there is no reason to keep the payloads thin.
tsximport { useNohmo } from 'nohmo/react-native'const { send, linkUser, trackConversion } = useNohmo()send('CHECKOUT_STARTED', { cartValue: 49.99, items: 3 })// Retroactively attaches all prior anonymous activity to the userawait linkUser(user.id, user.email, { plan: user.plan })trackConversion('purchase', { amount: 29.99, currency: 'USD' }) - 5
Tell Nohmo about each release
Point CI at the release endpoint so crash-free rate is compared build to build. Idempotent on version and platform, so re-running a pipeline is safe.
bashcurl -X POST https://www.nohmo.in/api/tracker/release/ \-H "X-API-Key: $NOHMO_API_KEY" \-H "Content-Type: application/json" \-d '{"version":"2.4.1","platform":"ios","commitSha":"'"$GIT_SHA"'"}'
What this does not do
The gaps, named specifically, so you find them here rather than three weeks into an integration.
- •Stack traces are raw and unsymbolicated. dSYM and ProGuard mapping are planned but not shipped, so native frames arrive as addresses rather than method names. Crashlytics and Sentry both do this today and we do not.
- •Android NDK and C++ crashes are not captured, and neither are ANRs or out-of-memory terminations. If your crashes are in native modules, Crashlytics or Embrace is the better tool.
- •No session replay, and it is not on the roadmap. The journey is reconstructed from events, which is a different thing from a recording.
- •Expo Go is not supported and cannot be, because the SDK ships native modules. Development builds and EAS are fine.
- •Tap and screen autocapture depend on the Babel plugin. Without it you still get sessions, errors and any events you send by hand, but screens need useScreenView() per screen.
- •No A/B testing, feature flags or remote config, and analytical depth is deliberately narrower than Amplitude or Mixpanel — no predictive cohorts or advanced behavioural modelling.
Frequently asked questions
What is the best analytics SDK for React Native?+
It depends what you are optimising for. For behavioural depth with an analyst to use it, Amplitude. For experimentation, Statsig or PostHog. For free at any volume, Firebase. Nohmo suits teams who want tap and screen autocapture with no instrumentation work, install attribution and crash monitoring in the same package, on a flat bill.
Do I have to add tracking code to every button?+
No. Add plugins: ["nohmo/babel-plugin"] to babel.config.js and every onPress and onLongPress is wrapped at compile time, with screen views taken from NavigationContainer. You only write code for your own business events.
Does Nohmo catch native crashes or only JavaScript errors?+
Both. On Android it catches uncaught Java and Kotlin exceptions on any thread; on iOS, Objective-C exceptions plus Swift fatalError, force-unwraps and memory crashes via signal handlers. Because a dying process cannot make a network call, the crash is written to disk and reported as APP_CRASH on next launch, attributed back to the session it happened in.
Do I need extra packages for native crash reporting?+
No. The native handlers ship inside the nohmo package for both platforms — nothing to add beyond pod install on iOS. AsyncStorage is recommended so the device ID persists across restarts, and Firebase messaging is only needed if you also want uninstall detection.
Does Nohmo work with Expo?+
With Expo development builds and EAS Build, yes — including Expo Router. Not with Expo Go, because the SDK ships native modules for crash capture and install attribution, and Expo Go can only load native modules compiled into it. Install expo-dev-client, run expo prebuild, and build a development profile with EAS.
My taps are not being captured under Expo — what did I miss?+
Usually the Metro cache. The Babel plugin transforms your source at compile time, so modules already cached before you edited babel.config.js are not re-transformed. Run npx expo start --clear, and confirm the plugin is listed in the config Expo actually loads.
Does Nohmo have session replay for React Native?+
No, and it is not on the roadmap. Nohmo reconstructs the journey from events — screens, taps and funnel position in order — which is not the same as a recording. If replay is what you need, LogRocket, Sentry, PostHog, UXCam, Instabug or Embrace are the tools to evaluate.
Why are the stack traces not symbolicated?+
Because we have not built it yet. Sentry and Crashlytics upload dSYM and ProGuard mapping files at build time and resolve frames against them; we show raw traces today. It is a genuine gap and the most common reason a team keeps a second reporter installed alongside.
Can I run this alongside Crashlytics or Sentry?+
Yes, and during an evaluation you should. Crash handlers chain rather than replace one another, so an existing reporter keeps receiving everything it did before. Ship both through one release and compare what each caught on real devices.
Is there a limit on event volume or event names?+
No cap on distinct event names, and events are unlimited on the flat plan — $49/month per project during early access, normally $79. Instrumenting more thoroughly never increases the bill, which is the opposite of how event-metered tools behave.
Will tracking slow the app down?+
Events are queued in memory, persisted to disk and flushed as a batch on an interval rather than sent individually. Nothing blocks the JS thread, and queued events survive the app being killed mid-session.
The verdict
If you need symbolicated native traces, ANR detection, NDK coverage or session replay, this is not the whole answer and Crashlytics, Sentry or LogRocket will serve you better on each — we would rather point you there than argue. If the actual problem is that instrumenting a React Native app is tedious and never quite finished, and that your analytics, attribution and crash data live in three products that disagree about what a session is, one Babel line and one provider replaces all of it for a flat fee that a bad release cannot move.
Feature parity across React Native, Flutter and web at the same SDK version.