Error tracking for Expo — and the same SDK for your web app

Catch JS crashes with readable Hermes stacks and native iOS and Android crashes (in beta), see every failed request, track crash-free sessions and screen views, and collect user feedback tied to the exact error they hit.

Install and initialize

One package, and it detects the runtime for you. On Expo it reads EXPO_PUBLIC_MARSAD_KEY, so you don't pass a key at all.

npx expo install @marsadco/sdk
App.tsx
tsx
import AsyncStorage from "@react-native-async-storage/async-storage";
import { init, ErrorBoundary } from "@marsadco/sdk/expo";

// storage keeps queued events across app restarts and offline periods
init({ storage: AsyncStorage });

export default function App() {
  return (
    <ErrorBoundary>
      <RootNavigator />
    </ErrorBoundary>
  );
}

With no fallback, the boundary renders a native “Something went wrong” view with the event's reference code and a one-tap report form — so a user can tell you what they were doing, attached to that exact crash.

Screens and navigation

React Native has no browser history to observe, so two hooks take your router's current path — one records navigation breadcrumbs for the next error, the other records screen views that land on the same Analytics page as your web traffic, attributed to iOS or Android.

app/_layout.tsx (Expo Router)
tsx
import { usePathname } from "expo-router";
import { useNavigationBreadcrumbs, useScreenViews } from "@marsadco/sdk/expo";

export default function Layout() {
  const pathname = usePathname();
  useScreenViews(pathname);
  useNavigationBreadcrumbs(pathname);
  // …
}

// React Navigation: pass useRoute().name instead

Hermes stack traces, symbolicated

A crash in a release build of a Hermes app gives you a stack of bytecode offsets — frame addresses against a compiled bundle, with no file and no line number. On its own it tells you almost nothing. Marsad resolves those frames server-side against the source map for that exact build, so the issue you open shows your real file, your real line, and the function you actually wrote.

The matching is by debug ID rather than by version string. Each build stamps an identifier that travels with both the bundle and its source map, so the right map is found even when two releases share a version number, or when a build is promoted between channels. There is no release name to keep in sync and no upload step to remember per deploy.

CI, after the bundle is built
bash
# authenticated by your ingest key ($MARSAD_KEY)
npx marsad upload-sourcemaps ./dist

Upload once per build from CI and every crash from that build arrives readable. If a map is missing, the frames still group into an issue — you get the crash and the event count, just without file names, so a forgotten upload degrades rather than loses data.

What you get

  • Every JS-thread error, including fatals, captured through ErrorUtils with the breadcrumbs that led there.
  • Native crashes below JavaScript, in beta with @marsadco/react-native: iOS signals, NSException, C++ exceptions and Swift traps; Android Kotlin/Java crashes, ANRs and C/C++ crashes.
  • Hermes stack traces symbolicated server-side — upload your source maps and minified bytecode frames resolve to your real files and lines.
  • Network capture: every fetch and XHR with method, status and duration, and secrets scrubbed before anything leaves the device.
  • Sessions and crash-free rate, so you can see release health per deploy.
  • Screen views and custom track() events, next to your web traffic.
  • A drop-in FeedbackWidget built from native primitives, and feedback linked to the issue by reference code.
  • Persisted, retried delivery — events survive app restarts, flaky networks and crashes.

Native crashes, in beta

Some crashes never reach JavaScript: a segfault in a native library, an uncaught NSException, a Kotlin exception on the main thread, an ANR. The @marsadco/react-native package catches those, on KSCrash on iOS and Android's own exit records, and sends each one on the next launch with the user, tags and breadcrumbs your app had set. If the app crashes before JavaScript even starts, the native side sends the report itself.

npx expo install @marsadco/react-native@beta

It's tested on release builds of Expo SDK 54 to 57 and bare React Native, and alongside Sentry, which keeps receiving every crash too. Release builds upload their dSYMs, native libraries and R8 mapping automatically.

What we don’t do yet

Native frames show as the binary and address for now, grouped so the same crash lands in one issue. Resolving them to function, file and line on our side is next; the debug files your builds already upload are what it will use.

Watchdog kills, hangs and out-of-memory terminations on iOS, and native stacks on Android 10 and older, aren't captured: the operating system ends those apps from outside, where no crash handler runs. The package hasn't been run on a physical iPhone yet either, which is part of why it's a beta.

One SDK, web and mobile

The same package instruments your Next.js app — errors, traces, Web Vitals and cookieless analytics — so your web and mobile projects sit side by side in one dashboard instead of two vendors. Free plan includes 5,000 errors and 10,000 views a month, no credit card; paid plans are flat per-organization from $9/mo, never per seat.

Start watching your apps in minutes

Install the SDK, trigger a test error, and watch it arrive symbolicated. Free plan, no credit card.