1. Why coverage tracking exists
Every analytics tool — leatmap included — only sees the events that made it through the browser. The events that never fired (because an adblocker dropped the SDK, because a JS exception killed init, because a consent denial short-circuited the queue) are invisible by definition.
Coverage tracking fixes that by anchoring the denominator on the server. Your SSR layer mints a page_keyper render and tells the collector “a page was rendered, it had the opportunity to be measured.” The browser SDK then stamps the same page_key on every event it successfully emits. The collector left-joins the two, producing the five-bucket transparency report on /coverage:
- observed — the renders the SDK reached AND the collector accepted.
- consent_denied— the SDK loaded but consent was “denied”.
- bot_filtered — the SSR layer flagged this render as a bot.
- blocked_or_failed — denominator present, numerator missing. The classic adblocker / network failure gap.
- unknown — events arrived without a matching page_key (legacy SDKs, mis-installed SSR helper, third-party event sources).
2. How the join works
The join key is the v4 UUID your SSR layer mints once per page render. The collector's coverage repository runs:
events.raw->>'page_key' = eligible_pageviews.page_keyEvery event your SDK sends carries page_key as a top-level field on the wire (see shared/src/events.schema.json). Every row in eligible_pageviews was written by a successful POST /v1/page-rendered from your SSR layer.
┌─────────────────────────────────────────────────────────────────┐
│ SSR render (your Next.js / edge / fastify route) │
│ │
│ mintPageKey() ──► "f47ac10b-58cc-4372-a567-0e02b2c3d479" │
│ │
│ │ │
│ ├──► registerPageRender({ pageKey, ... }) │
│ │ POST collect.leatmap.com/v1/page-rendered │
│ │ ▼ │
│ │ ┌──────────────────────────────────────┐ │
│ │ │ eligible_pageviews (denominator) │ │
│ │ │ { page_key, consent_state, path } │ │
│ │ └──────────────────────────────────────┘ │
│ │ │
│ └──► <meta name="leatmap-page-key" content="..."> │
│ │ │
└───────────────────────┼─────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Browser SDK init() reads the meta tag │
│ │
│ every event ──► { ..., page_key: "f47ac10b-..." } │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ events table (numerator) │ │
│ │ raw->>'page_key' joins denominator │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
/coverage dashboard — observed / unknown / lost3. Install — Next.js 16 SSR
Two functions live in @syntarie/tracking-node:
mintPageKey()— wrapscrypto.randomUUID(). Returns a fresh v4 UUID.registerPageRender(opts)— server-to-server POST to/v1/page-renderedwith the workspace bearer. Fire-and-forget; swallows every failure mode so a tracking-side hiccup never throws into your page render.
// app/layout.tsx — Next.js 16 App Router.
// Mints one page_key per render and threads it both ways:
// 1. server → collector via registerPageRender() (the eligible-pageview row)
// 2. server → browser via <meta>, where the SDK reads it on init()
import { mintPageKey, registerPageRender } from '@syntarie/tracking-node';
import type { ReactNode } from 'react';
export default async function RootLayout({ children }: { children: ReactNode }) {
const pageKey = mintPageKey();
// Fire-and-forget — registerPageRender swallows every failure
// mode (5xx, network, TLS) so a tracking-side hiccup never
// breaks your page render.
await registerPageRender({
pageKey,
siteId: process.env.LEATMAP_SITE_ID ?? '',
apiKey: process.env.LEATMAP_API_KEY ?? '',
consentState: 'granted', // or read from your consent banner cookie
path: '/',
});
return (
<html>
<head>
<meta name="leatmap-page-key" content={pageKey} />
</head>
<body>{children}</body>
</html>
);
}
The two arguments that always matter:
consentState— one of'granted','denied','unknown'. Read this from your consent banner cookie at SSR time. The collector validates against a CHECK constraint, so unrecognized values get a 400.path— the request path being rendered. Used for the per-page coverage breakdown on the dashboard. Query strings are truncated for display; raw is stored.
Optional arguments: sessionKey, referrerGroup, deviceClass, country, botFiltered. If you already classify these in your SSR middleware, pass them through — the dashboard will slice coverage by each.
4. Install — browser SDK
The browser SDK auto-detects page_key from a <meta name="leatmap-page-key"> tag in <head>. As long as your SSR layer renders the meta tag (the template in §3 does), the SDK needs no additional configuration:
// Wherever you bootstrap the browser SDK.
// The auto-detect path picks the page_key up from the <meta> tag
// rendered by your SSR layer (above). Pass pageKey explicitly only
// if you mint it client-side from a different source.
import { init } from '@syntarie/tracking';
init({
apiKey: process.env.NEXT_PUBLIC_LEATMAP_API_KEY!,
host: 'https://collect.leatmap.com',
// pageKey: 'abc-123', // optional override — meta tag wins by default
});
If you mint the page_key outside the SSR-meta path (e.g. a single-page app that reads it from a script tag or a fetched JSON blob), pass it explicitly via pageKey on init(). The explicit option overrides the meta tag.
5. Verifying the join key flows end-to-end
Open your site in a regular browser, then check three places:
- View source on any page — there should be a
<meta name="leatmap-page-key">tag in<head>with a UUID incontent. - Network tab → filter for
collect.leatmap.com. Outgoing event POSTs should each carry apage_keyfield in their JSON body matching the meta tag. - /coverage dashboard in the leatmap dashboard — the
observedcolumn should be non-zero within a minute or two of your test traffic. If it stays at zero, your numerator and denominator aren't joining — see §6.
6. When the page_key is missing
Events without a page_key still flow. The collector accepts them and the dashboard surfaces them in the unknown bucket. Common reasons for events landing in unknown rather than observed:
- SSR helper not installed yet. Pre-TRK-266 installations send events with no page_key field. Roll the helper into your root layout and the next deploy starts producing matching pairs.
- Meta tag stripped by CDN or HTML minifier. Some aggressive minifiers drop meta tags with custom names. Whitelist
leatmap-page-keyor passpageKeyexplicitly toinit()as a script-tag fallback. - Client-side navigation without re-render. A SPA-style transition that does not re-render
<head>reuses the previouspage_key. That is correct: a single SSR render covers every event fired during the same page session. A freshpage_keyappears on the next full server-rendered navigation. - Third-party event sources. Events forwarded from another tracker (server-side import, Segment rewriter, etc.) typically have no
page_key. That is expected — the coverage report is about renders this SSR layer initiated.