What's the difference in one line?
Client-side tracking sees context; server-side tracking sees truth. The browser knows the UTM parameters, the referrer, the device, and the page — and loses events to blockers. Your back end knows what actually happened — a subscription started, a payment failed — and knows nothing about how the user got there. This guide covers where to instrument. For what to track — naming and the starter event list — see the event tracking plan for startups; for why the data should be first-party at all, see first-party data and tracking. Both halves belong in one tracking plan.
What does client-side tracking see — and miss?
Client-side is where context lives. The SDK in the browser captures UTM parameters, the referrer, the landing page, the device, and session behavior — everything attribution and funnel analysis need. What it misses is whole users: per GWI data aggregated by Backlinko, 29.5% of internet users worldwide used an ad blocker at least sometimes in Q2 2025 — roughly 1.77 billion people. Many blockers stop analytics scripts along with ads, so a measurable share of your client-side events is never sent at all.
Even without a blocker, Safari makes client-side tracking lossy. Per WebKit's ITP documentation, Safari caps JavaScript-set cookies to a seven-day expiry and deletes all other script-writable storage — localStorage, IndexedDB — after seven days of Safari use without interaction on your site. Once that seven-days-of-use window passes, a returning visitor looks brand new, and identity breaks. The loss is measurable: when Plausible compared its own counts against Google Analytics on a site trending on Hacker News and Reddit in a late-August 2021 test, Google Analytics missed 58.67% of visitors — an extreme, tech-heavy case; Plausible cites earlier research putting blockage under 10% on lifestyle sites and above 25% on tech-focused ones.
What does server-side tracking see — and miss?
Server-side tracking is your back end reporting state changes to the events API: subscription_started, payment_failed, plan_changed. These events happen on your servers, not in the browser — recording them there is not a workaround, it is the honest source. No blocker sits in the path, no storage cap expires them, and the count matches your database because it comes from your database. A server-side event is authoritative because it records what your system did, not what a browser managed to report. The blind spot is context: your back end has no idea which campaign, referrer, or device brought the user in — that context only ever existed client-side.
Client vs server: which events go where?
| Event class | Track it | Why |
|---|---|---|
| Page views & campaign attribution | Client | UTM, referrer, and landing page only exist in the browser. |
| In-product feature usage | Client (server too if critical) | UI context matters; add a server copy when a metric depends on it. |
| Signup & login | Both | The identity join point — the same user ID on both paths. |
| Revenue & subscription state | Server | Money events happen in your back end; the count must match your database. |
| Email, webhook & integration events | Server | They arrive as webhooks from your providers — nothing client-side to capture. |
How should a small team split it?
- Anything money — trials, subscriptions, payments, refunds — goes server-side. When churn or MRR must be right, the server is the source of truth.
- Anything attribution — page views, campaigns, referrers — stays client-side. That context cannot be reconstructed later.
- Identify on both paths with the same user ID, so funnels stitch and A/B tests with low traffic assign each user to one variant across devices.
- Accept the gap. Client counts run low by design; use them for direction, and server events for anything you report.
Why do the two numbers never match?
Run both and the counts will disagree — permanently. The client number runs low because blockers and ITP eat events; the server number lacks the context to say where users came from. This is not a bug to fix but a property to name: expect a persistent gap between client analytics and server truth, know its rough size for your audience, and stop chasing it. Report each number from its honest side — attribution and funnel direction from client data; anything that reaches an investor deck or an invoice — churn rate, MRR, active subscriptions — computed from server events. Engagement platforms like Iterable and Ortto accept both sources for the same reason.
How does fromHello handle it?
fromHello treats the split as the default. The JS/TS SDK tracks client-side and keeps an offline queue backed by localStorage — events fired during a connection loss persist and are retried when the browser comes back online — while the same events API accepts server-side sends, so subscription_started comes straight from your back end with an API key. Both paths write to one profile: the attribution the SDK captured and the revenue events your server sent describe the same person. Segments, journeys, and analytics read from that single profile — you split the instrumentation, not the identity.