# Как тестировать рекомендации через внешний A/B-инструмент

Если вы уже распределяете пользователей в стороннем A/B-инструменте, используйте его группу для выбора selector Gravity Field. Для группы A запрашивайте одну рекомендательную кампанию, для B — другую; обе показывайте в одном месте сайта или приложения.

**Внешний инструмент** назначает и сохраняет группу, собирает данные эксперимента и рассчитывает его результат. **Gravity Field** подбирает рекомендации выбранной кампании и принимает события для своей аналитики. **Ваша интеграция** связывает эти части: выбирает selector, отображает контент и отправляет события каждой системе.

## 1. Подготовьте две кампании в GF

Создайте две [API-кампании с рекомендациями](../Campaigns/api_campaigns.md) или две мобильные inline-кампании, если используете [готовый inline-компонент SDK](../../Integration/SDK/inline.md).

| Внешняя группа | Кампания GF | Selector — пример |
| --- | --- | --- |
| A | Рекомендации A | `home-recs-a` |
| B | Рекомендации B | `home-recs-b` |

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

Для обеих кампаний:

- Настройте одинаковые placement, контекст, аудиторию, расписание и число товаров. Меняйте только то, что сравнивает тест, например рекомендательную стратегию; UI и правила обработки ошибок оставьте одинаковыми.
- В [сценарии](../Campaigns/campaign_structure.md) оставьте один вариант с **100% трафика**. Выберите ручное [распределение](./traffic_allocation.md), не включайте «Автопилот» и дополнительную контрольную вариацию. Внешние 50/50 относятся к выбору между кампаниями; внутри каждой кампании повторное 50/50 не нужно.
- Проверьте все ограничения охвата: таргетинг, дополнительные условия распределения, расписание и лимиты показов. У двух кампаний они должны соответствовать одному дизайну теста. Selector выбирает кампанию, но не отменяет её условия и внутренний выбор вариации.
- Если проект использует [глобальную контрольную группу](../Reports/global_control_group.md), заранее определите её участие. Например, исключите её из аудитории этого эксперимента до назначения A/B. Не меняйте общую контрольную группу проекта ради теста без согласованного решения.
- Опубликуйте сценарий/вариант и активируйте обе кампании. Проверьте выдачу каждой на тестовом пользователе.

В одной кампании могут использоваться рекомендации Gravity Field, в другой — клиентские, если такая стратегия уже подключена. Способ распределения пользователей и вызова selector остаётся тем же.

## 2. Закрепите внешнюю группу за пользователем

Создайте эксперимент в вашем A/B-инструменте и задайте доли A/B. Получайте **сохранённое назначение** для стабильного идентификатора пользователя. Не вычисляйте случайную группу заново при каждом просмотре, запросе или перезапуске приложения.

Согласуйте поведение при входе в аккаунт, смене устройства и для анонимных посетителей. Группа должна следовать правилам внешнего инструмента; идентификацию в GF продолжайте выполнять обычным способом. Не создавайте новый GF-профиль ради выбора A или B.

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

## 3. Выберите один selector и запросите контент

Пример логики интеграции: `assignedGroup` приходит из вашего A/B-инструмента. Это переменная приложения, а не новое поле API Gravity Field.

```javascript
function selectorForGroup(assignedGroup) {
  if (assignedGroup === "A") return "home-recs-a";
  if (assignedGroup === "B") return "home-recs-b";
  return null; // Назначения нет: блок эксперимента пока не загружаем.
}
```

Передайте выбранный selector стандартному пути вашей интеграции.

+++ API V2

В запросе [`/choose`](../../Integration/api_integration/v2/personalization/choose.md) передайте **один** элемент `data[]`. Ниже фрагмент для группы A; для B заменяется только selector:

```json
{
  "data": [{ "selector": "home-recs-a" }],
  "options": {
    "isBuildEngagementUrl": true,
    "isImplicitImpression": false
  }
}
```

Остальные поля (`sec`, `user`, `ctx`, `device`) заполните по стандартной интеграции. Сохраняйте пользователя и сессию, передавайте одинаковый [контекст](../../Integration/api_integration/v2/context.md) реального placement для обеих групп. Просмотр страницы отправляйте через `/visit` или `isImplicitPageview` один раз, без дублирования. Здесь `isImplicitImpression: false` отключает учёт impression при выдаче: приложение отправит его при отображении блока через URL из ответа.

