Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Vanilla Breeze ships with a small, cookieless, privacy-first analytics runtime. Pages fire a page.view on load; components dispatch typed events through Analytics.track(); a Cloudflare Pages Function writes aggregates to D1. The dashboard at /stats/ renders the results.
Set window.vbAnalyticsConfig before vanilla-breeze-core.js loads. The runtime reads it once at init.
<script> window.vbAnalyticsConfig = { siteId: 'my-site', transport: 'beacon', endpoint: '/api/analytics', sampleRate: 1, };</script><script type="module" src="/cdn/vanilla-breeze-core.js"></script>
| Key | Default | Purpose |
|---|---|---|
siteId |
'default' |
Written to the site_id column on every row; lets one D1 database serve multiple sites. |
transport |
'disabled' |
'beacon' (POST to the endpoint), 'console' (log to devtools — useful in dev), or 'disabled' (no-op). |
endpoint |
'/api/analytics' |
Base URL; individual paths are ${endpoint}/hit, /click, /events. |
sampleRate |
1 |
Fraction of events to record. See Sample rate. |
consent |
null |
Function returning a boolean. If present and returns false, no events fire. Evaluated every time, so you can flip consent without reloading. |
urlMasks |
null |
Array of { pattern, replace }. Sensitive path segments (user IDs, tokens) are rewritten before the beacon is sent. |
allowInIframe |
false |
By default, Analytics detects when it's running inside an iframe (<browser-window> demos, third-party embeds) and disables itself to avoid spamming events. Set true to override. |
The sampleRate option trades ingest volume for dashboard accuracy. At small scale (single-digit visits per day) leave it at 1 — recording everything gives the best signal. At ≥10,000 events per day you may want to sample down to keep D1 storage and query latency predictable.
sampleRate works per event:
function shouldSample() { const rate = state.config?.sampleRate ?? 1; return rate >= 1 ? true : Math.random() < rate;}
So sampleRate: 0.1 means every event has an independent 10 % chance of firing — page views, clicks, form submits, Web Vitals, errors, scroll, attention, everything.
| Volume | Recommendation | Why |
|---|---|---|
| < 1,000 events / day | sampleRate: 1 |
Record everything. Storage is tiny, and low-volume data is already noisy enough. |
| 1,000 – 100,000 / day | sampleRate: 1 (still) |
D1 writes are cheap; query cost at this scale is negligible. Only sample if you hit a specific bottleneck. |
| 100,000 – 1M / day | sampleRate: 0.25 to 0.1 |
Reduces D1 row count 4–10×. Statistical precision on top-10 queries stays high; rare events lose resolution. |
| > 1M / day | sampleRate: 0.01 or move to Analytics Engine |
At this scale a single D1 DB fills up fast. Consider splitting or swapping the transport to write-only Cloudflare Analytics Engine instead of row-per-event D1. |
When sampleRate < 1, every count on /stats/ under-reports by the sampling factor. For a population estimate:
const estimated = observed / sampleRate;// sampleRate: 0.1, observed: 42 → estimated ≈ 420 // Rule-of-thumb 95 % CI half-width:const halfWidth = 1.96 * Math.sqrt(observed) / sampleRate;// 42 observed at 0.1 → ±127. So estimated = 420 ± 127./code-block <p>For this reason, sampling always loses precision on <em>rare</em> events. If you need accurate counts of a specific error or conversion, don't sample — or sample differently per event (not supported out of the box; easiest to add a second Analytics.track call from your own code with its own gate).</p></section> <section> <h2 id="url-masking">URL masking</h2> <p>URLs end up on two D1 columns: <code>path</code> and <code>referrer</code>. Both are the raw values the browser had — including any sensitive path segments, query parameters, or session tokens. <code>urlMasks</code> rewrites the tracked <code>path</code> client-side, before the beacon is sent, so the server never sees the original.</p> <p>Each mask is a <code>{ pattern, replace }</code> pair. Masks are applied in order; the first match wins (so put the most specific patterns first). The input is <code>location.pathname + location.search</code>, so masks can target either the path or the query string.</p> <code-block language="js" label="Typical masks" data-escape>window.vbAnalyticsConfig = { urlMasks: [ // User profiles — collapse /users/id/... to /users/*/... { pattern: /^\/users\/[^/]+/, replace: '/users/*' }, // Preview drafts — hex or uuid ids anywhere in the path { pattern: /\/[0-9a-f]{8,}/g, replace: '/*' }, // Filter query strings on the stats dashboard so every combination // of ?site=…&window=… doesn't get its own row in Top Pages. { pattern: /^\/stats\/\?.*$/, replace: '/stats/' }, // Strip utm_*, ref, fbclid, gclid — keep acquisition info out of path { pattern: /[?&](utm_[^=]+|ref|fbclid|gclid)=[^&]*/g, replace: '' }, ],};
Masking is one-way and runs in the browser; there is no "unmask" step at query time. If you later want the unredacted path, you need to add it to props explicitly via Analytics.track().
beacon (production)Uses navigator.sendBeacon() with a fetch(..., { keepalive: true }) fallback. Beacons survive page unload and never block navigation. This is the only transport that produces server-side data; use it once your ingest endpoints are live.
console (dev visibility)Logs each event as [vb:analytics] hit {...} with a styled prefix. No network requests are made — useful for confirming events fire as expected before pointing at a real endpoint.
disabled (explicit opt-out)Every call to Analytics.track() is a no-op. Useful for local scripts, ephemeral demos, or subdomains where analytics aren't wanted.
Before every event fires, the runtime checks these signals in order. Any one returning "opted out" drops the event:
sessionStorage.getItem('vb_optout') === '1' — written by <analytics-panel>'s Pause button or Analytics.setConsent(false).navigator.globalPrivacyControl === true — browser-level GPC signal.navigator.doNotTrack === '1' — legacy DNT header.consent callback you supplied, if any.data-vb-no-track on <html> — opts the whole page out.Per-element exclusion via data-vb-no-track on any ancestor also drops data-vb-event firings from inside that subtree.
The stats dashboard renders totals, top pages, top events, referrers, countries, Web Vitals, engagement, and recent errors. All panels are scoped to the site query parameter (defaults to vb-docs), with time windows of 24 hours, 7 days, or 30 days.
If you've turned sampling on, remember to divide by sampleRate to estimate totals.
data-vb-event — declarative event tracking on any elementdata-vb-no-track — opt a subtree out<analytics-panel> — visitor-facing view of session data with Pause / Clear controls