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.
Initialize after your consent decision
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.viewedevent 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_idon accepted page views. The first-party browser session rotates after 30 minutes of inactivity. - A page view is one accepted stored
page.viewedevent. Server enrichment does not create an extra billable event. - The acquisition-only
createAttributionClienthelper emits exactly onesession.startedand the initialpage.viewedwhen it starts. Do not run it besidecreateBrowserAnalytics({ 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 parameter | stored 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.viewedhas a stable$page_view_id. - Cumulative
page.engagementcarries$page_view_id, monotonicsequence,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
sessionStorageand 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.
Consent and privacy boundaries
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:
- Collection is allowed, Global Privacy Control is off for the test profile, and
browserAnalytics.start()ran in a browser rather than during SSR. - The
pk_key targets the expected project and environment, andpage.viewedwas accepted rather than rejected. - The compatible
web_page_viewscount metric is active; manual or legacy page views without$browser_context = "1"are excluded. - The requested dates and
envinclude the event. Missing UTM source appears asdirect / unknown; missing trusted-proxy country appears asunknown. - Reserved Browser/UTM fields were not supplied or spoofed by application properties.
- After consent withdrawal, start a new allowed session; discarded queued events are not restored.