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 passServer-Timingthrough<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 enrichmentQueries 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.