Skip to main content

Navigation Instrumentation

Generates: Spans

Tracks three metrics per screen using React Navigation.

MetricWhat it measuresDefault
Screen LoadNavigation start → screen displayedOn
Screen SessionScreen in focus → user navigates awayOn
Screen InteractiveScreen displayed → your content is readyOff (opt-in)
Using Expo Router?

The setup is slightly different — see Expo Router Navigation for the useNavigationContainerRef + registerWhenContainerReady pattern.

Setup

Connect the hook to your NavigationContainer:

import { NavigationContainer, type NavigationContainerRef } from '@react-navigation/native';
import { Pulse } from '@dreamhorizonorg/pulse-react-native';

function App() {
const navigationRef = React.useRef<NavigationContainerRef>(null);
const onReady = Pulse.useNavigationTracking(navigationRef);

return (
<NavigationContainer ref={navigationRef} onReady={onReady}>
{/* your screens */}
</NavigationContainer>
);
}

Screen Load and Screen Session start tracking automatically once connected.

Greenfield React Native apps

In a pure React Native app, all navigation goes through React Navigation — native screen lifecycle events (Fragment transitions on Android, UIViewController transitions on iOS) map to the same screens and produce redundant data. Consider disabling them to keep your telemetry clean:

Android (MainApplication.kt) — start from Android APIs (links to the screen-lifecycle / fragment guide):

Pulse.initialize(...) {
fragment { enabled(false) }
// keep activity {} enabled — it powers the AppStart span
}

iOS — same idea; see iOS APIs and README-OBJC for PulseObjcInstrumentations in Objective-C:

PulseSDK.initialize(
apiKey: "...",
dataCollectionState: .allowed,
instrumentations: { config in
config.screenLifecycle { $0.enabled(false) }
}
)

Disable All Navigation Tracking

Pulse.start({ autoDetectNavigation: false });

Configure Individual Metrics

const onReady = Pulse.useNavigationTracking(navigationRef, {
screenSessionTracking: true, // default: on
screenNavigationTracking: true, // default: on
screenInteractiveTracking: false, // default: off
});

Screen Interactive Tracking

Opt-in. Measures time from screen display to when your content is actually usable — the screen_interactive span. This is the closest equivalent to Time To Interactive (TTI) for a screen.

Enable it, then call markContentReady() at the point where your meaningful content is visible to the user:

// Enable in hook options
const onReady = Pulse.useNavigationTracking(navigationRef, {
screenInteractiveTracking: true,
});

// In your screen — call after data is fetched and the UI has rendered
function HomeScreen() {
useEffect(() => {
fetchData().then(() => {
Pulse.markContentReady();
});
}, []);
}

If the user navigates away before markContentReady() is called, the span is discarded.

When to call markContentReady()

The right place depends on your screen:

ScenarioWhen to call
Screen loads data from an APIAfter the data fetch resolves and the list/content renders
Screen shows cached data immediatelyOn mount, after the first render completes
Screen has a loading skeletonWhen the skeleton is replaced with real content
Screen streams dataWhen enough content is visible to be useful

The goal is to mark the moment the screen is useful to the user, not just when it mounts.

Capturing actual render time

markContentReady() is called from JS, so it marks when your logic says content is ready — not when the native layer has finished drawing. For precise frame-accurate measurement of when components actually appear on screen, consider integrating Marco — it captures the actual onDraw of native views and can signal markContentReady() for you based on real render completion.

Attributes

All navigation spans include:

AttributeDescription
pulse.type"screen_load", "screen_session", or "screen_interactive"
screen.nameCurrent screen name
last.screen.namePrevious screen name (Screen Load only)
routeKeyUnique route identifier

Native Side

useNavigationTracking tracks React Navigation screen transitions. Native screen transitions (UIViewControllers on iOS, Activities/Fragments on Android) are tracked separately by the native SDKs. In brownfield apps or for native screens that don't go through React Navigation:

  • Android APIs — Then open Screen lifecycle in the Android doc map to tune Activity/Fragment (often disable Fragment overlap with JS navigation)
  • iOS APIs — Then Screen lifecycle in the iOS docs for native UIViewController tuning