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

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. Before initialization, set options.pv.core to true via the SDK configuration object.

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 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), 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:

{
  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:

  • 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.

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, with initialization logic residing in 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. 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →