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. It does not collect a replay.

Install the current SDK

The Browser SDK contract is @poolstatis/sdk version 0.1.0. The SDK is not generally published through the public npm registry yet, so pnpm add @poolstatis/sdk is not a valid public install command. The current public-source fallback is the reviewed commit that adds the ./browser export. Build one exact tarball, then install that local artifact:

git clone https://github.com/lim5max/poolstatis.git poolstatis-sdk-source
git -C poolstatis-sdk-source checkout --detach 48618143c23349ab46d0c25b01b0ec93740794f2
corepack pnpm@10.13.1 --dir poolstatis-sdk-source install --frozen-lockfile
corepack pnpm@10.13.1 --dir poolstatis-sdk-source/sdk pack --pack-destination "$PWD"
pnpm add ./poolstatis-sdk-0.1.0.tgz

The repository is public and this path requires no GitHub account or GitHub App. If GitHub access is unavailable, obtain the same reviewed tarball and its checksum from the Poolstatis operator before onboarding. Do not replace the source commit or tarball with an unverified @latest install. This temporary source-build path can be removed after the SDK has a verified public registry release.

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,
  consentPolicy: 'external',
  hasConsent: () => consent.has('product_analytics'),
  subscribeConsent: (listener) => consent.onChange(listener),
  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 keeps landing_path without query/hash and at most the referrer origin. SPA navigation preserves the original landing snapshot.

Production Cloud can propose the five bounded acquisition properties for explicit owner review. The pinned public npm runner @poolstatis/mcp@0.2.0 does not include propose_acquisition_properties; use the Cloud/Admin flow for that proposal until a later public runner advertises the tool. 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.2.0 does not include propose_browser_analytics or query_web_analytics. With that runner, use sample_events to verify canonical stored page.viewed evidence and query_trend against the activated page-view metric for counts or one property breakdown. Use the Cloud UI for the combined visitors/sessions/page-view summary until a later pinned runner exposes parity.

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. Without a trusted country source it is unknown. The newer local GeoIP path is not part of the deployed contract described here.

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 in the reviewed rollout has more than 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.

Foreground duration, engaged sessions, bounce rate, and measured coverage are not available in the current public 0.1.0 SDK fallback. It emits canonical page views and bounded context, but not the lifecycle evidence needed for those measures. Until a later SDK, Core, MCP, and UI release is explicitly pinned here, do not derive those numbers from timestamps. Poolstatis also does not provide video or DOM session replay; Browser Experience supplies privacy-bounded labelled clicks, scroll milestones, and exact-snapshot overlays instead.

Reviewed engagement rollout — not yet shipped

A separate reviewed branch defines the lifecycle contract below. It is still in review for Core integration and SDK publication, so do not instrument against it until the connected server exposes the matching Query DSL/MCP tools and the exact SDK package is published.

  • 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 more than 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 reviewed Query DSL branches are web_analytics, web_sessions, web_session, and page_engagement. Candidate 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 pending contract, not current public availability.

The default opt-in policy and the external policy require host-owned hasConsent and synchronous subscribeConsent callbacks. Explicit opt-out is a host choice and may start enabled, but Global Privacy Control still blocks collection in every mode. When the callback reports withdrawal, Browser analytics stops, removes its first-party visitor/session identity, and discards queued Browser events, including retry batches.

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 and consent source. 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 consented 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. Collection is allowed, Global Privacy Control is off for the test profile, and 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; missing trusted-proxy country appears as unknown.
  5. Reserved Browser/UTM fields were not supplied or spoofed by application properties.
  6. After consent withdrawal, start a new allowed session; discarded queued events are not restored.