Skip to content

AB Testing Enhancement Design Document ​

Overview ​

The AB Testing Enhancement extends the existing libs/ab-test library to provide comprehensive AB testing capabilities for the Campus educational platform. The design follows Campus architecture patterns with token-based dependency injection, zero inter-library dependencies, and integration with the existing data pipeline (API β†’ Firehose β†’ S3 β†’ Redshift β†’ PowerBI).

The system provides declarative template rendering, comprehensive user journey tracking, statistical analysis capabilities, and seamless integration with Campus authentication and user management systems.

Architecture ​

High-Level Architecture ​

Component Architecture ​

The library follows a modular architecture with clear separation of concerns:

  1. Template Rendering Layer: Handles variant assignment and template rendering
  2. Tracking Layer: Manages event collection and user journey tracking
  3. Analytics Layer: Processes events and calculates success metrics
  4. Storage Layer: Manages persistent state and user assignments
  5. Integration Layer: Connects with Campus services and external APIs

C4 Model - AB Test Library Components ​

User Assignment and Persistence Strategy ​

Variant Assignment Algorithm ​

The system uses a deterministic hash-based assignment algorithm to ensure consistent variant assignment:

  1. User Identification:

    • Only authenticated users are supported in Campus applications
    • Use stable user ID from Campus authentication system
    • Anonymous users are not supported for AB testing
  2. Assignment Calculation:

    typescript
    function assignVariant(testId: string, userId: string, trafficAllocation: TrafficAllocation): VariantType | null {
      // Create deterministic hash from testId + userId
      const hash = createHash(testId + userId);
      const hashValue = hash % 100; // Convert to 0-99 range
    
      // Check if user is included in test traffic
      if (hashValue >= trafficAllocation.totalTrafficPercentage) {
        return null; // User not included in test
      }
    
      // Distribute among variants based on their percentage allocation
      let cumulativePercentage = 0;
      for (const distribution of trafficAllocation.variantDistribution) {
        cumulativePercentage += distribution.percentage;
        if (hashValue < (cumulativePercentage * trafficAllocation.totalTrafficPercentage) / 100) {
          return distribution.variantId;
        }
      }
    
      return trafficAllocation.variantDistribution[0].variantId; // Fallback to first variant
    }
  3. Assignment Persistence:

    • Primary Storage: Server-side storage via Campus API for authenticated users
    • Local Cache: Browser localStorage for performance optimization
    • Cross-device Consistency: Server storage ensures same variant across devices
  4. Assignment Lifecycle:

    • Assignments are created on first test encounter
    • Assignments persist for the duration of the test (until endDate)
    • Assignments can be marked as "sticky" to persist beyond test end
    • Expired assignments are automatically cleaned up

Traffic Distribution Examples ​

typescript
// Example 1: 50/50 A/B test with 100% traffic
const simpleABTest: TrafficAllocation = {
  totalTrafficPercentage: 100,
  variantDistribution: [
    { variantId: 'A', percentage: 50 },
    { variantId: 'B', percentage: 50 },
  ],
};

// Example 2: A/B/C test with 80% traffic participation
const multiVariantTest: TrafficAllocation = {
  totalTrafficPercentage: 80,
  variantDistribution: [
    { variantId: 'A', percentage: 40 }, // 32% of total users (40% of 80%)
    { variantId: 'B', percentage: 35 }, // 28% of total users (35% of 80%)
    { variantId: 'C', percentage: 25 }, // 20% of total users (25% of 80%)
  ],
};

// Example 3: Gradual rollout test
const rolloutTest: TrafficAllocation = {
  totalTrafficPercentage: 10, // Only 10% of users see the test
  variantDistribution: [
    { variantId: 'A', percentage: 90 }, // 9% of total users see control
    { variantId: 'B', percentage: 10 }, // 1% of total users see new feature
  ],
};

NgRx State Management ​

The AB testing system includes NgRx state management within the libs/ab-test library for variant assignments and test configurations. This follows Campus architecture patterns where each library manages its own state:

State Model ​

typescript
interface ABTestState {
  assignments: Record<string, VariantAssignment>; // testId -> assignment
  activeTests: ABTestDefinition[];
  loading: boolean;
  error: string | null;
}

