Reference version

This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Expo Observe

A library that collects app performance metrics and dispatches them to EAS Observe.

Android
iOS
tvOS
Recommended version:
~57.0.12

expo-observe is a library that collects performance metrics and user-defined events from your app and dispatches them to EAS Observe, a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as Time to First Render (TTR) and Time to Interactive (TTI), from apps running in production.

Installation

Terminal
npx expo install expo-observe

If you are installing this in an existing React Native app, make sure to install expo in your project.

Configuration

In release builds, installing the expo-observe library is all that is needed to configure the library to start sending startup metrics (though you will need to follow the usage instructions below to call markInteractive to track TTI). The library will not dispatch events in debug builds, but you can change this and other config options such as sample rate, environment name and enabling integrations with the Observe.configure({}) call. For all available options, see the EAS Observe configuration guide.

Usage

Wrap your root layout with ObserveRoot to measure Time to First Render automatically. Then, call markInteractive when your app is ready for user interaction to record Time to Interactive:

src/app/_layout.tsx
import { ObserveRoot, useObserve } from 'expo-observe'; import { Stack } from 'expo-router'; import { useEffect } from 'react'; function RootLayout() { const { markInteractive } = useObserve(); useEffect(() => { // Call markInteractive() after any initialization work behind the // splash screen completes. markInteractive(); }, [markInteractive]); return <Stack />; } export default ObserveRoot.wrap(RootLayout);

For step-by-step instructions, including how to handle apps with multiple entry screens, see the EAS Observe Get started guide. To send custom events from your app, see User-defined events.

API

import { Observe, ObserveRoot, useObserve } from 'expo-observe';

Components

ObserveErrorBoundary

Type: React.Component<AppMetricsErrorBoundaryProps, State>

A React error boundary that records render-phase errors as non-fatal exception log events (with the React component stack) and renders a fallback in place of the subtree that threw.

Render-phase errors don't reach global.ErrorUtils, so a boundary is the only way to capture them with the component stack. Place one around any subtree, or let AppMetricsRoot mount one via its errorBoundaryFallback prop.

ObserveInteractiveMarker

Type: React.Element<ObserveInteractiveMarkerProps>

Declarative wrapper around useObserve().markInteractive(...). Renders nothing and calls markInteractive once when it first mounts, marking the moment the screen becomes interactive (used to compute the tti metric). Render it once the screen is ready for user interaction — for example, after its initial data has loaded.

Because markInteractive is only sent on mount, the marker is fire-once: changing params after the first render has no effect and warns in development. If you need to attach attributes that are only known later, call useObserve().markInteractive(...) imperatively instead.

Example

import { ObserveInteractiveMarker } from 'expo-observe'; function Feed({ items }) { if (!items) return <Spinner />; return ( <> <FeedList items={items} /> <ObserveInteractiveMarker params={{ cacheHit: true }} /> </> ); }

ObserveInteractiveMarkerProps

params

Optional • Type: MetricAttributes[params]

Custom parameters attached to the TTI metric, forwarded to markInteractive. Values can be strings, numbers, booleans, or other JSON-serializable values.

ObserveRoot

Type: React.Element<AppMetricsRootProps & { children: ReactNode }>

Static methods

getDerivedStateFromError(error)

ParameterType
errorunknown

Returns:
State

Component methods

wrap(Component)

ParameterType
ComponentComponentType<P>

Returns:
ComponentType<P>

Hooks

useObserve()

Returns:
{ markInteractive: (attributes: MetricAttributes) => void }

Interfaces

ExpoAppMetricsModuleType

PropertyTypeDescription
NetworkRequestObserverNetworkRequestObserver

Class for subscribing to HTTP requests observed by the native networking interceptor. Construct an instance to begin receiving requestStarted/requestCompleted events; release the instance (drop all references) to stop.

ExpoAppMetricsModuleType Methods

clearStoredEntries()

Returns:
Promise<void>

logEvent(name, options)

ParameterTypeDescription
namestring

Event name. Maps to the OpenTelemetry event.name attribute.

options(optional)LogEventOptions

Optional body, attributes, and severity overrides.


Records a log event against the current main session. The event is persisted locally and dispatched on the next dispatchEvents() flush as an OpenTelemetry log record sent to the /v1/logs endpoint.

Severity defaults to "info" when not provided.

Returns:
void

markFirstRender()

Returns:
void

markInteractive(attributes)

ParameterType
attributes(optional)MetricAttributes

Returns:
void

setGlobalAttributes(attributes)

ParameterType
attributes(optional)Record<string, LogAttributeValue> | null

Sets attributes merged into every subsequent metric and log event. Per-record keys win on collision. Pass null, undefined, or an empty object to clear.

Returns:
void

Example

AppMetrics.setGlobalAttributes({ subscription_tier: 'pro', experiment_variant: 'B', });

ObserveIntegrationsConfig

PropertyTypeDescription
expo-router(optional)boolean | ObserveNavigationIntegrationConfig

Enables the expo-router integration, which records navigation metrics (cold_ttr, warm_ttr, tti) from router state changes.

Requires expo-router to be installed.

Pass an object to filter exported route/query params.

Default:false
react-navigation(optional)boolean | ObserveNavigationIntegrationConfig

Enables the @react-navigation/native integration, which records navigation metrics (cold_ttr, warm_ttr, tti).

Requires @react-navigation/native to be installed and the app tree to be wrapped in <ObserveNavigationContainer> instead of the stock <NavigationContainer>.

Pass an object to filter exported route/query params.

Default:false

ObserveModule

Extends: NativeModule<ObserveModuleEvents>

ObserveModule Methods

configure(config)

ParameterTypeDescription
configObserveConfig

Observability settings to apply.


Configures how observability events are collected and dispatched at runtime, such as the environment label, dispatching behavior, sampling, and integrations.

Returns:
void

Example

import { Observe } from 'expo-observe'; Observe.configure({ environment: 'production', dispatchingEnabled: true, });

dispatchEvents()

Dispatches pending events to the server immediately.

Events are dispatched automatically when the app moves to the background. On Android, a background worker dispatches events once network connectivity is available. On iOS, dispatching happens when the app resigns active state or is about to terminate. Call this method to flush events manually, for example, during testing or to ensure events are sent before a specific point.

Returns:
Promise<void>

A promise that resolves when the pending events have been dispatched.

Example

import { Observe } from 'expo-observe'; await Observe.dispatchEvents();

getIntegrations()

Returns the integrations config from the most recent configure(...) call, or an empty object if configure has not run yet.

logEvent(name, options)

ParameterTypeDescription
namestring

Event name.

options(optional)LogEventOptions

Optional body, attributes, and severity overrides.


Records a log event against the current main session. The event is persisted locally and dispatched on the next dispatchEvents() flush.

Severity defaults to "info" when not provided.

Returns:
void

markFirstRender()

Marks the first render of the app. Used to compute the cold_ttr and warm_ttr metrics.

Returns:
void

markInteractive(attributes)

ParameterType
attributes(optional)MetricAttributes

Marks the moment the app becomes interactive. Used to compute the tti metric. Custom routeName and params can be attached via attributes.

Returns:
void

registerIntegration(name, callback)

ParameterTypeDescription
nameK

Integration name.

callback(config: ObserveIntegrationsConfig[K]) => void

Function called with the integration configuration.


Invokes a callback once when the named integration configuration becomes available.

Returns:
void

Example

Observe.registerIntegration('expo-router', config => { console.log(config); });

reportError(error)

ParameterTypeDescription
errorunknown

The caught value. An Error is preferred, but any thrown value is accepted.


Reports an error your code caught and handled, recorded as a non-fatal exception event. Use it to keep visibility into failures you recover from, which never reach the automatic global handler or an error boundary.

The thrown value is normalized: an Error's name, message, and stack are captured; any other value (a string, a plain object) is stringified as the message.

Returns:
void

Example

try { await syncCart(); } catch (error) { Observe.reportError(error); }

setBundleDefaults(defaults)

ParameterType
defaults{ environment: string, isJsDev: boolean }

Pushes JS-bundle-derived facts (process.env.NODE_ENV, __DEV__) into native storage. Called automatically once when the package is first imported; should not be called by host apps directly.

Returns:
void

setGlobalAttributes(attributes)

ParameterType
attributes(optional)ObserveAttributes | null

Sets attributes merged into every subsequent metric and log event. Per-record keys win on collision. Pass null, undefined, or an empty object to clear.

Returns:
void

Example

Observe.setGlobalAttributes({ subscription_tier: 'pro', experiment_variant: 'B', });

Types

AppMetricsErrorBoundaryFallbackProps

Arguments passed to a fallback render function.

PropertyTypeDescription
errorunknown

The value the subtree threw. Usually an Error, but any value can be thrown.

resetError() => void

Clears the caught error and re-renders the children. Use it to offer a "try again" action; the children re-mount, so they run from a clean state.

AppMetricsErrorBoundaryProps

PropertyTypeDescription
childrenReact.ReactNode
-
fallbackReact.ReactElement | null | (props: AppMetricsErrorBoundaryFallbackProps) => React.ReactNode

Rendered in place of the subtree after an error is caught. Provide one of:

  • a React element to render as-is,
  • a function receiving the error and a resetError callback (to show details and offer retry),
  • null to render nothing.

A boundary can't re-throw to reproduce React Native's default crash, so it always renders one of the above; there's no capture-only mode. Errors no boundary catches are still recorded by the global ErrorUtils handler.

AppMetricsRootProps

PropertyTypeDescription
childrenReact.ReactNode
-
errorBoundaryFallback(optional)AppMetricsErrorBoundaryProps[fallback]

When set, the app is wrapped in an AppMetricsErrorBoundary with this fallback, capturing React render-phase errors at the root. Omit it and no boundary is mounted, so render errors keep React Native's default behavior (they're still recorded by the global ErrorUtils handler, just without the component stack). Pass null to capture but render nothing.

To place a boundary deeper in the tree, use AppMetricsErrorBoundary directly.

LogAttributeValue

Value types accepted in a log event's attributes map. Strings, numbers, and booleans are stored as typed primitives; arrays and nested maps preserve their structure. Other JS values (functions, Date, undefined, etc.) are not supported and may be dropped by downstream consumers.

Type: string or number or boolean or object shaped as below:

PropertyTypeDescription
key(index signature)LogAttributeValue
-

LogEventOptions

Optional configuration accepted by logEvent. The event name is passed as the first positional argument since it's required and the only field most callers set.

PropertyTypeDescription
attributes(optional)Record<string, LogAttributeValue> | null

Custom attributes attached to the event. Each entry is preserved with its original value type — see LogAttributeValue for the supported shapes.

body(optional)string | null

Optional free-form message describing the event.

displayName(optional)string | null

Optional human-friendly label for the event. Unlike name (a stable machine identifier), this is meant for display in dashboards and is not constrained to a naming scheme.

severity(optional)LogSeverity | null

Severity of the event.

Default:"info"

LogSeverity

Literal type: string

Severity of a log event, ordered from least to most severe:

  • "trace" — Fine-grained tracing, typically only useful while reproducing a specific issue.
  • "debug" — Diagnostic detail useful during development; usually filtered out in production.
  • "info" — Routine, expected events that record normal app behavior.
  • "warn" — Unexpected but recoverable conditions worth investigating.
  • "error" — An operation failed; the app continues running but is in a degraded state.
  • "fatal" — A severe failure, often immediately followed by app termination.

Acceptable values are: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'

MetricAttributes

PropertyTypeDescription
params(optional)Record<string, unknown>

Custom parameters to attach to the metric.

routeName(optional)string | null

Name of the route associated with the metric. Some metrics populate this with a sensible default when omitted — for example, the TTI metric falls back to the initial route name detected from the router.

ObserveAttribute

Type: LogAttributeValue

Value types accepted as attribute values in setGlobalAttributes and the other Observe APIs. Strings, numbers, and booleans are stored as typed primitives; arrays and nested maps preserve their structure.

ObserveAttributes

Type: Record<string, ObserveAttribute>

A map of attribute key to value, as accepted by setGlobalAttributes and other Observe APIs that take a free-form attributes payload.

ObserveConfig

PropertyTypeDescription
dispatchInDebug(optional)boolean

Whether to dispatch metrics that were collected in a debug build of the host app.

When false, metrics produced by debug builds are marked as sent without being dispatched. When true, debug-build metrics are dispatched alongside release-build metrics.

Has no effect on release builds.

If dispatchingEnabled is false or this device is out-of-sample for sampleRate, nothing is dispatched regardless of dispatchInDebug.

Default:false
dispatchingEnabled(optional)boolean

Whether to dispatch observability events to the server.

When false, any pending metrics are marked as sent without being dispatched and no further metrics are dispatched until this is set back to true.

Default:true
environment(optional)string

The environment for observability events

Default:process.env.NODE_ENV
integrations(optional)ObserveIntegrationsConfig

Opt in to per-integration behavior. See the Expo Router and React Navigation integrations, or integrate your own package.

sampleRate(optional)number

Fraction of installations that should dispatch metrics, in [0, 1]. Values outside that range are clamped.

The decision is deterministic per installation — a device is either permanently in-sample or out-of-sample for a given rate, so the choice is stable across app launches.

Interaction with dispatchingEnabled:

  • If dispatchingEnabled is false, metrics are never dispatched
  • If dispatchingEnabled is true (or unset), metrics are dispatched only when this device is in-sample.
Default:undefined - metrics from all devices are sent

ObserveModuleEvents

Events emitted by the native ExpoObserve module.

PropertyTypeDescription
configure(payload: { integrations: ObserveIntegrationsConfig }) => void

Fired on every configure(...) call, carrying the resolved integrations config

ObserveNavigationIntegrationConfig

PropertyTypeDescription
filteredParams(optional)string[]

Route or query parameter keys to remove from exported navigation metric routeParams. When any configured parameter is removed from a metric, the exported resolved URL/path is replaced with urlHidden: true. Does not affect routeName.