+++ Android SDK

После получения группы, внутри coroutine:

```kotlin
val selector = when (assignedGroup) {
    "A" -> "home-recs-a"
    "B" -> "home-recs-b"
    else -> null
}
if (selector != null) {
    val response = GravitySDK.instance.getContentBySelector(
        org.json.JSONObject.quote(selector), pageContext
    )
    // Передайте response существующему рендереру вашего приложения.
}
```

В Android 1.0.4 кодируйте обычное имя selector через JSONObject.quote ровно один раз.

Сигнатура и структура ответа: [Android SDK](../../Integration/SDK/android/custom_ui.md#getcontentbyselector).

+++ iOS SDK

После получения группы, в async-контексте:

```swift
let selector: String? = assignedGroup == "A" ? "home-recs-a"
    : assignedGroup == "B" ? "home-recs-b" : nil
if let selector = selector {
    let response = await GravitySDK.instance.getContentBySelector(
        selector: selector, pageContext: pageContext
    )
    // Передайте response существующему рендереру вашего приложения.
}
```

Сигнатура и структура ответа: [iOS SDK](../../Integration/SDK/ios/custom_ui.md#getcontentbyselectorselectorpagecontext).

+++

Если используете встроенный inline-компонент, передайте ему выбранный selector вместо ручного запроса; подключение и tracking описаны в [inline-гайде](../../Integration/SDK/inline.md). Выберите один способ загрузки. Запросы обеих кампаний «для сравнения», общий group selector или переключение на selector B при пустом ответе A смешивают условия теста. При пустой выдаче/ошибке сохраняйте внешнюю группу и применяйте согласованный fallback.

## 4. Передавайте показы, клики и конверсии

Назначение A/B, получение ответа и фактический показ блока — разные события. Заранее договоритесь, что означает exposure во внешнем инструменте и какая аудитория входит в расчёт. Не отмечайте внешний exposure только потому, что `/choose` вернул ответ. В SDK `isImplicitImpression` по умолчанию включён: GF может учитывать impression уже при выдаче. Для ручного учёта impression при показе настройте `Options(isImplicitImpression = false)` на Android или `Options(isImplicitImpression: false)` на iOS через `setOptions(...)`, сохранив остальные настройки интеграции. Эти опции действуют на последующие запросы SDK, поэтому согласуйте режим для приложения целиком.

- **В GF:** при ручном рендеринге отправляйте [content/product engagement](../../Integration/api_integration/v2/personalization/engagement.md) с tracking-данными именно полученного ответа. В SDK используйте `sendContentEngagement(...)` / `sendProductEngagement(...)` с исходными `campaign`, `content`, `slot`; встроенный UI берёт часть tracking на себя. При клике на свою товарную карточку отправьте product click и выполните навигацию приложения.
- **В GF:** продолжайте отправлять просмотры, добавления в корзину и покупки через [стандартные события](../../Integration/api_integration/v2/personalization/events.md) или `trackView(...)` / `triggerEvent(...)` SDK. Сохраняйте идентичность пользователя, контекст и SKU из фида. Настройте одинаковые [цели](../Reports/Goals.md) и [атрибуцию](../Reports/attribution-in-reports.md) для двух кампаний.
- **Во внешний инструмент:** отдельно передавайте назначение, exposure и выбранные метрики средствами этого инструмента. Сохраняйте связь «эксперимент → пользователь → группа → selector/кампания» в своей аналитике. GF не отправляет результаты стороннему A/B-инструменту автоматически.

[Отчёты GF](../Reports/recs_campaigns_guide.md) покажут работу каждой кампании. Итог A/B между двумя внешними группами рассчитывайте в выбранной системе экспериментов с её правилами аудитории и метрик; это отдельный эксперимент от встроенного A/B внутри одного сценария GF.

## 5. Проверьте тест перед запуском

- Тестовый пользователь A запрашивает только `home-recs-a`, B — только `home-recs-b`; повторное посещение сохраняет группу.
- Обе кампании дают ожидаемые товары в одном placement и контексте, без второго случайного распределения и конфликтующего таргетинга.
- Фактические показы, товарные клики и покупки учитываются без дублей; группа во внешней аналитике совпадает с выбранным selector.
- Пустые ответы, ошибки и fallback не переводят пользователя между A и B. Отдельно видны назначенные пользователи и реально показанные блоки.
