ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOPMulti-Platform Sync

The Developer Publishing Hub. Write once, publish everywhere, and make your work citation-ready with built-in SEO, AEO, and GEO discovery support. Zero reader paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Changelog
  • Compare Platforms
  • Hashnode vs ZyVOP
  • DEV vs ZyVOP
  • Developer API & CLI
  • Author Handbook
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
All systems operational
HomeAnalytics Is Architecture: Stop Sprinkling track() Everywhere

Analytics Is Architecture: Stop Sprinkling track() Everywhere

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)
Staff Frontend Engineer
July 31, 2026Updated August 22, 2026
5 min read
Analytics Is Architecture: Stop Sprinkling track() Everywhere
#react#React Playbook#Architecture
👍1

Analytics Is Architecture: Stop Sprinkling track() Everywhere

This cluster has been about how the parts of an app communicate — event buses for ambient signals, feature flags for toggling behavior. Both articles kept circling the same lesson: cross-cutting concerns want a typed contract and a single home, not a scatter of stringly-typed calls. Analytics is the purest example of that lesson, and the one almost everyone gets wrong first — myself included, years ago.

Here's what analytics looks like in most codebases I've opened. A track('button_click') in one component. An analytics.logEvent('Button Clicked') in another. A raw window.gtag('event', 'cta') inline in a third because someone was in a hurry. Event names that mean the same thing spelled three ways. Payloads where one place sends userId and another sends user_id and a third helpfully includes the user's email into a third-party vendor. It works, in the sense that events show up somewhere. But it's un-auditable, un-typed, vendor-locked, and a privacy incident waiting to happen.

That's not a tracking problem. That's an architecture problem.

Analytics is a cross-cutting concern

Analytics has the same shape as logging, i18n, and error reporting: it touches almost every part of the app, but it belongs to none of them. And the smeared-component article named exactly this failure mode — logic that should live in one layer instead gets spread thin across the whole UI until you can't change it in one place. A track() call baked into a button's onClick is analytics logic living in the wrong layer. The button's job is to be a button. Knowing that clicking it should emit a "checkout started" event to three vendors is not the button's concern.

If you can't answer "what does this app track, and where does that data go?" by opening one folder, your analytics isn't a layer — it's a rash.

The fix is the same as every other cross-cutting concern in this series: pull it into a dedicated layer with a clear, typed boundary, and let the rest of the app talk to that boundary instead of to a vendor SDK.

A typed event catalog

Step one is to stop passing strings and start declaring events, once, with typed payloads. This is the same move that tamed the event bus: a catalog the compiler knows about.

// the single source of truth for everything the app can track
type AnalyticsEvents = {
  'checkout_started': { cartValueCents: number; itemCount: number };
  'checkout_completed': { orderId: string; amountCents: number };
  'search_performed': { query: string; resultCount: number };
  'signup_completed': { plan: 'free' | 'pro' | 'team' };
};

export function track<K extends keyof AnalyticsEvents>(
  event: K,
  properties: AnalyticsEvents[K],
) {
  analyticsBus.emit(event, properties);
}

Now track('checkout_started', { cartValueCents: 4200, itemCount: 3 }) autocompletes, the payload is type-checked, and a misspelled event name is a compile error instead of a silently-dropped data point six weeks of dashboards later. AnalyticsEvents is the catalog — one file that answers "what does this product measure?" That question having a single, readable answer is worth more than any individual event.

Decouple your code from the vendor

Step two is the part that pays off for years: your product code should emit domain events, and a separate adapter should decide what to do with them. Product code says "checkout started." It does not know or care whether that goes to Amplitude, GA4, Segment, a data warehouse, or all four.

interface AnalyticsSink {
  send(event: string, properties: Record<string, unknown>): void;
}

// each vendor is an adapter — swappable, testable, isolated
const amplitudeSink: AnalyticsSink = {
  send: (event, props) => amplitude.track(event, props),
};

// one subscriber wires the typed catalog to whatever sinks are configured
export function initAnalytics(sinks: AnalyticsSink[]) {
  analyticsBus.onAny((event, properties) => {
    for (const sink of sinks) sink.send(event, properties);
  });
}

