Skip to documentation
Browser analytics

Browser analytics

The optional Browser SDK turns accepted page.viewed events into privacy-bounded web analytics. It keeps a first-party visitor id, a browser-session id, a finite page vocabulary, coarse device context, and an immutable acquisition snapshot. This ./browser entrypoint does not collect replay; Poolstatis Session Replay is a separate explicit-consent source module.

Install the current SDK

The Browser SDK is published with the ./browser export. Install its exact version; do not replace the pin with @latest:

pnpm add @poolstatis/sdk@0.4.0

The base client, ./browser, ./attribution, and ./experience entrypoints are part of that same public package.

Initialize browser analytics

import { createClient } from '@poolstatis/sdk';
import { createBrowserAnalytics } from '@poolstatis/sdk/browser';

const client = createClient({
  url: 'https://analytics.example.com',
  ingestKey: 'pk_write_only',
});

const browserAnalytics = createBrowserAnalytics({
  client,
  captureAcquisition: true,
  mapPagePath: (pathname) => {
    if (pathname === '/') return '/home';
    if (pathname.startsWith('/docs/')) return '/docs/:page';
    return '/other';
  },
});

browserAnalytics.start();

The pk_ key is write-only and can be embedded in the product. An sk_ or pt_ key must stay in a trusted runtime. Use a finite mapPagePath vocabulary on both desktop and mobile; do not return account ids, slugs, search text, or other user-controlled path segments.

Canonical events, visitors, and sessions

  • Browser analytics emits the canonical page.viewed event once for each distinct bounded path, including History API and back/forward SPA navigation.
  • A visitor is a unique resolved actor with an accepted browser page.viewed. An audited anonymous-to-authenticated actor link can deduplicate that visitor at query time.
  • A session is a distinct non-empty session_id on accepted page views. The first-party browser session rotates after 30 minutes of inactivity.
  • A page view is one accepted stored page.viewed event. Server enrichment does not create an extra billable event.
  • The acquisition-only createAttributionClient helper emits exactly one session.started and the initial page.viewed when it starts. Do not run it beside createBrowserAnalytics({ captureAcquisition: true }); the combined Browser module owns its page views and uses the same session for traffic and acquisition context.

Call browserAnalytics.identify(user.id) only after authentication. It returns an actor-link handoff that a trusted backend must submit with an sk_ or pt_ credential. It does not perform a privileged link from browser code. Call resetIdentity() on logout or account switch.

UTM landing attribution

With captureAcquisition: true, the SDK reads the landing once after collection is allowed. It keeps only the first valid value for this allowlist:

query parameterstored event property
utm_source$utm_source
utm_medium$utm_medium
utm_campaign$utm_campaign
utm_term$utm_term
utm_content$utm_content

Values are trimmed, normalized to NFC, and limited to 256 characters. Unknown query parameters, click ids, duplicate values after the first valid one, and previous-session attribution are discarded. The SDK stores only the product-owned finite landing_route key and at most the referrer origin. It never sends a raw landing path. SPA navigation preserves the original landing snapshot.

Production Cloud and the pinned public @poolstatis/mcp@0.7.0 runner can propose the five bounded acquisition properties for explicit owner review with propose_acquisition_properties. Results are labelled session landing attribution. They are association, not multi-touch attribution or causal campaign credit.

Web analytics query

Production Cloud can propose the native properties and canonical web_page_views / web_visitors metrics, then run the Web analytics summary over a count metric sourced from page.viewed with $browser_context = "1". That summary returns visitors, sessions, page views, and bounded dimension tables.

The pinned public npm runner @poolstatis/mcp@0.7.0 exposes propose_browser_analytics, query_web_analytics, get_web_overview, list_web_sessions, get_web_session, get_session_engagement, and get_page_engagement. Use sample_events for stored-event inspection and these typed reads for aggregate and bounded timing evidence.

The Cloud summary has visitors, sessions, and page_views. Each requested dimension returns those three counts plus its share of page views. Supported dimensions are country, device, browser, os, language, timezone, and source ($utm_source). Each dimension is ranked by page views and bounded to 50 rows; check meta.truncated_dimensions.