interface VariantAssignment {
  testId: string;
  variantId: VariantType;
  userId: string;
  assignmentTime: Date;
  expirationTime?: Date;
  sticky: boolean;
}

Actions ​

typescript
// Variant Assignment Actions
export const VariantAssignmentActions = createActionGroup({
  source: 'Variant Assignment',
  events: {
    'Load Assignments': emptyProps(),
    'Load Assignments Success': props<{ assignments: VariantAssignment[] }>(),
    'Load Assignments Failure': props<{ error: string }>(),
    'Create Assignment': props<{ testId: string; variantId: VariantType }>(),
    'Create Assignment Success': props<{ assignment: VariantAssignment }>(),
    'Create Assignment Failure': props<{ error: string }>(),
  },
});

// AB Test Configuration Actions
export const ABTestConfigActions = createActionGroup({
  source: 'AB Test Config',
  events: {
    'Load Active Tests': emptyProps(),
    'Load Active Tests Success': props<{ tests: ABTestDefinition[] }>(),
    'Load Active Tests Failure': props<{ error: string }>(),
  },
});

// Event Tracking Actions
export const EventTrackingActions = createActionGroup({
  source: 'Event Tracking',
  events: {
    'Track Event': props<{ event: ABTestEvent }>(),
    'Batch Events': props<{ events: ABTestEvent[] }>(),
    'Send Events Success': emptyProps(),
    'Send Events Failure': props<{ error: string; events: ABTestEvent[] }>(),
  },
});

Effects ​

typescript
@Injectable()
export class ABTestEffects {
  private actions$ = inject(Actions);
  private abTestApiService = inject(AB_TEST_API_SERVICE);

  loadVariantAssignments$ = createEffect(() =>
    this.actions$.pipe(
      ofType(VariantAssignmentActions.loadAssignments),
      switchMap(() =>
        this.abTestApiService.getUserVariantAssignments().pipe(
          map((assignments) => VariantAssignmentActions.loadAssignmentsSuccess({ assignments })),
          catchError((error) => of(VariantAssignmentActions.loadAssignmentsFailure({ error: error.message })))
        )
      )
    )
  );

  createVariantAssignment$ = createEffect(() =>
    this.actions$.pipe(
      ofType(VariantAssignmentActions.createAssignment),
      switchMap(({ testId, variantId }) =>
        this.abTestApiService.createVariantAssignment(testId, variantId).pipe(
          map((assignment) => VariantAssignmentActions.createAssignmentSuccess({ assignment })),
          catchError((error) => of(VariantAssignmentActions.createAssignmentFailure({ error: error.message })))
        )
      )
    )
  );

  loadActiveTests$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ABTestConfigActions.loadActiveTests),
      switchMap(() =>
        this.abTestApiService.getActiveTests().pipe(
          map((tests) => ABTestConfigActions.loadActiveTestsSuccess({ tests })),
          catchError((error) => of(ABTestConfigActions.loadActiveTestsFailure({ error: error.message })))
        )
      )
    )
  );
}

Selectors ​

typescript
export const selectABTestState = createFeatureSelector<ABTestState>('abTest');

export const selectVariantAssignments = createSelector(selectABTestState, (state) => state.assignments);

export const selectVariantForTest = (testId: string) =>
  createSelector(selectVariantAssignments, (assignments) => assignments[testId]?.variantId);

export const selectActiveTests = createSelector(selectABTestState, (state) => state.activeTests);

export const selectABTestLoading = createSelector(selectABTestState, (state) => state.loading);

API Service Interface ​

typescript
interface ABTestApiService {
  getUserVariantAssignments(): Observable<VariantAssignment[]>;
  createVariantAssignment(testId: string, variantId: VariantType): Observable<VariantAssignment>;
  getActiveTests(): Observable<ABTestDefinition[]>;
}

export const AB_TEST_API_SERVICE = new InjectionToken<ABTestApiService>('ABTestApiService');

Library Structure ​

The NgRx state management will be organized within libs/ab-test as follows:

