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_mapoverlays normalized clicks, scroll reach and named-section drop-off on an immutable desktop/mobile release snapshot.compare_visual_experiencecompares two bounded route/version/device cohorts without claiming causality.get_experience_sessionreturns 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.