React Server Components, explained like a kitchen
Server Components confuse people because the mental model is missing. Here's the kitchen picture that makes the split click, and the rules that keep it clean.
How I wired GA4, Google Tag Manager and self-hosted Umami with Consent Mode v2 in Next.js, plus the GTM consent bug that silently dropped every first visit.

Analytics can mislead in two directions: a banner can promise more privacy than the tags deliver, and a dashboard can look like a complete census when it only reflects people the tracker could observe.
I rebuilt the analytics on this site in a day. Cookieless aggregate measurement, Google Analytics only for people who say yes, and a Tag Manager container ready for ad pixels later. Along the way I hit a bug that would have zeroed out every first-time visitor in GA4, and nothing in the console said a word about it.
Here's the whole setup, and the trap.
Analytics is really two jobs pretending to be one.
The first job is counting. How many people came, from where, to which pages. Aggregate counts can be useful without setting tracking cookies. That does not make every implementation anonymous or exempt from privacy obligations.
The second job is understanding. Which campaign brought the founder who booked a call, which article they read first, which ad was worth the money. In this setup, I gate Google analytics and advertising measurement behind separate consent choices.
So I stopped asking one tool to do both.
| Job | Tool | Cookies | Needs consent |
|---|---|---|---|
| Aggregate visits | Umami, self-hosted | None by default | Assess configuration and applicable rules |
| Behaviour and attribution | Google Analytics 4 | _ga | Yes, Analytics |
| Ad measurement, later | Meta, Google Ads, LinkedIn via GTM | _fbp, _gcl and friends | Yes, Marketing |
Umami runs in a container next to the site, sets no cookies, and doesn't store IP addresses. Cookieless does not automatically mean consent-exempt. Assess what the configured tracker accesses, what you collect, and the rules applying to your audience. Counts can also miss blocked scripts, failed requests and Do Not Track visitors. Hosting it myself costs a few hundred megabytes of RAM on a server I already pay for. That's the jugaad version of a privacy-friendly analytics subscription.
Umami tells you that something happened. GA4, connected to Search Console and ad platforms, tells you why it might have. For a business that will run campaigns, that link is worth the extra consent step.
Consent Mode is Google's way of letting tags know what a visitor allowed. Four signals matter:
analytics_storage for analytics cookiesad_storage for advertising cookiesad_user_data for sending user data to Google for adsad_personalization for personalised advertisingThe rule that makes everything else work: set them all to denied before any tag loads. In a Next.js App Router project, that means a tiny inline script in the root layout's <head>, running before Google Tag Manager:
// Plain JavaScript in an inline <script>. Runs before GTM: everything starts
// denied, then a stored choice is applied.
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("consent", "default", {
analytics_storage: "denied",
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
wait_for_update: 500,
});The same script reads the visitor's saved choice from localStorage and sends a consent update straight away. A returning visitor with a valid saved choice can reuse it. Missing, outdated or malformed preferences should fall back to denied; storage may be unavailable, so that path needs testing too.
Google distinguishes basic consent mode, which blocks Google measurement tags until consent, from advanced mode, which can send cookieless pings while consent is denied. A denied default alone does not prove basic-mode behaviour. This site loads the GTM container separately from the measurement tags it controls; loading that container is itself a network request. The container configuration and observed requests must support the narrower promise that measurement tags are gated. Do not describe that as “nothing contacts Google before consent.”
The banner asks one honest question with two separate answers: Analytics and Marketing. The first view offers Decline and Accept as identical outlined buttons. A quiet "Choose what to allow" link opens a checkbox for each category.
Equal weight isn't decoration. A big bright Accept next to a faint grey "manage options" is the oldest dark pattern on the web, and privacy regulators keep saying so, including the UK's ICO guidance on cookies. If the banner has to trick people into saying yes, the data you collect isn't worth much anyway.
A few details that matter more than they look:
Here's the part that would have hurt.
Everything worked in testing. Accept, click a button, watch the event arrive in GA4. Then I tested the live site the way a real visitor behaves: land, read the banner, click Accept, then do nothing else. No page view reached GA4. Not one.
On the first page load, the GA4 tag in Tag Manager is triggered by "All Pages". At that moment consent is denied, so GTM blocks it. Correct.
Then the visitor clicks Accept. Consent becomes granted. And GTM does... nothing. It doesn't go back and fire tags it blocked earlier. My local test only looked fine because I also clicked a tracked button, and that event tag loaded GA4 on its own.
Push an event the moment the visitor chooses. In TypeScript, declare the two globals the inline script created, and call them optionally so nothing breaks when a blocker stops the tags loading:
declare global {
interface Window {
dataLayer?: unknown[];
gtag?: (...args: unknown[]) => void;
}
}
window.gtag?.("consent", "update", state);
window.dataLayer?.push({ event: "consent_update" });Then give the GA4 Google tag a second trigger: a custom event called consent_update, alongside All Pages.
I made one more mistake on the way. I set the tag to fire "once per page" to avoid a double page view. GTM counted the blocked attempt at page load as that one firing, so the tag still never ran. Switching to "once per event" fixed it, but that setting alone does not prevent duplicates. Stored choices and cross-tab updates can also produce consent events, so verify initialisation and page-view behaviour separately.
After that: first visit, Accept, one page_view. Returning visitor, one page_view. Decline, nothing at all.
Reset event parameters on every push. GTM's data model remembers keys from earlier pushes. If a "case study click" carries a slug and the next "contact click" doesn't, the contact click inherits the old slug. Every push now sets every known key, most of them to undefined.
Clear cookies from an older consent version. When the banner grew a Marketing category, old single-choice answers stopped counting. Anyone without a current choice gets their stale _ga cookies removed on load.
Keep Umami off localhost. The tracker is limited to the production domain, so development clicks never pollute real numbers.
Open the Content Security Policy deliberately. The policy lists exactly the hosts Tag Manager, GA4, Google Ads, Meta and LinkedIn need. Adding a pixel later is a container change, not a code deploy, and anything else stays blocked.
A banner screenshot cannot prove what scripts send. Use a fresh browser profile and test the complete sequence:
| Action | Evidence to inspect |
|---|---|
| First visit, no choice | Consent defaults and requests made before interaction |
| Decline | Blocked measurement tags; no later navigation bypass |
| Accept Analytics only | Analytics allowed; marketing remains denied |
| Withdraw | Future collection stops; removable first-party cookies are expired |
| Change choice in another tab | The open tab adopts the new choice |
| Browser storage unavailable | Defaults remain safe and the current choice still behaves honestly |
Check page views as well as tag firing: an initialisation trigger and a route-change trigger can accidentally count the same visit twice. Repeated consent events need deduplication or appropriately scoped triggers; “once per event” is not a duplicate-prevention strategy by itself.
Keep a small record of the container version, browser, consent state and observed requests. It makes campaign reporting more trustworthy and makes the privacy statement testable. Google documents the basic/advanced distinction; the network panel tells you which behaviour you actually deployed.
There is no universal answer based on the product name. Check the actual configuration, device access, collected fields and applicable rules. The ICO guidance linked above covers technologies beyond cookies and currently notes that its detailed guidance is being revised. This article describes engineering choices, not a determination that a particular installation is exempt.
Choose deliberately and verify the implementation. Basic mode blocks Google measurement tags until consent; advanced mode can send cookieless pings under denied settings. Loading GTM and blocking measurement tags are separate actions, so describe the network behaviour precisely.
Google Tag Manager blocks a tag when consent is denied at page load, and it doesn't retry that tag when consent changes later. Push a custom event such as consent_update after the consent update, and add it as a trigger on the Google tag with "once per event" firing.
If you only ever run GA4, probably not. Once ad pixels from Meta, Google Ads or LinkedIn enter the picture, GTM lets you add, change and gate them by consent without redeploying the site. The trade is that anyone with GTM access can inject scripts, so keep that access small.
They may be, but the gap is not a reliable decline rate. Consent, blocking, Do Not Track, session definitions, bot filtering and delivery failures can all change the totals. Compare like-for-like events and measurement windows before interpreting a difference.
Analytics that trick people produce numbers nobody should trust. Two tools, one honest banner and a few lines of code gave me useful aggregate counts and consented attribution, with limitations I need to keep visible. If you're building something and want measurement that a privacy-minded buyer won't flinch at, tell me what you're building. It's the same care I put into the stack behind this site and every web build, and it pairs well with traffic that turns into pipeline.
Was this useful?
No account needed. One like per browser; click again to undo.
Prabir Singh
Full-stack engineer in Mumbai. Twenty years in sales taught me why people buy; seven years of code taught me how to build what they'll pay for. I still read error messages the way I once read objections: as the most honest feedback of the day. More about me or start a project.
Server Components confuse people because the mental model is missing. Here's the kitchen picture that makes the split click, and the rules that keep it clean.
One language from the phone to the database. Why every project I take on is TypeScript end to end, and why I stopped apologising for it.
Express shaped a generation of Node APIs. Why my new backends start with Hono instead, where it changes how I build, and when Express still wins.