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

Прорабатывается ТЗ и задача отдаётся разработчикам. Те ворчат, просят сделать излишнее ТЗ, ставят явно завышенные сроки, но по итогу делают задачу. Задача тестируется и уходит конечным пользователям. На этом жизненный цикл идеи завершён. Теперь остаётся дождаться массива свежей аналитики и отпраздновать…

За первую неделю допускаются погрешности и потери - мало данных, пользователи привыкают к обновлениям, параллельно выкладываются прочие улучшения, высокое влияние выбросов. Однако ко второй неделе становится очевидно, что идея не только не принесла новых клиентов, но и заставила некоторых пользователей меньше пользоваться этим продуктом.

Идея, прошедшая десятки обсуждений и получившая сотню восторженных комментариев провалилась.

Гипотеза провалилась

Объёмное получилось введение. Но начать эту тему захотелось с длинного пути гипотезы. Потому что сломалась она в самом начале - она была поддержана лишь схожими с её автором людьми. Однако эти люди не самая подходящая ЦА, а возможно и вовсе её редкие исключения.

Именно поэтому при изменениях существующего функционала не опираются на взгляды автора и команды. Чтобы сделать верный выбор проводят исследования, анализируют существующую аналитику продукта и рынка, сравнивают с конкурентами. Но за всеми этими способами зачастую идёт единственный надёжный способ проверить гипотезу именно на аудитории бизнеса. Это (внимание!) - проверить её именно на аудитории бизнеса.

Но, не на всей. Этот способ называется А/Б тестированием. И именно ему будет посвящено всё дальнейшее повествование.

А/Б тестирование

Как уже выяснили выше - А/Б тестирование это проверка гипотезы на самой аудитории бизнеса. Проверка эта происходит за счёт сравнения одного функционала (варианта А) и другого (варианта Б).

Порою А/Б тест проводят по-очерёдно - то есть сперва замеряют вариант А, а потом, следующую неделю замеряют вариант Б. Этот вариант в статье описан не будет, так как ничего интересного в техническом плане из себя не представляет (а о сборе и анализе в этой статье ничего не будет).

А/Б тестирование может проходит как для проверки изменений - в таком случае в качестве вариант А остаётся текущий функционал, так и для проверки нескольких реализаций новой идеи - в таком случае оба варианта содержат новый функционал и их сравнивают относительно друг друга.

Вариантов, вопреки названию, может быть любое количество. Главное что бы это позволяла аудитория. А именно, чтобы можно было собрать достаточное количество данных по каждому варианту, исключив выбросы и помехи.

Итак, допустим принимается решение о внесении критического изменения на сайт или в приложение. В этот момент оценивается серьёзность этого изменения и принимается решения о внесении его через А/Б тест. Вместе с тем в зависимости от рисков решается и как распределять трафик.

Нередко тест начинают с показа нового варианта лишь для 10% пользователей. Затем, если изменения не привели к резкому ухудшению метрик на этих 10% - его распространяют на половину пользователей, чтобы сравнение было полноценным. По результатам этой проверки принимают решение - оставлять новый вариант или возвращать прежний.

При этом, конечно же, по результатам тестирования, можно вернуть идею на доработку и затем запустить обновлённый тест. Так может повторяться десятки раз, пока нужное изменение не приведёт к росту метрик бизнеса.

Правила А/Б тестов

  • Варианты должны содержать только те изменения, которые тестируются. Базовое правило, но, например, вместе с добавлением нового блока на страницу может захотеться и поменять её цвета. Как итог на результаты будут влиять все изменения и понять как повлияло именно добавление блока будет невозможно.

  • Связанное с первым правило - все варианты теста должны выпадать пользователю с одинаковыми скоростью, задержкой и проблемами.

  • Ещё одно вытекающее правило - стараться обходить накладывающиеся тесты. Если на одной странице проводится 2 и более теста, то все они искажают результаты друг друга и выявить эти искажения практически невозможно.

  • Пользователь не должен понимать, что он участвует в тесте. Узнав это пользователь может повести себя иначе, например покинуть сервис или перезагружать страницу чтобы выйти из теста.

  • В тестах интерфейса, пользователь должен крутить рулетку только один раз. В дальнейшем он должен видеть тот вариант, который ему выпал. Здесь в первую очередь вопрос пользовательского опыта - если он не сможет при повторном входе сразу увидеть нужный контент - у него останется неприятный опыт от сервиса.

 Тест с “небольшим отличием”. Источник: Pinterest
