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.
EAS Observe is in Open Beta. The first 10,000 monthly active users are free. For higher usage, contact sales@expo.dev.
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.
expo-observeis not available in Expo Go. To use it, create a development build.
Installation
- npx expo install expo-observeIf 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:
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
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.
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 }} /> </> ); }
Type: React.Element<AppMetricsRootProps & {
children: ReactNode
}>
Static methods
Component methods
Hooks
{
markInteractive: (attributes: MetricAttributes) => void
}Interfaces
| Property | Type | Description |
|---|---|---|
| NetworkRequestObserver | NetworkRequestObserver | Class for subscribing to HTTP requests observed by the native networking interceptor.
Construct an instance to begin receiving |
ExpoAppMetricsModuleType Methods
Promise<void>| Parameter | Type | Description |
|---|---|---|
| name | string | Event name. Maps to the OpenTelemetry |
| 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.
void| Parameter | Type |
|---|---|
| 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.
voidExample
AppMetrics.setGlobalAttributes({ subscription_tier: 'pro', experiment_variant: 'B', });
| Property | Type | Description |
|---|---|---|
| expo-router(optional) | boolean | ObserveNavigationIntegrationConfig | Enables the Requires Pass an object to filter exported route/query params. Default: false |
| react-navigation(optional) | boolean | ObserveNavigationIntegrationConfig | Enables the Requires Pass an object to filter exported route/query params. Default: false |
Extends: NativeModule<ObserveModuleEvents>
| Parameter | Type | Description |
|---|---|---|
| config | ObserveConfig | Observability settings to apply. |
Configures how observability events are collected and dispatched at runtime, such as the environment label, dispatching behavior, sampling, and integrations.
voidExample
import { Observe } from 'expo-observe'; Observe.configure({ environment: 'production', dispatchingEnabled: true, });
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.
Promise<void>A promise that resolves when the pending events have been dispatched.
Example
import { Observe } from 'expo-observe'; await Observe.dispatchEvents();
Returns the integrations config from the most recent configure(...)
call, or an empty object if configure has not run yet.
ObserveIntegrationsConfig| Parameter | Type | Description |
|---|---|---|
| name | string | 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.
voidMarks the first render of the app. Used to compute the cold_ttr and
warm_ttr metrics.
void| Parameter | Type |
|---|---|
| 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.
Note: When the
expo-routeror@react-navigation/nativeintegration is active, preferuseObserve().markInteractive(...)— the hook fills inrouteNamefrom the current route, while this raw call does not.
void| Parameter | Type | Description |
|---|---|---|
| name | K | Integration name. |
| callback | (config: ObserveIntegrationsConfig[K]) => void | Function called with the integration configuration. |
Invokes a callback once when the named integration configuration becomes available.
voidExample
Observe.registerIntegration('expo-router', config => { console.log(config); });
| Parameter | Type | Description |
|---|---|---|
| error | unknown | The caught value. An |
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.
voidExample
try { await syncCart(); } catch (error) { Observe.reportError(error); }
| Parameter | Type |
|---|---|
| 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.
void| Parameter | Type |
|---|---|
| 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.
voidExample
Observe.setGlobalAttributes({ subscription_tier: 'pro', experiment_variant: 'B', });
Types
Arguments passed to a fallback render function.
| Property | Type | Description |
|---|---|---|
| error | unknown | The value the subtree threw. Usually an |
| 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. |
| Property | Type | Description |
|---|---|---|
| children | React.ReactNode | - |
| fallback | React.ReactElement | null | (props: AppMetricsErrorBoundaryFallbackProps) => React.ReactNode | Rendered in place of the subtree after an error is caught. Provide one of:
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 |
| Property | Type | Description |
|---|---|---|
| children | React.ReactNode | - |
| errorBoundaryFallback(optional) | AppMetricsErrorBoundaryProps[fallback] | When set, the app is wrapped in an To place a boundary deeper in the tree, use |
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:
| Property | Type | Description |
|---|---|---|
| key(index signature) | LogAttributeValue | - |
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.
| Property | Type | Description |
|---|---|---|
| attributes(optional) | Record<string, LogAttributeValue> | null | Custom attributes attached to the event. Each entry is preserved with its
original value type — see |
| body(optional) | string | null | Optional free-form message describing the event. |
| displayName(optional) | string | null | Optional human-friendly label for the event. Unlike |
| severity(optional) | LogSeverity | null | Severity of the event. Default: "info" |
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'
| Property | Type | Description |
|---|---|---|
| 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. |
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.
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.
| Property | Type | Description |
|---|---|---|
| dispatchInDebug(optional) | boolean | Whether to dispatch metrics that were collected in a debug build of the host app. When Has no effect on release builds. If Default: false |
| dispatchingEnabled(optional) | boolean | Whether to dispatch observability events to the server. When 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 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
Default: undefined - metrics from all devices are sent |
Events emitted by the native ExpoObserve module.
| Property | Type | Description |
|---|---|---|
| configure | (payload: {
integrations: ObserveIntegrationsConfig
}) => void | Fired on every |
| Property | Type | Description |
|---|---|---|
| filteredParams(optional) | string[] | Route or query parameter keys to remove from exported navigation metric
|