Skip to content

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:

typescript
// 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:

typescript
// 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)

typescript
{
  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)

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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:

typescript
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 ​

typescript
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):

typescript
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 ​

OptionTypeDescription
environment'development' | 'staging' | 'production'Deployment environment
debugModebooleanEnable debug logging to console
anonymizeDatabooleanHash user IDs before sending events
batchSizenumberNumber of events per batch (default: 50)
flushIntervalnumberMilliseconds between batch flushes (default: 5000)
maxRetriesnumberMax retry attempts for failed requests (default: 3)
enableGracefulDegradationbooleanReturn fallback on errors instead of throwing
fallbackVariantVariantTypeVariant shown to ineligible users (default: 'A')
circuitBreakerThresholdnumberError count before opening circuit breaker (default: 10)
autoTrackPageViewsbooleanAutomatically 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:

  1. EventBatcherService batches events
  2. AnalyticsService.sendEvents() called
  3. Bearer token retrieved from auth service
  4. sendBeacon() sends payload with { auth: token, events: [...] }
  5. Fallback to HTTP POST if sendBeacon fails or unavailable

Environment-Specific Settings ​

Development:

typescript
debugMode: true,
anonymizeData: false,
batchSize: 10,
flushInterval: 2000,

Production:

typescript
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') or null if not assigned

Example:

typescript
// 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:

typescript
// 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 identifier
  • variantId - Variant that was shown to the user

Example:

typescript
// 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 identifier
  • metricId - Success metric identifier (e.g., 'book_opened', 'purchase_complete')
  • value - Optional numeric value (e.g., revenue amount, items count)

Example:

typescript
// 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 trackEvent instead)

trackEvent(testId: string, eventName: string, properties?: Record<string, unknown>): void ​

Tracks custom events for detailed behavioral analysis within an AB test.

Parameters:

  • testId - Test identifier
  • eventName - Custom event name (e.g., 'button_clicked', 'video_played')
  • properties - Optional additional data

Example:

typescript
// 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 trackConversion instead)

Test Management ​

getActiveTests(): Observable<ABTestDefinition[]> ​

Retrieves all currently active AB tests.

Returns:

  • Observable of array of test definitions

Example:

typescript
// 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:

typescript
// 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:

typescript
// 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 ​

html
<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 ​

  1. Component determines user's variant assignment for testId
  2. Automatically tracks impression when rendered
  3. Shows only the matching variant content
  4. Other variants are not rendered (no DOM overhead)

Complete Example with Click Tracking ​

html
<!-- 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>
typescript
// 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 ​

html
<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 WhenUse Directive When
Multiple variants with different contentSimple show/hide based on variant
Complex variant differencesStyling or layout changes
Tracking impressions automaticallyConditional rendering of single elements
Preferred approach (recommended)Need more granular control

Angular Implementation Examples ​

Example 1: Simple UI Test ​

Test two different button styles:

html
<!-- 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>
typescript
// 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:

html
<!-- 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>
typescript
// 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:

typescript
// 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']);
  }
}
html
<!-- 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 ​

typescript
// 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:

typescript
// βœ… Good: Descriptive, kebab-case
'books-tile-layout';
'checkout-flow-v2';
'header-navigation';

// ❌ Bad: Unclear, inconsistent
'test1';
'BooksTest';
'new_thing';

Metric IDs:

typescript
// βœ… Good: Clear goal, snake_case
'book_opened';
'purchase_complete';
'signup_submitted';

// ❌ Bad: Vague or inconsistent
'clicked';
'done';
'Success';

Event Names:

typescript
// βœ… Good: Action-based, descriptive
'add_method_clicked';
'video_played';
'form_submitted';

// ❌ Bad: Generic
'click';
'event1';

2. When to Track Conversions ​

typescript
// βœ… 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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 meaningful

5. Combining with User Journey Tracking ​

typescript
// βœ… 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 ​

typescript
// βœ… 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');