MetrikStack

How merging works

Request ids, primary source selection, enrichment rows and the dedupe window.

When a site runs more than one source, the same page load can be observed by several of them at once. The browser script and a server SDK both see the same navigation, for example. MetrikStack merges these into a single pageview, enriched with whatever fields the primary source couldn't provide.

Request ids

Sources that can generate a per-response id do so, and expose it three ways so it reaches the browser through as many pipelines as possible:

  • Response header: X-MetrikStack-Request-Id: <uuidv7>
  • Server-Timing: metrikstack;desc=<uuidv7>, which survives CDNs that strip custom response headers but pass Server-Timing through
  • <meta name="metrikstack-request-id">, for sources that can rewrite HTML (e.g. Laravel via @metrikstackMeta)

The id is a UUIDv7, which is time-ordered.

The browser script reads it back (from the meta tag where present, otherwise from performance.getEntriesByType('navigation')[0].serverTiming) and sends it with its own event, closing the loop.

Primary source policy

Every site has a primary_source_policy: an ordered list of source kinds, defaulting to:

["edge_cloudflare", "edge_bunny", "server", "logs", "script"]

The first kind present and active for a site is its primary source for merge purposes. Edge sources rank first because they see the most complete traffic (including cache hits and bot traffic bypassing the origin); the script ranks last because it sees the least. The order can be overridden per site.

Merge rules at ingest

if event has request_id:
    if event.source is the site's primary source, or no other source has
    reported this request_id yet:
        write event as a pageview
    else:
        write event as an `enrichment` event_name (not counted as a pageview),
        containing only the fields the primary lacks
        (duration_ms, scroll_depth, screen, language)
else:
    # no request id (static hosting + script only, or a log line without correlation)
    if site has exactly one active source:
        write as pageview
    else:
        # fallback dedupe: same (site_id, visitor_hash, path) within 2 s from a
        # non-primary source is written as enrichment

Queries treat pageview rows as the count, and join enrichment rows by request_id for time-on-page and scroll metrics.

The fallback dedupe window

Not every source can carry a request id. A raw access-log line has no way to correlate with a browser event, for instance. For those, ingest falls back to matching (site_id, visitor_hash, path) from a non-primary source within a short window (2 seconds) of the primary's event, and writes the later one as an enrichment row instead of a second pageview. This is a best-effort fallback, not a replacement for request-id propagation: wire up request ids wherever the source supports it.

The single-source fast path

Single-source sites need none of this. The branch site has exactly one active source short-circuits before any merge lookup runs, so the common case (one script, one server SDK, one edge integration) involves no merge logic at all. Merging only activates once a site has two or more active sources.

On this page