npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ab-testing-lib

v0.1.1

Published

Lightweight A/B testing library for React with real-time updates via WebSocket

Readme

AB Testing Library

Легковесная библиотека для A/B тестирования и feature flags с детерминированным назначением вариантов, локальным хранением, обновлением конфигурации в реальном времени через WebSocket и синхронизацией состояния между вкладками.


Quick Start

npm install ab-testing-lib

Точки входа:

  • Vanilla JS — импортируйте только из "ab-testing-lib". React не требуется.
  • React — импортируйте из "ab-testing-lib/react". В проекте должен быть установлен React ^18.0.0 или ^19.0.0 (peer dependency, опциональная при установке пакета).

Минимальная конфигурация

Клиенту обязательно нужен realtime-провайдер. Без него в конструкторе выбрасывается ошибка.

import { ABClient, LocalStorageAdapter, WebSocketProvider } from "ab-testing-lib"

const client = new ABClient({
  realtime: new WebSocketProvider("ws://your-backend/ab-config"),
  // storage по умолчанию — LocalStorageAdapter, можно подключить ваш собственный (должен имплементировать interface StorageAdapter)
  // crossTabSync по умолчанию true в браузере, можно выключить (например, для тестов в окружении Node.js)
})

Library overview

Возможности

  • Эксперименты (A/B и мультивариантные) — ключ, варианты, веса, статус, доля трафика (splitPercentage).
  • Feature flags — вкл/выкл для доли пользователей с детерминированным rollout.
  • Детерминированное назначение — один и тот же пользователь стабильно получает один и тот же вариант (hash(userId + experimentKey)).
  • Sticky assignment — ранее назначенный вариант сохраняется при обновлении конфига (веса, splitPercentage), если он по-прежнему входит в список вариантов; UI не «скачет» при изменениях с бэкенда.
  • Rehydration — пользователь и варианты сохраняются в storage; при перезагрузке страницы состояние восстанавливается без пересчёта.
  • Real-time обновления — конфигурация приходит по WebSocket; при изменении split/weights/status варианты пересчитываются и подписчики уведомляются.
  • Синхронизация между вкладками — при изменении user/variants в другой вкладке (localStorage) текущая вкладка обновляет состояние и перерисовывает UI.
  • Override — переопределение вариантов/флагов для QA (query-параметры, админка, свой OverrideProvider).
  • Типизация — полная поддержка TypeScript; экспорт типов для конфигов и адаптеров.

React: обёртка приложения

Оберните дерево компонентов в ABProvider, чтобы использовать useExperiment, useFeatureFlag и useABClient:

import { ABProvider, LocalStorageAdapter, WebSocketProvider } from "ab-testing-lib/react"
import type { ABClientConfig } from "ab-testing-lib/react"

const config: ABClientConfig = {
  storage: new LocalStorageAdapter(),
  realtime: new WebSocketProvider("ws://localhost:3001"),
}

function App() {
  return (
    <ABProvider config={config}>
      <YourApp />
    </ABProvider>
  )
}

Важно: создавайте объект config один раз (вне компонента или через useMemo), иначе при каждом ре-рендере будет создаваться новый ABClient и вызываться destroy() у предыдущего.

Экспорты пакета

Точка входа "ab-testing-lib" (ядро, без React):

  • Классы: ABClient, ABClientError, LocalStorageAdapter, WebSocketProvider
  • Типы: ABClientConfig, ConfigItem, Experiment, FeatureFlag, UserData, VariantCallback, WebSocketProviderOptions, InitializeUserOptions, Logger, OverrideProvider, ABClientErrorCode
  • Интерфейсы: StorageAdapter, RealtimeProvider

Точка входа "ab-testing-lib/react" (ядро + React):

  • Всё перечисленное выше плюс: ABProvider, useABClient, useExperiment, useFeatureFlag

API documentation

ABClient

| Метод | Описание | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | initializeUser(data: UserData, options?: InitializeUserOptions) | Инициализирует пользователя. При совпадении id с сохранённым в storage восстанавливает данные оттуда. Сохраняет пользователя в storage и пересчитывает варианты. | | updateUser(data: UserData, options?: { reassignVariant?: boolean }) | Обновляет данные пользователя (merge с текущими). Если reassignVariant: true — пересчитывает варианты и уведомляет подписчиков. | | getVariant(experimentKey: string) | Возвращает вариант эксперимента (string \| null). Учитывает override. Требует предварительного initializeUser(). | | isFeatureEnabled(featureKey: string) | Возвращает, включён ли feature flag. Учитывает override. Требует предварительного initializeUser(). | | subscribe(experimentKey: string, callback: (variant: string \| null) => void) | Подписывается на изменения варианта/флага. Возвращает функцию отписки. Не требует инициализированного пользователя. | | isUserInitialized() | Проверяет, инициализирован ли пользователь (без throw). | | getUser() | Возвращает данные текущего пользователя (UserData \| null). Если пользователь не инициализирован — null. Бросает при вызове после destroy(). | | resetUser() | Сбрасывает пользователя и варианты в storage и памяти; подписчики получают null. После вызова нужно снова вызвать initializeUser(). | | destroy() | Отписывается от realtime, снимает listener storage, отключает WebSocket. После вызова вызовы методов клиента приводят к ABClientError (CLIENT_DESTROYED). |