libs/ab-test/src/lib/
β”œβ”€β”€ store/
β”‚   β”œβ”€β”€ ab-test.actions.ts
β”‚   β”œβ”€β”€ ab-test.effects.ts
β”‚   β”œβ”€β”€ ab-test.reducer.ts
β”‚   β”œβ”€β”€ ab-test.selectors.ts
β”‚   └── index.ts
β”œβ”€β”€ services/
β”‚   β”œβ”€β”€ ab-test.service.ts
β”‚   β”œβ”€β”€ event-batcher.service.ts
β”‚   └── variant-assignment.service.ts
β”œβ”€β”€ components/
β”‚   └── ab-test.component.ts
β”œβ”€β”€ directives/
β”‚   └── ab-test.directive.ts
β”œβ”€β”€ interfaces/
β”‚   β”œβ”€β”€ ab-test-config.interface.ts
β”‚   β”œβ”€β”€ ab-test-event.interface.ts
β”‚   └── variant-assignment.interface.ts
└── index.ts

Components and Interfaces ​

Core Interfaces ​

AB Test Configuration ​

typescript
interface ABTestDefinition {
  id: string;
  name: string;
  description?: string;
  variants: ABTestVariant[];
  trafficAllocation: TrafficAllocation;
  successMetrics: SuccessMetric[];
  targetingRules?: TargetingRule[];
  status: 'draft' | 'active' | 'paused' | 'completed';
  startDate?: Date;
  endDate?: Date;
}

interface TrafficAllocation {
  totalTrafficPercentage: number; // Percentage of total users to include in test (0-100)
  variantDistribution: VariantDistribution[]; // How traffic is split between variants
}

interface VariantDistribution {
  variantId: VariantType; // Standard A/B/C/D/E/F variant naming
  percentage: number; // Percentage of test traffic allocated to this variant
}

interface TargetingRule {
  field: string; // 'userRole', 'deviceType', 'location', 'customAttribute'
  operator: 'equals' | 'contains' | 'in' | 'not_in' | 'greater_than' | 'less_than';
  value: unknown;
  logicalOperator?: 'AND' | 'OR'; // For combining multiple rules
}

type VariantType = 'A' | 'B' | 'C' | 'D' | 'E' | 'F';

interface ABTestVariant {
  id: VariantType; // Standard A/B/C/D/E/F variant naming
  name: string; // Descriptive name for the variant
  weight: number; // Percentage allocation (deprecated - use trafficAllocation.variantDistribution)
  templateData?: unknown;
  isControl: boolean; // Typically variant 'A' is the control
}

interface SuccessMetric {
  id: string;
  name: string;
  type: 'conversion' | 'engagement' | 'revenue' | 'custom';
  eventName: string;
  weight: number;
  targetValue?: number;
}

Event Tracking ​

typescript
interface ABTestEvent {
  eventId: string;
  testId: string;
  variantId: VariantType; // Standard A/B/C/D/E/F variant naming
  userId: string;
  sessionId: string;
  eventType: 'impression' | 'interaction' | 'conversion' | 'journey';
  eventName: string;
  timestamp: Date;
  properties: Record<string, unknown>;
  context: EventContext;
}

interface EventContext {
  userAgent: string;
  url: string;
  referrer?: string;
  viewport: { width: number; height: number };
  deviceType: 'mobile' | 'tablet' | 'desktop';
  userRole?: string;
}

User Journey Tracking ​

typescript
interface UserJourney {
  sessionId: string;
  userId: string;
  testAssignments: Record<string, VariantType>; // testId -> variantId
  events: JourneyEvent[];
  startTime: Date;
  endTime?: Date;
  conversionEvents: ConversionEvent[];
}

interface JourneyEvent {
  timestamp: Date;
  eventType: string;
  page: string;
  action: string;
  elementId?: string;
  duration?: number;
  properties: Record<string, unknown>;
}

Service Interfaces ​

Following Campus architecture patterns, each service defines minimal interfaces:

AB Test Service Interface ​

typescript
interface ABTestService {
  getVariantAssignment(testId: string): Observable<VariantType>;
  trackEvent(event: Partial<ABTestEvent>): void;
  trackConversion(testId: string, metricId: string, value?: number): void;
  getActiveTests(): Observable<ABTestDefinition[]>;
  getUserJourney(): Observable<UserJourney>;
}

export const AB_TEST_SERVICE = new InjectionToken<ABTestService>('ABTestService');

Analytics Service Interface ​

typescript
interface ABTestAnalyticsService {
  sendEvents(events: ABTestEvent[]): Observable<void>;
}

export const AB_TEST_ANALYTICS_SERVICE = new InjectionToken<ABTestAnalyticsService>('ABTestAnalyticsService');

Template Rendering Components ​

Structural Directive ​

typescript
@Directive({
  selector: '[campusAbTest]',
})
export class AbTestDirective implements OnInit, OnDestroy {
  @Input() campusAbTest: string; // Test ID
  @Input() campusAbTestVariant?: string; // Specific variant (for testing)
  @Input() campusAbTestDefault?: TemplateRef<unknown>; // Default template

  private templateRef = inject(TemplateRef<unknown>);
  private viewContainer = inject(ViewContainerRef);
  private abTestService = inject(AB_TEST_SERVICE);

  ngOnInit(): void {
    // Get variant assignment for the test
    this.abTestService.getVariantAssignment(this.campusAbTest)
      .subscribe(variantId => {
        // If this directive's template matches the assigned variant, render it
        // The variant matching is handled by using multiple directives with different variant IDs
        if (this.campusAbTestVariant === variantId) {
          this.viewContainer.createEmbeddedView(this.templateRef);
          // Track impression
          this.abTestService.trackEvent({
            testId: this.campusAbTest,
            variantId,
            eventType: 'impression',
            eventName: 'variant_shown'
          });
        } else if (!this.campusAbTestVariant && this.campusAbTestDefault) {
          // This is a default template and no specific variant matched
          this.viewContainer.createEmbeddedView(this.campusAbTestDefault);
        }
      });
  }
}

// Usage example:
// <div *campusAbTest="'homepage-cta-test'; variant: 'A'">
//   <button class="btn-primary">Sign Up Now</button>
// </div>
// <div *campusAbTest="'homepage-cta-test'; variant: 'B'">
//   <button class="btn-secondary">Get Started Today</button>
// </div>
//
// For multi-variant tests (A/B/C/D/E/F):
// <div *campusAbTest="'navigation-test'; variant: 'A'">Traditional Menu</div>
// <div *campusAbTest="'navigation-test'; variant: 'B'">Hamburger Menu</div>
// <div *campusAbTest="'navigation-test'; variant: 'C'">Tab Navigation</div>
// <div *campusAbTest="'navigation-test'; variant: 'D'">Sidebar Navigation</div>

Wrapper Component ​

typescript
@Component({
  selector: 'campus-ab-test',
  template: `
    <ng-container [ngSwitch]="currentVariant$ | async">
      <ng-container *ngSwitchCase="'A'">
        <ng-content select="[variant='A']"></ng-content>
      </ng-container>
      <ng-container *ngSwitchCase="'B'">
        <ng-content select="[variant='B']"></ng-content>
      </ng-container>
      <ng-container *ngSwitchCase="'C'">
        <ng-content select="[variant='C']"></ng-content>
      </ng-container>
      <ng-container *ngSwitchCase="'D'">
        <ng-content select="[variant='D']"></ng-content>
      </ng-container>
      <ng-container *ngSwitchCase="'E'">
        <ng-content select="[variant='E']"></ng-content>
      </ng-container>
      <ng-container *ngSwitchCase="'F'">
        <ng-content select="[variant='F']"></ng-content>
      </ng-container>
      <ng-container *ngSwitchDefault>
        <!-- Fallback content when no variant matches -->
        <ng-content></ng-content>
      </ng-container>
    </ng-container>
  `,
})
export class AbTestComponent implements OnInit {
  @Input() testId!: string;
  @Input() trackImpressions = true;

  currentVariant$: Observable<VariantType>;

  private abTestService = inject(AB_TEST_SERVICE);

  ngOnInit(): void {
    this.currentVariant$ = this.abTestService.getVariantAssignment(this.testId);

    if (this.trackImpressions) {
      this.currentVariant$.subscribe((variantId) => {
        this.abTestService.trackEvent({
          testId: this.testId,
          variantId,
          eventType: 'impression',
          eventName: 'variant_shown',
        });
      });
    }
  }
}

// Usage example:
// <campus-ab-test testId="homepage-cta-test">
//   <div variant="A">
//     <button class="btn-primary">Sign Up Now</button>
//   </div>
//   <div variant="B">
//     <button class="btn-secondary">Get Started Today</button>
//   </div>
// </campus-ab-test>