Тест с “небольшим отличием”. Источник: Pinterest

Схема А/Б тестов

Теперь, когда разобрались что, зачем и как проводится, можно, наконец, перейти к самому интересному - технической части вопроса.

И начать стоит с базовой схемы работы приложения:

Клиент - сервер - клиент

Очень простая схема общения. Клиент обратился по нужному адрему, сервер обработал этот запрос и вернул клиенту ответ.

С появлением А/Б тестов эта схема начинает работать немного иначе. Теперь делая идентичные запросы, в одно время и в одних условиях ожидаются разные ответы - те самые вариант А или вариант Б.

На практике же это обычно выполняет прослойка - либо на уровне CDN, либо обычный middleware на сервере, либо другие промежуточные инструменты, как, например, nginx (модуль для проведения А/Б тестов в nginx). В дальнейшем для простоты повествования будет использоваться просто middleware.

На самом деле А/Б тесты могут проводиться и целиком на стороне клиента. Так, например, работал гугл оптимайзер (но в сентябре 2023 года он был отключен). Главной проблемой такого подхода было то, что пользователя которому выпадал вариант б перенаправляло на другую страницу. Это, в свою очередь, делало вариант Б менее комфортным для пользователя и выдавало проведение тестирования.

Такой подход можно схематично описать так:

Реализация А/Б тестов

Ниже будет описываться решение на next.js, но его можно будет повторить на любой другой технологии, которая может менять cookie и делать реврайты (или возвращать конкретную страницу).

В next.js же это делается посредством middleware, который выполняется в так называемом edge-рантайме, то есть на уровне CDN. По факту же, вне Vercel (платформа для разворачивания приложений, владеющая next.js), это просто часть сервера, которая работает до обработки роутов.

Первый и самый простой способ тестирования - показ одного из вариантов без всяких условий:

import { NextResponse, NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/home') {
    if (rollVariant() === 1) {
      return NextResponse.rewrite(new URL('/home-animated', request.url));
    } else {
      return NextResponse.rewrite(new URL('/home', request.url));
    }
  }
}

Пользователь зашёл на страницу /home, в middleware выбирается случайный вариант. Если пользователю выпал вариант Б - возвращается страница home-animated, иначе стандартная home.

Удобнее делать варианты теста интерфейса отдельными страницами - новый вариант - новая страница.

root
--app
----about
------page.tsx
----home
------page.tsx
----home-animated
------page.tsx

Как выбрать, какой вариант показывать пользователю? Просто выбросить кубики! Если до половины - вариант а, иначе - вариант б.

const rollVariant = () => Math.random() < 0.5 ? 1 : 0;

Теперь пользователь, в зависимости от выпавшего значения, будет получать от сервера либо стандартную страницу, либо home-animated. За одинаковое время и незаметно для пользователя.

Однако, при каждом входе пользователю будет выпадать случайный вариант. Чтобы такого не происходило можно записывать в базу, что клиент стал участником А/Б теста. В случае же анонимных тестов информацию о тесте можно сохранить в cookie и в дальнейшем считывать из них.

Так, если у клиента уже записана cookie - можно пропускать шаги с проверкой запроса и выбора варианта, а сразу выдавать нужную страницу.

import { NextResponse, NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/home') {
    const prevVariant = request.cookies.get('ab_variant');
    const variant = prevVariant ?? rollVariant();
    let next: NextResponse;
    if (variant === 1) {
      next = NextResponse.rewrite(new URL('/home-animated', request.url));
    } else {
      next = NextResponse.rewrite(new URL('/home', request.url));
    }

    next.cookies.set('ab_variant', variant.toString());
    return next;
  }
}

Конечно же, эти данные нужно анализировать. Здесь 2 варианта - посылать данные с сервера, параллельно выдаче результата пользователю, или уже на клиенте, предварительно передав результаты теста с сервера. Для последнего можно использовать созданные прежде cookie.

