# How Web-Tracing Handles Routing Changes and Tracking in Single-Page Applications

> Discover how web-tracing tracks routing changes and page views in SPAs. It monitors SPA navigation via the History API and browser events for accurate dwell-time calculations.

- Repository: [m-cheng-web/web-tracing](https://github.com/m-cheng-web/web-tracing)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/m-cheng-web/web-tracing/blob/main/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`](https://github.com/m-cheng-web/web-tracing/blob/main/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.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/common.ts)** include `HISTORYPUSHSTATE`, `HISTORYREPLACESTATE`, `HASHCHANGE`, `POPSTATE`, and `BEFOREUNLOAD`.

## The Page-View Lifecycle in [`pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/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`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/index.ts)**, executes once when the SDK starts. It performs three critical tasks:

1. **Initial Page View**: Immediately emits a PV event for the landing page using `sendPageView({ referer: document.referrer })`.
2. **Listener Registration**: Attaches handlers for all five routing event types through `eventBus.addEvent()`.
3. **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 in [`common.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/common.ts)).
- **`triggerPageUrl`**: Current location via `getLocationHref()`.
- **`referer`**: Previous URL stored in `oldURL`.
- **`title`**: `document.title` (with a 17ms delayed fallback for dynamic titles).
- **`action`**: Categorization as *navigation*, *reload*, or *page load* derived from `performance.navigation.type`.

The payload is queued through `sendData.emit(sendObj)` in **[`packages/core/src/lib/sendData.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/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:

1. The SDK computes `durationTime = now - durationStartTime`.
2. If a previous page exists, it emits a `SEDNEVENTTYPES.PVDURATION` event containing the milliseconds elapsed.
3. 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`](https://github.com/m-cheng-web/web-tracing/blob/main/examples/vue3/src/main.ts)), initialization occurs after mounting the router:

```typescript
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:

```typescript
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 native `hashchange`/`popstate` events to detect all routing changes uniformly.
- **Event Bus**: A pub/sub system in [`eventBus.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/eventBus.ts) decouples navigation detection from analytics logic, using constants from [`common.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/common.ts).
- **Payload Construction**: The [`pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/pv.ts) module generates structured PV events containing URL, referrer, title, and navigation action via `handleSendPageView()`.
- **Duration Metrics**: The SDK calculates dwell time between routes and emits `PVDURATION` events before each new page view and on `beforeunload`.
- **Framework Agnostic**: Works with Vue 2/3, React, Nuxt, and plain HTML without router-specific code, as demonstrated in [`examples/vue3/src/main.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/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.