A navigation breadcrumb answers one question: where was the user, and where did
they go? TraceItX keeps a single chronological chain per process and emits one
crumb per transition — Home → Checkout — into
payload.breadcrumbs, on the same clock as
the session replay.
What you get automatically
The rule is simple: TraceItX auto-captures navigation only where the platform has a screen primitive it can observe. UIKit and Android Activities have one. React Native, SwiftUI, Jetpack Compose and single-Activity Android do not — their navigation is ordinary application state, invisible from outside. For those, marking a screen is a one-liner.
| SDK | Automatic | Needs one line |
|---|---|---|
| React (web) | All History-API routing — React Router, Next.js, TanStack Router | Hash-only routing (see below) |
| React Native | Nothing — RN renders inside one host screen | Every navigator, including react-navigation and expo-router |
| iOS | UIKit pushes and modal presentations | SwiftUI navigation, tab switches |
| Android | Activity → Activity transitions | Compose, Fragments, Jetpack Navigation |
React (web)
Nothing to wire up. The SDK patches history.pushState /
history.replaceState and listens for popstate, so every
client-side route change — and every back/forward — becomes a crumb. The screen
name is pathname + search:
// nothing beyond the normal provider
<TraceItXProvider config={{ apiKey: 'txx_live_…' }}>
<App />
</TraceItXProvider>
// → breadcrumb: "/cart → /checkout?step=2" Same-URL transitions are dropped, and the patch installs once per page no matter how many times the provider remounts (React StrictMode and Fast Refresh are safe).
pathname and search only, so a router that navigates
purely in the fragment (/#/checkout, React Router’s
HashRouter) never changes the tracked URL and emits no crumbs. Switch
that route tree to BrowserRouter, or the equivalent History-API mode
in your router, to get the trail.
React Native
An RN app is a single native screen for its entire lifetime — one
UIViewController, one Activity — so the native
auto-capture has nothing to observe, and navigator state lives only in JavaScript.
TraceItX installs nothing into your JS runtime unless you ask it to, so navigation
capture is explicit.
react-navigation and expo-router
Import the integration from its own subpath and list it in
config.integrations. expo-router is react-navigation underneath, so
the same wiring covers it:
import { TraceItXProvider } from '@traceitx/react-native';
import { reactNavigationIntegration } from '@traceitx/react-native/integrations/react-navigation';
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
const navigationRef = createNavigationContainerRef();
const txNav = reactNavigationIntegration({ navigationRef });
export default function App() {
return (
<TraceItXProvider config={{ apiKey, integrations: [txNav] }}>
<NavigationContainer ref={navigationRef} onReady={txNav.onReady}>
{/* … your stack … */}
</NavigationContainer>
</TraceItXProvider>
);
}
Two details matter. The integration ships as a subpath export so it
never enters your bundle unused — importing it from the package root gives you
undefined. And onReady is what records the
initial route: the provider mounts before the navigation container
is ready, so without it the trail starts at the user’s second screen. Add
onStateChange={txNav.onStateChange} as well if your
react-navigation version’s ref listener proves unreliable — duplicate markers for
the same screen are suppressed, so double-wiring is harmless.
Any other navigator
Wix react-native-navigation, react-router-native,
hand-rolled tab state — all of them are one line per screen with
useTXScreen:
import { useTXScreen, recordScreen } from '@traceitx/react-native';
// Navigators that unmount hidden screens — mount is the appearance.
function CheckoutScreen() {
useTXScreen('Checkout');
…
}
// Stacks/tabs that keep screens mounted — pass focus instead.
useTXScreen(route.name, { focused: useIsFocused() });
// Wix react-native-navigation.
componentDidAppear() { recordScreen(this.props.screenName); } <TXScreen name="Checkout" /> is the declarative form for class
components. Both feed the same native chain as everything else.
iOS (Swift)
UIKit navigation is automatic. TraceItX observes
UIViewController.viewDidAppear and emits a crumb when the appearing
controller is either pushed onto a UINavigationController or presented
modally. The screen name is the view controller’s class name:
// Automatic — no code required.
navigationController?.pushViewController(CheckoutViewController(), animated: true)
// → breadcrumb: "CartViewController → CheckoutViewController" Container churn and tab re-selections are deliberately excluded, so the trail stays readable rather than logging every internal re-appearance. That also means tab switches emit nothing — mark them explicitly if they matter.
SwiftUI navigation is not automatic. A
NavigationStack / NavigationLink push is not backed by
discrete view controllers, so there is nothing to observe. Mark screens from
.onAppear:
struct CheckoutView: View {
var body: some View {
Form { … }
.onAppear { TraceItX.shared.recordScreen("Checkout") }
}
}
Markers and UIKit auto-capture share one chain, so a mixed app reads as a single
trail: RootViewController → Checkout → ConfirmationViewController.
Android (Kotlin)
Activity transitions are automatic once TraceItX.start() has run — the
SDK records each onResume, using the Activity’s simple class name.
Modern single-Activity apps therefore see one screen for the whole session unless
you mark the rest.
Compose — works with NavHost, state-based routing, Voyager, Decompose:
import com.traceitx.TXScreen
@Composable fun CheckoutScreen(…) {
TXScreen(name = "Checkout")
…
}
In pagers and other keep-alive containers, off-screen pages stay composed — pass
active = pagerState.currentPage == page so the marker fires on
selection rather than composition.
Jetpack Navigation — one listener covers the whole app:
navController.addOnDestinationChangedListener { _, dest, _ ->
TraceItX.recordScreen(dest.route ?: dest.displayName)
} Fragments and plain Views:
override fun onResume() {
super.onResume()
TraceItX.recordScreen("Checkout")
} How the chain behaves
The semantics are identical on every SDK, and manual markers feed the same chain as automatic capture — so a mixed app reads as one coherent trail rather than two interleaved ones.
- One global chain, in chronological order. Not per-stack: a tab switch correctly reads
TabA → TabB, which per-stack scoping would hide. - The first screen emits nothing on iOS, Android and React Native — there is no screen to come from yet. It still becomes the
fromof the next transition. On web the landing URL is known up front, so the first route change does produce a crumb. - A → A is suppressed. Re-appearances from backgrounding, refocus, recomposition or a double-wired integration never produce a crumb.
- Blank names are dropped, and markers are no-ops before the SDK starts.
- Failures stay silent. Marker calls run inside your navigator’s own dispatch, so they never throw into host navigation. A misconfigured integration logs a warning and captures nothing.
Naming screens
OrderDetail, not Order #4471 — jane@example.com. Names
travel in the report and are shown verbatim in triage, so a name built from user
data is a privacy leak that no redaction pass can undo. Ids belong in the optional
data map, if anywhere.
Keep names stable across releases so trails stay comparable, and keep them consistent with whatever your team already calls the screen in code.
What lands in the envelope
Navigation crumbs are entries in payload.breadcrumbs,
the unified action timeline that also carries taps, console, network, lifecycle and
error entries:
{
"t": 1755500000000,
"seq": 42,
"kind": "navigation",
"message": "Cart → Checkout",
"data": { "from": "Cart", "to": "Checkout" }
} t is the shared epoch-millisecond clock that also stamps the session
replay, so a receiver can align a crumb to a replay frame by direct comparison.
A report carries at most 128 crumbs; when older entries are trimmed, one synthetic
crumb per kind records how many were dropped
("+7 navigation hidden", with data.droppedCount).
Troubleshooting
| Symptom | Likely cause |
|---|---|
| No navigation crumbs at all in a React Native app | The integration isn’t listed in config.integrations, or it was imported from the package root instead of @traceitx/react-native/integrations/react-navigation. |
| Trail starts at the second screen (RN) | onReady isn’t passed to NavigationContainer. |
| Only one screen ever appears (Android) | Single-Activity app. Add TXScreen(), a destination listener, or recordScreen in onResume. |
| Only one screen ever appears (iOS) | SwiftUI-only navigation, or navigation that is neither a UINavigationController push nor a modal presentation. Mark screens from .onAppear. |
| Nothing on a web app that clearly changes routes | Hash-based routing, or a router that re-renders without touching the History API. |
| Tab switches missing | Expected on iOS — tab re-appearances are excluded from auto-capture. Mark them explicitly. |
| Crumbs stop the moment the reporter opens | Expected. The trail is frozen when the report is opened so the reporter’s own UI never pollutes the evidence. |