Event Batching System ​

Event Batcher ​

typescript
@Injectable()
class EventBatcher implements OnDestroy {
  private eventQueue: ABTestEvent[] = [];
  private batchSize = 50;
  private flushInterval = 5000; // 5 seconds
  private maxRetries = 3;
  private flushTimer$?: Subscription;
  private destroy$ = new Subject<void>();

  private analyticsService = inject(AB_TEST_ANALYTICS_SERVICE);

  addEvent(event: ABTestEvent): void {
    this.eventQueue.push(event);

    // Start flush timer if not already running
    if (!this.flushTimer$) {
      this.flushTimer$ = timer(this.flushInterval)
        .pipe(takeUntil(this.destroy$))
        .subscribe(() => this.flush());
    }

    // Immediate flush if batch size reached
    if (this.eventQueue.length >= this.batchSize) {
      this.flush();
    }
  }

  // Public method to force immediate flush (e.g., on journey complete, page unload)
  forceFlush(): void {
    this.flush();
  }

  private flush(): void {
    if (this.eventQueue.length === 0) return;

    // Clear the timer
    if (this.flushTimer$) {
      this.flushTimer$.unsubscribe();
      this.flushTimer$ = undefined;
    }

    const batch = [...this.eventQueue];
    this.eventQueue = [];

    this.analyticsService.sendEvents(batch).subscribe({
      error: (error) => this.handleBatchError(batch, error),
    });
  }

  // Handle page unload to flush remaining events
  onPageUnload(): void {
    if (this.eventQueue.length > 0) {
      // Use sendBeacon for reliable delivery during page unload
      this.sendBatchBeacon(this.eventQueue);
    }
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    this.forceFlush(); // Flush any remaining events
  }
}

Data Models ​

Variant Assignment Storage ​

typescript
interface VariantAssignment {
  testId: string;
  variantId: VariantType; // Standard A/B/C/D/E/F variant naming
  userId: string;
  assignmentTime: Date;
  expirationTime?: Date;
  sticky: boolean; // Whether assignment persists across sessions
}

User Segmentation ​

typescript
interface UserSegment {
  id: string;
  name: string;
  criteria: SegmentCriteria[];
  userCount?: number;
}

interface SegmentCriteria {
  field: string; // 'userRole', 'deviceType', 'location', etc.
  operator: 'equals' | 'contains' | 'in' | 'greaterThan' | 'lessThan';
  value: unknown;
}

Error Handling ​

Error Types ​

typescript
enum ABTestErrorType {
  VARIANT_ASSIGNMENT_FAILED = 'VARIANT_ASSIGNMENT_FAILED',
  TRACKING_EVENT_FAILED = 'TRACKING_EVENT_FAILED',
  TEMPLATE_RENDER_FAILED = 'TEMPLATE_RENDER_FAILED',
  ANALYTICS_API_ERROR = 'ANALYTICS_API_ERROR',
  CONFIGURATION_ERROR = 'CONFIGURATION_ERROR',
}

interface ABTestError {
  type: ABTestErrorType;
  message: string;
  testId?: string;
  variantId?: VariantType;
  originalError?: Error;
  context?: Record<string, unknown>;
}

Error Handling Strategy ​

  1. Graceful Degradation: Always show control variant if assignment fails
  2. Event Queuing: Queue failed events for retry when connectivity restored
  3. Logging: Comprehensive error logging for debugging
  4. Fallback Templates: Default templates when variant templates fail
  5. Circuit Breaker: Disable AB testing if error rate exceeds threshold

Testing Strategy ​

Unit Testing ​

  • Mock services for variant assignment testing
  • Template rendering verification
  • Event tracking validation
  • Event batching and fire-and-forget behavior

Integration Testing ​

  • End-to-end AB test flow
  • Analytics pipeline integration
  • Cross-browser compatibility
  • Performance impact measurement

Testing Utilities ​

typescript
class ABTestTestingUtils {
  static forceVariant(testId: string, variantId: VariantType): void;
  static mockAnalyticsService(): MockABTestAnalyticsService;
  static createTestEvent(overrides?: Partial<ABTestEvent>): ABTestEvent;
  static resetTestState(): void;
}

