GA4 makes you learn its event model and a cookie banner to know if a signup button got clicked. Here are the two lines of code that answer that question without either.
A pageview tells you someone loaded a URL. It doesn't tell you whether they clicked "Start free trial," submitted a form, or bailed out through an outbound link. That's what a custom event is for, and you don't need GA4's event model, Google Tag Manager, or a cookie banner to record one.
data-gm-event="signup-click" to any element for automatic click tracking, or call window.ghostmetrics.track('name', { section: 'context' }) from your own JS for form submits and custom logic.The rest of this guide is the code, the naming convention, and the honest limits — what an event does and doesn't store, and when you actually need GA4 instead.
A pageview is automatic: someone requests a URL, the script fires, a row lands in your dashboard. A custom event is deliberate — you decide it matters, you wire it up, and it fires when a specific action happens: a click, a submit, a scroll past a certain point, a JS function completing. Pageviews tell you where people went. Events tell you what they did once they got there. If your dashboard only has pageviews, you can see that your pricing page got traffic — you can't see whether anyone actually clicked "Buy."
Most sites over-engineer this. You don't need thirty events to run a business. You need a short list that maps to the decisions you'll actually make. For the majority of marketing sites, SaaS landing pages, and content sites, five events cover it:
Pick from that list before you write any code. Every event you add beyond it is a maintenance cost with no matching decision behind it — a lesson that applies just as much to measuring basic website traffic as it does to events: more numbers don't automatically produce more insight.
There are exactly two mechanisms, and which one you use depends on whether the action is a plain click or something driven by your own JavaScript.
Add a data-gm-event attribute to any clickable element. GhostMetrics scans the page after DOMContentLoaded and wires up a click listener automatically — no event-name registration, no dashboard configuration step, no code beyond the attribute itself. One limit to know: that scan runs once. Elements added to the page later — by a client-side router or a JS widget — aren't picked up, so use the track() call below for anything that renders after load.
<button data-gm-event="signup-click">Start free trial</button>
That's the entire implementation. The click is recorded with the page path it happened on and any UTM values already in the session. Use this for anything that's a simple click: a CTA button, a nav link, a pricing-tier button.
When the action isn't a plain click — a form submit, a multi-step signup flow, a condition your own code evaluates — call window.ghostmetrics.track() directly. It takes an event name and an optional object where only one key, section, is stored as metadata.
window.ghostmetrics.track('checkout-start', { section: 'pricing' });
Here's that call wired to an actual form submit handler:
<form id="checkout-form" action="/checkout" method="POST">
<input type="email" name="email" required>
<button type="submit">Start checkout</button>
</form>
<script>
document.getElementById('checkout-form').addEventListener('submit', function () {
window.ghostmetrics.track('checkout-start', { section: 'pricing' });
});
</script>
The listener fires the event, then lets the form's own submit proceed normally — no preventDefault(), no waiting on a network callback before the user moves on. The same pattern covers outbound links. GhostMetrics sends via navigator.sendBeacon, which the browser delivers even as the tab navigates away — so this works whether or not the link opens in a new tab:
<a href="https://partner-site.example" target="_blank" rel="noopener external"
onclick="window.ghostmetrics.track('outbound-click', { section: 'partner-directory' })">
Visit our partner
</a>
Be honest with yourself about what section is: one short string of context, not a general-purpose properties bag. There is no revenue or value field, and no way to stitch one visitor's checkout-start today to their purchase next week — GhostMetrics events don't carry a cross-session identifier. For a yes/no conversion count, that's the whole job. For a funnel that spans multiple visits, it isn't — more on that in the GA4 section below.
Every recorded event lands in the dashboard's Custom Events panel, listed by event name with a count and a trend over time. Each event carries the page path it fired on, so if you need the homepage-CTA-vs-blog-button breakdown, export the raw rows (CSV or JSON, every plan) and group by path — the panel itself shows the name, the count, and when it last fired.
Each event also carries whatever first-touch UTM values were present in that visitor's session: utm_source, utm_medium, utm_campaign. These are captured once, at the first pageview of the session, stored in sessionStorage, and attached to every event and pageview that follows in that same tab. Close the tab and the session — and the UTM data with it — is gone. There's no cookie holding it open across visits.
That gives you a real, if bounded, attribution answer — the catch is that you do the joining, not the dashboard. Export the raw event rows (CSV or JSON, on every plan) and group by utm_campaign, and you can say "campaign X produced Y submits this week" without setting a single cookie. What you can't do is attribute an event to a campaign from three weeks ago if the visitor closed their browser in between — that requires a persistent identifier GhostMetrics deliberately doesn't set.
An event list rots the same way a spreadsheet does: one inconsistent name at a time. A small convention up front keeps five events readable a year later, when a dozen people have touched the codebase.
signup-click, not signupClick or Signup_Click. One format means you can scan the Custom Events panel without mentally normalizing names.form-submit, file-download, outbound-click — the action first or the object first, pick one order and stay consistent. Reversing it event-by-event ("submit-form" here, "download-file" there) makes the list harder to sort mentally.section for the variant, not the event name. One pricing-view event with section: 'pro-tier' or section: 'enterprise-tier' beats two separate event names for the same underlying action.The reason event tracking usually doesn't need a consent banner comes down to what's actually stored. A GhostMetrics event is a name, a page path, and optionally a UTM value and a section string — a count added to a tally. There's no cookie, no persistent visitor ID, and no identifier that lets you join today's signup-click to next Tuesday's form-submit from the same person. Without that join, there's no individual being tracked across time — just aggregate counts of an action happening.
Cookie banners exist to get consent for non-essential cookies — tracking pixels, cross-site identifiers, anything that lets a site (or an ad network) recognize the same person later. A cookieless event doesn't set any of that, so there's frequently nothing that requires asking permission for. We cover the legal mechanics in more depth in do I need a cookie banner, and the broader cookieless setup — pageviews and events together — in web analytics without a consent banner. Both come with the same caveat this section does: this is general guidance, not legal advice, and it doesn't cover ad pixels, embeds, or other cookie-setting scripts you might also be running.
Every analytics tool with an events feature approaches it a little differently — in how you fire an event, whether "goals" or "conversions" are a separate configurable concept, whether you'll need a cookie banner in the EU, and how the tool is priced. Here's the honest, qualitative comparison.
| Tool | How you fire an event | Goals/conversions | EU consent banner | Pricing model |
|---|---|---|---|---|
| GhostMetrics | Declarative attribute or one JS call | Custom Events panel, no separate goal config | Generally not required (cookieless) | Free-forever single site; flat paid tier for more |
| Plausible | JS call (plausible('name', {props})) or built-in outbound-link/file-download tracking | Yes — dashboard Goals tied to events or pageviews | Generally not required (cookieless) | Hosted subscription tiered by pageviews, or self-host free (open source) |
| Fathom | JS call (fathom.trackEvent('name')) | Yes — dashboard-configurable goals | Generally not required (cookieless) | Hosted subscription tiered by pageviews |
| Umami | Declarative attribute (data-umami-event) or JS call | Basic event counts; no dedicated funnel builder | Generally not required (cookieless) | Free to self-host (open source); hosted plan available |
| Matomo | JS API call or Tag Manager | Yes — goals and funnels comparable to GA | Often required by default; configurable toward cookieless | Free self-hosted (open source) or hosted subscription tiered by traffic |
| GA4 | gtag('event', ...) or Google Tag Manager | Yes — mark any event as a Conversion, plus Explorations/funnels | Yes — sets first-party cookies by default, with ad-platform identity available on top | Free with usage limits; paid enterprise tier (GA360) |
Where each genuinely wins: Plausible and Umami are the picks if open-source self-hosting or EU-only data residency is a hard requirement. Matomo gets you closer to GA-level funnel depth while keeping the option to self-host. GA4 wins on raw configurability and ad-platform integration once your event tracking needs go beyond counting yes/no actions. For a broader head-to-head, see our GA4 vs Plausible vs GhostMetrics comparison.
When you actually need GA4. If your events need to feed a deep e-commerce funnel — cart adds, checkout steps, refunds, revenue per event, product-level performance — GA4's event model has the fields for that and GhostMetrics doesn't (no value/revenue field, no cross-session funnel). Same story for ad-platform attribution: importing conversions into Google Ads, multi-touch attribution modeling across channels, or feeding Meta/Google's own optimization algorithms all expect GA4's or a tag manager's data structure. If what you need is "did this button get clicked, from which campaign, how often" — the simpler tools above, including ours, are enough. If you need "which a specific order came from which ad, three touches later" — use GA4.
signup-click, form-submit, and one of pricing-view, download, or outbound-click.data-gm-event on the buttons and links, and a one-line window.ghostmetrics.track() call into the submit handlers or JS logic that needs it. No Tag Manager container to build, no triggers to configure.pricing-view count that's a fraction of your pricing page's pageviews, or a form-submit count near zero, usually means the attribute is on the wrong element or the JS never fires — not that nobody's converting.That's the whole setup. No consent banner to add because you added events, no Tag Manager to learn, and the same script tag you already use for pageviews is doing the work.
GhostMetrics is our own cookieless analytics tool, and custom events are part of the free-forever plan — not a paid add-on. Add a data-gm-event attribute or one track() call, and it shows up in the Custom Events panel with the page path and any session UTM attached, no cookie and no consent banner required.
Need more than one site, or a shareable public dashboard? Pro is $9.99/mo flat with a 30-day free trial. Raw event export to CSV or JSON works the same on both plans. See real events fire in the live demo before you decide anything.
Start free — no card, no banner See the live demoDisclosure: GhostMetrics is built by Null Agency and we use it on this site, so we're not a neutral party about our own product — we've kept the claims to what the free and paid plans actually do, and named where GA4, Matomo, Plausible, and Umami fit better. Nothing here is legal advice; confirm your own privacy and cookie obligations for your jurisdiction.