WordPress
WordPress analytics without cookies. The MetrikStack plugin adds the browser script, tracks pageviews server-side, or both, and counts each view once.
The WordPress plugin works like the Laravel package. It can add the
browser script to wp_head, track pageviews server-side on
template_redirect, or both. With both on, request ids make sure each page
load counts as one pageview.
Requirements
- PHP 8.1+
- WordPress 6.4+
Installation
The plugin is not publicly downloadable; it is available on request. Once you have it, activate it from the Plugins list.
Configure
Go to Settings → MetrikStack:
-
API endpoint: the MetrikStack API base URL. The default is
https://api.metrikstack.com. -
Site ID: the site's UUID from your analytics account.
-
Ingest token: the server-side ingest token; only needed for server-side tracking. Shown masked once saved.
-
Inject browser script: adds
<script defer src="{endpoint}/js/script.js" data-site="{site id}"></script>to
wp_headon every front-end page, the same tag as the browser script install. -
Server-side tracking: defaults on the first time you save a token; toggle it off to run browser-only.
-
Logged-in users: off by default. When off, neither the browser script nor server-side tracking runs for logged-in WordPress users.
-
Admin & bot paths: on by default. wp-admin, the REST API, cron, feeds and previews are always excluded; this setting additionally excludes
wp-login.php,xmlrpc.php, and requests with an approximate bot/crawler User-Agent. This is a quick filter in the plugin. MetrikStack's own bot classification still labels bots from every source, whatever this setting. -
Behind a proxy: off by default. Enable only on a site that sits behind a reverse proxy or CDN you control, so server-side tracking reads the visitor IP from
X-Forwarded-For(falling back toCF-Connecting-IP) instead ofREMOTE_ADDR.
All fields go through the WordPress Settings API with nonces, the
manage_options capability and sanitize callbacks. The endpoint must be a
valid URL and the site ID must look like a UUID; invalid input is rejected
and the previous value kept.
How merging works
On a trackable front-end request (GET/HEAD, Accept: text/html,
non-asset path, front-end only, never admin/REST/cron/feeds/previews) the
plugin generates a UUIDv7 request id on template_redirect, before any
output, and sends:
X-MetrikStack-Request-Id: <id>response headerServer-Timing: metrikstack;desc=<id>response header<meta name="metrikstack-request-id" content="<id>">in<head>
like the Laravel package and the Node SDK. The browser script reads the
meta tag first, then falls back to
performance.getEntriesByType('navigation')[0].serverTiming. That still
works behind a CDN that caches the HTML (and the meta tag with it) but
sends fresh response headers.
Both sources then send the same request_id and are merged into one
pageview. The site's primary source counts the pageview, and the other
source adds only the fields the primary lacks (time on page, scroll depth,
screen size, language). See How merging works.
The headers and meta tag are sent on every trackable page load, whether or not Server-side tracking is on. A site that runs only the browser script today can add a server or edge source later without changes.
What is sent
Server-side tracking sends these fields and nothing else, as a
single-event batch to POST {endpoint}/v1/batch:
| Field | Source |
|---|---|
name | always pageview |
url | full URL, query string stripped to utm_* and ref only |
referrer | the Referer header |
request_id | UUIDv7 generated per request |
ts | UTC timestamp |
status_code | http_response_code() at shutdown, or 200 |
method | GET / HEAD |
ip | REMOTE_ADDR, or X-Forwarded-For/CF-Connecting-IP when Behind a proxy is on; hashed and discarded at ingest |
user_agent | the User-Agent header |
accept_language | the Accept-Language header |
Never sent: cookies, session data, form/request bodies, post content, authenticated user identity, or the full query string.
Delivery
The event is POSTed with Authorization: Bearer <token> from the
shutdown action, using wp_remote_post() with blocking => false and a
1 second timeout, after the response has reached the visitor, so tracking
adds no latency. There is no batching or retry: each tracked pageview is one
non-blocking POST.
This keeps the plugin simple and needs nothing extra on your server, but it
makes one outbound request per pageview, where the Node SDK and the Laravel
package send several events in one batch. If your traffic is high enough
for that to matter, batch server-side events yourself with WP-Cron: write
rows to a small custom table on shutdown, then POST them as one batch from
a wp_schedule_event callback every minute or so.
With most full-page caches, a cached response never runs
template_redirect, so it produces no server-side event; only the browser
script in the cached HTML fires. Every integration that runs on the origin
has this limitation under a page cache.
Verify
Once the plugin is active and has served at least one pageview, check the site's Sources page in the dashboard, or run this with the MetrikStack CLI:
metrikstack sources status <site>Look for a server source with a recent last event. If the browser script
is enabled too, the script source next to it should show about the same
pageview count for the same period, which means the two are merged rather
than counted twice.
Uninstalling
Deleting the plugin (not only deactivating it) removes its one option row,
metrikstack_wp_options, including on every site in a multisite network
(uninstall.php).
Laravel
Server-side analytics for Laravel 11 and 12. A Composer package that records pageviews on your own queue after the response has been sent.
Vercel
Vercel analytics without a script. Point a Vercel Log Drain at MetrikStack and every page request served by Vercel is counted, bots and AI crawlers included.