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

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

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

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

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

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

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

1. **Рабочая гибридная интеграция:** бэкенд умеет взаимодействовать с API Gravity Field. [Подробнее о гибридной интеграции](/Integration/hybrid_integration/hybrid_integrations.md).
2. **API-ключи:** есть API-ключ для аутентификации запросов. [Подробнее об управлении API-ключами](/Integration/api_integration/manage_api_keys.md).
3. **API-селектор:** в кампании задан selector, по которому бэкенд будет запрашивать вариацию.

---

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

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

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

**Вариация А:**

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

**Вариация Б:**

```json
{
  "button_text": "Купить сейчас",
  "button_color": "#28a745"
}
```

6. Настройте распределение трафика и сохраните кампанию.

[Подробнее о создании API-кампаний](/personalization/Campaigns/api_campaigns.md).

---

## Шаг 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[]`.

```bash
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`](/Integration/api_integration/personalization/choose.md).

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

```json
{
  "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` |

```bash
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`](/Integration/api_integration/v2/personalization/choose.md).

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

```json
{
  "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](/Integration/api_integration/v2/migration.md).

---

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

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

### Tracking в API V1

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

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

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

```javascript
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](/Integration/api_integration/personalization/engagement.md).

### Tracking в API V2

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

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

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

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

```javascript
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](/Integration/api_integration/v2/personalization/engagement.md).

---

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

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

[Подробнее об анализе отчётов по кампаниям](/personalization/Reports/non_recs_campaigns_guide.md).

---

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

```mermaid
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-тест не сможет корректно связать показанную вариацию с кликами, видимыми показами и итоговыми метриками.
