Skip to main content

Docs

Coverage tracking (page_key)

Server-anchor every page render so the coverage dashboard can show what your current analytics tool is hiding from you. Mint a page_key in SSR, thread it into the rendered HTML, and the browser SDK stamps it on every event.

Last updated:

On this page

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_key

Every 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 / lost

3. Install — Next.js 16 SSR

Two functions live in @syntarie/tracking-node:

  • mintPageKey() — wraps crypto.randomUUID(). Returns a fresh v4 UUID.
  • registerPageRender(opts) — server-to-server POST to /v1/page-rendered with the workspace bearer. Fire-and-forget; swallows every failure mode so a tracking-side hiccup never throws into your page render.
app/layout.tsxtsx
// 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:

sdk-init.tstsx
// 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:

  1. View source on any page — there should be a <meta name="leatmap-page-key"> tag in <head> with a UUID in content.
  2. Network tab → filter for collect.leatmap.com. Outgoing event POSTs should each carry a page_key field in their JSON body matching the meta tag.
  3. /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-key or pass pageKey explicitly to init() as a script-tag fallback.
  • Client-side navigation without re-render. A SPA-style transition that does not re-render <head> reuses the previous page_key. That is correct: a single SSR render covers every event fired during the same page session. A fresh page_key appears 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.