UserData

type UserData = {
  id?: string
  email?: string
  attributes?: Record<string, unknown>
}

Если id не передан: при первом initializeUser сначала проверяется storage; если там есть сохранённый пользователь с id — используется он; иначе генерируется новый (crypto.randomUUID() или fallback).

React

| Экспорт | Описание | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | ABProvider | Провайдер контекста: создаёт один экземпляр ABClient из config, при размонтировании вызывает client.destroy(). | | useABClient() | Возвращает экземпляр ABClient из контекста. Бросает ABClientError (MISSING_PROVIDER), если вызван вне ABProvider. | | useExperiment(experimentKey: string) | Возвращает текущий вариант эксперимента (string \| null). Подписывается на обновления; при смене конфига по WebSocket значение обновляется. | | useFeatureFlag(featureKey: string) | Возвращает boolean — включён ли флаг. Подписывается на обновления (внутри используется subscribe с преобразованием "on"/"off" в boolean). |

Конфигурация ABClient (ABClientConfig)

interface ABClientConfig {
  storage?: StorageAdapter // по умолчанию: new LocalStorageAdapter()
  realtime: RealtimeProvider // обязательно
  crossTabSync?: boolean // true — синхронизация по storage event (по умолчанию true в браузере). При false (например, в Node/тестах) listener на storage не вешается.
  logger?: Logger // опционально: warn/error для неизвестных ключей, сбоев WS и т.д.
  override?: OverrideProvider // опционально: переопределение вариантов/флагов для QA
}

StorageAdapter

interface StorageAdapter {
  get<T>(key: string): T | null
  set<T>(key: string, value: T): void
  remove(key: string): void
}

Ключи, которые использует библиотека: ab-testing:user, ab-testing:variants (см. константы в коде; при кастомном адаптере можно не зависеть от них, если не нужна cross-tab синхронизация через storage event).

RealtimeProvider

interface RealtimeProvider {
  connect(): void
  disconnect(): void
  onConfigUpdate(cb: (config: ConfigItem[]) => void): () => void
}

При получении новой конфигурации клиент передаёт её в ExperimentManager; варианты пересчитываются и подписчики уведомляются.

WebSocketProvider

new WebSocketProvider(url: string, options?: WebSocketProviderOptions)

interface WebSocketProviderOptions {
  autoReconnect?: boolean          // true — автопереподключение при обрыве
  maxReconnectDelay?: number       // 30000 мс — макс. задержка перед переподключением
  initialReconnectDelay?: number   // 1000 мс — начальная задержка
}

Logger

interface Logger {
  info?(message: string, context?: Record<string, unknown>): void
  warn(message: string, context?: Record<string, unknown>): void
  error(message: string, context?: Record<string, unknown>): void
}

Используется для предупреждений об неизвестных ключах экспериментов/флагов и для ошибок (например, сбой подключения WebSocket, ошибка в callback обновления конфига). Если не передан — логи не выводятся.

OverrideProvider

interface OverrideProvider {
  getOverride(key: string): string | boolean | null
}
  • Для эксперимента: возвращать строку (вариант) или null.
  • Для feature flag: возвращать boolean или null.

Пример: чтение из query (?ab_checkout-button=B) или из ответа админки.

Ошибки (ABClientError)

type ABClientErrorCode =
  | "USER_NOT_INITIALIZED" // getVariant/isFeatureEnabled до initializeUser
  | "CLIENT_DESTROYED" // вызов метода после destroy()
  | "INVALID_CONFIG" // нет realtime, неверный callback в subscribe и т.д.
  | "INVALID_EXPERIMENT_KEY" // пустой или не строка
  | "MISSING_PROVIDER" // useABClient вне ABProvider

Обработка:

try {
  const v = client.getVariant("checkout")
} catch (e) {
  if (e instanceof ABClientError && e.code === "USER_NOT_INITIALIZED") {
    // вызвать initializeUser() и повторить или показать fallback
  }
}

Example usage snippets

Vanilla JS: инициализация и эксперимент

import { ABClient, LocalStorageAdapter, WebSocketProvider } from "ab-testing-lib"

const ab = new ABClient({
  storage: new LocalStorageAdapter(),
  realtime: new WebSocketProvider("ws://localhost:3001"),
})

ab.initializeUser({
  id: "user-123",
  attributes: { country: "KZ" },
})

const variant = ab.getVariant("checkout-button")

const unsubscribe = ab.subscribe("checkout-button", (newVariant) => {
  console.log("Вариант изменён:", newVariant)
})

Vanilla JS: feature flag

if (ab.isFeatureEnabled("new_dashboard")) {
  renderNewDashboard()
}

React: эксперимент и feature flag

