← Back to blog

Analytics counters do not belong in theme files

Published on
4 mins read
--- views

Tracking gets installed the fastest way available, which is a snippet pasted into a theme template. That decision is invisible for months and then produces three separate problems, none of which announce themselves.

It disappears

Theme code does not survive a theme. An update, a switch to a child theme, a redesign, a handover, and the counter is gone. Nothing errors. The dashboard just goes quiet, and by the time anyone notices you have lost a month of data you cannot backfill.

The same applies to the second counter someone added last year, in a different template file, which nobody can find now.

The fix is a plugin with a settings screen: an empty field means no output at all, which also gives you a clean way to keep a staging copy silent without touching code.

It competes with the page

Loaded normally, an analytics script asks for the browser's attention during the exact seconds a visitor is deciding whether your site feels fast. You are spending page-load responsiveness, which is itself a ranking signal, on measuring page-load responsiveness.

Wait for the browser to be idle instead:

if ('requestIdleCallback' in window) {
  requestIdleCallback(loadMetrika, { timeout: 2000 });
} else {
  setTimeout(loadMetrika, 2000);
}

The timeout matters. Without it, a page that never goes idle never loads the counter, and you have quietly stopped measuring your busiest pages, the ones that matter most. The fallback branch matters for the same reason: requestIdleCallback is not everywhere, and a feature check without a fallback is a silent hole in your data rather than a graceful degradation.

It double-counts

Snippets get pasted twice. Someone adds it to the theme, someone else adds it through a tag manager, and now every session is counted twice. Traffic appears to jump, and the jump is indistinguishable from a real one until somebody checks the HTML.

Guard the insertion:

for (var j = 0; j < document.scripts.length; j++) {
  if (document.scripts[j].src === r) {
    return;
  }
}

Five lines. Compare that against a quarter of decisions made on doubled numbers.

Escape everything, including the booleans

Counter ids and option values come from the database, and the database is edited by humans through a form. Escape the id, and normalise the booleans explicitly rather than interpolating whatever PHP had:

$clickmap_value = $clickmap ? 'true' : 'false';

PHP's false renders as an empty string inside a JavaScript object literal, which is a syntax error, which kills the whole script, including anything else that shares the file. This is a small detail that produces a total outage of measurement, and it is very hard to spot because the page still looks fine.

The free-form head block, and its cost

A settings field whose contents are printed into <head> covers site verification tags, ad platform snippets and one-off scripts. It stops routine marketing requests from being deploys, and it is genuinely useful.

Be honest about what it is: an administrator-only field that injects unescaped markup into every page. That is acceptable because administrators can already edit plugin files, so it grants no new capability. It stops being acceptable the moment a lower-privileged role can reach it, or the value comes from anywhere but a human typing into the admin.

Decide what you are collecting

Session replay records what visitors do on the page: movements, clicks, sometimes form input. Click maps and accurate bounce tracking are milder but still behavioural.

Making these toggles rather than flags buried in a snippet has a side effect worth naming: it makes the answer to "what are we collecting" a screen someone can look at, rather than an archaeology expedition through minified JavaScript. If your privacy notice has to describe this, and it does, then the configuration should be readable by the person writing it.

Open for contract collaboration

I am available for contract-based collaboration. If you have an interesting project idea, schedule a call via Calendly.

Schedule a 30-min call