A/B тестирование в TypeScript: как не гадать на кофейной гуще

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Жизнь до A/B тестов: почему интуиция подводит

Вы когда-нибудь спорили с дизайнером, какая кнопка лучше — красная или синяя? Или с менеджером, какой заголовок привлечет больше кликов? Раньше такие споры решались громким голосом или положением в иерархии. Сегодня у нас есть A/B тестирование в TypeScript — научный метод, который превращает догадки в данные.

Представьте: вы запускаете интернет-магазин. Кажется, что если добавить отзывы на карточку товара, продажи вырастут. А если убрать карусель изображений и оставить одно большое фото? Без сплит-тестирования вы никогда не узнаете правду. Именно здесь на помощь приходит TypeScript — строгая типизация и современные инструменты делают A/B тесты надежными и удобными.

Что такое A/B тестирование простыми словами

A/B тест (он же сплит-тест) — это эксперимент, в котором двум группам пользователей показывают разные версии одного элемента. Контрольная группа (A) видит текущую версию, экспериментальная (B) — измененную. Затем мы сравниваем метрики: конверсию, время на сайте, клики.

Почему это важно? Потому что даже опытные продакты ошибаются в 70% случаев. Тестирование гипотез с помощью A/B тестов позволяет принимать решения на основе данных, а не интуиции. TypeScript добавляет к этому безопасность типов и удобство разработки.

Как работает A/B тестирование в TypeScript приложениях

В TypeScript приложениях A/B тестирование обычно реализуется через:

  • Распределение пользователей — какой вариант показать конкретному пользователю
  • Трекинг событий — сбор данных о действиях пользователя
  • Анализ результатов — расчет статистической значимости

Рассмотрим каждый этап на примере. Допустим, мы хотим протестировать новый заголовок на странице входа. Вот как это выглядит в коде:

// Определяем типы для эксперимента
type ExperimentVariant = 'control' | 'variant';
type ExperimentConfig = {
  id: string;
  variants: ExperimentVariant[];
  weights: number[]; // сумма должна быть 100
};

// Конфигурация A/B теста
const headlineExperiment: ExperimentConfig = {
  id: 'headline_test_2024',
  variants: ['control', 'variant'],
  weights: [50, 50] // 50% видят старый заголовок, 50% - новый
};

// Функция рандомного распределения
function assignVariant(config: ExperimentConfig): ExperimentVariant {
  const random = Math.random() * 100;
  let cumulativeWeight = 0;
  
  for (let i = 0; i < config.variants.length; i++) {
    cumulativeWeight += config.weights[i];
    if (random < cumulativeWeight) {
      return config.variants[i];
    }
  }
  
  return config.variants[0]; // fallback
}

// Использование
const userVariant = assignVariant(headlineExperiment);
console.log(`Пользователь получит вариант: ${userVariant}`);

Сегментация пользователей: чтобы не было путаницы

Важно, чтобы один пользователь всегда видел один и тот же вариант. Иначе данные будут испорчены. Для этого используем идентификатор пользователя или куки:

function getStableVariant(
  userId: string, 
  config: ExperimentConfig
): ExperimentVariant {
  // Используем хеш от userId для детерминированного распределения
  const hash = simpleHash(userId + config.id);
  const normalizedHash = hash % 100;
  
  let cumulativeWeight = 0;
  for (let i = 0; i < config.variants.length; i++) {
    cumulativeWeight += config.weights[i];
    if (normalizedHash < cumulativeWeight) {
      return config.variants[i];
    }
  }
  
  return config.variants[0];
}

function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash |= 0; // Convert to 32bit integer
  }
  return Math.abs(hash);
}

Трекинг событий: собираем данные

Без данных A/B тест — просто игра. Нужно отправлять события в аналитику. TypeScript помогает строго типизировать эти события:

// Типизируем события для трекинга
type ExperimentEvent = {
  experimentId: string;
  variant: ExperimentVariant;
  eventType: 'impression' | 'conversion' | 'click';
  userId: string;
  timestamp: number;
  metadata?: Record<string, string | number>;
};

// Функция отправки события
async function trackExperimentEvent(
  event: ExperimentEvent
): Promise<void> {
  try {
    await fetch('/api/track', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event)
    });
  } catch (error) {
    console.error('Ошибка трекинга:', error);
    // Никогда не ломаем пользовательский опыт
  }
}

// Пример использования
async function onPageLoad(userId: string, variant: ExperimentVariant) {
  await trackExperimentEvent({
    experimentId: 'headline_test_2024',
    variant,
    eventType: 'impression',
    userId,
    timestamp: Date.now()
  });
}

Библиотеки для A/B тестирования в TypeScript

Писать всё с нуля не обязательно. Вот несколько библиотек для A/B тестов, которые отлично работают с TypeScript:

  • GrowthBook — опенсорс платформа с хорошей типизацией
  • Flagsmith — feature flags + A/B тесты
  • Split — мощная платформа для экспериментов
  • LaunchDarkly — enterprise решение с SDK для TS

Пример интеграции с GrowthBook:

import { GrowthBook } from '@growthbook/growthbook';

const gb = new GrowthBook({
  apiHost: 'https://cdn.growthbook.io',
  clientKey: 'sdk-abc123',
  // Передаем атрибуты пользователя
  attributes: {
    id: userId,
    country: 'RU',
    beta: true
  }
});

// Получаем значение для эксперимента
const headline = gb.getFeatureValue('new-headline', 'Старый заголовок');
console.log('Заголовок:', headline);

Расчет статистической значимости: не делайте выводов слишком рано

Самая частая ошибка — остановить A/B тест через час, потому что вариант B показывает +10%. Это может быть просто случайность. Нужно дождаться статистической значимости.

Вот простая функция для расчета p-value (уровня значимости) с использованием z-теста:

function calculatePValue(
  controlConversions: number,
  controlTotal: number,
  variantConversions: number,
  variantTotal: number
): number {
  const p1 = controlConversions / controlTotal;
  const p2 = variantConversions / variantTotal;
  
  const pPooled = (controlConversions + variantConversions) / 
                  (controlTotal + variantTotal);
  
  const se = Math.sqrt(
    pPooled * (1 - pPooled) * (                

Блоги

Рекомендаций по книгам