MetrikStack

How merging works

How MetrikStack counts a page load once when the browser script, a server SDK, a CDN or access logs all see it, using request ids and a dedupe window.

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

Request ids

Sources that can generate an id per response do so, and expose it in three ways so it reaches the browser through as many setups 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.

Primary source policy

Every site has a primary_source_policy: an ordered list of source kinds, by default:

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

The first kind that is present and active on a site is its primary source for merging. Edge sources rank first because they see the most traffic, including cache hits and bots that never reach the origin. The script ranks last because it sees the least. You can change the order 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: the same (site_id, visitor_hash, path) already
        # counted by another source within 60 s, and not matched yet: write
        # this one as enrichment. Whichever source arrives first counts.

Reports count the pageview rows and take time on page and scroll depth from the enrichment rows with the same request_id.

The fallback dedupe window

Some sources can't carry a request id. A raw access-log line has no way to point at a browser event, for instance. For those, ingest matches (site_id, visitor_hash, path) across sources within 60 seconds. The first source to report a view counts it, and the same view from another source is written as an enrichment row instead of a second pageview. Order doesn't matter; with log forwarding the script usually arrives first. Each match is used once, so a reload that only one source saw still counts.

Matching needs both sources to see the same visitor, which means the same IP address and user agent. A visitor whose browser reaches your site over IPv6 and the script over IPv4 can't be matched and is counted twice. If your site is reachable over IPv6, serve the script over IPv6 as well, or from your own domain. The window is a fallback: wherever a source supports request ids, use them.

Sites with one source

A site with one active source skips all of this. The site has exactly one active source check runs before any merge lookup, so the common case (only the script, only a server SDK or only an edge integration) involves no merging. Merging starts once a site has two or more active sources.

On this page