Analytics (GA4) Overview
ONECMS tracks user behavior via Google Analytics 4 using a typed, envelope-based event system. The architecture supports two delivery modes — one where the CMS owns the full pipeline (script injection through to gtag calls), and another where the CMS generates a GA-compatible payload and hands it off to a third-party library.
Related Decisions
- ADR-018: Typed Analytics Architecture
- RFC-001: GA4 Event Contract
- RFC-002: GA4 Authentication Event Contract
- RFC-003: GA4 Customer Support Event Contract
Delivery Modes

| Mode | Script ownership | Delivery | Status |
|---|---|---|---|
| Owned pipeline | CMS injects gtag.js via GoogleAnalytics component |
window.gtag('event', ...) |
Active |
| Third-party mode | External package provides its own tags/scripts | Payload handed to external SDK | Planned |
In both modes, the event generation and normalization layers are identical — only the last-mile delivery differs.
Source Layout
src/
lib/external/GA.ts # GA transport class
modules/GoogleAnalytics.tsx # Script injection component (Mode 1)
adapters/analytics/
normalizeAnalyticsPayload.adapter.ts # Payload normalization
types/analytics/
index.ts # Envelope, categories, registry
authentication/index.ts # Auth events + payload map
customer-support/index.ts # CS events + payload map
upload/index.ts # Upload events + payload map
content/index.ts # Content events (stub)
monetization/index.ts # Monetization events (stub)
navigation/index.ts # Navigation events (stub)
appcms/index.ts # AppCMS events (stub)
lib/helpers/customer-support/analytics.ts # Domain-specific tracking helpers
config/config.ts # GA config (measurementId, limits)
| File | Role |
|---|---|
GA.ts |
Static transport — normalizes envelope, gates on production, calls gtag |
GoogleAnalytics.tsx |
Next.js <Script> tags for gtag.js + dataLayer init |
normalizeAnalyticsPayload.adapter.ts |
Truncates strings, joins arrays, enforces param limit |
types/analytics/index.ts |
Envelope type, category union, CategoryEventMap, AllAnalyticsEnvelopes |
types/analytics/{domain}/index.ts |
Per-domain event names, payload types, payload maps |
config.ts → Config.GA |
measurementId, schema_version, string_char_limit, param_limit |
helpers/.../analytics.ts |
Domain-scoped convenience functions (e.g., customer support) |
Configuration
All analytics configuration lives in Config.GA:
GA: {
measurementId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID,
schema_version: '1.0',
string_char_limit: 100, // max chars per string param
param_limit: 25, // max params per event (GA4 limit)
}
| Setting | Source | Purpose |
|---|---|---|
measurementId |
NEXT_PUBLIC_GA_MEASUREMENT_ID |
GA4 property identifier |
schema_version |
Hardcoded in config | Payload contract version — increment on breaking changes only |
string_char_limit |
NEXT_PUBLIC_GA_STRING_LIMIT (default 100) |
Truncation threshold for string values |
param_limit |
NEXT_PUBLIC_GA_PARAM_LIMIT (default 25) |
Max event parameters sent to GA4 |
Design Principles
- Type safety end-to-end — every event is typed from domain code through to the
gtagcall; invalid category/event combinations are compile errors - Production-only emission — non-production events are logged to console and never sent to GA4, preventing data pollution
- Normalization as an adapter — payload truncation, array joining, and param limits are isolated in a single adapter, not scattered across callers
- Schema versioning — a
schema_versionfield travels with every event so dashboards can distinguish incompatible payload shapes - Fire-and-forget — event emission never throws, never blocks UI, never retries
- Transport-agnostic envelope — the
AllAnalyticsEnvelopestype is the contract; how the payload reaches GA4 is a delivery concern, not a domain concern
Doc Index
| Page | Covers |
|---|---|
| Pipeline | Mode 1 — script injection, GA.ts transport, consent, production gating |
| Event Contracts | Type system — envelope, categories, payload maps, adding new events |
| Third-Party Mode | Mode 2 — payload-only delivery via external libraries (planned) |