Navigation Instrumentation
Generates: Spans
Tracks three metrics per screen using React Navigation.
| Metric | What it measures | Default |
|---|---|---|
| Screen Load | Navigation start → screen displayed | On |
| Screen Session | Screen in focus → user navigates away | On |
| Screen Interactive | Screen displayed → your content is ready | Off (opt-in) |
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.
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:
- Swift
- Objective-C
PulseSDK.initialize(
apiKey: "...",
dataCollectionState: .allowed,
instrumentations: { config in
config.screenLifecycle { $0.enabled(false) }
}
)
PulseObjcInstrumentations *inst = [PulseObjcInstrumentations new];
inst.screenLifecycle = [PulseObjcEnabledConfig disabled];
[PulseSDK pulseInitialize:@"your-api-key"
dataCollectionState:@"ALLOWED"
globalAttributes:nil
configuration:nil
instrumentations:inst];
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:
| Scenario | When to call |
|---|---|
| Screen loads data from an API | After the data fetch resolves and the list/content renders |
| Screen shows cached data immediately | On mount, after the first render completes |
| Screen has a loading skeleton | When the skeleton is replaced with real content |
| Screen streams data | When 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.
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:
| Attribute | Description |
|---|---|
pulse.type | "screen_load", "screen_session", or "screen_interactive" |
screen.name | Current screen name |
last.screen.name | Previous screen name (Screen Load only) |
routeKey | Unique 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