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 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.
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.
Access logs
Server-side analytics from your nginx, Caddy or Traefik access logs. Ship them to MetrikStack with Vector or Fluent Bit, without changing your app.
AI crawlers and bots
How MetrikStack sorts traffic into people, AI assistants, AI crawlers, search engines and other bots, and checks each bot's IP against its operator's list.