function CheckoutButton() {
  const variant = useExperiment("checkout-button")
  const showBeta = useFeatureFlag("beta_ui")

  const label = variant === "B" ? "Купить сейчас" : "Добавить в корзину"

  return (
    <>
      <button className={variant === "B" ? "btn-primary" : "btn-default"}>{label}</button>
      {showBeta && <span>Beta</span>}
    </>
  )
}

React: условный рендер до инициализации пользователя

function App() {
  const ab = useABClient()
  const [ready, setReady] = useState(ab.isUserInitialized())

  useEffect(() => {
    if (ab.isUserInitialized()) setReady(true)
  }, [ab])

  if (!ready) {
    return (
      <LoginScreen
        onLogin={(user) => {
          ab.initializeUser(user)
          setReady(true)
        }}
      />
    )
  }

  return <Dashboard />
}

Смена пользователя и пересчёт вариантов

ab.resetUser()
ab.initializeUser({ id: "other-user", attributes: {} })
// getVariant / подписчики получают новые значения

Получение данных текущего пользователя

const user = ab.getUser()
if (user) {
  console.log(user.id, user.email, user.attributes)
}

Обновление атрибутов без пересчёта вариантов

ab.updateUser({ attributes: { plan: "premium" } })
// варианты не меняются

Пересчёт вариантов при обновлении пользователя

ab.updateUser({ attributes: { segment: "vip" } }, { reassignVariant: true })

How to simulate remote config updates

Обновления конфигурации приходят по WebSocket. Клиент принимает два формата сообщений.

Формат 1: объект с типом

{
  "type": "config_update",
  "experiments": [
    {
      "key": "checkout-button",
      "variants": ["A", "B"],
      "weights": [50, 50],
      "status": "active",
      "splitPercentage": 100
    }
  ]
}

Формат 2: массив экспериментов/флагов напрямую

[
  {
    "key": "checkout-button",
    "variants": ["A", "B"],
    "weights": [50, 50],
    "status": "active"
  },
  {
    "type": "feature_flag",
    "key": "new_dashboard",
    "rolloutPercentage": 50,
    "status": "active"
  }
]

Структура эксперимента (Experiment)

| Поле | Тип | Обязательное | Описание | | ----------------- | -------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | key | string | да | Уникальный ключ эксперимента | | variants | string[] | да | Список вариантов (например ["A", "B"]) | | weights | number[] | нет | Веса вариантов (длина = variants.length). По умолчанию равные доли | | status | "active" | "disabled" | нет | disabled — эксперимент не участвует, все получают null | | splitPercentage | number (0–100) | нет | Доля пользователей, участвующих в эксперименте. 100 — все; 0 — никто. Детерминировано по пользователю. Sticky: уже попавшие в эксперимент пользователи сохраняют вариант при уменьшении доли. |

Структура feature flag (FeatureFlag)

| Поле | Тип | Обязательное | Описание | | ------------------- | -------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | type | "feature_flag" | да | Отличие от эксперимента | | key | string | да | Ключ флага | | rolloutPercentage | number (0–100) | нет | Процент пользователей, для которых флаг включён. Sticky только для «on»: раз фича включена — не отключаем при уменьшении rollout. Для «off» пересчитываем — при увеличении rollout пользователи могут получить «on». | | status | "active" | "disabled" | нет | Аналогично эксперименту |

Локальная симуляция (без бэкенда)

  1. Свой RealtimeProvider — реализуйте интерфейс RealtimeProvider: при вызове connect() можно сразу вызвать onConfigUpdate с тестовым массивом ConfigItem[], либо эмулировать задержку и затем отправить конфиг.
  2. WebSocket-сервер в репозитории — в монорепе есть demo-app-backend: поднимает WebSocket на ws://localhost:3001, при подключении отправляет текущий конфиг; при получении сообщения { type: "set_experiments", experiments: [...] } обновляет конфиг и рассылает его всем клиентам. Запуск: из корня npm run backend или npm run dev (вместе с frontend и админкой).
  3. Админка (demo-app-admin-panel) — подключается к тому же WebSocket, позволяет менять эксперименты (ключ, варианты, веса, статус, splitPercentage) и feature flags (ключ, rolloutPercentage, статус). После сохранения бэкенд рассылает обновление — демо-приложение и все открытые вкладки получают новый конфиг и пересчитывают варианты.

Стратегия логирования в коде

  • ABClientError — ошибки использования API (не вызван initializeUser, вызов после destroy(), невалидный ключ, useABClient вне провайдера). Разработчик видит явный throw с кодом и сообщением.
  • logger.error (если передан) — инфраструктурные сбои (WebSocket не подключился, ошибка в callback обновления конфига). Библиотека продолжает работать.
  • logger.warn — подозрительные случаи (неизвестный ключ эксперимента/флага при уже загруженном конфиге). Помогает находить опечатки в ключах.

Рекомендации при изменении API

  • Не ломать контракт StorageAdapter и RealtimeProvider — сторонние адаптеры и провайдеры должны продолжать работать.
  • Новые опциональные поля в конфиге и в типах экспериментов/флагов — добавлять как опциональные, с обратной совместимостью.
  • При добавлении новых кодов ошибок — дополнять ABClientErrorCode и описание в README.

Лицензия

ISC