🧪 Руководство: Гибридное A/B-тестирование

В этом руководстве описан процесс внедрения A/B-теста в гибридной модели интеграции. Бэкенд сайта запрашивает вариацию кампании через API во время server-side rendering и сразу включает выбранный контент в HTML. Пользователь видит финальную версию страницы без мерцания, а взаимодействия отправляются в Gravity Field для аналитики теста.

Для новых hybrid-сценариев используйте API V2. Если у вас уже работает /ssapi/* интеграция, можно оставить API V1 и переносить тесты постепенно.

Преимущества гибридного подхода

  • Отсутствие мерцания: вариация встраивается в HTML на стороне сервера.
  • Высокая производительность: критичный контент загружается без ожидания клиентского JavaScript.
  • Надёжность: выбор вариации выполняется на бэкенде и меньше зависит от браузерной среды.

Предварительные требования

Перед началом убедитесь, что у вас настроено следующее:

  1. Рабочая гибридная интеграция: бэкенд умеет взаимодействовать с API Gravity Field. Подробнее о гибридной интеграции.
  2. API-ключи: есть API-ключ для аутентификации запросов. Подробнее об управлении API-ключами.
  3. API-селектор: в кампании задан selector, по которому бэкенд будет запрашивать вариацию.

Шаг 1: Настройка кампании в интерфейсе

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

  1. Перейдите в раздел Campaigns -> API Campaigns и нажмите Создать кампанию.
  2. Выберите тип кампании Custom JSON. Этот тип позволяет вернуть данные в произвольном JSON-формате, который ваш бэкенд сможет обработать.
  3. Задайте API-селектор. Например: homepage_cta_test.
  4. Создайте как минимум две вариации.
  5. В каждой вариации определите JSON-объект, который описывает контент.

Вариация А:

{
  "button_text": "Узнать больше",
  "button_color": "#007bff"
}

Вариация Б:

{
  "button_text": "Купить сейчас",
  "button_color": "#28a745"
}
  1. Настройте распределение трафика и сохраните кампанию.

Подробнее о создании API-кампаний.


Шаг 2: Реализация на бэкенде

Логика выполняется на сервере при каждом запросе страницы, где проводится тест:

  1. Получить идентификаторы пользователя и сессии.
  2. Запросить вариацию по selector.
  3. Передать данные вариации в шаблонизатор.
  4. Сохранить идентификаторы или tracking-данные, которые понадобятся фронтенду.
  5. При ошибке API отрендерить fallback-контент.

Установите для API-вызова строгий timeout, например 150-200 мс. Если ответ не получен вовремя, рендерьте контент по умолчанию.

Ниже приведены два варианта API-запроса. Для новых интеграций используйте API V2: backend мапит web-cookie в user.uid и user.ses, вызывает /v2/choose, получает контент вариации и готовые tracking URL. Вариант V1 нужен для существующих интеграций через /ssapi/*.

Вариант A: API V1

V1 использует cookie _slid_server и _slsession, endpoint /ssapi/choose и decisionId для tracking.

plaintext
// Псевдокод
userId = getCookie('_slid_server') ?? getCookie('_slid');
sessionId = getCookie('_slsession');

Если cookie отсутствуют, передайте null в user.slid и session.sl. Gravity Field создаст идентификаторы и вернёт их в cookies[].

curl --request POST \
--url 'https://evs-01.gravityfield.ai/ssapi/choose' \
--header 'content-type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
  "user": {
    "slid": "USER_ID_FROM_COOKIE"
  },
  "session": {
    "sl": "SESSION_ID_FROM_COOKIE"
  },
  "context": {
    "page": {
      "type": "HOMEPAGE",
      "location": "https://your-site.com/"
    },
    "device": {
      "userAgent": "USER_AGENT_FROM_REQUEST",
      "ip": "USER_IP_FROM_REQUEST"
    }
  },
  "selector": {
    "names": [
      "homepage_cta_test"
    ]
  }
}'

Подробнее об API V1 /choose.

Пример V1-ответа:

{
  "choices": [
    {
      "name": "Homepage CTA Test",
      "variations": [
        {
          "name": "Вариация Б",
          "payload": {
            "type": "CUSTOM_JSON",
            "data": {
              "custom": {
                "button_text": "Купить сейчас",
                "button_color": "#28a745"
              }
            }
          }
        }
      ],
      "decisionId": "65cde...:65cdf...:65cde...:65cdf..."
    }
  ],
  "cookies": [
    {
      "name": "_slid_server",
      "value": "NEW_OR_EXISTING_USER_ID",
      "maxAge": "31556926"
    },
    {
      "name": "_slsession",
      "value": "NEW_OR_EXISTING_SESSION_ID",
      "maxAge": "1800"
    }
  ]
}

Для V1:

  • извлеките данные вариации из choices[0].variations[0].payload.data.custom;
  • сохраните choices[0].decisionId и передайте его во frontend;
  • если в ответе есть cookies[], установите их через Set-Cookie.

Вариант B: API V2

V2 использует endpoint /v2/choose, объект user с uid и ses, контекст ctx и готовые tracking URL из ответа. В web-hybrid сценарии не заводите отдельные идентификаторы для V2: мапьте существующие web-cookie в поля V2.

plaintext
// Псевдокод
uid = getCookie('_slid_server') ?? getCookie('_slid');
ses = getCookie('_slsession');

Маппинг:

Web cookie Поле в API V2
_slid_server, если есть; иначе _slid user.uid
_slsession user.ses
curl --request POST \
--url 'https://evs-01.gravityfield.ai/v2/choose' \
--header 'content-type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
  "sec": "YOUR_SECTION_ID",
  "user": {
    "uid": "VALUE_FROM__slid_server_OR__slid",
    "ses": "VALUE_FROM__slsession"
  },
  "ctx": {
    "type": "HOMEPAGE",
    "data": [],
    "location": "https://your-site.com/"
  },
  "device": {
    "ua": "USER_AGENT_FROM_REQUEST",
    "ip": "USER_IP_FROM_REQUEST"
  },
  "options": {
    "isImplicitPageview": true,
    "isBuildEngagementUrl": true
  },
  "data": [
    {
      "selector": "homepage_cta_test"
    }
  ]
}'

Подробнее об API V2 /choose.

Пример V2-ответа:

{
  "user": {
    "uid": "665f0a000000000000000001",
    "ses": "7356efc2-6ffd-4553-bade-b9ab5d9ce141"
  },
  "data": [
    {
      "selector": "homepage_cta_test",
      "payload": [
        {
          "variationId": "665f0d000000000000000001",
          "contents": [
            {
              "variables": {
                "button_text": "Купить сейчас",
                "button_color": "#28a745",
                "onVisibleImpression": {
                  "action": "WRIMP"
                },
                "onClick": {
                  "action": "CLICK"
                }
              },
              "events": [
                {
                  "type": "WRIMP",
                  "urls": [
                    "https://evs-01.gravityfield.ai/v2/engagement?..."
                  ]
                },
                {
                  "type": "CLICK",
                  "urls": [
                    "https://evs-01.gravityfield.ai/v2/engagement?..."
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Для V2:

  • если в response.user пришли uid или ses, обновите web-cookie _slid_server и _slsession;
  • извлеките данные вариации из data[0].payload[0].contents[0].variables;
  • передайте во frontend нужные content.events[].urls[], чтобы frontend мог отправить tracking;
  • для товарных рекомендаций дополнительно передавайте products.slots[].events[].urls[].

Подробнее о миграции с V1 на V2.


Шаг 3: Реализация на фронтенде

Страница уже приходит в браузер с отрендеренной вариацией. Задача фронтенда - отправить tracking для реально показанного контента и действий пользователя.

Tracking в API V1

В V1 бэкенд передаёт decisionId в HTML, например в data- атрибут.

<button
  id="cta-button"
  style="background-color: #28a745;"
  data-decision-id="65cde...:65cdf...:65cde...:65cdf..."
>
  Купить сейчас
</button>

При клике отправьте engagement через JS SDK:

document.addEventListener('DOMContentLoaded', function() {
  const ctaButton = document.getElementById('cta-button');

  if (ctaButton) {
    ctaButton.addEventListener('click', function() {
      const decisionId = this.getAttribute('data-decision-id');

      if (decisionId && window.SL && window.SL.ServerUtils) {
        window.SL.ServerUtils.reportEngagement(decisionId, 'CLICK');
      }
    });
  }
});

Подробнее о V1 engagement.

Tracking в API V2

В V2 не нужно собирать /engagement вручную. Если в /choose передан options.isBuildEngagementUrl: true, Gateway возвращает готовые URL в content.events[].urls[] и products.slots[].events[].urls[].

Передайте нужные URL в HTML или в JSON-модель страницы:

<button
  id="cta-button"
  style="background-color: #28a745;"
  data-click-url="https://evs-01.gravityfield.ai/v2/engagement?..."
>
  Купить сейчас
</button>

При действии пользователя вызовите все URL подходящего события:

function sendTrackingUrls(urls) {
  urls.forEach(function(url) {
    fetch(url, {
      method: 'GET',
      keepalive: true,
      credentials: 'include'
    }).catch(function() {});
  });
}

document.addEventListener('DOMContentLoaded', function() {
  const ctaButton = document.getElementById('cta-button');

  if (ctaButton) {
    ctaButton.addEventListener('click', function() {
      const clickUrl = this.getAttribute('data-click-url');

      if (clickUrl) {
        sendTrackingUrls([clickUrl]);
      }
    });
  }
});

Для visible impression вызывайте URL один раз, когда элемент стал видим пользователю. Для товарных рекомендаций отправляйте URL из products.slots[].events[] для конкретной карточки, а не только события контентного блока.

Подробнее о V2 engagement.


Шаг 4: Анализ результатов

После запуска кампании и сбора данных откройте отчёт по кампании в Gravity Field и сравните метрики вариаций.

Подробнее об анализе отчётов по кампаниям.


Полная диаграмма рабочего процесса

sequenceDiagram
    participant Browser as Браузер
    participant ClientBackend as Бэкенд клиента
    participant GFV1 as Gravity Field API V1
    participant GFV2 as Gravity Field API V2

    Browser->>+ClientBackend: GET /page
    Note over ClientBackend: Чтение идентификаторов пользователя и сессии

    alt API V1
        ClientBackend->>+GFV1: POST /ssapi/choose<br/>user.slid, session.sl, selector.names
        GFV1-->>-ClientBackend: choices[], decisionId, cookies[]
        Note over ClientBackend: Рендер вариации<br/>Set-Cookie при наличии cookies[]<br/>decisionId передаётся во frontend
    else API V2
        ClientBackend->>+GFV2: POST /v2/choose<br/>user.uid, user.ses, ctx, data[].selector
        GFV2-->>-ClientBackend: data[], user, content.events[].urls[]
        Note over ClientBackend: Рендер вариации<br/>обновление _slid_server/_slsession<br/>tracking URL передаются во frontend
    end

    ClientBackend-->>-Browser: 200 OK HTML с отрендеренной вариацией
    Note over Browser: Пользователь видит страницу без мерцания

    Browser->>Browser: Пользователь видит блок или кликает

    alt API V1 tracking
        Browser->>+GFV1: POST /ssapi/engagement или JS SDK<br/>decisionId
        GFV1-->>-Browser: 200 OK
    else API V2 tracking
        Browser->>+GFV2: GET /v2/engagement?...<br/>URL из ответа /choose
        GFV2-->>-Browser: 200 OK
    end

FAQ

Q: Что делать, если API /choose отвечает слишком долго?
A: Рендерьте fallback-контент и не задерживайте HTML-ответ. Для server-side запроса задайте короткий timeout.

Q: Как обрабатывать новых пользователей в V1? A: Передавайте null в user.slid и session.sl. API V1 вернёт новые значения в cookies[], которые нужно установить пользователю.

Q: Как обрабатывать новых пользователей в V2? A: Передайте пустой user или не передавайте uid/ses. Если Gateway вернёт response.user.uid или response.user.ses, установите их в web-cookie _slid_server и _slsession и используйте эти cookie в следующих запросах.

Q: Почему важно отслеживать взаимодействия? A: Без engagement A/B-тест не сможет корректно связать показанную вариацию с кликами, видимыми показами и итоговыми метриками.