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:
- Template Rendering Layer: Handles variant assignment and template rendering
- Tracking Layer: Manages event collection and user journey tracking
- Analytics Layer: Processes events and calculates success metrics
- Storage Layer: Manages persistent state and user assignments
- 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:
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
Assignment Calculation:
typescriptfunction 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 }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
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 β
// 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 β
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 β
// 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 β
@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 β
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 β
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.tsComponents and Interfaces β
Core Interfaces β
AB Test Configuration β
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 β
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 β
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 β
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 β
interface ABTestAnalyticsService {
sendEvents(events: ABTestEvent[]): Observable<void>;
}
export const AB_TEST_ANALYTICS_SERVICE = new InjectionToken<ABTestAnalyticsService>('ABTestAnalyticsService');Template Rendering Components β
Structural Directive β
@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 β
@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 β
@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 β
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 β
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 β
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 β
- Graceful Degradation: Always show control variant if assignment fails
- Event Queuing: Queue failed events for retry when connectivity restored
- Logging: Comprehensive error logging for debugging
- Fallback Templates: Default templates when variant templates fail
- 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 β
class ABTestTestingUtils {
static forceVariant(testId: string, variantId: VariantType): void;
static mockAnalyticsService(): MockABTestAnalyticsService;
static createTestEvent(overrides?: Partial<ABTestEvent>): ABTestEvent;
static resetTestState(): void;
}E2E Testing Support β
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 β
- Lazy Loading: Load AB test configurations on demand
- Caching: Cache variant assignments and test configurations
- Debouncing: Debounce rapid event tracking calls
- Background Processing: Process analytics in web workers
- 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:
// 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 β
// 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 β
@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 β
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:
- API Layer: Campus backend receives batched events
- Firehose: Events streamed to AWS Kinesis Firehose
- S3 Storage: Raw events stored in S3 buckets
- Redshift: Data warehouse for analytics processing
- PowerBI: Business intelligence and reporting
Configuration Management β
- Environment-specific configurations
- Feature flag integration
- Dynamic configuration updates
- A/B test lifecycle management through admin interfaces