How Web-Tracing Handles Routing Changes and Tracking in Single-Page Applications
Web-tracing monitors SPA navigation by monkey-patching the History API and routing native browser events through a centralized event bus to generate structured page-view (PV) payloads with accurate dwell-time calculations.
The m-cheng-web/web-tracing SDK provides framework-agnostic routing analytics for Vue, React, and vanilla JavaScript applications. By intercepting pushState, replaceState, and other navigation events at the browser level, the library records precise page transitions without requiring framework-specific router hooks.
Architecture: Intercepting Browser Navigation
The routing tracking system is implemented in packages/core/src/lib/pv.ts, which orchestrates event listeners and payload generation. Rather than relying on framework routers, the SDK patches native browser APIs to guarantee consistent behavior across all single-page applications.
History API Monkey-Patching
Web-tracing overrides the native History.prototype.pushState and History.prototype.replaceState methods to detect programmatic navigation. When these patched methods execute, they trigger internal events that feed into the event bus. The system also attaches native listeners for hashchange, popstate, and beforeunload to capture hash-based routing, back-button usage, and page exits.
This low-level interception ensures that route changes from Vue Router, React Router, or manual history manipulation are all captured uniformly.
The Event Bus Pattern
The SDK decouples event detection from data processing using packages/core/src/lib/eventBus.ts. This lightweight publish/subscribe system allows the routing layer to broadcast events without direct dependencies on the analytics logic.
- Registration:
eventBus.addEvent({ type: EVENTTYPES.HISTORYPUSHSTATE, callback: handler })registers a listener. - Emission: When a native navigation event fires, the patched wrapper calls
eventBus.runEvent(type)to notify all subscribers. - Event Types: Constants defined in
packages/core/src/common.tsincludeHISTORYPUSHSTATE,HISTORYREPLACESTATE,HASHCHANGE,POPSTATE, andBEFOREUNLOAD.
The Page-View Lifecycle in pv.ts
The core routing logic resides in the page-view module, which initializes listeners, constructs telemetry payloads, and manages session duration tracking.
Initialization via initPv()
The initPv() function, exported from packages/core/index.ts, executes once when the SDK starts. It performs three critical tasks:
- Initial Page View: Immediately emits a PV event for the landing page using
sendPageView({ referer: document.referrer }). - Listener Registration: Attaches handlers for all five routing event types through
eventBus.addEvent(). - State Initialization: Records the start time for duration calculations and stores the initial URL as
oldURL.
Generating Page-View Data
Each routing event triggers handleSendPageView(), which constructs a payload containing:
eventType:SEDNEVENTTYPES.PV(defined incommon.ts).triggerPageUrl: Current location viagetLocationHref().referer: Previous URL stored inoldURL.title:document.title(with a 17ms delayed fallback for dynamic titles).action: Categorization as navigation, reload, or page load derived fromperformance.navigation.type.
The payload is queued through sendData.emit(sendObj) in packages/core/src/lib/sendData.ts, which handles batching and network transmission.
Duration Tracking Between Routes
Web-tracing calculates the time a user spends on each page before navigating away. When a new route is detected:
- The SDK computes
durationTime = now - durationStartTime. - If a previous page exists, it emits a
SEDNEVENTTYPES.PVDURATIONevent containing the milliseconds elapsed. - The timer resets for the new page.
The BEFOREUNLOAD listener ensures the final dwell time is captured when the user closes the tab or navigates to an external domain.
Framework Integration Examples
Because the SDK listens to native History events, it requires no special configuration for framework routers.
Vue 3 Implementation
In the Vue 3 example application (examples/vue3/src/main.ts), initialization occurs after mounting the router:
import { init } from 'web-tracing'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
// Start routing and page-view tracking
init() // Internally calls initPv()
The standard app.use(router) enables Vue Router, and init() automatically begins intercepting pushState calls from router.push() and router.replace().
Manual Control and Cleanup
For custom tracking or SPA teardown, the SDK exposes direct control methods:
import { handleSendPageView, destroyPv } from 'web-tracing'
// Manually emit a page view with custom parameters
handleSendPageView({
action: 'navigation',
params: { section: 'dashboard' },
title: 'User Dashboard'
})
// Remove all routing listeners (e.g., during logout)
destroyPv()
The destroyPv() function clears all event-bus registrations for routing events, preventing memory leaks in long-lived applications or micro-frontends.
Summary
- Monkey-patching: Web-tracing intercepts
history.pushState,replaceState, and nativehashchange/popstateevents to detect all routing changes uniformly. - Event Bus: A pub/sub system in
eventBus.tsdecouples navigation detection from analytics logic, using constants fromcommon.ts. - Payload Construction: The
pv.tsmodule generates structured PV events containing URL, referrer, title, and navigation action viahandleSendPageView(). - Duration Metrics: The SDK calculates dwell time between routes and emits
PVDURATIONevents before each new page view and onbeforeunload. - Framework Agnostic: Works with Vue 2/3, React, Nuxt, and plain HTML without router-specific code, as demonstrated in
examples/vue3/src/main.ts.
Frequently Asked Questions
How does web-tracing detect route changes in React Router?
Web-tracing detects React Router transitions by monkey-patching the browser's history.pushState and history.replaceState methods. When React Router calls these APIs during navigation, the SDK's patched versions trigger internal events that generate page-view telemetry. This approach requires no React-specific code and works identically for Vue Router or manual history manipulation.
What data is included in a page-view event?
Each page-view event includes the current URL (triggerPageUrl), the previous page URL (referer), document.title, a navigation action type (navigation, reload, or page load), and a unique event identifier. The payload is structured as SEDNEVENTTYPES.PV and emitted through the sendData module for transmission to your analytics endpoint.
How is dwell time calculated between pages?
The SDK records a durationStartTime timestamp when a page becomes active. Upon the next route change, it calculates durationTime = Date.now() - durationStartTime and emits a SEDNEVENTTYPES.PVDURATION event containing this value. This process repeats for every navigation and finalizes when the beforeunload event fires, capturing the last page's session length.
Can I manually trigger a page view outside of route changes?
Yes. Import handleSendPageView from the SDK to programmatically emit a page view. This is useful for tracking modal dialogs, tab switches, or other virtual "page" states that don't trigger History API changes. You can pass custom parameters, titles, and actions to the function to enrich the telemetry data.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →