7 раз отрежь, один релизни. А/Б тесты статических сайтов

b6c6c5f4faa8c001328f0f4f28b4fdab.png

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

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

5bf1cea9a953d9d63d68472cb28502a8.png

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

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

0cd4cac5a0cbcfa1222bcd294111d984.png

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

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

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

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

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

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

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

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

d1465d9688287fed133ee7ee7e3ec5d7.png

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

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

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

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

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

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

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

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

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

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

Тест с

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

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

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

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

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

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

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

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

f3d3be6b5d685bf5c3c9ca542a412bcb.png

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

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

d6467532d28f9202570c53f1b7194509.png

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

Ниже будет описываться решение на 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 и в дальнейшем считывать из них.

39ceccdc61b9cc197de4c32be37bce11.png

Так, если у клиента уже записана 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.

25d2347d241e54cc9c2dd1c7551d1cf7.png

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

fb18e3272f74c0ba00826a2626187d52.png

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

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.

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

87ea4686e477cb75847f49671f5e8141.png

Главное преимущество пакета — принцип поиска подходящего теста. Каждый тест может включать ключи 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.

© Habrahabr.ru