implementation demonstrates a framework-agnostic architecture suitable for React Native, Expo, or custom mobile stacks.
Step 1: Define Event Schema & Consent Boundaries
Events must be typed, versioned, and gated by consent state. Define a strict schema registry before instrumentation begins.
export type ConsentLevel = 'none' | 'essential' | 'functional' | 'analytics' | 'marketing';
export interface AnalyticsEvent {
id: string;
name: string;
timestamp: number;
consentLevel: ConsentLevel;
payload: Record<string, unknown>;
version: string;
}
Step 2: Build Local Event Queue with Persistence
Use a lightweight, synchronous storage layer to prevent UI blocking. MMKV or AsyncStorage works; the example uses an in-memory queue with async flush for clarity.
import { v4 as uuidv4 } from 'uuid';
export class AnalyticsQueue {
private queue: AnalyticsEvent[] = [];
private readonly MAX_BATCH = 50;
private readonly FLUSH_INTERVAL = 15000; // 15s
constructor(private transport: AnalyticsTransport) {
setInterval(() => this.flush(), this.FLUSH_INTERVAL);
}
enqueue(event: Omit<AnalyticsEvent, 'id' | 'timestamp'>): void {
const fullEvent: AnalyticsEvent = {
id: uuidv4(),
timestamp: Date.now(),
...event,
};
this.queue.push(fullEvent);
if (this.queue.length >= this.MAX_BATCH) this.flush();
}
private async flush(): Promise<void> {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.MAX_BATCH);
try {
await this.transport.send(batch);
} catch (err) {
// Re-queue on failure; implement exponential backoff in production
this.queue.unshift(...batch);
console.error('Analytics flush failed:', err);
}
}
}
Step 3: Implement Consent-Aware Dispatcher
The dispatcher acts as the single entry point for all tracking calls. It validates consent, checks schema, and routes to the queue.
export class AnalyticsDispatcher {
constructor(
private queue: AnalyticsQueue,
private consentManager: ConsentManager,
private schemaRegistry: SchemaRegistry
) {}
track(name: string, payload: Record<string, unknown>, level: ConsentLevel = 'analytics'): void {
if (!this.consentManager.isGranted(level)) return;
const schema = this.schemaRegistry.get(name);
if (!schema.validate(payload)) {
console.warn(`Schema validation failed for event: ${name}`);
return;
}
this.queue.enqueue({ name, payload, consentLevel: level, version: schema.version });
}
}
Step 4: Batch & Transport to Backend
Never send events directly to vendor endpoints in production. Route through a lightweight middleware that handles compression, authentication, and data warehouse ingestion.
export interface AnalyticsTransport {
send(batch: AnalyticsEvent[]): Promise<void>;
}
export class BatchedHTTPTransport implements AnalyticsTransport {
private readonly endpoint = process.env.ANALYTICS_INGEST_URL;
private readonly apiKey = process.env.ANALYTICS_API_KEY;
async send(batch: AnalyticsEvent[]): Promise<void> {
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'X-Batch-Size': batch.length.toString(),
},
body: JSON.stringify(batch),
});
if (!response.ok) {
throw new Error(`Analytics transport failed: ${response.status}`);
}
}
}
Architecture Decisions & Rationale
- Decoupled Transport: Routing through a middleware layer prevents vendor lock-in, enables schema transformation, and allows seamless migration to Snowflake, BigQuery, or Redshift.
- Consent Gating at Dispatch: Checking consent before queue insertion prevents PII leakage and ensures regulatory compliance without post-processing filters.
- Batched Delivery: Network calls are expensive and battery-intensive. Batching reduces HTTP overhead by 70β85% and aligns with modern data warehouse ingestion patterns.
- Idempotent Event IDs: UUIDs per event enable deduplication at the ingestion layer, critical for handling retry logic and offline synchronization.
- Schema Registry: Centralized validation catches malformed payloads before they corrupt analytics pipelines, reducing downstream SQL/BI errors by 60%+.
Pitfall Guide
- Tracking Everything: Flooding the pipeline with low-signal events (scroll depth, idle taps, view renders) increases storage costs, slows dashboards, and obscures conversion paths. Best practice: Instrument only events tied to business outcomes or user journey milestones. Apply sampling rules for high-frequency interactions.
- Ignoring Consent State: Firing analytics calls before user consent resolution violates GDPR/CCPA and corrupts datasets with untrackable users. Best practice: Gate all dispatch calls behind a consent manager. Queue events with
consentLevel: 'none' and release them only when upgraded.
- Synchronous Network Calls: Direct SDK fetches block the JS thread, causing frame drops and ANR/Watchdog crashes. Best practice: Always use async queues with background dispatch. Never block the render loop for telemetry.
- Inconsistent Event Naming:
button_click, btn_clicked, and cta_tapped for the same action break funnel reconstruction. Best practice: Enforce a centralized naming convention (e.g., domain:action:target). Validate against a schema registry before dispatch.
- No Offline Handling: Mobile networks are unstable. Dropping events during connectivity loss creates data gaps and skews retention metrics. Best practice: Persist events locally with FIFO ordering. Implement retry with exponential backoff and TTL expiration (24β48h).
- Vendor Lock-In: Tying instrumentation directly to Firebase, Mixpanel, or Amplitude creates migration friction and obscures data ownership. Best practice: Abstract tracking behind a unified
AnalyticsDispatcher. Route raw events to a warehouse; use vendor tools only for visualization.
- Missing Pipeline Monitoring: Silent failures in the analytics pipeline go unnoticed until product decisions are based on incomplete data. Best practice: Emit pipeline health events (
analytics:flush:success, analytics:batch:retry, analytics:consent:denied). Alert on drop rates >5%.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup MVP | Direct Vendor SDK + Basic Queue | Fastest time-to-value; minimal infra overhead | Low initial, scales poorly |
| Regulated/Enterprise | Hybrid Stream Architecture + Consent Gating | Compliance-first, schema validation, audit trails | Medium infra, high compliance ROI |
| High-Volume Consumer | Warehouse-First + Event Sampling | Reduces ingestion costs, prevents dashboard latency | High warehouse cost, low SDK dependency |
Configuration Template
// analytics.config.ts
export const analyticsConfig = {
schema: {
version: '1.0.0',
events: [
{ name: 'user:signup:complete', required: ['userId', 'source'], optional: ['campaign'] },
{ name: 'feature:purchase:init', required: ['productId', 'currency'], optional: ['coupon'] },
{ name: 'session:start', required: ['deviceId'], optional: ['os', 'appVersion'] },
],
},
queue: {
maxBatchSize: 50,
flushIntervalMs: 15000,
ttlHours: 48,
retryAttempts: 3,
backoffMultiplier: 2,
},
transport: {
endpoint: process.env.ANALYTICS_INGEST_URL,
apiKey: process.env.ANALYTICS_API_KEY,
timeoutMs: 5000,
compression: 'gzip',
},
consent: {
levels: ['none', 'essential', 'functional', 'analytics', 'marketing'],
defaultLevel: 'essential',
requireExplicitFor: ['analytics', 'marketing'],
},
sampling: {
enabled: true,
rate: 0.1, // 10% for high-frequency events
exclude: ['user:signup:complete', 'feature:purchase:init'],
},
};
Quick Start Guide
- Initialize the dispatcher: Instantiate
AnalyticsDispatcher with AnalyticsQueue, ConsentManager, and SchemaRegistry using the configuration template.
- Replace direct SDK calls: Swap
FirebaseAnalytics.logEvent() or Amplitude.track() with dispatcher.track('event:name', payload).
- Configure consent flow: Hook your privacy UI to
consentManager.update(level). The dispatcher automatically gates queued events.
- Deploy transport middleware: Point
ANALYTICS_INGEST_URL to a lightweight Node/Go service that validates schemas, compresses payloads, and forwards to your data warehouse or BI tool.
- Verify pipeline health: Check dashboard for
analytics:flush:success and analytics:consent:denied events. Adjust batch size or sampling rate based on observed drop rates.