}
track(event: Partial<AcquisitionEvent>): void {
const enrichedEvent: AcquisitionEvent = {
event_id: crypto.randomUUID(),
user_id: event.user_id ?? null,
session_id: this.getSessionId(),
timestamp: Date.now(),
event_type: event.event_type!,
properties: event.properties ?? {},
source: this.resolveSource(event.source),
};
this.queue.push(enrichedEvent);
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
private async flush(): Promise<void> {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.batchSize);
try {
await fetch(this.apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batch),
});
} catch (error) {
// Implement retry logic or dead-letter queue here
console.error('Tracking flush failed:', error);
this.queue.unshift(...batch);
}
}
private resolveSource(source?: Partial<AcquisitionEvent['source']>): AcquisitionEvent['source'] {
// Fallback to URL params or cookies if source not explicitly provided
return source ?? { channel: 'organic' };
}
}
### 2. Multi-Touch Attribution Engine
Single-touch attribution (last-click) misallocates budget. Implement a decay-based multi-touch model to credit all touchpoints accurately.
**Architecture Decision:** Use a server-side function triggered by conversion events to process attribution. This allows access to the full user journey stored in the database.
```typescript
// src/attribution/engine.ts
interface Touchpoint {
event_id: string;
channel: string;
timestamp: number;
}
interface Conversion {
user_id: string;
value: number;
timestamp: number;
}
export class AttributionEngine {
// Time-decay model: credit decreases exponentially with time distance from conversion
private halfLifeHours = 72;
calculateCredit(conversion: Conversion, touchpoints: Touchpoint[]): Record<string, number> {
const credits: Record<string, number> = {};
const conversionTime = conversion.timestamp;
touchpoints.forEach(tp => {
const timeDiffHours = (conversionTime - tp.timestamp) / (1000 * 60 * 60);
const decayFactor = Math.pow(0.5, timeDiffHours / this.halfLifeHours);
credits[tp.channel] = (credits[tp.channel] || 0) + decayFactor;
});
// Normalize credits to sum to conversion value
const totalWeight = Object.values(credits).reduce((a, b) => a + b, 0);
if (totalWeight === 0) return { organic: conversion.value };
for (const channel in credits) {
credits[channel] = (credits[channel] / totalWeight) * conversion.value;
}
return credits;
}
}
3. Growth Loop Implementation
Growth loops replace linear funnels. A common loop is the referral system. The technical implementation must ensure idempotency, fraud detection, and instant reward crediting.
Architecture: Use a message queue (e.g., Redis Streams or Kafka) to process referral validations asynchronously, decoupling the user experience from reward calculation.
// src/growth/referral-loop.ts
import { GrowthTrackingClient } from '../tracking/client';
export interface ReferralPayload {
referrer_id: string;
referee_id: string;
referral_code: string;
status: 'pending' | 'validated' | 'fraud_detected';
}
export class ReferralLoop {
constructor(
private db: any, // Database client
private tracker: GrowthTrackingClient,
private rewardAmount: number
) {}
async validateReferral(payload: ReferralPayload): Promise<void> {
// 1. Idempotency check
const existing = await this.db.referrals.findUnique({
where: { referral_code: payload.referral_code, referee_id: payload.referee_id }
});
if (existing) return;
// 2. Fraud detection heuristic
const isFraud = await this.detectFraud(payload.referrer_id, payload.referee_id);
const status = isFraud ? 'fraud_detected' : 'validated';
// 3. Persist and trigger reward
await this.db.referrals.create({
data: { ...payload, status, validated_at: new Date() }
});
if (status === 'validated') {
await this.grantReward(payload.referrer_id);
this.tracker.track({
user_id: payload.referrer_id,
event_type: 'referral_reward_granted',
properties: { amount: this.rewardAmount, source: payload.referee_id }
});
}
}
private async detectFraud(referrer: string, referee: string): Promise<boolean> {
// Check IP overlap, device fingerprint, or rapid succession
const referrerDevice = await this.db.users.findUnique({ where: { id: referrer } });
const refereeDevice = await this.db.users.findUnique({ where: { id: referee } });
return referrerDevice?.device_fingerprint === refereeDevice?.device_fingerprint;
}
private async grantReward(userId: string): Promise<void> {
await this.db.users.update({
where: { id: userId },
data: { credits: { increment: this.rewardAmount } }
});
}
}
Architecture Rationale
- Event-Driven Design: Decouples tracking from business logic. Events flow into a warehouse (e.g., Snowflake/BigQuery) via a streaming pipeline, enabling batch analysis without impacting production latency.
- First-Party Data Dominance: All attribution relies on server-side events correlated by
user_id or deterministic device graphs, minimizing dependency on browser cookies.
- Real-Time Feedback: Growth loops trigger rewards within milliseconds, reinforcing user behavior immediately. This reduces friction and increases the viral coefficient.
Pitfall Guide
-
Tracking Everything, Analyzing Nothing:
- Mistake: Instrumenting every click without defining a schema aligned to business goals.
- Fix: Implement a strict event registry. Every event must map to a metric in the OKR hierarchy. Drop events that do not drive decisions.
-
Ignoring PII and Compliance:
- Mistake: Storing raw emails or IPs in analytics events without hashing or consent checks.
- Fix: Implement a PII scrubber in the tracking client. Hash emails (
sha256) before storage. Ensure event dispatch respects consent_mode flags.
-
Hardcoded Attribution Models:
- Mistake: Embedding attribution logic directly in the conversion handler.
- Fix: Abstract attribution into a service that accepts a model configuration. This allows switching between time-decay, position-based, or data-driven models without code redeployment.
-
Breaking the Growth Loop:
- Mistake: Failing to credit the referrer instantly or hiding the reward mechanism.
- Fix: Use optimistic UI updates for rewards. If backend processing lags, show a "Processing" state with a guaranteed timeline. Ensure the loop is visible in the product interface.
-
Vanity Metric Obsession:
- Mistake: Optimizing for signups rather than activated users.
- Fix: Define the "Activation Event" (e.g., first key action). Attribute acquisition only when the activation event occurs, not at signup. This prevents paying for low-quality traffic.
-
High Latency in Feedback Loops:
- Mistake: Running attribution calculations nightly.
- Fix: Use streaming analytics for real-time CAC calculation. If nightly is required, implement a "fast-path" approximation for dashboarding while the nightly job refines accuracy.
-
Neglecting Server-Side Tracking:
- Mistake: Relying solely on client-side JavaScript for event collection.
- Fix: Implement server-side tracking for critical events (signups, purchases). This bypasses ad blockers and provides higher data fidelity.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| B2B SaaS, High ACV | Multi-touch attribution with Sales CRM sync | Long sales cycles require credit distribution across touchpoints; CRM sync aligns marketing with sales. | Moderate (Integration dev time) |
| B2C App, Viral Focus | Engineered Referral Loop with Instant Rewards | Viral coefficient ($k$) is the primary growth lever; instant rewards maximize loop velocity. | Low (Standard implementation) |
| Bootstrapped, Limited Dev | First-Click Attribution + Server-Side Events | Simplifies logic while maintaining data accuracy; reduces engineering overhead. | Minimal |
| Enterprise, Compliance Heavy | Zero-Party Data Collection + Deterministic IDs | Avoids privacy risks; relies on user-provided data and first-party cookies. | High (UX friction management) |
Configuration Template
Use this TypeScript configuration to initialize the growth engine with environment-specific settings.
// src/config/growth.config.ts
export interface GrowthConfig {
tracking: {
endpoint: string;
batchSize: number;
flushIntervalMs: number;
enableServerSide: boolean;
};
attribution: {
model: 'time_decay' | 'last_click' | 'position_based';
decayHalfLifeHours: number;
lookbackWindowDays: number;
};
growthLoops: {
referral: {
enabled: boolean;
rewardAmount: number;
fraudDetection: {
enabled: boolean;
maxReferralsPerDay: number;
};
};
viralContent: {
enabled: boolean;
watermarkUrl: string;
};
};
}
export const defaultConfig: GrowthConfig = {
tracking: {
endpoint: process.env.TRACKING_ENDPOINT!,
batchSize: 20,
flushIntervalMs: 5000,
enableServerSide: true,
},
attribution: {
model: 'time_decay',
decayHalfLifeHours: 72,
lookbackWindowDays: 30,
},
growthLoops: {
referral: {
enabled: true,
rewardAmount: 10,
fraudDetection: {
enabled: true,
maxReferralsPerDay: 5,
},
},
viralContent: {
enabled: false,
watermarkUrl: '',
},
},
};
Quick Start Guide
- Initialize Tracking Client: Import
GrowthTrackingClient and configure with your endpoint. Add track() calls to all user interactions.
npm install @codcompass/growth-sdk # Hypothetical package reference
- Define Conversion Events: Identify your activation event. Add a conversion handler that triggers the
AttributionEngine.
- Deploy Referral Hook: Add the
ReferralLoop service to your backend. Expose an API endpoint /api/validate-referral for the frontend to call.
- Verify Pipeline: Use the provided test script to simulate events and check the attribution output. Ensure rewards are credited in the database.
// test/growth.test.ts
const engine = new AttributionEngine();
const result = engine.calculateCredit(
{ user_id: 'u1', value: 100, timestamp: Date.now() },
[{ channel: 'paid', timestamp: Date.now() - 3600000 }]
);
console.assert(result.paid === 100, 'Attribution failed');
- Monitor Metrics: Set up alerts for CAC spikes and $k$-factor drops below 0.8. Iterate on loop friction based on data.