Topics
v6.0.0
On this page · sections

Client Analytics SDK Design

A client SDK wraps identity, events, and context — queuing locally and flushing in batches without blocking UI. Interviewers test API design, privacy, SPA navigation, and schema governance.

ConcernPatternPitfall
API surfaceinit · identify · track · page · resetAd-hoc string event names
TransportBatch queue + flush triggersUnbounded queue memory leak
PrivacyConsent gate + PII scrubSending email in properties
SPAVirtual pageviews on route changeMissing navigations after first load

Public API Surface

Definition

type AnalyticsConfig = { writeKey: string; endpoint?: string; batchSize?: number; flushIntervalMs?: number; }; export interface Analytics { init(config: AnalyticsConfig): void; identify(userId: string, traits?: Record<string, unknown>): void; track<K extends string>(event: K, properties?: Record<string, unknown>): void; page(name?: string, properties?: Record<string, unknown>): void; reset(): void; }
// Consumer usage analytics.init({ writeKey: 'pk_live_…', batchSize: 20, flushIntervalMs: 5000 }); analytics.identify('user_42', { plan: 'pro' }); analytics.track('Checkout Started', { cartValue: 99.5, itemCount: 3 });
  • init — once at bootstrap
  • identify — tie events to user after auth
  • track — named events with typed property bag
  • page — virtual pageviews for SPAs
  • reset — clear identity on logout (critical for shared devices)

Enforce event names as constants or codegen — no ad-hoc 'click' strings in 200 files.

const queue: Array<Record<string, unknown>> = []; let config: AnalyticsConfig | null = null; function track(event: string, properties: Record<string, unknown> = {}) { queue.push({ type: 'track', event, properties, ts: Date.now() }); if (queue.length >= (config?.batchSize ?? 20)) flush(); }

Interview: Lead with init · identify · track · page · reset — maps to Segment/Amplitude mental model.

Event Batching & Transport

Definition

const queue = []; const MAX_QUEUE = 500; function enqueue(event) { if (queue.length >= MAX_QUEUE) queue.shift(); // drop oldest queue.push(event); } async function flush({ useBeacon = false } = {}) { if (!queue.length) return; const batch = queue.splice(0, 50); const body = JSON.stringify({ batch }); if (useBeacon && navigator.sendBeacon) { navigator.sendBeacon('/api/analytics', new Blob([body], { type: 'application/json' })); return; } try { await fetch('/api/analytics', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, keepalive: true, }); } catch { persistOffline(batch); queue.unshift(...batch); } } document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') flush({ useBeacon: true }); });

Flush triggers:

  • Queue hits batch size (10–50)
  • Timer interval (5–10s)
  • visibilitychange hidden and beforeunload via sendBeacon

sendBeacon ~64KB limit — good for unload only. Retry 429/5xx with backoff. Server dedupes with event uuid. Never unbounded queue.

High-priority events (purchase) — immediate flush, don’t wait for batch timer.

Interview: Beacon for tab close; fetch with keepalive for normal flushes.

Definition

let consent = { analytics: false, marketing: false }; function setConsent(next) { consent = { ...consent, ...next }; if (!consent.analytics) { queue.length = 0; localStorage.removeItem(OFFLINE_KEY); } } const PII_KEYS = ['email', 'phone', 'ssn', 'password']; function scrub(properties) { const clean = { ...properties }; for (const key of PII_KEYS) { if (key in clean) delete clean[key]; } return clean; } function track(event, properties) { if (!consent.analytics) return; enqueue({ event, properties: scrub(properties) }); }

GDPR/CCPA: no events until consent granted. reset() on logout. Hash userId if required — never send raw email in event properties.

IP geolocation belongs server-side. Document data retention policy. Honor Do Not Track where legally required.

Server-side duplicate for authoritative conversions (payment succeeded) — dedupe with shared eventId.

Interview: Client analytics is not authoritative for revenue — server events for conversions.

SPA Navigation & Page Views

Definition

MPAs fire pageviews on full navigation. SPAs need virtual pageviews on client route changes.

'use client'; import { useEffect } from 'react'; import { usePathname, useSearchParams } from 'next/navigation'; export function AnalyticsPageTracker() { const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { analytics.page(pathname, { search: searchParams.toString(), referrer: document.referrer, }); }, [pathname, searchParams]); return null; }
// React Router v6 function usePageTracking() { const location = useLocation(); useEffect(() => { analytics.page(location.pathname + location.search); }, [location]); }

Include route params as properties, not in event name strings. Debounce rapid replace navigations. Initial pageview on first mount — don’t double-fire with SSR hydration.

History API popstate for non-framework routers. Hash routers need hash change listener.

Interview: SPA without virtual pageviews undercounts navigation — only first load tracked.

Reliability & Schema Governance

Definition

Staff concerns:

  • Schema registry — event name + required properties versioned
  • Codegen — TypeScript unions from registry JSON
  • CI validation — reject PRs with unknown event names
  • Sampling — high-volume events sampled in prod
  • Dead letter — failed batches logged, not silently dropped
// Generated from schema registry type AnalyticsEvent = | { name: 'Checkout Started'; properties: { cartValue: number; itemCount: number } } | { name: 'Button Clicked'; properties: { buttonId: string } }; function track<E extends AnalyticsEvent['name']>( event: E, properties: Extract<AnalyticsEvent, { name: E }>['properties'] ) { /* ... */ }

Monitor: flush success rate, queue depth p99, event volume per minute, schema violation count.

Feature teams propose events via PR to registry — analytics platform team approves. Breaking property changes require version bump.

Offline queue with size cap and TTL — replay on online event. Idempotency keys prevent duplicate counts on retry.

Interview: Schema governance prevents 50 teams inventing incompatible event shapes.

Senior Interview Summary

API

init · identify · track · page · reset. Singleton for one product surface.

Batching

Size + timer + visibility hidden beacon. Max queue cap. Offline spill with limit.

Privacy

Consent gate. PII scrub. reset on logout. Server authoritative for conversions.

SPA

Virtual pageview on route change. Don’t double-fire on hydration.

Governance

Schema registry + codegen + CI validation. Platform team owns event taxonomy.

Best mental model

Enqueue fast → never block UI on network Flush reliably → batch + beacon + retry Type events → registry prevents drift

Practical rules

Shared devices → reset() on logout mandatory Tab close → sendBeacon flush Revenue events → server-side with eventId dedupe Event names → constants or codegen, never raw strings

What to say in interviews

Design the public API, explain batch flush triggers, describe consent + scrub, and show SPA page tracking. Mention schema registry for staff-level governance.

Last updated on