Skip to main content
ASoc
Comparison

Vercel Analytics vs Google Analytics 4: The Consent Banner Decides It

A banner-gated tool measures the subset of visitors who agreed to be measured. The cookieless trade, the typed event union, and the CSP origin we nearly added for nothing.

The ASoc Team10 min read

Pick your analytics by the consent banner, not the feature list. Google Analytics 4 sets cookies, so in the EU and UK it needs consent — and a banner that suppresses it until someone clicks Accept measures a self-selected sample. Cookieless tools skip the banner entirely and count everyone, at the cost of cross-session identity.

That trade is the whole decision. Everything else on the comparison table is a preference; this one changes what your numbers mean.

The measurement most teams never make

A consent banner does not reduce your data by the share of people who decline. It reduces it by the share who decline plus the share who ignore the banner and browse anyway, plus everyone who bounced before the banner rendered. Those three groups are not a random sample of your audience — they skew toward the impatient, the privacy-conscious, and the mobile visitor on a slow connection, which is to say the people whose experience you most need to see.

If your analytics requires consent, you are not measuring your site. You are measuring the subset of visitors who agreed to be measured.

The comparison that matters

Vercel Web AnalyticsGoogle Analytics 4Plausible / Fathom / Umami
CookiesNone_ga by defaultNone
Consent banner in EU/UKNot required for the analytics itselfRequired in practiceNot required for the analytics itself
SampleEvery visitorPost-consent subsetEvery visitor
Cross-session identityNoYesNo
Custom eventsYes, name + flat propsYes, rich parameter modelYes, name + props
Funnels, cohorts, audiencesNoYesLimited or none
Ads / remarketing integrationNoYes, this is the point of itNo
Where the script comes fromInjected at the edge on Vercelgoogletagmanager.comVendor CDN or self-hosted
Extra CSP origins neededOne script-src hostTwo or more, incl. connect-srcOne, or none if self-hosted
Cost modelPer event, by planFree at storefront volumePaid, or free self-hosted
Data export / portabilityLimitedBigQuery exportVaries; self-host means you own it

Read that table from the bottom row up if you run ads. GA4's remarketing and conversion-import integration is a genuine, hard-to-replace capability, and no cookieless tool substitutes for it. If paid acquisition is your channel, you are going to run GA4 and a consent banner, and the right move is to accept that and instrument carefully rather than pretend a cookieless tool covers it.

If your acquisition is organic — search, content, word of mouth — the cookieless option measures more of your actual audience and costs you a cross-session identity you were probably not using.

What we run, and why

This storefront is organic-acquisition and statically rendered, so it runs Vercel Web Analytics with no consent banner. Two lines:

// src/app/layout.tsx
import { Analytics } from "@vercel/analytics/next";

// …inside <body>
<Analytics />

The script is injected by Vercel at the edge, on real deployments only. That has one consequence worth knowing before you spend an afternoon on it, covered under Mistakes below.

Custom events are where the value is, and the type is the discipline

Pageviews tell you a page is popular. They do not tell you it earned anything. The events are what connect content to revenue, and the single highest-leverage decision is to make the event names a closed union rather than free strings:

// src/lib/analytics.ts
import { track as vercelTrack } from "@vercel/analytics";

type ConversionEvent =
  | "preview_opened"
  | "buy_clicked"
  | "waitlist_joined"
  | "blog_cta_clicked"
  | "wishlist_added";

export function track(
  event: ConversionEvent,
  props?: Record<string, string | number>,
) {
  try {
    vercelTrack(event, props);
  } catch {
    // Analytics must never break UX — swallow any client-side error.
  }
}

Five names, one per funnel step. A typo is a build error, not a new event that quietly splits a metric in half for three months before anyone notices. Every event-tracking system decays the same way — buy_click, buyClicked, buy_clicked_v2 — and the union is a two-line fix for it.

The try/catch is not defensive padding. An analytics call sits inside a click handler on a buy button; a throw there is a broken purchase.

The props are the reporting

The event that pays for the blog is this one:

track("blog_cta_clicked", { post: postSlug, target: productSlug });

Both halves matter. With only post you learn that an article converts but not to what. With only target you learn a product gets blog traffic but not which article sent it. With both, every article has a per-destination click-through rate, which is the number that decides what to write next.

Compare that to session-scoped reporting, where a reader who lands on an article, browses four products and buys one shows up as "the blog contributed to a session". Per-event attribution answers the editorial question; per-session attribution answers nothing you can act on.

One rule: never pass PII

No email, no name, no user id, ever, in an event prop. This is not only a privacy rule — it is what keeps the tool cookieless in substance and not just in mechanism. A cookieless analytics stream that carries a stable user id has re-identified the visitor and re-acquired every obligation you switched tools to avoid.

The third-party script tax nobody budgets for

