AB Testing Documentation β
Overview β
The AB Test Service provides a complete solution for running controlled experiments in your Angular application. It handles variant assignment, impression tracking, conversion tracking, and seamlessly integrates with user journey analytics.
What it Does β
- Variant Assignment: Deterministic, hash-based assignment ensuring consistent user experience
- Impression Tracking: Automatically tracks when users see a variant
- Conversion Tracking: Measures success metrics and goal completion
- Event Tracking: Custom event tracking for detailed analysis
- Template Rendering: Angular components and directives for displaying variants
- Analytics Integration: Seamless integration with analytics services
- Privacy-First: User opt-out support and data anonymization
Key Features β
- β Deterministic Assignment - Consistent variant assignment per user
- β Template Components - Declarative variant rendering in templates
- β Automatic Impression Tracking - Tracks when variants are shown
- β Conversion Attribution - Links conversions to test variants
- β Scroll Depth Tracking - Measures user engagement depth
- β Event Batching - Efficient network usage with batched events
- β Circuit Breaker - Graceful degradation on errors
- β NgRx Integration - State management for test configuration
- β Testing Utilities - Mock services and test helpers
Setup Guide β
1. Module Import β
Import the ABTestModule in your app module:
// app.module.ts
import { ABTestModule } from '@campus/ab-test';
@NgModule({
imports: [
// ... other imports
ABTestModule,
],
})
export class AppModule {}2. Dependency Configuration β
Configure the required dependency tokens in your app-token.module.ts:
// app-token.module.ts
import { AB_TEST_AUTH_SERVICE, AB_TEST_API_SERVICE, AB_TEST_ANALYTICS_SERVICE } from '@campus/ab-test';
import { AuthService, AnalyticsService, ANALYTICS_SERVICE_TOKEN } from '@campus/dal';
@NgModule({
providers: [
// Authentication service for user context
{
provide: AB_TEST_AUTH_SERVICE,
useExisting: AuthService,
},
// Analytics service for event tracking
{
provide: ANALYTICS_SERVICE_TOKEN,
useClass: AnalyticsService,
},
{
provide: AB_TEST_ANALYTICS_SERVICE,
useExisting: ANALYTICS_SERVICE_TOKEN,
},
// API service for loading test configurations
{
provide: AB_TEST_API_SERVICE,
useValue: {
getActiveTests: () =>
of([
/* test definitions */
]),
},
},
],
})
export class AppTokenModule {}3. Configure AB Test API β
The API service only needs to provide active test definitions. The service handles variant assignment client-side using deterministic hashing.
Option A: Static Configuration (Development/Testing)
{
provide: AB_TEST_API_SERVICE,
useValue: {
getActiveTests: () => of([
{
id: 'books-tile',
name: 'Books Tile Layout Test',
variants: ['A', 'B'],
trafficAllocation: {
totalTrafficPercentage: 100,
variantDistribution: [
{ variantId: 'A', percentage: 50 },
{ variantId: 'B', percentage: 50 },
],
},
status: 'active',
},
]),
},
}Option B: API Integration (Production)
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ABTestApiIntegrationService {
constructor(private http: HttpClient) {}
getActiveTests(): Observable<ABTestDefinition[]> {
return this.http.get<ABTestDefinition[]>('/api/ab-tests/active');
}
}
// In providers:
{
provide: AB_TEST_API_SERVICE,
useClass: ABTestApiIntegrationService,
}Note: Variant assignment is handled client-side using deterministic hashing based on user ID and test ID. Assignments are persisted in localStorage for consistency across sessions.
AB Test Definition β
Creating Test Definitions β
Test definitions configure how experiments run. They specify variants, traffic allocation, and targeting rules.
Test Definition Structure β
interface ABTestDefinition {
id: string; // Unique test identifier (e.g., 'books-tile-layout')
name: string; // Human-readable test name
description?: string; // Optional description
variants: ABTestVariant[]; // Variant configurations
trafficAllocation: TrafficAllocation; // Traffic distribution settings
targetingRules?: TargetingRule[]; // Optional user targeting (permissions, roles, etc.)
status: 'draft' | 'active' | 'paused' | 'completed'; // Test lifecycle status
}Variant Configuration β
interface ABTestVariant {
id: VariantType; // 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
name: string; // Descriptive name (e.g., 'Grid Layout')
}Note: The control/baseline variant is determined by the fallbackVariant in the environment configuration, not by a field in the variant definition.
Traffic Allocation β
interface TrafficAllocation {
totalTrafficPercentage: number; // % of users included in test (0-100)
variantDistribution: VariantDistribution[]; // Distribution across variants
}
interface VariantDistribution {
variantId: VariantType; // Which variant
percentage: number; // % of included users (must sum to 100)
}Targeting Rules (Optional) β
Target specific users based on permissions, roles, or custom attributes:
interface TargetingRule {
field: string; // Field to evaluate ('permissions', 'role', etc.)
operator: 'equals' | 'contains' | 'in' | 'not_in' | 'greater_than' | 'less_than';
value: unknown; // Value to compare against
logicalOperator?: 'AND' | 'OR'; // Combine with other rules
}Example: Complete Test Definition β
const booksLayoutTest: ABTestDefinition = {
id: 'books-tile-layout',
name: 'Books Tile Layout Experiment',
description: 'Test grid vs list layout for book tiles',
variants: [
{ id: 'A', name: 'Current List' },
{ id: 'B', name: 'New Grid Layout' },
],
trafficAllocation: {
totalTrafficPercentage: 50, // Only 50% of users in test
variantDistribution: [
{ variantId: 'A', percentage: 50 }, // 25% of all users see A
{ variantId: 'B', percentage: 50 }, // 25% of all users see B
],
},
targetingRules: [
{
field: 'permissions',
operator: 'equals',
value: 'books.read',
logicalOperator: 'AND',
},
],
status: 'active',
};Important Notes:
- Users not meeting targeting rules receive the fallback variant (configured in environment)
- Variant assignments are deterministic and sticky per user
- Users outside traffic allocation see the fallback variant
- The fallback variant is always stored in NgRx state for consistent rendering
Environment Configuration β
Configure AB testing behavior in your environment files (environment.ts, environment.prod.ts):
export const environment = {
// ... other config
abTestConfig: {
environment: 'development', // 'development' | 'staging' | 'production'
debugMode: true, // Enable console logging
anonymizeData: false, // Anonymize user IDs in events
batchSize: 50, // Events per batch
flushInterval: 5000, // Flush interval (ms)
maxRetries: 3, // Max retry attempts for failed events
enableGracefulDegradation: true, // Fall back on errors
fallbackVariant: 'A', // Default variant for ineligible users
circuitBreakerThreshold: 10, // Errors before circuit opens
autoTrackPageViews: true, // Auto-track page navigation
},
};Configuration Options β
| Option | Type | Description |
|---|---|---|
environment | 'development' | 'staging' | 'production' | Deployment environment |
debugMode | boolean | Enable debug logging to console |
anonymizeData | boolean | Hash user IDs before sending events |
batchSize | number | Number of events per batch (default: 50) |
flushInterval | number | Milliseconds between batch flushes (default: 5000) |
maxRetries | number | Max retry attempts for failed requests (default: 3) |
enableGracefulDegradation | boolean | Return fallback on errors instead of throwing |
fallbackVariant | VariantType | Variant shown to ineligible users (default: 'A') |
circuitBreakerThreshold | number | Error count before opening circuit breaker (default: 10) |
autoTrackPageViews | boolean | Automatically track route navigation (default: true) |
Performance: sendBeacon API β
Event tracking uses the navigator.sendBeacon() API for optimal performance:
Benefits:
- Non-blocking - Doesn't delay page navigation or unload
- Reliable - Browser guarantees delivery even after page closes
- Efficient - Asynchronous, no impact on user experience
- Automatic fallback - Uses standard HTTP POST if sendBeacon unavailable
Authentication: The bearer token is included in the request payload (not headers, as sendBeacon doesn't support custom headers). The backend must extract the token from the payload's auth field.
Flow:
EventBatcherServicebatches eventsAnalyticsService.sendEvents()called- Bearer token retrieved from auth service
sendBeacon()sends payload with{ auth: token, events: [...] }- Fallback to HTTP POST if sendBeacon fails or unavailable
Environment-Specific Settings β
Development:
debugMode: true,
anonymizeData: false,
batchSize: 10,
flushInterval: 2000,Production:
debugMode: false,
anonymizeData: true,
batchSize: 100,
flushInterval: 10000,API Reference β
Variant Assignment β
getVariantAssignment(testId: string): Observable<VariantType | null> β
Gets the variant assignment for a specific test. Returns the variant ID the user is assigned to.
Parameters:
testId- Unique identifier for the AB test
Returns:
- Observable of variant ID (
'A' | 'B' | 'C' | 'D' | 'E' | 'F') ornullif not assigned
Example:
// In a component
export class MethodsOverviewComponent implements OnInit {
booksTileVariant$: Observable<VariantType | null>;
constructor(private abTestService: ABTestService) {}
ngOnInit(): void {
this.booksTileVariant$ = this.abTestService.getVariantAssignment('books-tile');
// Use the variant
this.booksTileVariant$.pipe(take(1)).subscribe((variant) => {
console.log('User assigned to variant:', variant);
// Variant 'A' or 'B'
});
}
}When to use:
- β When you need to programmatically check which variant a user has
- β For conditional logic based on variant
- β For logging or debugging
- β Don't use for template rendering (use
<campus-ab-test>component instead)
getAllVariantAssignments(): Observable<VariantAssignment[]> β
Retrieves all variant assignments for the current user.
Returns:
- Observable of array of variant assignments
Example:
// Load all assignments
this.abTestService.getAllVariantAssignments().subscribe((assignments) => {
console.log('All test assignments:', assignments);
// [
// { testId: 'books-tile', variantId: 'B', userId: '123', assignmentTime: Date, sticky: true },
// { testId: 'header-nav', variantId: 'A', userId: '123', assignmentTime: Date, sticky: true }
// ]
});When to use:
- β During app initialization
- β For updating user journey tracker with test assignments
- β For analytics dashboards
- β Not needed for individual test checks
Event Tracking β
trackImpression(testId: string, variantId: VariantType): void β
Tracks when a user sees a specific variant. This is usually called automatically by the <campus-ab-test> component.
Parameters:
testId- Test identifiervariantId- Variant that was shown to the user
Example:
// Usually automatic via component, but can be called manually:
ngAfterViewInit(): void {
// Manually track impression when custom content is shown
this.abTestService.trackImpression('custom-banner', 'B');
}When to use:
- β
Automatically handled by
<campus-ab-test>component (recommended) - β Manual tracking for custom rendering logic
- β When variant is displayed via JavaScript/TypeScript
- β Don't track multiple times for the same view
trackConversion(testId: string, metricId: string, value?: number): void β
Tracks when a user completes a goal or success metric associated with an AB test.
Parameters:
testId- Test identifiermetricId- Success metric identifier (e.g.,'book_opened','purchase_complete')value- Optional numeric value (e.g., revenue amount, items count)
Example:
// Track book opened conversion
onBookOpen(book: Book): void {
this.abTestService.trackConversion(
'books-tile',
'book_opened'
);
// Open the book...
this.openBook(book);
}
// Track with value
onPurchaseComplete(order: Order): void {
this.abTestService.trackConversion(
'checkout-flow',
'purchase_complete',
order.totalAmount
);
}
// Track feature adoption
onFeatureUsed(): void {
this.abTestService.trackConversion(
'new-feature-test',
'feature_used',
1
);
}What it captures:
- Conversion event with timestamp
- Time from impression to conversion
- Scroll depth at conversion time
- User context and device type
- AB test variant attribution
When to use:
- β After user completes the goal action
- β When success metric is achieved
- β After successful API calls that represent goal completion
- β Before API confirmation
- β For every click (use
trackEventinstead)
trackEvent(testId: string, eventName: string, properties?: Record<string, unknown>): void β
Tracks custom events for detailed behavioral analysis within an AB test.
Parameters:
testId- Test identifiereventName- Custom event name (e.g.,'button_clicked','video_played')properties- Optional additional data
Example:
// Track button clicks
onMethodAddClick(): void {
this.abTestService.trackEvent(
'books-tile',
'add_method_clicked',
{
source: 'books-tile-card',
methodsCount: this.methods.length
}
);
}
// Track video engagement
onVideoPlay(videoId: string): void {
this.abTestService.trackEvent(
'video-player-test',
'video_played',
{ videoId, autoplay: false }
);
}
// Track filter usage
onFilterApply(filters: Filter[]): void {
this.abTestService.trackEvent(
'filter-ui-test',
'filters_applied',
{
filterCount: filters.length,
filterTypes: filters.map(f => f.type)
}
);
}When to use:
- β User interactions within a test
- β Micro-conversions and engagement signals
- β Behavioral patterns
- β Core success metrics (use
trackConversioninstead)
Test Management β
getActiveTests(): Observable<ABTestDefinition[]> β
Retrieves all currently active AB tests.
Returns:
- Observable of array of test definitions
Example:
// Load active tests
this.abTestService.getActiveTests().subscribe((tests) => {
console.log('Active tests:', tests);
tests.forEach((test) => {
console.log(`Test: ${test.name}, Variants: ${test.variants.join(', ')}`);
});
});When to use:
- β Admin dashboards
- β Test monitoring
- β Debugging
- β Not needed for normal test usage
getTestById(testId: string): Observable<ABTestDefinition | null> β
Retrieves a specific test definition by ID.
Parameters:
testId- Test identifier
Returns:
- Observable of test definition or null
Example:
// Get specific test
this.abTestService.getTestById('books-tile').subscribe((test) => {
if (test) {
console.log('Test found:', test.name);
console.log('Variants:', test.variants);
console.log('Status:', test.status);
}
});flushEvents(): void β
Forces immediate flush of all pending events to analytics service.
Example:
// Force flush before logout
onLogout(): void {
this.abTestService.flushEvents();
this.authService.logout();
}
// Force flush before navigation away
@HostListener('window:beforeunload')
onBeforeUnload(): void {
this.abTestService.flushEvents();
}When to use:
- β Before logout
- β Before page unload
- β Before critical navigation
- β Not needed regularly (automatic batching handles this)
Template Rendering β
Component-Based Rendering β
The <campus-ab-test> component provides declarative variant rendering in templates.
Basic Usage β
<campus-ab-test testId="books-tile">
<ng-container variant="A">
<!-- Original design (Control) -->
<div class="books-grid-layout">
<campus-books-tile *ngFor="let book of books" [book]="book"> </campus-books-tile>
</div>
</ng-container>
<ng-container variant="B">
<!-- New design (Treatment) -->
<div class="books-card-layout">
<dcr-card *ngFor="let book of books">
<h4>{{ book.title }}</h4>
<dcr-image [src]="book.cover"></dcr-image>
</dcr-card>
</div>
</ng-container>
</campus-ab-test>How It Works β
- Component determines user's variant assignment for
testId - Automatically tracks impression when rendered
- Shows only the matching variant content
- Other variants are not rendered (no DOM overhead)
Complete Example with Click Tracking β
<!-- methods-overview.component.html -->
<campus-ab-test testId="books-tile">
<!-- Variant A: List Layout -->
<ng-container variant="A">
<div class="books-list">
<div *ngFor="let book of books" class="book-item" (click)="onBookOpen(book, 'A')">
<h3>{{ book.title }}</h3>
<p>{{ book.description }}</p>
</div>
</div>
</ng-container>
<!-- Variant B: Card Layout -->
<ng-container variant="B">
<div class="books-grid">
<dcr-card *ngFor="let book of books" (click)="onBookOpen(book, 'B')">
<div slot="hero">
<dcr-image [src]="book.cover"></dcr-image>
</div>
<h4>{{ book.title }}</h4>
</dcr-card>
</div>
</ng-container>
</campus-ab-test>// methods-overview.component.ts
export class MethodsOverviewComponent {
constructor(private abTestService: ABTestService) {}
onBookOpen(book: Book, variant: string): void {
// Track conversion
this.abTestService.trackConversion('books-tile', 'book_opened');
// Track interaction event
this.abTestService.trackEvent('books-tile', 'book_clicked', { bookId: book.id, variant });
// Open book logic
this.openBook(book);
}
}Directive-Based Rendering β
The campusAbTest directive provides variant-based rendering for individual elements.
Basic Usage β
<div *campusAbTest="'header-nav'; variant: 'A'" class="original-header">
<!-- Original header design -->
</div>
<div *campusAbTest="'header-nav'; variant: 'B'" class="new-header">
<!-- New header design -->
</div>When to Use Directive vs Component β
| Use Component When | Use Directive When |
|---|---|
| Multiple variants with different content | Simple show/hide based on variant |
| Complex variant differences | Styling or layout changes |
| Tracking impressions automatically | Conditional rendering of single elements |
| Preferred approach (recommended) | Need more granular control |
Angular Implementation Examples β
Example 1: Simple UI Test β
Test two different button styles:
<!-- button-test.component.html -->
<campus-ab-test testId="cta-button-style">
<ng-container variant="A">
<!-- Original button -->
<button class="btn-primary" (click)="onCtaClick()">Get Started</button>
</ng-container>
<ng-container variant="B">
<!-- New button -->
<button class="btn-accent" (click)="onCtaClick()">Start Now β</button>
</ng-container>
</campus-ab-test>// button-test.component.ts
export class ButtonTestComponent {
constructor(private abTestService: ABTestService) {}
onCtaClick(): void {
// Track conversion
this.abTestService.trackConversion('cta-button-style', 'cta_clicked');
// Navigate to signup
this.router.navigate(['/signup']);
}
}Example 2: Navigation Menu Test β
Test different navigation layouts:
<!-- navigation.component.html -->
<campus-ab-test testId="main-nav-layout">
<ng-container variant="A">
<!-- Horizontal navigation -->
<nav class="nav-horizontal">
<a *ngFor="let item of navItems" [routerLink]="item.route" (click)="trackNavClick(item.name)">
{{ item.label }}
</a>
</nav>
</ng-container>
<ng-container variant="B">
<!-- Sidebar navigation -->
<nav class="nav-sidebar">
<div class="nav-group" *ngFor="let group of navGroups">
<h4>{{ group.title }}</h4>
<a *ngFor="let item of group.items" [routerLink]="item.route" (click)="trackNavClick(item.name)">
<mat-icon>{{ item.icon }}</mat-icon>
{{ item.label }}
</a>
</div>
</nav>
</ng-container>
</campus-ab-test>// navigation.component.ts
export class NavigationComponent {
constructor(private abTestService: ABTestService) {}
trackNavClick(itemName: string): void {
this.abTestService.trackEvent('main-nav-layout', 'nav_item_clicked', { itemName });
}
}Example 3: Onboarding Flow Test β
Test different onboarding experiences:
// onboarding.component.ts
export class OnboardingComponent implements OnInit {
variant$: Observable<VariantType | null>;
currentStep = 1;
constructor(
private abTestService: ABTestService,
private userJourneyTracker: UserJourneyTrackerService,
) {}
ngOnInit(): void {
this.variant$ = this.abTestService.getVariantAssignment('onboarding-flow');
// Track funnel start
this.userJourneyTracker.trackFunnelStep('onboarding_funnel', 'start');
}
onStepComplete(step: number): void {
// Track funnel step
this.userJourneyTracker.trackFunnelStep('onboarding_funnel', `step_${step}_complete`);
// Track AB test event
this.abTestService.trackEvent('onboarding-flow', `step_${step}_completed`);
this.currentStep++;
}
onOnboardingComplete(): void {
// Track conversion
this.abTestService.trackConversion('onboarding-flow', 'onboarding_completed');
// Track funnel completion
this.userJourneyTracker.trackFunnelStep('onboarding_funnel', 'complete');
this.router.navigate(['/dashboard']);
}
onSkip(): void {
// Track dropoff
this.userJourneyTracker.trackFunnelDropoff('onboarding_funnel', `step_${this.currentStep}`, 'user_skipped');
this.router.navigate(['/dashboard']);
}
}<!-- onboarding.component.html -->
<campus-ab-test testId="onboarding-flow">
<ng-container variant="A">
<!-- Original: Single-page onboarding -->
<div class="onboarding-single-page">
<h2>Welcome! Tell us about yourself</h2>
<form (ngSubmit)="onOnboardingComplete()">
<!-- All fields on one page -->
<input type="text" placeholder="Name" />
<input type="email" placeholder="Email" />
<select name="role">
<option>Teacher</option>
</select>
<button type="submit">Get Started</button>
</form>
<button (click)="onSkip()">Skip</button>
</div>
</ng-container>
<ng-container variant="B">
<!-- Treatment: Multi-step onboarding -->
<div class="onboarding-wizard">
<div *ngIf="currentStep === 1">
<h2>Step 1: Basic Info</h2>
<input type="text" placeholder="Name" />
<button (click)="onStepComplete(1)">Next</button>
</div>
<div *ngIf="currentStep === 2">
<h2>Step 2: Contact</h2>
<input type="email" placeholder="Email" />
<button (click)="onStepComplete(2)">Next</button>
</div>
<div *ngIf="currentStep === 3">
<h2>Step 3: Your Role</h2>
<select>
<option>Teacher</option>
</select>
<button (click)="onOnboardingComplete()">Complete</button>
</div>
<button (click)="onSkip()">Skip</button>
</div>
</ng-container>
</campus-ab-test>Example 4: E-commerce Checkout Test β
// checkout.component.ts
export class CheckoutComponent implements OnInit {
private testId = 'checkout-flow';
private funnelId = 'purchase_funnel';
constructor(
private abTestService: ABTestService,
private userJourneyTracker: UserJourneyTrackerService,
private orderService: OrderService,
) {}
ngOnInit(): void {
// Track funnel start
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'checkout_start');
}
onShippingComplete(): void {
this.abTestService.trackEvent(this.testId, 'shipping_completed');
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'shipping_complete');
}
onPaymentComplete(): void {
this.abTestService.trackEvent(this.testId, 'payment_completed');
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'payment_complete');
}
onOrderSubmit(order: Order): void {
this.orderService.createOrder(order).subscribe(
(createdOrder) => {
// Track conversion after successful API call
this.abTestService.trackConversion(this.testId, 'purchase_complete', createdOrder.totalAmount);
// Track funnel completion
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'purchase_complete');
// Also track as conversion event in journey
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: this.testId,
variantId: 'B', // Get actual variant
metricId: 'purchase_complete',
value: createdOrder.totalAmount,
properties: {
orderId: createdOrder.id,
itemCount: createdOrder.items.length,
},
});
this.router.navigate(['/order-confirmation', createdOrder.id]);
},
(error) => {
// Track failure
this.userJourneyTracker.trackFunnelDropoff(this.funnelId, 'payment_complete', 'api_error');
},
);
}
}Best Practices β
1. Naming Conventions β
Test IDs:
// β
Good: Descriptive, kebab-case
'books-tile-layout';
'checkout-flow-v2';
'header-navigation';
// β Bad: Unclear, inconsistent
'test1';
'BooksTest';
'new_thing';Metric IDs:
// β
Good: Clear goal, snake_case
'book_opened';
'purchase_complete';
'signup_submitted';
// β Bad: Vague or inconsistent
'clicked';
'done';
'Success';Event Names:
// β
Good: Action-based, descriptive
'add_method_clicked';
'video_played';
'form_submitted';
// β Bad: Generic
'click';
'event1';2. When to Track Conversions β
// β
CORRECT: Track after API success
createTask(task: Task): void {
this.api.createTask(task).subscribe(
(created) => {
// NOW track conversion - we know it succeeded
this.abTestService.trackConversion('task-creation-ui', 'task_created');
this.router.navigate(['/tasks', created.id]);
}
);
}
// β WRONG: Track before API call
createTask(task: Task): void {
// Don't track here - API might fail
this.abTestService.trackConversion('task-creation-ui', 'task_created');
this.api.createTask(task).subscribe(/* ... */);
}3. Impression Tracking β
// β
CORRECT: Let component handle it
// Just use the component - impression tracked automatically
<campus-ab-test testId="books-tile">
<ng-container variant="A"><!-- ... --></ng-container>
<ng-container variant="B"><!-- ... --></ng-container>
</campus-ab-test>
// β WRONG: Manual impression tracking (usually unnecessary)
ngOnInit(): void {
this.variant$ = this.abTestService.getVariantAssignment('books-tile');
this.variant$.subscribe(variant => {
this.abTestService.trackImpression('books-tile', variant); // Not needed!
});
}4. Test Granularity β
// β
GOOD: One test per feature/page
'books-tile-layout'; // Tests tile design
'checkout-payment-ui'; // Tests payment form
'signup-form-fields'; // Tests signup form
// β BAD: Too broad or too narrow
'entire-app-redesign'; // Too broad - can't isolate impact
'button-color-shade-12'; // Too narrow - not meaningful5. Combining with User Journey Tracking β
// β
CORRECT: Track both AB test and funnel
export class CheckoutComponent {
onOrderComplete(order: Order): void {
// 1. Track AB test conversion
this.abTestService.trackConversion('checkout-flow', 'purchase_complete', order.totalAmount);
// 2. Track funnel completion
this.userJourneyTracker.trackFunnelStep('purchase_funnel', 'complete');
// 3. Track conversion in journey (for attribution)
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'checkout-flow',
variantId: this.currentVariant,
metricId: 'purchase_complete',
value: order.totalAmount,
});
}
}6. Error Handling β
// β
CORRECT: Defensive tracking
trackConversionSafely(testId: string, metricId: string): void {
try {
this.abTestService.trackConversion(testId, metricId);
} catch (error) {
console.error('Failed to track conversion:', error);
// Don't let tracking errors break user flow
}
}
// β οΈ Circuit breaker is built-in
// The service automatically handles errors gracefully
// Just call the methods - they won't throw
this.abTestService.trackConversion('test', 'metric');