Expo Router integration for EAS Observe prerequisites
The Expo Router integration for EAS Observe requires Expo SDK 56 or later. On earlier SDKs, expo-observe tracks app-wide metrics but per-route navigation events are not emitted. The app must already have EAS Observe installed with expo-observe package. Expo Router must be installed in the app; if not installed, the integration becomes a silent no-op.
Enable Expo Router integration in EAS Observe
Call Observe.configure() with the expo-router integration flag at module scope, before any screen mounts. The integration must be enabled before mount and cannot be toggled at runtime. Calling configure() after the app has mounted, or toggling the flag mid-session, throws an error. Example: Observe.configure({ integrations: { 'expo-router': true } })
Filter sensitive URL parameters in EAS Observe Expo Router integration
Available in SDK 57 and later. Pass sensitive parameter keys to filteredParams in Observe.configure() to remove them from routeParams. When parameters are filtered, the event omits url and includes urlHidden: true instead. routeName is not affected because it is a pattern and never contains parameter values. Example: Observe.configure({ integrations: { 'expo-router': { filteredParams: ['userId', 'token'] } } })
warm_ttr metric: Per-route warm render timing
warm_ttr measures time from navigation dispatch to screen focus for screens that were already rendered before focus, typically because they were preloaded via Link prefetch or user navigated back. Event params: routeName (string, route pattern), url (string, resolved pathname), urlHidden (boolean, present as true when url is omitted due to filtered parameter), routeParams (object, resolved route params).
routeName is a pattern, not a resolved URL
routeName is a route pattern (for example /(tabs)/sessions/[sessionId]), not a resolved URL (for example /sessions/abc). This keeps metrics stable across distinct parameter values so the dashboard buckets them together. Resolved values are still available on the event via url and routeParams.
router.prefetch does not seed cold_ttr or warm_ttr
Calls to router.prefetch() do not count as user navigation and never seed a cold_ttr or warm_ttr measurement. The next user-driven navigation to that route emits warm_ttr because the screen has already rendered.
markInteractive outside screen component error
If markInteractive() logs 'Calling markInteractive on unmounted screen' or 'No metadata available for the current screen', the call ran outside a screen component or after unmount. Move the call into a useEffect inside the screen component.
EAS Insights dashboard tabs
EAS Insights is a dashboard that surfaces trends across your project, grouped into three tabs: App usage (usage of your app across platforms, app store versions, and time, aggregated from EAS Update requests and the expo-insights library), Workflows (run counts, success rates, and trends for EAS Workflows), and Maestro (pass, flake, and failure trends for Maestro end-to-end tests run in EAS Workflows).
EAS Observe performance monitoring metrics
EAS Observe is a performance monitoring service from Expo that tracks how your app performs in production, giving visibility in startup metrics (cold launch time, time to first render, time to interactive), rendering performance, and app user experience across different devices, networks, and conditions. It also records user-defined events that you log from your app to track custom signals alongside performance data.
React Navigation integration available in SDK 56+
The React Navigation integration for EAS Observe is available on SDK 56 and later. On earlier SDKs, expo-observe still tracks app-wide metrics, but per-screen navigation events are not emitted.
React Navigation integration requires @react-navigation/native v7+
The integration depends on @react-navigation/native (v7.0.0 or later) at runtime. If the package is not installed, the integration becomes a silent no-op.
Observe.configure() must be called before app mount
The integration must be enabled before mount and cannot be toggled at runtime. Calling configure() after the app has mounted, or toggling the flag mid-session, throws an error.
Enable React Navigation integration with Observe.configure()
Call Observe.configure() with the 'react-navigation' integration flag at module scope, before any screen mounts. Example: Observe.configure({ integrations: { 'react-navigation': true } });
Use ObserveNavigationContainer with dynamic React Navigation config
For dynamic configuration, replace your top-level NavigationContainer with ObserveNavigationContainer. It wraps the stock container, accepts the same props, and forwards the same ref. It also subscribes to navigation state changes so it can record per-screen render timings.
Use ObserveNavigationProvider with static React Navigation config
With static configuration, create the navigation ref yourself using useNavigationContainerRef(), pass it to the returned Navigation element, and wrap the element in ObserveNavigationProvider with the same ref. The provider listens to navigation events through the ref and records per-screen render timings. ObserveNavigationProvider does not render a container of its own and must be an ancestor of every screen so that useObserve() works inside them.
useObserve() hook provides markInteractive scoped to current screen
Use the useObserve() hook to get a markInteractive function that is automatically scoped to the current screen. The emitted event is tagged with the screen's path. If the integration is disabled or @react-navigation/native is not installed, useObserve() falls back to the global AppMetrics.markInteractive.
Filter sensitive route parameters in React Navigation integration (SDK 57+)
Available in SDK 57 and later. Pass sensitive parameter keys to filteredParams in Observe.configure() to remove them from routeParams. The integration removes filtered keys and includes urlHidden: true on the event. routeName is not affected because it is built from route names and never contains parameter values. Example: Observe.configure({ integrations: { 'react-navigation': { filteredParams: ['userId', 'token'] } } });
cold_ttr metric measures first render time per screen
cold_ttr measures time from when a navigation action is dispatched (for example, navigation.navigate()) to when the destination screen first becomes focused. For the very first focus after app launch, the measurement is taken from when the JS bundle is loaded, and the event includes isAppLaunch: true. Event params: routeName (string, route-name path like /Tabs/Sessions), urlHidden (boolean, present as true when a route parameter was filtered), routeParams (object, focused route params like { sessionId: 'abc' }), isAppLaunch (boolean, true when measured against process start, false for subsequent navigation).
warm_ttr metric measures re-render time for previously rendered screens
warm_ttr measures time from when a navigation action is dispatched to when a screen first becomes focused, but for screens that were already rendered before focus, typically because they were preloaded or the user navigated back to them. Tab-navigator siblings count as warm only once they have been mounted. With React Navigation v7's default lazy: true, unfocused tabs stay unmounted and their first focus is recorded as cold_ttr. Event params: routeName (string, route-name path like /Tabs/Sessions), urlHidden (boolean, present as true when a route parameter was filtered), routeParams (object, focused route params like { sessionId: 'abc' }).
tti metric measures time to interactive per screen
tti (time to interactive) measures time from when a navigation action is dispatched to when markInteractive() is called on the destination screen. Only the first call per navigation is recorded, so it is safe to call markInteractive() multiple times. Event params: routeName (string, route-name path like /Tabs/Sessions), urlHidden (boolean, present as true when a route parameter was filtered), routeParams (object, focused route params), and any custom params passed via markInteractive({ ... }).
routeName built from route names not params for stable metrics
routeName is built from route names (for example /Tabs/Sessions), so route params never appear in the path. This keeps metrics stable across distinct param values so the dashboard buckets them together. Param values are still available on the event via routeParams.
markInteractive() only records when screen is focused
markInteractive() only records once the screen is focused. Calls made on an unfocused screen update internal state but do not emit a tti event until the screen is focused.
useObserve() must be called inside screen component
Call useObserve() inside the screen component, not in a higher-level wrapper. If the screen's identity changes between renders, the hook logs a warning. If markInteractive() logs 'Calling markInteractive on unmounted screen' or 'No metadata available for the current screen', the call ran outside a screen component or after unmount. Move the call into a useEffect inside the screen component.
Static config navigation ref must be same for both provider and Navigation
With static configuration, pass the same ref to both ObserveNavigationProvider and the Navigation element. If the provider receives a ref that is not connected to a navigation container, no per-screen metrics are emitted.
Use Expo Router integration instead for apps using Expo Router
If your app uses Expo Router, use the Expo Router integration instead of the React Navigation integration. The React Navigation integration is for apps that use React Navigation directly.