E2E Testing Support ​

typescript
interface ABTestE2EHelpers {
  setVariantForTest(testId: string, variantId: VariantType): Promise<void>;
  getActiveVariant(testId: string): Promise<VariantType>;
  waitForEventTracking(): Promise<void>;
  verifyEventWasTracked(eventName: string): Promise<boolean>;
}

Performance Considerations ​

Optimization Strategies ​

  1. Lazy Loading: Load AB test configurations on demand
  2. Caching: Cache variant assignments and test configurations
  3. Debouncing: Debounce rapid event tracking calls
  4. Background Processing: Process analytics in web workers
  5. CDN Integration: Serve test configurations from CDN

Memory Management ​

  • Automatic cleanup of expired assignments
  • Event queue size limits
  • Periodic garbage collection of old journey data
  • Efficient data structures for large-scale tracking

Security and Privacy ​

Data Protection ​

  • Anonymization of sensitive user data
  • GDPR compliance with consent management
  • Secure transmission of analytics data
  • Data retention policy enforcement

Access Control ​

  • Role-based access to AB test management
  • API authentication for analytics endpoints
  • Audit logging for test configuration changes
  • Rate limiting for event tracking endpoints

Backend Data Models ​

NestJS/TypeORM Entities ​

The backend uses NestJS with TypeORM for data persistence:

typescript
// AB Test Definition Entity
@Entity('ab_test_definitions')
export class ABTestDefinitionEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar', length: 255 })
  name: string;

  @Column({ type: 'text', nullable: true })
  description?: string;

  @Column({ type: 'json' })
  variants: ABTestVariant[];

  @Column({ type: 'json' })
  trafficAllocation: TrafficAllocation;

  @Column({ type: 'json' })
  successMetrics: SuccessMetric[];

  @Column({ type: 'json', nullable: true })
  targetingRules?: TargetingRule[];

  @Column({
    type: 'enum',
    enum: ['draft', 'active', 'paused', 'completed'],
    default: 'draft',
  })
  status: 'draft' | 'active' | 'paused' | 'completed';

  @Column({ type: 'timestamp', nullable: true })
  startDate?: Date;

  @Column({ type: 'timestamp', nullable: true })
  endDate?: Date;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @OneToMany(() => VariantAssignmentEntity, (assignment) => assignment.abTest)
  assignments: VariantAssignmentEntity[];
}

// Variant Assignment Entity
@Entity('variant_assignments')
export class VariantAssignmentEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'uuid' })
  testId: string;

  @Column({
    type: 'enum',
    enum: ['A', 'B', 'C', 'D', 'E', 'F'],
  })
  variantId: VariantType;

  @Column({ type: 'uuid' })
  userId: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  assignmentTime: Date;

  @Column({ type: 'timestamp', nullable: true })
  expirationTime?: Date;

  @Column({ type: 'boolean', default: false })
  sticky: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @ManyToOne(() => ABTestDefinitionEntity, (abTest) => abTest.assignments)
  @JoinColumn({ name: 'testId' })
  abTest: ABTestDefinitionEntity;

  @Index(['testId', 'userId'], { unique: true })
  static readonly uniqueUserTestAssignment: void;
}

// AB Test Event Entity (for analytics)
@Entity('ab_test_events')
export class ABTestEventEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar', length: 255 })
  eventId: string;

  @Column({ type: 'uuid' })
  testId: string;

  @Column({
    type: 'enum',
    enum: ['A', 'B', 'C', 'D', 'E', 'F'],
  })
  variantId: VariantType;

  @Column({ type: 'uuid' })
  userId: string;

  @Column({ type: 'varchar', length: 255 })
  sessionId: string;

  @Column({
    type: 'enum',
    enum: ['impression', 'interaction', 'conversion', 'journey'],
  })
  eventType: 'impression' | 'interaction' | 'conversion' | 'journey';

  @Column({ type: 'varchar', length: 255 })
  eventName: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  timestamp: Date;

  @Column({ type: 'json', nullable: true })
  properties?: Record<string, unknown>;

  @Column({ type: 'json', nullable: true })
  context?: EventContext;

  @CreateDateColumn()
  createdAt: Date;

  @Index(['testId', 'variantId'])
  static readonly testVariantIndex: void;

  @Index(['userId', 'timestamp'])
  static readonly userTimeIndex: void;
}

