WordPress
Browser script injection and server-side pageview tracking for WordPress, as a plugin.
The WordPress plugin mirrors the Laravel package: it can inject the browser
tracker into wp_head, track pageviews server-side on template_redirect,
or both, with the same request-id merging so both sources count as one
pageview.
Requirements
- PHP 8.1+
- WordPress 6.4+
Installation
The plugin is not publicly downloadable during the private beta. 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 cheap opt-in filter, not MetrikStack's own bot classification: bots are still detected and labeled from every source regardless of this toggle. -
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, the site ID must look like a UUID, and invalid input is rejected
with 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>
exactly 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, which
survives a CDN that caches the HTML (and the meta tag with it) but passes
response headers through fresh.
Both sources then send the same request_id, and they are merged into one
pageview: the site's configured primary source counts as the pageview, the
other source contributes only the fields the primary lacks (time on page,
scroll depth, screen size, language). See How merging
works.
The headers and meta tag are emitted for every trackable page load regardless of whether Server-side tracking is switched on, so a site running the browser script only still cooperates with a server or edge source you add later in front of the same site.
What is sent
Server-side tracking sends exactly 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 already reached the client, so
tracking never adds latency. There is no batching or retry: it is one
non-blocking POST per tracked pageview.
Trade-off: this is simpler and adds no infrastructure, at the cost of
one outbound request per pageview instead of amortizing several into a
batch (as the Node SDK's in-memory buffer or the Laravel package's queued
job do). 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.
Under most full-page-cache setups, a cached response never runs
template_redirect, so a fully cached request produces no server-side
event; only the browser script (baked into the cached HTML) fires. This
matches the same limitation any origin-side integration has under a page
cache.
Verify
From the MetrikStack CLI, once the plugin is active and has served at least one pageview:
metrikstack sources status --site <site-id>Look for a server source with a recent last_event_at and, if the
browser script is also enabled, a script source alongside it with a
matching or near-identical pageview count for the same period, confirming
the two sources are merging rather than double-counting.
Uninstalling
Deleting the plugin (not just deactivating it) removes its one option row,
metrikstack_wp_options, including on every site in a multisite network
(uninstall.php).