Skip to content
OTFotf
All posts

EAS Observe in production: tie startup metrics to every build and update

D
DaveAuthor
8 min read
EAS Observe in production: tie startup metrics to every build and update

Crash reporters tell you when the binary died. They stay quiet when an OTA makes the home screen take three seconds longer on mid-range Android. That gap is where most "the app feels worse" tickets come from — and where release blame usually dissolves into guesswork across three native builds and two JavaScript updates.

This post is the production wiring guide for EAS Observe (expo-observe): install it, call markInteractive() honestly, read release markers next to cold/warm launch and TTI, and keep Sentry (or similar) for mature crash triage. It complements Sentry error tracking for React Native in production and Release health for AI-built apps; it is not a substitute for staged OTA rollouts or a rollback plan.

What Observe owns (and what it does not)

EAS Observe is Expo's production performance service for real devices. Per the introduction docs, it tracks cold and warm launch, bundle load, time to first render (TTR), time to interactive (TTI), and EAS Update download time. On SDK 56+, Expo Router or React Navigation integrations add per-route render and interactive timings. You can also emit Observe.logEvent signals onto the same session timeline.

What it is not, per Expo's own when-to-use table and GA write-up:

  • Development-time profiling (use React Native DevTools / Expo Atlas)
  • A full APM or backend trace product
  • Session replay or product analytics
  • A reason to delete Sentry tomorrow — Expo's marketing FAQ is explicit that Observe owns the EAS pipeline view while crash reporters still own deep error workflows

Platforms: Android, iOS, and tvOS. Not Expo Go — you need a development or production build because expo-observe is a native module. Prerequisite: Expo SDK 55+ and an EAS project (extra.eas.projectId).

EAS Observe public marketing page showing install command and dashboard overview

Source: https://expo.dev/services/eas-observe — public marketing page captured 2026-09-15.

Why mobile monitoring needs build + update identity

On the web, one deploy often means one active version. Mobile is messier: users update on their own schedule, and EAS Update layers JavaScript on top of native binaries. Three native releases plus two updates can mean five active combinations in the field. A monitoring tool that only knows a marketing version string averages those combinations into one p90 and hides which OTA moved the needle.

Observe's differentiator (stated on both the service page and GA announcement) is release attribution: every metric is joined to the EAS build and update that produced the session. Markers land on the chart when the first event for that build or update arrives. That is the failure mode crash tools miss — a JS update that does not crash but raises TTR/TTI.

If your release process already stages OTAs, keep that discipline: Staged OTA rollouts catch bad Expo updates before everyone gets them. Observe tells you which update hurt; staging limits who gets hurt first.

Byte and Dex wire Observe into an owned Expo kit repo

One codebase. iOS, Android, and web.

The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.

See the live demo

Install and wrap the root (SDK-aware)

npx expo install --fix
npx expo install expo-observe

SDK 56 and later

import { Stack } from 'expo-router';
import { ObserveRoot } from 'expo-observe';

function RootLayout() {
  return <Stack />;
}

export default ObserveRoot.wrap(RootLayout);

SDK 55

Use the legacy names: AppMetricsRoot.wrap(RootLayout) instead of ObserveRoot.

Wrapping measures TTR for you. Launch (cold/warm) and bundle load arrive with the native library. TTI is the one metric you must mark yourself — only your code knows when splash work, auth, and first data are done.

Call markInteractive() when the app is actually usable

From the get started guide:

import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { ObserveRoot, useObserve } from 'expo-observe';
import { useEffect, useState } from 'react';

SplashScreen.preventAutoHideAsync();

function RootLayout() {
  const [isReady, setIsReady] = useState(false);
  const { markInteractive } = useObserve();

  useEffect(() => {
    async function prepare() {
      try {
        await authenticateUser();
        await fetchInitialData();
      } catch (e) {
        console.warn(e);
      } finally {
        setIsReady(true);
      }
    }
    prepare();
  }, []);

  useEffect(() => {
    if (isReady) {
      SplashScreen.hide();
      markInteractive();
    }
  }, [isReady, markInteractive]);

  if (!isReady) return null;
  return <Stack />;
}

export default ObserveRoot.wrap(RootLayout);

Rules that bite teams in production:

  1. Call it after splash work finishes — update checks, auth, first fetch, splash animation.
  2. Multiple entry screens (onboarding, login, deep links): call markInteractive on each. Only the first call in a session records; if the deep-linked screen never calls it, that session has no TTI.
  3. SDK 56+ with navigation integration: move markInteractive() into screen components. A call outside a screen records nothing for route-scoped TTI; app-wide TTI still depends on those screen calls.
  4. Frame/slow-frame context attaches to the TTI event — no markInteractive() means no frame data for that screen.