Country is a coarse ISO alpha-2 value added only from a configured trusted proxy or reviewed server-side GeoIP resolver. Production Cloud has this resolver active for new events; older events are not retroactively backfilled and may remain unknown. Self-hosted installations without a trusted country source also return unknown. The browser cannot supply, infer, or spoof country from locale.

Foreground engagement time

Do not use the distance between the first and last event as "time on site." These terms have different grains:

  • Foreground engagement time is the sum of intervals while the page is visible and the browser window is focused. Background tabs and suspended time do not count.
  • Session span is wall-clock time from the first accepted event to the last accepted event. It can include idle or background gaps and is not an attention metric.
  • An engaged session has at least 10 seconds of foreground engagement, at least two canonical page views, or a configured active native key event.
  • A bounce is the complement only for a fully measured single-page session. An incomplete/crashed session or missing close evidence is unknown, not zero, and must not be counted as a bounce.
  • Measured coverage is the share of eligible sessions with enough lifecycle evidence to classify foreground engagement and bounce. Always show it beside the rates.

The public @poolstatis/sdk@0.4.0 Browser entrypoint and production Core implement this engagement contract. Browser Experience separately supplies privacy-bounded labelled clicks, scroll milestones, and exact-snapshot overlays. Current Core also includes opt-in DOM Session Replay, published through the separate @poolstatis/sdk/replay entrypoint in 0.4.0.

Lifecycle snapshots

  • Each canonical page.viewed has a stable $page_view_id.
  • Cumulative page.engagement carries $page_view_id, monotonic sequence, foreground_ms, elapsed_ms, max_scroll_pct, interaction_count, and a bounded lifecycle reason.
  • Foreground time advances only while the document is visible and the window is focused. The reviewed heartbeat is 10 seconds and one suspended gap is capped at 30 seconds.
  • Core keeps the highest sequence for each page so retries and out-of-order cumulative heartbeats do not double count.
  • A browser session lives in one tab's sessionStorage and rotates after 30 minutes of inactivity. Page time is scoped by project + env + $page_view_id; session measures are scoped by project + env + session_id.
  • An engaged session has at least 10 seconds of foreground time, at least two canonical page views, or a configured active native key event. Bounce is the complement only for fully measured sessions; an incomplete/crashed session remains null.
  • Coverage reports timed/total page views and measured/incomplete sessions beside engagement and bounce.

The Query DSL branches are web_analytics, web_sessions, web_session, and page_engagement. Pinned public MCP reads include get_web_overview, list_web_sessions, get_web_session, get_session_engagement, get_page_engagement, get_click_map, and get_scroll_map. These names describe the current @poolstatis/mcp@0.7.0 read contract.

Collection and payload boundaries

Browser analytics starts immediately after start(). Poolstatis does not require a consent state and does not inspect Global Privacy Control. The integrating product owns any notice, consent, pause, or deletion workflow it chooses to implement.

Poolstatis does not store raw IP, a full URL, query string, URL fragment, full referrer URL, full User-Agent, exact device model, DOM, page text, form values, selectors, or PII through this module. Browser/OS/device are coarse families; viewport and screen values are buckets. Do not put personal data in event properties, route mappings, UTM tags, labels, or ids.

Upgrade from acquisition-only

If the product already uses createAttributionClient, stop and remove that helper before starting the combined Browser module with captureAcquisition: true. Keep the same safe route vocabulary. Do not backfill old browser identity or copy UTM values from a previous session. Register the Browser and acquisition property definitions, activate the canonical page-view count metric, deploy, then verify a fresh session.

No data: troubleshooting

If the Cloud Web analytics summary or the public-runner query_trend proof returns no data, check in this order:

  1. browserAnalytics.start() ran in a browser rather than during SSR.
  2. The pk_ key targets the expected project and environment, and page.viewed was accepted rather than rejected.
  3. The compatible web_page_views count metric is active; manual or legacy page views without $browser_context = "1" are excluded.
  4. The requested dates and env include the event. Missing UTM source appears as direct / unknown; pre-GeoIP history or a self-host without a trusted country source appears as unknown.
  5. Reserved Browser/UTM fields were not supplied or spoofed by application properties.
  6. If the host explicitly paused collection, start a new session after resuming; discarded queued events are not restored.