Дальше может понадобиться запускать А/Б тесты только на определённую группу. Это может быть определённая доля пользователей, пользователи конкретных браузеров, пользователи из конкретных компаний или что угодно ещё.

То есть нужно проверить пользователя на совпадение и в зависимости от результата либо пропускать его в тест, либо нет:

import { NextResponse, NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/home' && request.nextUrl.searchParams.has('utm_campaign')) {
    // ...
  }
}

Также может потребоваться, чтобы участвовали только новые пользователи. Но, формально это таже задача, что и описана выше. Это группа пользователей которые не были на сайте прежде. В случае анонимных пользователей это можно определить, например, по отсутсвию кук теста, принятия политик или аналитики.

Конечно же, одним тестом не обойдётся и потребуется запускать параллельно десятки, а то и сотни тестов. Для этого будет использоваться таже логика, но проверяться уже будет по массиву инструкций запущенных тестов до первого подходящего.

Конечно же, в каждой компании будут разные условия, разные требования и разные порядки, выше описан базовый пример возможной реализации, исходя из которой каждый может решить, что именно нужно и как.

Тем не менее, было решено попробовать реализовать универсальный пакет для проведения А/Б тестов в next.js - @nimpl/ab-tests.

@nimpl/ab-tests

Первое, что важно отметить - то, что пакет соответствует всему описанному выше, в том числе всем правилам. При этом обладает рядом приятных возможностей, выполненных в привычном для next.js разработчиков API.

Схему работы пакета можно описать так:

Главное преимущество пакета - принцип поиска подходящего теста. Каждый тест может включать ключи has и missing. Те, кто знаком с next.js прекрасно знают эти ключи из работы с реврайтами и редиректами. Например, тест может быть описан так:

{
  id: 'some-id',
  source: '/en-(?<country>de|fr|it)',
  has: [
    {
      type: 'query',
      key: 'ref',
      value: 'utm_(?<ref>moogle|daybook)',
    }
  ],
  variants: [
    {
      weight: 0.5,
      destination: '/en-:country/:ref'
    },
    {
      weight: 0.5,
      destination: '/en-:country/:ref/new'
    }
  ],
}

Этот тест выполнится для всех пользователей, приходящих на страницу с англоязычными локалями и с меткой utm_*. Затем пользователь увидит либо базовую страницу под эту компанию, либо новую.

Также каждый тест содержи и другие ключи, такие как:

id - идентификатор теста, который будет записан cookie;

source - ещё один привычный из next.js ключ - путь на котором проводится тест;

variants - список вариантов, которых может быть любое количество.

У каждого варианта описывается weight - вес и destination (вновь привычный из next.js ключ). Главное правило - чтобы суммарный вес равнялся единице.

Дополнительная часть

Разработкой пакета всё не завершилась. Во время работы над пакетом было решено проверить его работу в нескольких проектах. Однако, добавить простой middleware оказалось настоящим приключением. Проблема в том, что в проектах уже были middleware - один с next-intl, один с next-auth.

Удивительно, но ни на одном из проектов не было прежде задачи поддерживать два сторонних middleware (только вместе с внутренними). В результате поиска не удалось найти никаких решений. Все существующие решения работают за счёт своих собственных API - сделаны под стиль express.js или вообще в своём видении. Они полезны, хорошо реализованы и удобны. Но только в тех случаях, когда можно обновить под них каждый используемый middleware.

Здесь же ситуация совсем другая. Нужно чтобы каждый middleware работал как оригинальный middleware от next.js. В общем, нужно было ещё одно новое решение. За него я и взялся.

Так появился @nimpl/middleware-chain:

import { default as authMiddleware } from "next-auth/middleware";
import createMiddleware from "next-intl/middleware";
import { chain } from "@nimpl/middleware-chain";

const intlMiddleware = createMiddleware({
  locales: ["en", "dk"],
  defaultLocale: "en",
});

export default chain([
  intlMiddleware,
  authMiddleware,
]);

Небольшая и аккуратная вставка.

Эти и другие пакеты для next.js можно посмотреть на nimpl.tech, возможно некоторые из решений вы найдёте полезными (как например геттер getPathname для серверных компонент или минификатор классов). Также я открыл в публичный доступ утилиту, которую использую для редактирования групп JSON-файлов - @nimpl/inio.

Комментарии (0)