Hone logo
Hone
Problems

Angular A/B Testing Implementation

This challenge involves creating a flexible A/B testing system within an Angular application. A/B testing is crucial for product development, allowing you to experiment with different versions of features or UI elements to determine which performs better with your users. You will build a service and a directive that abstract away the complexity of assigning users to variations and displaying the appropriate content.

Problem Description

Your task is to implement an A/B testing mechanism in an Angular application. This system should allow developers to define experiments, assign users to different variations (A or B), and conditionally render content based on the assigned variation.

Key Requirements:

  1. Experiment Definition: Ability to define an A/B test, specifying a unique experiment ID and the variations (e.g., 'A', 'B', 'control', 'variant').
  2. User Assignment: A mechanism to assign a user to a specific variation for a given experiment. This assignment should be persistent for the user's session (e.g., using local storage or session storage). The assignment should be consistent across page reloads within the same session.
  3. Variation Retrieval: A way to programmatically retrieve the assigned variation for a given experiment ID.
  4. Directive for Conditional Rendering: A custom Angular directive that can be applied to DOM elements. This directive should accept an experiment ID and a mapping of variations to content or components to render. It should automatically render the correct content based on the user's assigned variation for that experiment.
  5. Service Abstraction: Encapsulate the logic for experiment definition, user assignment, and variation retrieval within an Angular service.

Expected Behavior:

  • When a user first encounters an experiment, they should be randomly assigned to one of its variations.
  • Subsequent visits or reloads within the same session should consistently show the same variation for that experiment.
  • The directive should dynamically render the appropriate content or component based on the assigned variation.
  • The system should be easily extensible to support more than two variations.

Edge Cases:

  • What happens if an experiment ID is not defined?
  • How should the system handle situations where a user's assignment is somehow lost or corrupted (e.g., storage cleared)?
  • Consider how to ensure a fair distribution of users across variations (e.g., 50/50 split).

Examples

Example 1: Basic Directive Usage

Let's assume an experiment named 'homepage-hero-banner' with variations 'A' and 'B'.

Component Template (my-component.component.html):

<div *abTest="'homepage-hero-banner'"
     [abTestVariations]="{ 'A': 'This is the original banner!', 'B': 'This is the new, exciting banner!' }">
</div>

Expected Behavior:

If the user is assigned to variation 'A' for 'homepage-hero-banner', the div will display "This is the original banner!". If assigned to 'B', it will display "This is the new, exciting banner!". If the experiment isn't defined or the user isn't assigned, the div might render nothing or a default state (depending on implementation).

Example 2: Programmatic Variation Check

Component (my-component.component.ts):

import { Component, OnInit } from '@angular/core';
import { ABTestingService } from './ab-testing.service'; // Assuming your service

@Component({
  selector: 'app-my-component',
  template: `
    <h1>Welcome!</h1>
    <ng-container *ngIf="showNewFeature">
      <p>Here's our brand new feature!</p>
    </ng-container>
    <ng-container *ngIf="!showNewFeature">
      <p>Enjoy the classic experience.</p>
    </ng-container>
  `,
})
export class MyComponent implements OnInit {
  showNewFeature: boolean = false;

  constructor(private abTestingService: ABTestingService) {}

  ngOnInit(): void {
    const experimentId = 'new-feature-rollout';
    const assignedVariation = this.abTestingService.getVariation(experimentId);

    if (assignedVariation === 'B') { // Assuming 'B' is the new feature variant
      this.showNewFeature = true;
    }
  }
}

Explanation:

The ABTestingService is used to fetch the assigned variation for the 'new-feature-rollout' experiment. If the user is assigned to variation 'B', the showNewFeature flag is set to true, leading to the display of the "brand new feature" message.

Example 3: Multiple Variations and Default Behavior

Consider an experiment 'button-color' with variations 'red', 'blue', and 'green'.

Component Template (button.component.html):

<button *abTest="'button-color'"
        [abTestVariations]="{
          'red': { text: 'Click Me (Red)', color: 'red' },
          'blue': { text: 'Click Me (Blue)', color: 'blue' },
          'green': { text: 'Click Me (Green)', color: 'green' }
        }"
        [style.background-color]="assignedConfig.color"
        (click)="onClick()">
  {{ assignedConfig.text }}
</button>

Component Logic (button.component.ts):

import { Component, Input, OnInit } from '@angular/core';
import { ABTestingService } from './ab-testing.service';

@Component({
  selector: 'app-button',
  templateUrl: './button.component.html',
})
export class ButtonComponent implements OnInit {
  assignedConfig: { text: string; color: string } = { text: '', color: '' };

  constructor(private abTestingService: ABTestingService) {}

  ngOnInit(): void {
    const experimentId = 'button-color';
    const assignedVariation = this.abTestingService.getVariation(experimentId);

    // Assume abTestVariations input property provides access to the mapping
    // In a real scenario, you might pass this mapping to the directive itself
    // For demonstration, we're accessing it here.
    const variationMap = this.getVariationMapFromDirective(); // Hypothetical getter

    if (variationMap && variationMap[assignedVariation]) {
      this.assignedConfig = variationMap[assignedVariation];
    } else {
      // Default behavior if no variation or mapping is found
      this.assignedConfig = { text: 'Default Button', color: 'gray' };
    }
  }

  onClick() {
    console.log(`Button clicked with variation: ${this.abTestingService.getVariation('button-color')}`);
  }

  // Hypothetical method to get the mapping from the directive
  // In practice, the directive would likely expose this data.
  private getVariationMapFromDirective(): any {
    // This is a placeholder. In a real directive, you'd have access
    // to the [abTestVariations] input and could retrieve it.
    return {
      'red': { text: 'Click Me (Red)', color: 'red' },
      'blue': { text: 'Click Me (Blue)', color: 'blue' },
      'green': { text: 'Click Me (Green)', color: 'green' }
    };
  }
}

Explanation:

The directive applies to the button. The [abTestVariations] input defines the mapping. The assignedConfig in the component is populated based on the user's assigned variation. If an unknown variation is assigned or the experiment doesn't exist, a default button configuration is used.

Constraints

  • The A/B testing service should be provided at the root level (providedIn: 'root').
  • User assignments should persist for the duration of the browser session. Use sessionStorage or localStorage.
  • The random assignment for new users should aim for a close to 50/50 split for binary experiments, or an even distribution for experiments with more variations.
  • The directive should be performant and not introduce significant rendering overhead.
  • The solution should use TypeScript and adhere to Angular best practices.

Notes

  • Consider using a library for generating random numbers if you need more sophisticated distribution logic.
  • Think about how you might integrate this with analytics tracking to measure conversion rates for different variations.
  • The directive's abTestVariations input can accept various types of data, including simple strings, numbers, or even component configurations for dynamic component loading.
  • When implementing user assignment, be mindful of potential race conditions if multiple experiments are being initialized simultaneously.
Loading editor...
typescript