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.
| Concern | Pattern | Pitfall |
|---|---|---|
| API surface | init · identify · track · page · reset | Ad-hoc string event names |
| Transport | Batch queue + flush triggers | Unbounded queue memory leak |
| Privacy | Consent gate + PII scrub | Sending email in properties |
| SPA | Virtual pageviews on route change | Missing 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 bootstrapidentify— tie events to user after authtrack— named events with typed property bagpage— virtual pageviews for SPAsreset— 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)
visibilitychangehidden andbeforeunloadviasendBeacon
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.
Privacy, Consent & PII Scrubbing
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 driftPractical 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 stringsWhat 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.