Anything you mount in the root layout runs on every page, including the ones that do not need it. We learned this the expensive way with our own dependencies rather than with an analytics vendor: the Supabase auth client was imported at module scope by the site header, which put @supabase/ssr plus auth-js68 KiB gzipped, 255 KiB parsed — into the initial bundle of the home page, the blog, the docs and the pricing page. Four routes with no account UI on them at all, parsing an auth SDK before they could paint.

A tag manager in the root layout is the same shape of mistake with a bigger constant. Before you add one, decide which pages actually need the measurement, and check what the script costs on a phone rather than on your laptop.

The CSP consequence, and an allowlist trap

Any analytics you add has to be named in your Content Security Policy, and this is where allowlists quietly rot. Ours needs exactly one extra origin:

script-src 'self' 'unsafe-inline' … https://va.vercel-scripts.com
connect-src 'self' <supabase-origin>

The instinct is to add va.vercel-scripts.com to connect-src as well — the script is loaded from there, so surely it reports back there. Reading the package source showed the beacon posts to a first-party path on the site's own origin, already covered by 'self'. The extra origin would have been permanent, invisible, and pure attack surface.

The rule that falls out: every origin in the policy must be traceable to a line of code that loads it, with the reason written next to it. GA4 legitimately needs more origins than a first-party beacon does; that is a real cost of the choice, not a reason to avoid it. Our full policy and why it lives in next.config.ts rather than middleware is in the static-rendering CSP post.

Mistakes and how they show up

MistakeWhat happensFix
Reading local Lighthouse as a real scoreBest Practices caps at 96 — the beacon 404s locallyRe-measure on the deployment; it reads 100 there
Expecting events in next devThe script only exists on a real deployment; track() no-opsVerify on a preview deployment, not localhost
Free-string event namesMetrics silently fork across near-duplicate namesClosed union type on the track wrapper
Event name only, no propsYou see that something converted, not what toCarry source and destination in every funnel event
Tracking removals as well as addsCounts become uninterpretableInstrument the funnel step, not the toggle
PII in propsRe-identifies the visitor; consent obligations returnSlugs, tiers and counts only
Analytics call outside try/catchA vendor error breaks a buy buttonWrap it; analytics must never break UX
Adding the vendor's host to every directiveAllowlist grows past auditabilityOne origin per line of code that loads it
Banner-gated analytics treated as complete dataDecisions made on a self-selected sampleEither accept the bias explicitly or go cookieless

Frequently asked questions

Is cookieless analytics automatically GDPR-compliant? No — "no cookies" removes the ePrivacy consent trigger for storage on the device, but you are still processing data and still owe a privacy notice and a lawful basis. What it removes is the banner, which is a UX and data-quality win, not a compliance exemption. Check the specifics with someone qualified for your jurisdiction; this is an engineering post, not legal advice.

Can I run both? Yes, and for a site with paid acquisition it is common: GA4 behind consent for ads and audiences, a cookieless tool unconditionally for the honest traffic picture. The cost is two sets of numbers that will never agree, so decide in advance which one is the source of truth for each question and write it down.

Why not just self-host something? It is a good option — Umami or Plausible self-hosted means the data never leaves your infrastructure and the script can be first-party, which removes the CSP origin entirely. The cost is that you now operate a database and a dashboard. Worth it if data ownership is a requirement; not worth it to save a subscription.

Does an analytics script hurt Core Web Vitals? A small, deferred, first-party beacon is close to free. A tag manager loading several vendors is not, and it lands on the main thread during hydration, which is exactly when a mobile device is busiest. Measure it under Lighthouse's mobile preset — 4× CPU throttle on slow 4G — because on a desktop connection almost nothing looks expensive.

How do I know my events are actually firing? Deploy to a preview URL and click the funnel yourself with the network panel open. Local verification is impossible by design here, and a "the event never appeared" bug is almost always someone testing on localhost.

Templates with the measurement surface already built

Instrumentation is easiest when the page already has the conversion points in it. Three of ours are built around live dashboard previews, which is the pattern that makes an event worth firing in the first place.

ASoc Ledger is a financial-management SaaS site built around a live analytics dashboard preview with net-sales trends and connected trackers. ASoc Beacon markets mobile-device-management software and leads with a device-console mock — enrolled devices, compliance state, OS split. ASoc Relay is a messaging-platform site with a live chat-widget hero and outcome stats across 142 countries.

Browse the full set of Next.js landing page templates or the Tailwind landing page templates. If you are wiring the events into forms, the Server Actions contact-form post covers the submission path they hang off.

Keep reading

Comparison7 min read

Vercel vs. AWS Amplify: The Bundled Backend Is the Real Question

13 runtime dependencies, zero AWS SDK packages, and a build that splits 392 static pages from 8 dynamic routes with no deployment config authored for either.

Read more
Comparison9 min read

Vercel vs Cloudflare Pages: Count the Routes That Need a Runtime

414 prerendered pages any CDN serves the same way, 8 dynamic routes, and exactly 2 that pin the Node runtime for node:crypto. Those two lines are the whole decision.

Read more