7 раз отрежь, один релизни. А/Б тесты статических сайтов
Релиз начинается с идеи. Когда в потоке мозгового штурма приходит та самая идея, которая понравится всем пользователям и привлечёт новых клиентов. Идея презентуется команде менеджеров, маркетологов и безоговорочно поддерживается всеми.
Прорабатывается ТЗ и задача отдаётся разработчикам. Те ворчат, просят сделать излишнее ТЗ, ставят явно завышенные сроки, но по итогу делают задачу. Задача тестируется и уходит конечным пользователям. На этом жизненный цикл идеи завершён. Теперь остаётся дождаться массива свежей аналитики и отпраздновать…
За первую неделю допускаются погрешности и потери — мало данных, пользователи привыкают к обновлениям, параллельно выкладываются прочие улучшения, высокое влияние выбросов. Однако ко второй неделе становится очевидно, что идея не только не принесла новых клиентов, но и заставила некоторых пользователей меньше пользоваться этим продуктом.
Идея, прошедшая десятки обсуждений и получившая сотню восторженных комментариев провалилась.
Гипотеза провалилась
Объёмное получилось введение. Но начать эту тему захотелось с длинного пути гипотезы. Потому что сломалась она в самом начале — она была поддержана лишь схожими с её автором людьми. Однако эти люди не самая подходящая ЦА, а возможно и вовсе её редкие исключения.
Именно поэтому при изменениях существующего функционала не опираются на взгляды автора и команды. Чтобы сделать верный выбор проводят исследования, анализируют существующую аналитику продукта и рынка, сравнивают с конкурентами. Но за всеми этими способами зачастую идёт единственный надёжный способ проверить гипотезу именно на аудитории бизнеса. Это (внимание! ) — проверить её именно на аудитории бизнеса.
Но, не на всей. Этот способ называется А/Б тестированием. И именно ему будет посвящено всё дальнейшее повествование.
А/Б тестирование
Как уже выяснили выше — А/Б тестирование это проверка гипотезы на самой аудитории бизнеса. Проверка эта происходит за счёт сравнения одного функционала (варианта А) и другого (варианта Б).
Порою А/Б тест проводят по-очерёдно — то есть сперва замеряют вариант А, а потом, следующую неделю замеряют вариант Б. Этот вариант в статье описан не будет, так как ничего интересного в техническом плане из себя не представляет (а о сборе и анализе в этой статье ничего не будет).
А/Б тестирование может проходит как для проверки изменений — в таком случае в качестве вариант А остаётся текущий функционал, так и для проверки нескольких реализаций новой идеи — в таком случае оба варианта содержат новый функционал и их сравнивают относительно друг друга.
Вариантов, вопреки названию, может быть любое количество. Главное что бы это позволяла аудитория. А именно, чтобы можно было собрать достаточное количество данных по каждому варианту, исключив выбросы и помехи.
Итак, допустим принимается решение о внесении критического изменения на сайт или в приложение. В этот момент оценивается серьёзность этого изменения и принимается решения о внесении его через А/Б тест. Вместе с тем в зависимости от рисков решается и как распределять трафик.
Нередко тест начинают с показа нового варианта лишь для 10% пользователей. Затем, если изменения не привели к резкому ухудшению метрик на этих 10% — его распространяют на половину пользователей, чтобы сравнение было полноценным. По результатам этой проверки принимают решение — оставлять новый вариант или возвращать прежний.
При этом, конечно же, по результатам тестирования, можно вернуть идею на доработку и затем запустить обновлённый тест. Так может повторяться десятки раз, пока нужное изменение не приведёт к росту метрик бизнеса.
Правила А/Б тестов
Варианты должны содержать только те изменения, которые тестируются. Базовое правило, но, например, вместе с добавлением нового блока на страницу может захотеться и поменять её цвета. Как итог на результаты будут влиять все изменения и понять как повлияло именно добавление блока будет невозможно.
Связанное с первым правило — все варианты теста должны выпадать пользователю с одинаковыми скоростью, задержкой и проблемами.
Ещё одно вытекающее правило — стараться обходить накладывающиеся тесты. Если на одной странице проводится 2 и более теста, то все они искажают результаты друг друга и выявить эти искажения практически невозможно.
Пользователь не должен понимать, что он участвует в тесте. Узнав это пользователь может повести себя иначе, например покинуть сервис или перезагружать страницу чтобы выйти из теста.
В тестах интерфейса, пользователь должен крутить рулетку только один раз. В дальнейшем он должен видеть тот вариант, который ему выпал. Здесь в первую очередь вопрос пользовательского опыта — если он не сможет при повторном входе сразу увидеть нужный контент — у него останется неприятный опыт от сервиса.
Схема А/Б тестов
Теперь, когда разобрались что, зачем и как проводится, можно, наконец, перейти к самому интересному — технической части вопроса.
И начать стоит с базовой схемы работы приложения:
Клиент — сервер — клиент
Очень простая схема общения. Клиент обратился по нужному адрему, сервер обработал этот запрос и вернул клиенту ответ.
С появлением А/Б тестов эта схема начинает работать немного иначе. Теперь делая идентичные запросы, в одно время и в одних условиях ожидаются разные ответы — те самые вариант А или вариант Б.
На практике же это обычно выполняет прослойка — либо на уровне 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-(?de|fr|it)',
has: [
{
type: 'query',
key: 'ref',
value: 'utm_(?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.