Skip to main content
  1. Blog/

Clarity and Google Analytics behind one consent gate

·1338 words·7 mins·
Rik Hepworth
Author
Rik Hepworth
Rik is CEO and Co-founder of Zure in the UK. He is a recipient of the Microsoft MVP Award and a Microsoft Regional Director. He is a member of the admin team for the Global Azure worldwide community event.
Rebuilding the blog with Claude - This article is part of a series.
Part 2: This Article
Note

This series of posts is both an account and an experiment. I got Claude to help me revitalise my blog: Move to a new Hugo theme; review, sort, and categorise the existing content; wire in analytics, which I didn’t have before. I also got it to create skills to help me maintain this site, and to help me generate new content without sacrificing my voice to generic ‘AI slop’. This series is my first experiment with some of those skills.

The last thing on the list when I rebuilt this site was analytics. I’ve gone without for a while, which is fine right up until you want to know whether anyone actually reads the thing you spent an evening writing.

What I didn’t want was the usual arrangement, where the site loads the tag, sets the cookie, and then displays a banner asking permission it has already taken. I also didn’t want to withhold the tags until someone clicks Allow, because then I learn nothing at all from the people who decline, and most people decline.

Both Microsoft Clarity and Google Analytics support a middle option, and the documentation for each of them is decent on its own terms. What neither covers is the case I actually had: two tags, one choice, a static site, and no tag manager to hide the wiring in. So this is a write-up of what I ended up with, and the three things that caught me out on the way.

Note

One of the benefits of working with AI, it turns out, is that if you have the commit history of the repo, and the documentation you got the AI to create during the process, you can make the AI parse all that and write up what was done. This post is an example of that, whilst also using the skill that at least tries to write like I do.

The shape of it
#

The tags both load on every page. Neither is withheld. What changes is that each one is told the reader’s consent state immediately, before it has a chance to write anything, and both of them respect that.

Until analytics storage is granted, Clarity runs in no-consent mode: each page view gets a one-off identifier that is never persisted, and no first-party or third-party cookie is set. Google Analytics sends cookieless pings that can’t be joined into a session. I still get page-level insight from readers who say no, which withholding the tag would not give me, and they still get a cookie-free visit. That trade felt like the right one.

Ad storage is denied in both, permanently and regardless of consent. There’s no advertising here, so there’s nothing to permit.

Consent is signalled explicitly rather than left to the vendors’ defaults. Google’s Consent Mode is only on by default for visitors in the EEA, the UK and Switzerland, and I’d rather everyone got the same cookie-free starting point.

Trap one: don’t let Hugo set up Google Analytics for you
#

Hugo has a built-in Google Analytics configuration key, services.googleAnalytics.ID, and Blowfish wires it into a template for you. It is exactly the kind of thing I would have used if I didn’t have Claude, and it would not have been the right solution.

That built-in tag loads gtag.js unconditionally, with no consent signal in front of it, and writes _ga on the first page view before the reader has been asked anything. Worse, if you set it and do your own thing, you tag every page twice.

The measurement ID lives in params.googleAnalytics.measurementId instead, which is my own parameter, read only by my own partial. Hugo’s key stays empty.

Trap two: ordering, and only for Google
#

Clarity’s loader queues any calls you make before its remote script arrives, so you can signal consent to it on the line after you load it and it’ll be honoured. That’s forgiving, and it hid the fact that the other one isn’t.

gtag has to be told the consent default before config runs, or the cookie is written on that first page view and you’re back where you started:

window.gtag("consent", "default", {
  ad_storage: "denied",
  ad_user_data: "denied",
  ad_personalization: "denied",
  analytics_storage: grantedAtLoad ? "granted" : "denied"
});

window.gtag("js", new Date());
window.gtag("config", GA_ID);

Note that the default is read from stored state rather than hardcoded to denied. Hardcoding it looks safer and isn’t: a reader who consented last week gets a denied ping on arrival and loses the start of their session before the update lands.

Clarity’s side of the same signal:

window.clarity("consentv2", {
  ad_Storage: "denied",
  analytics_Storage: granted ? "granted" : "denied"
});

Note the change in casing over the Google snippet - this is important and Clarity will silently ignore you if you get it wrong.

Trap three: withdrawing consent isn’t symmetric#

This is the one I would have got wrong on my own, because it’s the case nobody tests.

Clarity does the decent thing on clarity("consent", false): it erases its own cookies and ends the session, immediately, no reload needed. Google Analytics has no equivalent. Updating consent to denied stops it writing anything new, but everything already on the reader’s machine stays there.

So withdrawal clears _ga* by hand, and it has to do it on both the host and the domain, because that’s where gtag.js writes it:

var host = window.location.hostname;
var domains = ["", host, "." + host];
var parts = host.split(".");
if (parts.length > 2) {
  domains.push("." + parts.slice(-2).join("."));
}

It only runs when consent had previously been granted. There’s no point clearing cookies that were never set.

One state, one key
#

Note

I’ll be honest, I’ve not rewritten much in this post, even though part of me wants to. Claude seems to get very excited by its achievements in a way that I would not normally, at least in a post like this. That’s what human-in-the-loop is for - AI is great at the research but it isn’t me. ‘Rewrite it in your own words’ was drummed into me as a child when given assignments in subjects like history that involved research and I like to think of the AI generated content in the same way.

The thing I’m most pleased with is the least clever part. There’s a single window.siteConsent object with state, grant, deny and reset, backed by one analytics-consent key in local storage. The banner drives it. So does a shortcode on the privacy page, which lets you change your mind later and only offers you the choice that would actually change something.

Adding a third tag means adding a case to signal() and forget(). It does not mean a second banner, a second key, or a second opinion about what the reader wanted.

The banner’s wording and the privacy page prose both name the tools that are live, and they get that name from one partial that mirrors the same test used to decide whether to emit each tag. It’s a small thing, but it means the banner can’t tell you about Clarity on a build where Clarity isn’t loaded.

Both tags are production-only, so hugo server shows no tags and no banner at all. To see the consent UI locally without sending anything to the real projects:

HUGO_PARAMS_CLARITY_TRACKDEVELOPMENT=true \
HUGO_PARAMS_CLARITY_PROJECTID=previewonly \
HUGO_PARAMS_GOOGLEANALYTICS_TRACKDEVELOPMENT=true \
HUGO_PARAMS_GOOGLEANALYTICS_MEASUREMENTID=G-PREVIEWONLY hugo server

The caveat
#

I’m not a lawyer and this isn’t legal advice. It’s what I’m comfortable putting on my own site: nothing stored until you say so, ad storage never, and a privacy page that says specifically what each tool collects in each mode rather than gesturing at “we value your privacy”.

If you’re doing this for something with actual commercial consequences, get someone qualified to look at it. And check the vendor docs rather than trusting this post in a year’s time, because consent APIs are exactly the sort of thing that change over time.

Next in this series: the skills I’ve written so that none of these conventions have to be reconstructed from memory the next time I touch the site.

Rebuilding the blog with Claude - This article is part of a series.
Part 2: This Article