GA4 Event Tracking Without Google Tag Manager
Every business site eventually needs event tracking: which button gets clicked, which form gets submitted, which links carry visitors away. For years the standard answer was Google Tag Manager — a container script that lets you add tracking without touching the site's code. This month, jkjrdev.com built out its event tracking and skipped that layer entirely. Every lead action on this site now reaches Google Analytics 4 from about 130 lines of the site's own code.
This article covers what was built, why code won over a tag manager for this site, and where a tag manager still makes sense.
Why Google Tag Manager became the default
Tag managers solved a real problem. When they appeared, changing a site's code meant a developer, a ticket, a release window. Marketing needed a pixel live this week, not this quarter. Once a developer installed the GTM container, tracking could be added and changed from a browser — no codebase access, no deploy, running in minutes.
That tradeoff made sense as long as code changes were the bottleneck. The costs were accepted as the price of speed:
- One more third-party script on every page, loaded before it does anything useful.
- A second system to manage — container versions, workspaces, trigger logic — separate from the site's own version history.
- Tracking logic that lives apart from the elements it measures. Rename a button or restyle a section, and a trigger built on a CSS selector quietly stops firing. Nothing errors. The data just stops.
Most small-business sites carry all three costs to send a handful of events.
What changed: coding is getting easier
The tag manager rests on one assumption: touching site code is slow and hard. That assumption is weakening. With AI-assisted development, adding an event listener is a described change and a reviewed diff, not a project. The judgment is still human work — what to measure, what to name it, what not to collect.
This is a method, not a promise. It says nothing about cost or profit. It says the implementation barrier is lower than it was, and this site is doing its best with that. What goes away is the reason to keep a separate tagging layer just to avoid touching code.
What code-owned event tracking looks like
The whole system on this site is one delegated click listener plus a naming convention. The listener sits on the document, looks at every click, and classifies it into exactly one event:
// one listener, one event per click, in priority order contact_click // tel:, mailto:, Calendly, Google Business Profile share // the social share buttons cta_click // internal links to /contact that carry a data-cta attribute outbound_click // any link leaving the site, with its destination domain
Elements that need identity carry one HTML attribute — data-cta="header", data-cta="home_hero" — and the listener reads it as the event's placement. The two contact forms send generate_lead on success (a GA4 recommended event name), plus form_start and form_error. Seven events, four registered parameters, no container.
Because the tracking lives in the codebase:
- It versions with the site. A button and its measurement change in the same reviewed commit. There is no second system to keep in sync.
- Coverage is deterministic. The listener runs in the capture phase, so it sees clicks even when a script intercepts the link and opens it programmatically — a case automatic click tracking can miss.
- Consent is enforced in one place. Analytics loads only after the visitor accepts it, and every event respects that gate because there is only one gate.
- Naming stays consistent. Event and parameter names are checked in code review, not by container discipline across a trigger list.
The GA4 console setup that makes it count
The code is half the system. GA4 receives everything the site sends, but surfaces none of it until the console is told what's coming. The setup is about fifteen minutes and five steps — and three of the five have traps worth knowing.
1. Register the parameters as custom dimensions, and do it first. Admin → Custom definitions → Create custom dimension, one per parameter, scope Event. The trap: registration is not retroactive. GA4 collects unregistered parameters but can never break out the days before you register them. Every day this step waits is a day of permanently unlabeled data, which makes it the first thing to do once events start flowing.
2. Mark the conversion events as Key Events. Admin → Events → Create event lets you register an event by name before it has ever fired: type the name exactly, toggle "Mark as key event," and choose Create with code — the site already sends the event; you are only telling GA4 the name. The trap is the other option: "Create without code" builds a new derived event from triggers, which is a different thing and double-counts. Two settings in the same panel: don't set a default key event value (a made-up dollar amount pollutes revenue reports; add a real one only when you know what a lead is worth), and keep the counting method at "Once per event" for every conversion so they sum on the same basis.
3. Turn off the automatic tracking the code replaced. Admin → Data streams → Enhanced measurement: switch off "Outbound clicks" and "Form interactions." The code's versions carry more detail, and leaving both on counts everything twice.
4. Activate — actually activate — the developer traffic filter. The site flags its staging deploys as developer traffic, and a GA4 data filter excludes that traffic from reports. The trap is the filter's default state, Testing, which only labels matching events while still counting them. A filter can exist and do nothing. Admin → Data filters → set the Developer traffic filter to Active (Exclude), and accept the permanence warning: dropped test events are gone for good, which for test events is the point.
5. Verify in DebugView with two windows. An incognito window on the staging site generates events from a clean consent state; your logged-in window on Admin → DebugView watches them arrive, seconds later, with every parameter attached. The first time a cta_click lands on the timeline with its placement in place, the whole chain is proven: code, consent gate, flagging, filtering, labels.
None of this needed a tag manager either.
Where Google Tag Manager still fits
The point is not that tag managers are obsolete. GTM still earns its place when:
- A marketing team ships and edits tags without access to the codebase.
- The site carries many third-party tags beyond analytics — ad platforms, remarketing, A/B tools — that change on marketing's schedule, not engineering's.
- Server-side tagging is in play.
- An agency manages tags across many sites it does not develop.
None of those describe a small-business site with a developer involved. For that site, the default has flipped: the simple path is now the direct one.
The bottom line
GTM exists so people can add tracking without touching site code. Touching code got easier here, so the layer earned nothing: one less script, one less login, one system of record. The measurement lives where the site lives.
JKJR Digital Development configures GA4 analytics and SEO and builds sites for businesses in Northern Virginia and the DC metro area. This article was drafted with AI assistance, then edited and verified by a human.