Router integration and configuration (one configure call)

On SDK 56+, enable per-route metrics once at module scope before any screen mounts:

import { Observe } from 'expo-observe';

Observe.configure({
  integrations: { 'expo-router': true },
  // sampleRate: 0.25, // optional: deterministic per installation
  // dispatchInDebug: true, // only while testing instrumentation
});

Important details from the configuration docs:

  • Each Observe.configure() replaces the whole config — keep a single call.
  • sampleRate is deterministic per installation, not per session. Out-of-sample devices drop pending metrics.
  • Debug builds do not dispatch by default. dispatchInDebug: true is for verifying plumbing; leave it off in production or you pollute medians with simulator/dev timing.
  • Offline metrics buffer on device and flush on background (or via dispatchEvents()).
  • Users are anonymous install IDs — not PII, reset on reinstall. You can find a bad session; you cannot name the customer from Observe alone.

Rebuild after install. Expo is clear: turning Observe on requires a new binary; you cannot flip it from eas.json alone today.

Read the dashboard and CLI like a release engineer

After traffic hits a release build, open the project's Observe tab. Useful CLI companions (see get started):

eas observe:versions
eas observe:metrics-summary
eas observe:metrics
eas observe:routes
eas observe:session
eas observe:events

Practical loop:

  1. Compare median cold launch / TTI for the latest build or update vs the previous marker.
  2. Sort slow sessions (eas observe:metrics) and check device, thermal state, network, low-power mode — fields web APM tools often omit.
  3. Use eas observe:routes when one screen (not "the app") is the regression.
  4. Check the Update downloads view when TTI spikes after an OTA — a large asset on cellular can look like "slow React" when it is download time.

Expo also ships a Hand off to AI flow that copies dashboard context into a prompt for Cursor/Claude/Codex, plus Expo Skills (eas-observe / setup, metrics, queries). Useful when you already trust agents in the repo; still verify the release ID yourself before blaming a teammate's PR.

Dex and Luna compare Build vs Update release markers without fabricating scores

Errors preview: keep Sentry in the stack

On SDK 57+, Observe can record JavaScript errors (unhandled automatically; render errors via ObserveErrorBoundary / ObserveRoot's errorBoundaryFallback; handled via Observe.reportError). Native crashes are recorded with expo-observe 57.0.21+ on Android/iOS (not tvOS / iOS Simulator; ANR and OOM not recorded yet). This path is in preview.

For symbolicated JS stacks from EAS Build:

{
  "build": {
    "production": {
      "uploadSourceMaps": true
    }
  }
}

Requires EAS CLI 22.0.0+ and cloud EAS Build (not --local). Source maps for EAS Update errors and native symbol upload (ProGuard/dSYM) are still listed as coming later. Native stacks in the dashboard are not fully symbolicated the way Sentry users expect.

That is why this post links the Sentry checklist instead of replacing it: Sentry error tracking for React Native in production, crash triage that ships fixes, and error boundaries that hold. Observe answers "which build/update made startup or this route worse?"; Sentry still answers deep crash grouping, alerting maturity, and native symbolication workflows many teams already run.

Production checklist

  1. SDK 55+ and extra.eas.projectId present (eas init only if you intend a new project).
  2. npx expo install expo-observe; wrap root with ObserveRoot / AppMetricsRoot.
  3. markInteractive() on every entry screen after splash/auth/data ready.
  4. SDK 56+: one Observe.configure({ integrations: { 'expo-router': true } }) at module scope.
  5. Ship a new development or production build (not Expo Go).
  6. Confirm events in Observe tab; optionally eas observe:metrics-summary for the new version.
  7. Keep staged rollouts + rollback for OTAs; use Observe markers to see if an update stepped TTI.
  8. Keep Sentry (or equivalent) for crash workflows; enable Observe errors preview only if you accept preview limits and set uploadSourceMaps where appropriate.
  9. Leave dispatchInDebug off in production.
  10. If volume is high, set a stable sampleRate and document which fraction of installs you trust for percentiles (Expo does not reweight undersampled percentiles).

Decision rule

Add Observe when you ship Expo apps on EAS and need production startup/route metrics attributed to builds and OTAs. Skip it (for now) if you are still on SDK 54 or earlier, live only in Expo Go, or your only pain is crash volume with no release-performance questions.

Pair it with release hygiene you already own: health verdicts per build (release health), staged updates, and an explicit rollback plan. The metric without a rollback path is just a sad chart.

Sources

architectureagentsreact-native
OTF Fitness Kit

Stop wiring. Start shipping.

  • Login, database, and backend already connected — nothing to set up
  • iOS + Android + web from one codebase
  • AI configs pre-tuned + 40+ tested prompts included