This is dependency injection applied to tracking: the product depends on an abstraction (track), and the concrete vendor is injected at the edge. Swap Amplitude for Segment by changing one adapter — not by grep-and-replacing three hundred call sites. Add a second vendor without touching a single component. Run tests with a fake sink that just records calls, so you can assert "checkout_completed fired once with this payload" without any network. And notice this is a textbook good use of the event bus from earlier in the cluster: the emitter (product code) genuinely should not know who's listening (the vendors), so the indirection is real decoupling, not hidden coupling.

Governance is the whole point, not an afterthought

Once analytics is a layer, the things that were impossible when it was scattered become easy — and these are the things that actually matter when the company grows up:

  • Naming. One catalog means one convention. No more Button Clicked versus button_click versus btnClick for the same action.
  • PII control. A single choke point is where you enforce "no emails, no raw tokens, no free-text that might contain personal data." You can scrub, allowlist, or type-forbid sensitive fields in one place. When legal asks "are we sending any personal data to this vendor," you have a file to point at instead of a codebase to spelunk.
  • Auditing. GDPR, CCPA, a privacy review — all of them ask "what do you collect and where does it go?" With a catalog and adapters, that's a five-minute answer. Sprinkled track() calls make it a five-day investigation.

None of that governance is possible when tracking is smeared across components. It's trivial when tracking flows through one typed layer. That's the real argument for the architecture — not elegance, but the fact that a scattered approach becomes a liability the moment anyone with a compliance question walks in.

Closing the cluster

That wraps this run on communication between the parts of an app, and the three pieces rhyme on purpose. Event buses, feature flags, and analytics all start life as innocent one-liners sprinkled through the UI — emit, if (flag), track — and all three rot the same way: stringly-typed, un-auditable, impossible to change in one place. And all three are fixed the same way: a typed contract, a single home, and a boundary the rest of the app talks to instead of reaching past. Communication between parts done well isn't ambient magic. It's explicit, typed, discoverable contracts — the exact opposite of "anything can talk to anything."

Next, the series turns to a short but overdue detour — the panic over React re-renders, why rendering is the framework's superpower rather than its disease, and the architecture people quietly break trying to avoid it. Right after that it gets into how an app looks and feels at an architectural level, starting with theming and dark mode. That's where we go.

If your app tracks analytics, try the one-folder test right now: can you open a single place and read the full list of what you collect and where it's sent? If the honest answer is "no, it's everywhere," you've found your next refactor — and I'd like to hear how scattered it turned out to be.

Comments (0)

Login to post a comment.

Maksym Kuzmitskyi (MaximusFT)
Maksym Kuzmitskyi (MaximusFT)

Staff Frontend Engineer

Passionate developer sharing knowledge about modern web technologies and best practices.

Subscribe to Maksym Kuzmitskyi (MaximusFT)'s Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

More from Maksym Kuzmitskyi (MaximusFT)

View profile

You Only Get to See It From the Outside Once

The most valuable thing an experienced engineer brings to a new project isn't a skill — it's a vantage point. For a short window, you can still see the thing from the outside, the way a user or a newcomer does. Six months in, that view is gone. And most teams burn it in the first week without noticing.

6 minAug 11

The Command Palette Is an Architecture, Not a Widget

The theming article ended on a move from how an app looks to how power users drive it. The command palette — the Cmd+K menu that fuzzy-searches everything you c...

5 minAug 7

Code-Splitting Is a Boundary Decision, Not a Bundle Trick

The command palette article closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-s...

6 minAug 7

You Probably Don't Need Multi-Agents

I work across six or seven repositories on one project — a big hybrid thing, part microfrontend, part backend, several apps that all talk to each other. When I ...

6 minAug 7

Feature Flags: Every Flag Is a Branch You Promised to Delete

The event bus article ended on a warning about a certain kind of switch teams sprinkle everywhere with a casual "it's just a toggle" attitude — and then can't g...

5 minJul 30