Repository Interfaces ​

typescript
// AB Test Definition Repository
export interface ABTestDefinitionRepository {
  findActiveTests(): Promise<ABTestDefinitionEntity[]>;
  findById(id: string): Promise<ABTestDefinitionEntity | null>;
  create(definition: Partial<ABTestDefinitionEntity>): Promise<ABTestDefinitionEntity>;
  update(id: string, updates: Partial<ABTestDefinitionEntity>): Promise<ABTestDefinitionEntity>;
  delete(id: string): Promise<void>;
}

// Variant Assignment Repository
export interface VariantAssignmentRepository {
  findByUserId(userId: string): Promise<VariantAssignmentEntity[]>;
  findByTestAndUser(testId: string, userId: string): Promise<VariantAssignmentEntity | null>;
  create(assignment: Partial<VariantAssignmentEntity>): Promise<VariantAssignmentEntity>;
  update(id: string, updates: Partial<VariantAssignmentEntity>): Promise<VariantAssignmentEntity>;
  deleteExpired(): Promise<number>;
}

// AB Test Event Repository
export interface ABTestEventRepository {
  createBatch(events: Partial<ABTestEventEntity>[]): Promise<ABTestEventEntity[]>;
  findByTestId(testId: string, limit?: number): Promise<ABTestEventEntity[]>;
  findByUserId(userId: string, limit?: number): Promise<ABTestEventEntity[]>;
  deleteOlderThan(date: Date): Promise<number>;
}

NestJS Service Layer ​

typescript
@Injectable()
export class ABTestService {
  constructor(
    @InjectRepository(ABTestDefinitionEntity)
    private readonly abTestRepo: Repository<ABTestDefinitionEntity>,
    @InjectRepository(VariantAssignmentEntity)
    private readonly assignmentRepo: Repository<VariantAssignmentEntity>,
    @InjectRepository(ABTestEventEntity)
    private readonly eventRepo: Repository<ABTestEventEntity>
  ) {}

  async getActiveTests(): Promise<ABTestDefinitionEntity[]> {
    return this.abTestRepo.find({
      where: { status: 'active' },
      order: { createdAt: 'DESC' },
    });
  }

  async getUserVariantAssignments(userId: string): Promise<VariantAssignmentEntity[]> {
    return this.assignmentRepo.find({
      where: { userId },
      relations: ['abTest'],
    });
  }

  async createVariantAssignment(
    testId: string,
    userId: string,
    variantId: VariantType
  ): Promise<VariantAssignmentEntity> {
    const assignment = this.assignmentRepo.create({
      testId,
      userId,
      variantId,
      assignmentTime: new Date(),
      sticky: false,
    });

    return this.assignmentRepo.save(assignment);
  }

  async trackEvents(events: Partial<ABTestEventEntity>[]): Promise<void> {
    await this.eventRepo.save(events);
  }
}

Integration Points ​

Campus Authentication Integration ​

typescript
interface CampusAuthIntegration {
  getCurrentUser(): Observable<CampusUser>;
  getUserRole(): Observable<string>;
  checkUserConsent(consentType: string): Observable<boolean>;
}

export const CAMPUS_AUTH_INTEGRATION = new InjectionToken<CampusAuthIntegration>('CampusAuthIntegration');

Analytics Pipeline Integration ​

The system integrates with the existing Campus data pipeline:

  1. API Layer: Campus backend receives batched events
  2. Firehose: Events streamed to AWS Kinesis Firehose
  3. S3 Storage: Raw events stored in S3 buckets
  4. Redshift: Data warehouse for analytics processing
  5. PowerBI: Business intelligence and reporting

Configuration Management ​

  • Environment-specific configurations
  • Feature flag integration
  • Dynamic configuration updates
  • A/B test lifecycle management through admin interfaces

Flow Diagrams ​

1. User Variant Assignment Flow ​

2. Event Tracking and Batching Flow ​

3. Template Rendering Flow ​

4. Variant Assignment Algorithm Flow ​

5. NgRx State Management Flow ​

6. Complete User Journey Flow ​

7. Error Handling and Fallback Flow ​