Personalization has evolved from a nice-to-have feature to a business imperative. Studies show that e-commerce sites implementing AI-driven personalization see conversion increases of 35% or more, while users increasingly expect experiences tailored to their preferences. Netflix attributes $1 billion in annual savings to their recommendation system by reducing subscriber churn.
In this comprehensive guide, we'll explore how to build sophisticated personalization engines using modern web technologies. From recommendation algorithms and collaborative filtering to real-time content adaptation with TensorFlow.js, you'll learn practical techniques that transform generic web applications into intelligent, user-aware experiences.
Understanding AI Personalization
AI-driven personalization goes beyond simple rule-based systems ("show product A to users who bought product B"). Modern personalization engines analyze user behavior patterns, predict preferences, and adapt content in real-time using machine learning models. According to McKinsey research, companies that excel at personalization generate 40% more revenue from those activities than average players.
Types of Personalization
There are several approaches to personalization, each suited for different scenarios:
// personalization-types.ts
// 1. Content-Based Filtering
// Recommends items similar to what user has liked before
interface ContentBasedPersonalization {
userProfile: UserPreferences;
itemFeatures: ItemFeature[];
similarity: 'cosine' | 'euclidean' | 'jaccard';
}
// 2. Collaborative Filtering
// Recommends based on similar users' preferences
interface CollaborativeFiltering {
type: 'user-based' | 'item-based' | 'matrix-factorization';
userItemMatrix: number[][];
latentFactors?: number;
}
// 3. Hybrid Systems
// Combines multiple approaches for better accuracy
interface HybridPersonalization {
contentWeight: number;
collaborativeWeight: number;
contextualWeight: number;
fallbackStrategy: 'popularity' | 'random' | 'editorial';
}
// 4. Contextual Personalization
// Adapts based on real-time context
interface ContextualPersonalization {
timeOfDay: 'morning' | 'afternoon' | 'evening' | 'night';
device: 'mobile' | 'tablet' | 'desktop';
location?: GeoLocation;
sessionBehavior: SessionData;
}
Building the User Behavior Tracking System
Effective personalization requires understanding user behavior. Here's a comprehensive tracking system that captures interactions while respecting privacy:
// behavior-tracker.ts
import { v4 as uuidv4 } from 'uuid';
interface UserEvent {
eventId: string;
userId: string;
sessionId: string;
eventType: EventType;
timestamp: number;
data: EventData;
context: EventContext;
}
type EventType =
| 'page_view'
| 'product_view'
| 'add_to_cart'
| 'purchase'
| 'search'
| 'click'
| 'scroll_depth'
| 'time_on_page'
| 'hover'
| 'wishlist_add';
interface EventData {
itemId?: string;
category?: string;
price?: number;
query?: string;
position?: number;
scrollPercentage?: number;
dwellTime?: number;
}
interface EventContext {
pageUrl: string;
referrer: string;
device: DeviceInfo;
viewport: { width: number; height: number };
}
class BehaviorTracker {
private userId: string;
private sessionId: string;
private eventQueue: UserEvent[] = [];
private flushInterval: number = 5000;
private batchSize: number = 10;
constructor(userId?: string) {
this.userId = userId || this.getOrCreateAnonymousId();
this.sessionId = this.getOrCreateSessionId();
this.initializeTracking();
}
private getOrCreateAnonymousId(): string {
let id = localStorage.getItem('anon_user_id');
if (!id) {
id = uuidv4();
localStorage.setItem('anon_user_id', id);
}
return id;
}
private getOrCreateSessionId(): string {
let sessionId = sessionStorage.getItem('session_id');
if (!sessionId) {
sessionId = uuidv4();
sessionStorage.setItem('session_id', sessionId);
}
return sessionId;
}
private initializeTracking(): void {
// Auto-track page views
this.trackPageView();
// Track scroll depth
this.initScrollTracking();
// Track time on page
this.initDwellTimeTracking();
// Flush events periodically
setInterval(() => this.flushEvents(), this.flushInterval);
// Flush on page unload
window.addEventListener('beforeunload', () => this.flushEvents(true));
}
public track(eventType: EventType, data: EventData = {}): void {
const event: UserEvent = {
eventId: uuidv4(),
userId: this.userId,
sessionId: this.sessionId,
eventType,
timestamp: Date.now(),
data,
context: this.getContext()
};
this.eventQueue.push(event);
// Immediate flush for critical events
if (['purchase', 'add_to_cart'].includes(eventType)) {
this.flushEvents();
} else if (this.eventQueue.length >= this.batchSize) {
this.flushEvents();
}
}
private getContext(): EventContext {
return {
pageUrl: window.location.href,
referrer: document.referrer,
device: this.getDeviceInfo(),
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
};
}
private getDeviceInfo(): DeviceInfo {
const ua = navigator.userAgent;
return {
type: /Mobile|Android|iPhone/.test(ua) ? 'mobile' :
/Tablet|iPad/.test(ua) ? 'tablet' : 'desktop',
browser: this.detectBrowser(ua),
os: this.detectOS(ua)
};
}
private initScrollTracking(): void {
let maxScroll = 0;
const milestones = [25, 50, 75, 90, 100];
const tracked = new Set();
window.addEventListener('scroll', () => {
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = Math.round((window.scrollY / scrollHeight) * 100);
if (scrollPercent > maxScroll) {
maxScroll = scrollPercent;
milestones.forEach(milestone => {
if (scrollPercent >= milestone && !tracked.has(milestone)) {
tracked.add(milestone);
this.track('scroll_depth', { scrollPercentage: milestone });
}
});
}
}, { passive: true });
}
private initDwellTimeTracking(): void {
const startTime = Date.now();
let isVisible = true;
document.addEventListener('visibilitychange', () => {
isVisible = document.visibilityState === 'visible';
});
window.addEventListener('beforeunload', () => {
if (isVisible) {
const dwellTime = Date.now() - startTime;
this.track('time_on_page', { dwellTime });
}
});
}
private async flushEvents(sync: boolean = false): Promise {
if (this.eventQueue.length === 0) return;
const events = [...this.eventQueue];
this.eventQueue = [];
const payload = JSON.stringify({ events });
if (sync && navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics/events', payload);
} else {
try {
await fetch('/api/analytics/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload
});
} catch (error) {
// Re-queue events on failure
this.eventQueue = [...events, ...this.eventQueue];
}
}
}
}
// React hook for tracking
export function useTracker() {
const tracker = useMemo(() => new BehaviorTracker(), []);
const trackProductView = useCallback((productId: string, category: string, price: number) => {
tracker.track('product_view', { itemId: productId, category, price });
}, [tracker]);
const trackAddToCart = useCallback((productId: string, price: number) => {
tracker.track('add_to_cart', { itemId: productId, price });
}, [tracker]);
const trackSearch = useCallback((query: string) => {
tracker.track('search', { query });
}, [tracker]);
return { trackProductView, trackAddToCart, trackSearch };
}
Implementing Collaborative Filtering
Collaborative filtering is the foundation of most recommendation systems, pioneered by researchers at Amazon and Spotify. Here's a complete implementation including both user-based and item-based approaches:
// collaborative-filter.ts
interface UserItemInteraction {
userId: string;
itemId: string;
rating: number; // Can be explicit (1-5) or implicit (view=1, cart=3, purchase=5)
timestamp: number;
}
interface SimilarityScore {
id: string;
similarity: number;
}
class CollaborativeFilteringEngine {
private userItemMatrix: Map> = new Map();
private itemUserMatrix: Map> = new Map();
private userSimilarityCache: Map = new Map();
private itemSimilarityCache: Map = new Map();
constructor(interactions: UserItemInteraction[]) {
this.buildMatrices(interactions);
}
private buildMatrices(interactions: UserItemInteraction[]): void {
interactions.forEach(({ userId, itemId, rating }) => {
// User-Item matrix
if (!this.userItemMatrix.has(userId)) {
this.userItemMatrix.set(userId, new Map());
}
this.userItemMatrix.get(userId)!.set(itemId, rating);
// Item-User matrix (transposed)
if (!this.itemUserMatrix.has(itemId)) {
this.itemUserMatrix.set(itemId, new Map());
}
this.itemUserMatrix.get(itemId)!.set(userId, rating);
});
}
// Cosine similarity between two users
private cosineSimilarity(
vectorA: Map,
vectorB: Map
): number {
const commonItems = [...vectorA.keys()].filter(key => vectorB.has(key));
if (commonItems.length === 0) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
commonItems.forEach(item => {
const a = vectorA.get(item)!;
const b = vectorB.get(item)!;
dotProduct += a * b;
});
vectorA.forEach(val => normA += val * val);
vectorB.forEach(val => normB += val * val);
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Find similar users
public findSimilarUsers(userId: string, k: number = 10): SimilarityScore[] {
if (this.userSimilarityCache.has(userId)) {
return this.userSimilarityCache.get(userId)!.slice(0, k);
}
const targetUserRatings = this.userItemMatrix.get(userId);
if (!targetUserRatings) return [];
const similarities: SimilarityScore[] = [];
this.userItemMatrix.forEach((ratings, otherUserId) => {
if (otherUserId !== userId) {
const similarity = this.cosineSimilarity(targetUserRatings, ratings);
if (similarity > 0) {
similarities.push({ id: otherUserId, similarity });
}
}
});
similarities.sort((a, b) => b.similarity - a.similarity);
this.userSimilarityCache.set(userId, similarities);
return similarities.slice(0, k);
}
// Find similar items
public findSimilarItems(itemId: string, k: number = 10): SimilarityScore[] {
if (this.itemSimilarityCache.has(itemId)) {
return this.itemSimilarityCache.get(itemId)!.slice(0, k);
}
const targetItemRatings = this.itemUserMatrix.get(itemId);
if (!targetItemRatings) return [];
const similarities: SimilarityScore[] = [];
this.itemUserMatrix.forEach((ratings, otherItemId) => {
if (otherItemId !== itemId) {
const similarity = this.cosineSimilarity(targetItemRatings, ratings);
if (similarity > 0) {
similarities.push({ id: otherItemId, similarity });
}
}
});
similarities.sort((a, b) => b.similarity - a.similarity);
this.itemSimilarityCache.set(itemId, similarities);
return similarities.slice(0, k);
}
// User-based recommendation
public recommendForUser(userId: string, n: number = 10): string[] {
const userRatings = this.userItemMatrix.get(userId) || new Map();
const ratedItems = new Set(userRatings.keys());
const similarUsers = this.findSimilarUsers(userId, 20);
const itemScores: Map = new Map();
similarUsers.forEach(({ id: similarUserId, similarity }) => {
const similarUserRatings = this.userItemMatrix.get(similarUserId)!;
similarUserRatings.forEach((rating, itemId) => {
if (!ratedItems.has(itemId)) {
const current = itemScores.get(itemId) || { score: 0, count: 0 };
current.score += similarity * rating;
current.count += 1;
itemScores.set(itemId, current);
}
});
});
// Calculate weighted average scores
const recommendations: Array<{ itemId: string; score: number }> = [];
itemScores.forEach(({ score, count }, itemId) => {
recommendations.push({ itemId, score: score / count });
});
recommendations.sort((a, b) => b.score - a.score);
return recommendations.slice(0, n).map(r => r.itemId);
}
// Item-based recommendation ("Users who liked this also liked...")
public recommendSimilarItems(itemIds: string[], n: number = 10): string[] {
const excludeItems = new Set(itemIds);
const itemScores: Map = new Map();
itemIds.forEach(itemId => {
const similarItems = this.findSimilarItems(itemId, 20);
similarItems.forEach(({ id: similarItemId, similarity }) => {
if (!excludeItems.has(similarItemId)) {
const currentScore = itemScores.get(similarItemId) || 0;
itemScores.set(similarItemId, currentScore + similarity);
}
});
});
const recommendations = [...itemScores.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, n)
.map(([itemId]) => itemId);
return recommendations;
}
}
Real-Time Personalization with TensorFlow.js
TensorFlow.js enables client-side machine learning for personalization. This approach improves privacy, reduces latency, and allows for real-time adaptation without server round-trips. The library supports both training models directly in the browser and running pre-trained models from TensorFlow Hub:
// tensorflow-personalization.ts
import * as tf from '@tensorflow/tfjs';
interface UserEmbedding {
userId: string;
embedding: Float32Array;
}
interface ItemEmbedding {
itemId: string;
embedding: Float32Array;
features: ItemFeatures;
}
interface ItemFeatures {
category: string;
price: number;
tags: string[];
popularity: number;
}
class TensorFlowPersonalizationEngine {
private model: tf.LayersModel | null = null;
private userEmbeddingSize: number = 32;
private itemEmbeddingSize: number = 32;
private itemEmbeddings: Map = new Map();
async initialize(): Promise {
// Try to load pre-trained model, or create new one
try {
this.model = await tf.loadLayersModel('/models/personalization/model.json');
console.log('Loaded pre-trained personalization model');
} catch {
console.log('Creating new personalization model');
this.model = this.createModel();
}
}
private createModel(): tf.LayersModel {
// Neural Collaborative Filtering model
const userInput = tf.input({ shape: [1], name: 'user_input' });
const itemInput = tf.input({ shape: [1], name: 'item_input' });
const contextInput = tf.input({ shape: [10], name: 'context_input' });
// User embedding
const userEmbedding = tf.layers.embedding({
inputDim: 10000, // Max users
outputDim: this.userEmbeddingSize,
name: 'user_embedding'
}).apply(userInput) as tf.SymbolicTensor;
const userFlat = tf.layers.flatten().apply(userEmbedding) as tf.SymbolicTensor;
// Item embedding
const itemEmbedding = tf.layers.embedding({
inputDim: 50000, // Max items
outputDim: this.itemEmbeddingSize,
name: 'item_embedding'
}).apply(itemInput) as tf.SymbolicTensor;
const itemFlat = tf.layers.flatten().apply(itemEmbedding) as tf.SymbolicTensor;
// Combine user, item, and context
const concatenated = tf.layers.concatenate().apply([
userFlat,
itemFlat,
contextInput
]) as tf.SymbolicTensor;
// Deep neural network layers
let x = tf.layers.dense({
units: 128,
activation: 'relu',
kernelRegularizer: tf.regularizers.l2({ l2: 0.01 })
}).apply(concatenated) as tf.SymbolicTensor;
x = tf.layers.dropout({ rate: 0.3 }).apply(x) as tf.SymbolicTensor;
x = tf.layers.dense({
units: 64,
activation: 'relu'
}).apply(x) as tf.SymbolicTensor;
x = tf.layers.dropout({ rate: 0.2 }).apply(x) as tf.SymbolicTensor;
x = tf.layers.dense({
units: 32,
activation: 'relu'
}).apply(x) as tf.SymbolicTensor;
// Output: probability of interaction
const output = tf.layers.dense({
units: 1,
activation: 'sigmoid',
name: 'output'
}).apply(x) as tf.SymbolicTensor;
const model = tf.model({
inputs: [userInput, itemInput, contextInput],
outputs: output
});
model.compile({
optimizer: tf.train.adam(0.001),
loss: 'binaryCrossentropy',
metrics: ['accuracy']
});
return model;
}
// Encode context features
private encodeContext(context: UserContext): tf.Tensor {
return tf.tidy(() => {
const features = [
context.hourOfDay / 24,
context.dayOfWeek / 7,
context.isMobile ? 1 : 0,
context.sessionDuration / 3600,
context.pageViewCount / 50,
context.cartValue / 1000,
context.previousPurchases / 100,
context.daysSinceLastPurchase / 365,
context.isReturningUser ? 1 : 0,
context.engagementScore
];
return tf.tensor2d([features]);
});
}
// Predict user interest in items
async predictInterest(
userId: number,
itemIds: number[],
context: UserContext
): Promise> {
if (!this.model) {
throw new Error('Model not initialized');
}
const predictions = await tf.tidy(() => {
const userTensor = tf.tensor2d([[userId]].concat(
Array(itemIds.length - 1).fill([userId])
));
const itemTensor = tf.tensor2d(itemIds.map(id => [id]));
const contextTensor = this.encodeContext(context).tile([itemIds.length, 1]);
return this.model!.predict([
userTensor,
itemTensor,
contextTensor
]) as tf.Tensor;
});
const scores = await predictions.data();
predictions.dispose();
return itemIds.map((itemId, index) => ({
itemId,
score: scores[index]
})).sort((a, b) => b.score - a.score);
}
// Online learning: update model with new interaction
async updateWithInteraction(
userId: number,
itemId: number,
interacted: boolean,
context: UserContext
): Promise {
if (!this.model) return;
await tf.tidy(() => {
const userTensor = tf.tensor2d([[userId]]);
const itemTensor = tf.tensor2d([[itemId]]);
const contextTensor = this.encodeContext(context);
const labelTensor = tf.tensor2d([[interacted ? 1 : 0]]);
// Single step of gradient descent
this.model!.fit(
[userTensor, itemTensor, contextTensor],
labelTensor,
{
epochs: 1,
verbose: 0
}
);
});
}
// Save model for persistence
async saveModel(): Promise {
if (!this.model) return;
await this.model.save('localstorage://personalization-model');
}
// Get user embedding for visualization/clustering
async getUserEmbedding(userId: number): Promise {
if (!this.model) {
throw new Error('Model not initialized');
}
const embeddingLayer = this.model.getLayer('user_embedding');
const weights = embeddingLayer.getWeights()[0];
const embedding = weights.slice([userId, 0], [1, this.userEmbeddingSize]);
const data = await embedding.data() as Float32Array;
embedding.dispose();
return data;
}
}
// React hook for TensorFlow.js personalization
export function usePersonalization() {
const [engine, setEngine] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const initEngine = async () => {
const eng = new TensorFlowPersonalizationEngine();
await eng.initialize();
setEngine(eng);
setIsLoading(false);
};
initEngine();
}, []);
const getRecommendations = useCallback(async (
userId: number,
candidateItems: number[],
context: UserContext
) => {
if (!engine) return [];
return engine.predictInterest(userId, candidateItems, context);
}, [engine]);
const recordInteraction = useCallback(async (
userId: number,
itemId: number,
interacted: boolean,
context: UserContext
) => {
if (!engine) return;
await engine.updateWithInteraction(userId, itemId, interacted, context);
}, [engine]);
return { isLoading, getRecommendations, recordInteraction };
}
Dynamic Content Adaptation
Beyond product recommendations, personalization can adapt entire page layouts, messaging, and user interfaces based on user segments and behavior:
// dynamic-content.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
interface PersonalizationConfig {
heroVariant: 'minimal' | 'featured' | 'carousel';
ctaText: string;
ctaColor: string;
showSocialProof: boolean;
pricingDisplay: 'monthly' | 'annual' | 'both';
contentOrder: string[];
urgencyMessages: boolean;
}
interface UserSegment {
id: string;
name: string;
conditions: SegmentCondition[];
config: Partial;
priority: number;
}
interface SegmentCondition {
field: string;
operator: 'equals' | 'contains' | 'gt' | 'lt' | 'between';
value: any;
}
const defaultConfig: PersonalizationConfig = {
heroVariant: 'featured',
ctaText: 'Get Started',
ctaColor: '#007bff',
showSocialProof: true,
pricingDisplay: 'both',
contentOrder: ['hero', 'features', 'pricing', 'testimonials'],
urgencyMessages: false
};
// User segments with personalization rules
const userSegments: UserSegment[] = [
{
id: 'high-intent-buyer',
name: 'High Intent Buyer',
conditions: [
{ field: 'cartValue', operator: 'gt', value: 100 },
{ field: 'sessionCount', operator: 'gt', value: 3 }
],
config: {
ctaText: 'Complete Your Purchase',
ctaColor: '#28a745',
urgencyMessages: true,
showSocialProof: true
},
priority: 10
},
{
id: 'price-sensitive',
name: 'Price Sensitive',
conditions: [
{ field: 'viewedPricingPage', operator: 'gt', value: 2 },
{ field: 'addedCouponCode', operator: 'equals', value: true }
],
config: {
pricingDisplay: 'annual', // Show savings
ctaText: 'See Our Best Deals',
heroVariant: 'minimal'
},
priority: 8
},
{
id: 'returning-customer',
name: 'Returning Customer',
conditions: [
{ field: 'previousPurchases', operator: 'gt', value: 0 }
],
config: {
heroVariant: 'carousel', // Show new products
ctaText: 'See What\'s New',
contentOrder: ['hero', 'new-arrivals', 'recommendations', 'features']
},
priority: 7
},
{
id: 'mobile-user',
name: 'Mobile User',
conditions: [
{ field: 'device', operator: 'equals', value: 'mobile' }
],
config: {
heroVariant: 'minimal',
showSocialProof: false // Reduce clutter on mobile
},
priority: 5
}
];
class PersonalizationEngine {
private userData: Record = {};
setUserData(data: Record): void {
this.userData = { ...this.userData, ...data };
}
private evaluateCondition(condition: SegmentCondition): boolean {
const value = this.userData[condition.field];
switch (condition.operator) {
case 'equals':
return value === condition.value;
case 'contains':
return Array.isArray(value)
? value.includes(condition.value)
: String(value).includes(condition.value);
case 'gt':
return Number(value) > condition.value;
case 'lt':
return Number(value) < condition.value;
case 'between':
return Number(value) >= condition.value[0] &&
Number(value) <= condition.value[1];
default:
return false;
}
}
private matchesSegment(segment: UserSegment): boolean {
return segment.conditions.every(condition =>
this.evaluateCondition(condition)
);
}
getConfig(): PersonalizationConfig {
// Find all matching segments
const matchingSegments = userSegments
.filter(segment => this.matchesSegment(segment))
.sort((a, b) => b.priority - a.priority);
// Merge configs with priority
let config = { ...defaultConfig };
matchingSegments.reverse().forEach(segment => {
config = { ...config, ...segment.config };
});
return config;
}
getMatchingSegments(): UserSegment[] {
return userSegments.filter(segment => this.matchesSegment(segment));
}
}
// React Context for personalization
const PersonalizationContext = createContext<{
config: PersonalizationConfig;
segments: UserSegment[];
updateUserData: (data: Record) => void;
}>({
config: defaultConfig,
segments: [],
updateUserData: () => {}
});
export function PersonalizationProvider({ children }: { children: React.ReactNode }) {
const [engine] = useState(() => new PersonalizationEngine());
const [config, setConfig] = useState(defaultConfig);
const [segments, setSegments] = useState([]);
const updateUserData = useCallback((data: Record) => {
engine.setUserData(data);
setConfig(engine.getConfig());
setSegments(engine.getMatchingSegments());
}, [engine]);
// Initialize with basic data
useEffect(() => {
const isMobile = /Mobile|Android|iPhone/.test(navigator.userAgent);
updateUserData({
device: isMobile ? 'mobile' : 'desktop',
viewport: window.innerWidth
});
}, [updateUserData]);
return (
{children}
);
}
export function usePersonalizationConfig() {
return useContext(PersonalizationContext);
}
// Personalized component wrapper
export function PersonalizedContent({
children,
segment
}: {
children: React.ReactNode;
segment?: string;
}) {
const { segments } = usePersonalizationConfig();
if (segment && !segments.some(s => s.id === segment)) {
return null;
}
return <>{children}>;
}
// Example usage: Personalized Hero
function PersonalizedHero() {
const { config } = usePersonalizationConfig();
const heroComponents = {
minimal: MinimalHero,
featured: FeaturedHero,
carousel: CarouselHero
};
const HeroComponent = heroComponents[config.heroVariant];
return (
{config.showSocialProof && }
{config.urgencyMessages && }
);
}
A/B Testing Integration
Personalization should be continuously optimized through A/B testing. According to Harvard Business Review, online experiments are essential for validating personalization hypotheses. Here's an integrated testing framework:
// ab-testing.ts
interface Experiment {
id: string;
name: string;
variants: Variant[];
allocation: number; // % of users in experiment
status: 'draft' | 'running' | 'paused' | 'completed';
startDate: Date;
endDate?: Date;
targetSegments?: string[];
}
interface Variant {
id: string;
name: string;
weight: number; // Percentage of experiment traffic
config: Partial;
}
interface ExperimentResult {
variantId: string;
sampleSize: number;
conversions: number;
conversionRate: number;
revenue: number;
confidence: number;
}
class ABTestingEngine {
private experiments: Map = new Map();
private assignments: Map> = new Map();
addExperiment(experiment: Experiment): void {
this.experiments.set(experiment.id, experiment);
}
private hashUserId(userId: string, experimentId: string): number {
// Consistent hashing for deterministic assignment
const str = `${userId}-${experimentId}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash) % 100;
}
getVariant(userId: string, experimentId: string): Variant | null {
const experiment = this.experiments.get(experimentId);
if (!experiment || experiment.status !== 'running') {
return null;
}
// Check if user already has assignment
const userAssignments = this.assignments.get(userId);
if (userAssignments?.has(experimentId)) {
const variantId = userAssignments.get(experimentId)!;
return experiment.variants.find(v => v.id === variantId) || null;
}
// Determine if user is in experiment
const userHash = this.hashUserId(userId, experimentId);
if (userHash >= experiment.allocation) {
return null; // User not in experiment
}
// Assign variant based on weights
const variantHash = this.hashUserId(userId, `${experimentId}-variant`);
let cumulative = 0;
for (const variant of experiment.variants) {
cumulative += variant.weight;
if (variantHash < cumulative) {
// Store assignment
if (!this.assignments.has(userId)) {
this.assignments.set(userId, new Map());
}
this.assignments.get(userId)!.set(experimentId, variant.id);
return variant;
}
}
return experiment.variants[0]; // Fallback to first variant
}
// Statistical significance calculation
calculateSignificance(
controlConversions: number,
controlSampleSize: number,
treatmentConversions: number,
treatmentSampleSize: number
): number {
const controlRate = controlConversions / controlSampleSize;
const treatmentRate = treatmentConversions / treatmentSampleSize;
const pooledRate = (controlConversions + treatmentConversions) /
(controlSampleSize + treatmentSampleSize);
const standardError = Math.sqrt(
pooledRate * (1 - pooledRate) *
(1/controlSampleSize + 1/treatmentSampleSize)
);
const zScore = (treatmentRate - controlRate) / standardError;
// Convert z-score to confidence level (two-tailed)
const confidence = this.normalCDF(Math.abs(zScore)) * 2 - 1;
return confidence;
}
private normalCDF(x: number): number {
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
const sign = x < 0 ? -1 : 1;
x = Math.abs(x) / Math.sqrt(2);
const t = 1.0 / (1.0 + p * x);
const y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return 0.5 * (1.0 + sign * y);
}
}
// React hook for A/B testing
export function useExperiment(experimentId: string) {
const [variant, setVariant] = useState(null);
const engine = useABTestingEngine();
const userId = useUserId();
useEffect(() => {
const assignedVariant = engine.getVariant(userId, experimentId);
setVariant(assignedVariant);
if (assignedVariant) {
// Track exposure
trackEvent('experiment_exposure', {
experimentId,
variantId: assignedVariant.id
});
}
}, [experimentId, userId, engine]);
const trackConversion = useCallback((value?: number) => {
if (variant) {
trackEvent('experiment_conversion', {
experimentId,
variantId: variant.id,
value
});
}
}, [experimentId, variant]);
return { variant, trackConversion, isInExperiment: variant !== null };
}
Measuring Personalization Impact
Track the effectiveness of your personalization engine with comprehensive metrics:
// personalization-metrics.ts
interface PersonalizationMetrics {
recommendations: RecommendationMetrics;
segmentation: SegmentationMetrics;
experiments: ExperimentMetrics;
business: BusinessMetrics;
}
interface RecommendationMetrics {
clickThroughRate: number;
conversionRate: number;
coverageRate: number; // % of catalog recommended
diversityScore: number;
noveltyScore: number;
averagePosition: number;
}
class PersonalizationAnalytics {
async calculateMetrics(
dateRange: { start: Date; end: Date }
): Promise {
const [
recommendations,
segmentation,
experiments,
business
] = await Promise.all([
this.calculateRecommendationMetrics(dateRange),
this.calculateSegmentationMetrics(dateRange),
this.calculateExperimentMetrics(dateRange),
this.calculateBusinessMetrics(dateRange)
]);
return { recommendations, segmentation, experiments, business };
}
private async calculateRecommendationMetrics(
dateRange: { start: Date; end: Date }
): Promise {
const events = await this.fetchEvents(dateRange);
const impressions = events.filter(e => e.type === 'recommendation_impression');
const clicks = events.filter(e => e.type === 'recommendation_click');
const conversions = events.filter(e => e.type === 'recommendation_conversion');
const ctr = clicks.length / impressions.length;
const conversionRate = conversions.length / clicks.length;
// Calculate diversity (how varied are recommendations)
const recommendedItems = new Set(
impressions.map(e => e.data.itemId)
);
const totalItems = await this.getCatalogSize();
const coverageRate = recommendedItems.size / totalItems;
// Calculate novelty (are we recommending non-obvious items)
const popularItems = await this.getPopularItems(100);
const novelItems = [...recommendedItems].filter(
item => !popularItems.includes(item)
);
const noveltyScore = novelItems.length / recommendedItems.size;
return {
clickThroughRate: ctr,
conversionRate,
coverageRate,
diversityScore: this.calculateDiversity(impressions),
noveltyScore,
averagePosition: this.calculateAveragePosition(clicks)
};
}
private calculateDiversity(impressions: Event[]): number {
// Intra-list diversity using category distribution
const sessionRecommendations = this.groupBySession(impressions);
let totalDiversity = 0;
sessionRecommendations.forEach(session => {
const categories = session.map(e => e.data.category);
const uniqueCategories = new Set(categories);
totalDiversity += uniqueCategories.size / categories.length;
});
return totalDiversity / sessionRecommendations.size;
}
generateReport(metrics: PersonalizationMetrics): string {
return `
# Personalization Performance Report
## Recommendation Engine
- **Click-Through Rate**: ${(metrics.recommendations.clickThroughRate * 100).toFixed(2)}%
- **Conversion Rate**: ${(metrics.recommendations.conversionRate * 100).toFixed(2)}%
- **Catalog Coverage**: ${(metrics.recommendations.coverageRate * 100).toFixed(1)}%
- **Novelty Score**: ${(metrics.recommendations.noveltyScore * 100).toFixed(1)}%
## Business Impact
- **Revenue from Recommendations**: $${metrics.business.recommendationRevenue.toLocaleString()}
- **Revenue Lift vs Control**: ${metrics.business.revenueLift}%
- **Average Order Value**: $${metrics.business.averageOrderValue.toFixed(2)}
- **Customer Lifetime Value Impact**: +${metrics.business.clvImpact}%
## Segment Performance
${metrics.segmentation.segments.map(s => `
### ${s.name}
- Users: ${s.userCount.toLocaleString()}
- Conversion Rate: ${(s.conversionRate * 100).toFixed(2)}%
- Revenue: $${s.revenue.toLocaleString()}
`).join('')}
## Active Experiments
${metrics.experiments.active.map(e => `
### ${e.name}
- Status: ${e.status}
- Leading Variant: ${e.leadingVariant}
- Confidence: ${(e.confidence * 100).toFixed(1)}%
- Estimated Impact: ${e.estimatedImpact}%
`).join('')}
`;
}
}
Key Takeaways
Remember These Points
- Start with data collection: Comprehensive behavior tracking is the foundation of effective personalization
- Combine approaches: Hybrid systems using collaborative filtering, content-based, and contextual signals outperform single approaches
- Use TensorFlow.js for privacy: Client-side models protect user data while enabling real-time personalization
- Segment thoughtfully: Create meaningful user segments with clear rules and priority ordering
- Always A/B test: Personalization hypotheses must be validated with statistical rigor
- Measure holistically: Track both engagement metrics (CTR) and business outcomes (revenue, CLV)
- Balance relevance and discovery: Avoid filter bubbles by maintaining diversity in recommendations
Conclusion
AI-driven personalization transforms web applications from static experiences into intelligent systems that understand and adapt to each user. The implementation patterns covered in this guide provide a foundation for building personalization engines that can realistically achieve the 35%+ conversion improvements seen in industry leaders.
Start with solid behavior tracking, implement collaborative filtering for quick wins, then graduate to neural approaches with TensorFlow.js for sophisticated real-time personalization. For deeper learning, explore Google's Recommendation Systems course and the Neural Collaborative Filtering paper.
The key to success isn't just the algorithms; it's building a culture of continuous experimentation and optimization around your personalization stack. Tools like Segment for data collection and LaunchDarkly for feature flags can accelerate your personalization journey.