GA4 replaced Universal Analytics in 2023. The migration was rough — the UI is busier, the data model is event-based instead of session-based, and the default dashboards bury what SMB owners actually want to know. Two years in, the pattern that works has settled. Five conversion events, three custom dimensions, two dashboards. Here’s the configuration we run across the FH client book.
The data model shift you have to internalize
Universal Analytics was session-based: a session contained pageviews, events, transactions, and time-on-site. GA4 is event-based: everything (pageview, click, form submit, scroll) is an event. Sessions are derived from event timing. Once you accept this, the reporting starts to make sense — you’re always asking ‘what events happened, on what page, for what user.’
The five conversion events for SMB sites
- `form_submission` — a lead form was successfully submitted. Fires on the thank-you page or in a server action callback.
- `phone_click` — someone clicked your phone number link. Fires on any `tel:` link click.
- `email_click` — someone clicked an email link. Fires on any `mailto:` link click.
- `chat_started` — someone opened your chat widget (if you have one).
- `booking_scheduled` — someone completed a booking flow (if applicable).
That’s it. Don’t add `scroll_depth`, `engaged_session`, `outbound_click`, or any of the dozen other defaults GA4 surfaces. Those are noise for SMB reporting. Track the events that produced the customer outcome you care about, ignore the rest.
Wiring events in a Next.js app
GA4 fires events via `gtag('event', name, params)` or by sending to the Measurement Protocol from your server. We use both: client-side gtag for interactions, server-side for form_submission (after validation succeeds in the server action).
// app/components/Analytics.tsx — client component
"use client";
import Script from "next/script";
export function Analytics() {
const id = process.env.NEXT_PUBLIC_GA4_ID;
if (!id) return null;
return (
<>
<Script src={`https://www.googletagmanager.com/gtag/js?id=${id}`} strategy="afterInteractive" />
<Script id="ga4-init" strategy="afterInteractive">{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${id}', { send_page_view: true });
`}</Script>
</>
);
}
// Track a click
export function PhoneLink({ number }: { number: string }) {
return (
<a
href={`tel:${number}`}
onClick={() => (window as any).gtag?.("event", "phone_click", { number })}
>
{number}
</a>
);
}Marking an event as a conversion
In GA4 → Admin → Events, find your event, toggle ‘Mark as key event.’ This is the GA4 term for what used to be called ‘conversion.’ Key events appear in conversion reports and the homepage scorecard. Mark the five events above; don’t mark anything else.
Server-side firing from a server action
Client-side gtag is unreliable — ad blockers, slow page loads, and JavaScript errors all lose events. Critical conversion events (form_submission especially) should fire server-side via the Measurement Protocol.
// app/contact/actions.ts
"use server";
import "server-only";
async function fireGA4Event(name: string, params: Record<string, unknown>, clientId: string) {
await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA4_MEASUREMENT_ID}&api_secret=${process.env.GA4_API_SECRET}`,
{
method: "POST",
body: JSON.stringify({
client_id: clientId,
events: [{ name, params }],
}),
}
);
}
export async function submitLead(formData: FormData) {
// … validate and insert …
const clientId = formData.get("_ga_client_id") as string ?? "server";
await fireGA4Event("form_submission", { form: "contact", lead_score: score }, clientId);
return { ok: true };
}Custom dimensions you should set up
- `page_type` (event-scoped): homepage, service, blog, location, contact. Lets you slice every event by page type without parsing URLs.
- `tenant_id` (user-scoped): your `site_id` if you share GA4 across tenants. Lets you filter every report to one tenant.
- `lead_score` (event-scoped): the score computed at form submission. Lets you weight conversions by quality.
The two reports we actually use
GA4’s default reports are mostly noise for SMB use cases. We replace them with two custom Explorations:
- Acquisition by Source/Medium with conversion rate — which channels are bringing leads, weighted by the lead_score dimension.
- Landing page performance — which entry pages convert, broken out by Source/Medium so we can see ‘paid landing page converts 4x organic landing page’ kinds of insights.
Joining GA4 with Search Console
GA4 → Admin → Search Console links → connect your GSC property. After 24 hours, you get a Search Console report inside GA4 that joins query data with conversion data. This is the closest thing to ‘which organic queries actually convert,’ and it’s the most valuable joined view in SMB analytics.
Consent mode v2
Google requires Consent Mode v2 for ads-data flow from European users. Even if your audience is mostly US, install it — it’s a one-time setup and it handles future regulatory shifts cleanly. The consent mode post walks the wiring.
Filtering internal traffic
GA4 → Admin → Data Streams → Configure tag settings → Define internal traffic. Add your office IP, your team’s VPN IPs, anyone whose traffic shouldn’t pollute the data. Then exclude internal in the Data Filters. Without this, every team member’s visits show up as legitimate user behavior — and you’ll wonder why your blog has 200 sessions on Wednesday afternoons.
What about pageviews?
GA4 fires `page_view` automatically. It’s useful for traffic-volume reporting. Don’t treat it as a conversion — pageviews are not value. We report pageviews only in trend context (‘blog traffic up 18% YoY’), never as a primary KPI.
GA4 vs Plausible vs Fathom
Privacy-focused analytics (Plausible, Fathom, Simple Analytics) are easier to use, easier to read, and don’t require consent banners. They lack GA4’s depth on attribution and audience analysis. For most SMB sites, Plausible is plenty — we recommend it to clients who don’t need ads-data flow and want a clean privacy posture. GA4 is the right tool when you’re running Google Ads (the integration is too valuable to give up) or need the BigQuery export.
BigQuery export
GA4 → Admin → BigQuery Links → connect. Every event flows into a BigQuery table. Free up to 1M events per day. Lets you SQL-query your analytics, join with other data, and answer questions GA4’s UI can’t. We use this on two retail clients with substantial volume.
How this lands across FH client work
Every FH client site has GA4 wired with these five events, the three custom dimensions, the two custom Explorations, and the GSC join. The reporting takes 10 minutes a week. The data answers ‘what channels produce leads that close’ — which is the only question SMB analytics needs to answer well. If your GA4 is wired with the defaults and you don’t know what to look at, book a consultation — the configuration is a one-day engagement that ships a reporting setup you’ll actually use.