Skip to documentation
Browser analytics

Browser Experience

Browser Experience answers where visitors allowed by the host's analytics policy click, how far they scroll, and which safe route categories they visit. It is deliberately not a DOM replay and does not collect page text, input values, CSS selectors, raw URLs, query strings, error messages, or stack traces.

Browser analytics policy modes

The separate @poolstatis/sdk/browser module supports three host-controlled policies: opt-in is the backward-compatible default, opt-out is an explicit reversible choice, and external delegates the decision to a CMP. Global Privacy Control disables collection in every mode. Changing policy never expands the fixed property allowlist.

Poolstatis uses opt-out only on its own public poolstatis.xyz site. It counts anonymous visitors, sessions, allowlisted page categories, labelled CTA clicks, scroll milestones, bounded campaign source, primary language, timezone and coarse device/browser/OS, viewport/screen buckets. The public site does not render a global analytics-control footer. An unset preference allows the bounded baseline and matching Browser Experience signals; explicit prior decline and Global Privacy Control stay disabled and fail-closed. The site does not send exact dimensions, form values, page text, DOM replay, full URLs/query strings, full referrers, raw IP, precise location, stacks, pointer paths, email or direct identifiers. This describes the product configuration; it is not a claim of legal compliance.

1. Create a semantic surface

Use MCP so the surface has a real purpose before any interaction is stored:

create_experience_surface({
  project: "acme-product",
  key: "marketing",
  name: "Marketing site",
  purpose: "Find which public-page sections lose qualified visitors before setup.",
  status: "active"
})

Register canonical route keys separately:

register_experience_route({
  project: "acme-product",
  surface: "marketing",
  route: { key: "home", name: "Landing home", path_pattern: "/" }
})

2. Mark meaningful controls

The observer ignores arbitrary clicks. Label only controls that correspond to a product or content decision:

<section data-poolstatis-section="hero">
<a href="/docs/quickstart" data-poolstatis-label="hero.open_docs">
  Read quickstart
</a>
</section>

Labels must be stable semantic keys. Never put user text or identifiers in data-poolstatis-label.

3. Start only when the host policy allows

import { createClient } from '@poolstatis/sdk';
import { BrowserExperience } from '@poolstatis/sdk/experience';

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

const experience = new BrowserExperience({
  client,
  surface: 'marketing',
  distinctId: anonymousVisitorId,
  sessionId: anonymousSessionId,
  route: 'home',
  version: import.meta.env.VITE_RELEASE_SHA,
  hasConsent: () => consent.has('product_analytics'),
});

await experience.start();

Use a finite route vocabulary; never pass location.href or location.pathname. In this external/CMP example, no listener is attached before hasConsent() returns true. A host that explicitly chooses opt-out may treat an unset preference as allowed, but must still block stored decline and Global Privacy Control. If permission is withdrawn, call experience.stop() so unsent interaction data is discarded. Capture is chunked to at most 25 signals per request, held within a bounded queue, and guarded by a default 120-signals/minute browser limit.

4. Ask the agent

  • get_visual_experience_map overlays normalized clicks, scroll reach and named-section drop-off on an immutable desktop/mobile release snapshot.
  • compare_visual_experience compares two bounded route/version/device cohorts without claiming causality.
  • get_experience_session returns one known consented session timeline.

The primary map is valid only when the project has an exact snapshot matching surface + route + version + device + env; events then also match its viewport and document dimensions. The human UI renders the overlay. MCP returns bounded aggregates and snapshot metadata, not the image. Without an exact snapshot, normalized aggregate coordinates remain a secondary fallback: they can mix layouts and devices unless the query is filtered and must not be interpreted as layout-accurate.

Snapshot upload accepts raw PNG/WebP bytes up to 5 MiB; the server does not fetch a caller-supplied URL. Self-hosted backups must preserve the database and snapshot artifact volume together.

Treat these as observational signals. A click pattern can suggest a usability problem, but it does not prove why a visitor behaved that way.