# How Web-Tracing Handles Page View (PV) Tracking: A Deep Dive into the Navigation Monitor

> Explore how web-tracing tracks page views and dwell time using navigation APIs for accurate PV tracking and route change event emission.

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

---

**Web-tracing captures page views by hooking into browser navigation APIs and emitting `pv` events on every route change and `pv-duration` events on page unload to measure dwell time.**

The **web-tracing** open-source SDK provides a framework-agnostic solution for monitoring single-page application (SPA) navigation and traditional page loads. By intercepting History API calls and lifecycle events, the library automatically records user journeys without requiring manual instrumentation in your routing logic.

## Core Event Types and Data Model

Web-tracing emits two distinct events to provide complete page-view analytics. The `pv` event fires immediately when navigation occurs, while `pv-duration` calculates how long the previous page remained visible.

The `pv` payload includes:
- **eventType**: `'pv'`
- **triggerPageUrl**: Current location href
- **referer**: Previous page URL
- **title**: Document title
- **action**: Navigation type (navigation, reload, back_forward, reserved)
- **triggerTime**: Timestamp of the event

The `pv-duration` payload contains the elapsed time (`durationTime`) between the current and previous page views, sent via the `BEFOREUNLOAD` hook or before the next navigation occurs.

## Enabling and Configuring PV Collection

The PV collector respects a feature toggle defined in [`packages/core/src/lib/options.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/options.ts). Before initialization, set `options.pv.core` to `true` via the SDK configuration object.

```typescript
import { init } from 'web-tracing'

init({
  dsn: 'https://your-collector.com/endpoint',
  pv: true  // Enables { core: true } automatically
})

```

When disabled, the SDK skips the `initPv()` call entirely, ensuring zero overhead from navigation listeners.

## Initialization and Event Listener Registration

The `initPv()` function in [`packages/core/src/lib/pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/pv.ts) orchestrates the tracking system. During SDK startup, it performs three critical operations:

1. Emits an initial `pv` event for the first page load using `document.referrer` as the referer
2. Registers listeners on the internal `eventBus` for navigation-related browser events
3. Captures the start time for duration calculations

The following event types are monitored via the event bus:
- **HISTORYPUSHSTATE**: Intercepts `history.pushState()` calls
- **HISTORYREPLACESTATE**: Intercepts `history.replaceState()` calls
- **HASHCHANGE**: Tracks hash-based routing changes
- **POPSTATE**: Detects back/forward button usage
- **BEFOREUNLOAD**: Triggers dwell-time calculation on page exit

## Action Classification and Navigation Semantics

Inside `sendPageView()` (lines 126-161 of [`pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/pv.ts)), web-tracing categorizes every navigation into a specific **action** type:

- **navigation**: Standard page changes via links, programmatic router pushes, or script-driven URL updates
- **reload**: Page refreshes detected via `performance.navigation.type` or `location.reload()`
- **back_forward**: Browser back/forward button interactions originating from `popstate` events
- **reserved**: Fallback category for unrecognized navigation sources

The SDK maps `performance.navigation.type` values to these semantic actions, ensuring consistent reporting across different browser behaviors.

## Payload Construction and Timing

To ensure accurate `document.title` capture (which may update asynchronously after navigation), `sendPageView()` wraps payload assembly in a `setTimeout`. The function constructs the final event object as implemented in [`packages/core/src/lib/pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/pv.ts):

```typescript
{
  eventType: SEDNEVENTTYPES.PV,  // 'pv' constant
  eventId: baseInfo.pageId,
  triggerPageUrl: getLocationHref(),
  referer: previousUrl,
  params: customParameters,
  title: title || document.title,
  action: calculatedAction,
  triggerTime: getTimestamp()
}

```

Concurrently, the function generates the `pv-duration` event by comparing the current timestamp against `durationStartTime` from the previous view.

## Edge-Case Handling and Duplicate Prevention

Web-tracing implements safeguards against common SPA tracking pitfalls in [`packages/core/src/lib/pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/pv.ts):

- **Rapid navigation deduplication**: A `repetitionRoute` flag prevents double-counting when `replaceState` and `pushState` fire within 100ms of each other
- **Hash-mode routing**: The SDK respects the event order `replaceState` → `popstate` → `hashchange`, ignoring `popstate` when the URL fragment remains unchanged
- **Reload detection**: Explicit checks against `performance.navigation.type` distinguish hard reloads from soft navigations

## Manual PV Reporting API

For custom routing scenarios that bypass the History API, the SDK exports `handleSendPageView()` (lines 66-82). This function bypasses automatic listeners and allows explicit control over the payload.

```typescript
import { handleSendPageView } from 'web-tracing'

// Report a custom page view not captured by automatic listeners
handleSendPageView({
  triggerPageUrl: '/app/dashboard/custom-modal',
  title: 'Custom Dashboard View',
  action: 'navigation',
  params: { viewId: 'analytics' }
}, true)  // Second argument triggers immediate flush

```

## Cleanup and Teardown

When removing the SDK or disabling tracking dynamically, call `destroyPv()` to detach all navigation listeners. This function removes event bus subscriptions for `HISTORYPUSHSTATE`, `HISTORYREPLACESTATE`, `HASHCHANGE`, `POPSTATE`, and `BEFOREUNLOAD`, preventing memory leaks in long-running SPAs.

## Summary

- Web-tracing monitors page views through **browser API interception** rather than framework-specific hooks, making it compatible with Vue, React, Angular, or vanilla JavaScript.
- The system emits **`pv`** events immediately on navigation and **`pv-duration`** events on page unload to track dwell time.
- Configuration is controlled by **`options.pv.core`** in [`packages/core/src/lib/options.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/options.ts), with initialization logic residing in [`packages/core/src/lib/pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/pv.ts).
- Navigation actions are classified into **navigation**, **reload**, **back_forward**, or **reserved** based on the triggering mechanism and `performance.navigation.type`.
- Duplicate navigation within 100ms is suppressed via the **`repetitionRoute`** flag to ensure accurate analytics.
- Developers can manually trigger tracking via **`handleSendPageView()`** or completely remove listeners using **`destroyPv()`**.

## Frequently Asked Questions

### How does web-tracing detect SPAs that use hash-based routing?

The SDK listens for the `HASHCHANGE` event via the internal event bus, as implemented in [`packages/core/src/lib/pv.ts`](https://github.com/m-cheng-web/web-tracing/blob/main/packages/core/src/lib/pv.ts). For hash-mode routers, it correctly handles the sequence where `replaceState` may fire before `hashchange`, ensuring the `popstate` event is ignored if the URL fragment hasn't actually changed.

### Can I disable automatic PV tracking but still send manual page views?

Yes. Initialize the SDK with `pv: false` (or omit the option entirely) to prevent `initPv()` from running. You retain full access to `handleSendPageView()` for explicit instrumentation of specific routes or components without the overhead of automatic History API monitoring.

### What is the difference between `pv` and `pv-duration` events?

The **`pv`** event fires at the start of a page view, capturing metadata like URL, title, and referrer. The **`pv-duration`** event fires when the page unloads or navigates away, reporting the milliseconds elapsed since the previous `pv` event. Together, they provide both entry-point analytics and engagement time metrics.

### How does web-tracing handle browser back/forward buttons?

The SDK listens for `POPSTATE` events via the event bus. When detected, `sendPageView()` assigns the action type **`back_forward`** to the event payload. This distinguishes history traversals from standard navigations or reloads in your analytics data.