# SDK Gravity Field (Flutter)

Gravity Field SDK — это лёгкий клиент для интеграции мобильных приложений (iOS / Android) с платформой персонализации и A/B-тестирования. Он позволяет запускать кампании без необходимости реализовывать собственную логику таргетинга, выбора и аналитики.

SDK работает по принципу "тонкого клиента": все решения принимает сервер (бекенд Gravity Field), а SDK:

  • передаёт контекст текущего экрана,
  • получает кампании и их содержимое в виде JSON или встроенных шаблонов,
  • активирует показ inline и in-app кампаний
  • трекает взаимодействия пользователя (просмотры, клики, покупки и т.д.),
  • и помогает фиксировать конверсии для аналитики и обучения моделей.

📦 Эта документация предназначена для мобильных разработчиков и инженеров, которые интегрируют SDK в приложение.

Gravity Field SDK

# Добавление библиотеки в проект

  1. Из корня проекта вызовите команду:
flutter pub add gravity_sdk
  1. После добавления плагина в файле pubspec.yaml появится строка с зависимостью:
dependencies:
  gravity_sdk: ^0.10.2 # Замените на актуальную версию
  1. Добавьте импорт:
import 'package:gravity_sdk/gravity_sdk.dart';

Требования к версиям:

  • Dart SDK: >=3.6.0
  • Flutter: >=1.17.0

Для iOS: Добавьте в файл ios/Runner/Info.plist ключ NSUserTrackingUsageDescription для запроса разрешения на отслеживание:

<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>

# Быстрый старт

Этот раздел проведет вас через основные шаги интеграции SDK, от инициализации до запуска вашей первой кампании.

# Шаг 1: Инициализация SDK

Сначала необходимо инициализировать SDK с вашим apiKey и section. Это лучше всего делать в функции main() вашего приложения. Критически важно подключить gravityEventCallback для обработки навигации и других действий.

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gravity_sdk/gravity_sdk.dart';

void main() async {
  // Обязательно до runApp
  WidgetsFlutterBinding.ensureInitialized();

  // Инициализация SDK
  await GravitySDK.instance.initialize(
    apiKey: 'YOUR_API_KEY',
    section: 'YOUR_SECTION_ID',
    // Обработка действий
    gravityEventCallback: (event) {
      print('Gravity SDK Event: ${event.runtimeType}');

      // Обработка перехода по внешней ссылке
      if (event is FollowUrlEvent) {
        launchUrl(Uri.parse(event.url), mode: LaunchMode.externalApplication);
      }

      // Обработка перехода по диплинку
      if (event is FollowDeeplinkEvent) {
        // Здесь ваша логика навигации по диплинкам.
        // Например, с использованием GoRouter:
        // context.go(event.deeplink);
        print('Follow Deeplink: ${event.deeplink}');
      }
    },
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ...
  }
}

# Почему важен gravityEventCallback?

SDK не выполняет навигацию самостоятельно. Когда пользователь нажимает на кнопку в кампании, SDK генерирует событие (например, FollowUrlEvent) и передает его в gravityEventCallback. Ваше приложение должно «поймать» это событие и выполнить соответствующее действие (открыть ссылку, перейти на другой экран). Без этого интерактивные элементы работать не будут.

Для открытия URL мы использовали пакет url_launcher. Не забудьте добавить его в ваш pubspec.yaml:

dependencies:
  url_launcher: ^6.3.1

Подробнее о событиях см. раздел Обработка обратных вызовов (Callbacks). Для аналитики действий из колбэка (клики по ссылкам/диплинкам, запросы разрешений и т.д.) используйте triggerEvent с CustomEvent.

# Шаг 2: Идентификация пользователя

Когда пользователь входит в систему, важно связать его действия, совершенные анонимно, с его постоянным профилем. Для этого после авторизации отправляется LoginEvent. Это рекомендуемый способ идентификации.

// Вызывается после успешной авторизации пользователя
void onUserLoggedIn(BuildContext context, String userCustomId) {
  final loginEvent = LoginEvent(cuid: userCustomId, cuidType: 'mySystemUserId');

  GravitySDK.instance.triggerEvent(
    context: context,
    events: [loginEvent],
    pageContext: PageContext(
      type: ContextType.other,
      data: [],
      location: 'app://login',
    ),
  );
}

Отправка LoginEvent "склеивает" анонимный профиль с профилем авторизованного пользователя, сохраняя всю историю его действий.

Подробнее о различных способах идентификации читайте в разделе Идентификация пользователя.

# Шаг 3: Отслеживание просмотров экранов

Gravity Field принимает решение о показе персонализированного контента на основе контекста страницы, который передаёт SDK. Давайте отследим просмотр главной страницы.

// В коде виджета вашей главной страницы
void trackHomepageView(BuildContext context) {
  GravitySDK.instance.trackView(
    context: context,
    pageContext: PageContext(
      type: ContextType.homepage,
      data: [],
      location: 'app://homepage',
    ),
  );
}

Если для этого события настроена in-app кампания, SDK автоматически покажет ее.

# Почему SDK требует BuildContext?

SDK использует BuildContext для доступа к дереву виджетов и ThemeData вашего приложения. Это позволяет:

  1. Найти ScaffoldMessenger для показа SnackBar.
  2. Использовать Navigator для отображения диалогов, шторок и полноэкранных кампаний.
  3. Наследовать стили (шрифты, цвета) из глобальной темы, чтобы кампании выглядели нативно.

# Шаг 4: Отслеживание событий

Теперь отследим добавление товара в корзину. Это событие может запустить кампанию с товарными рекомендациями (например, "с этим товаром покупают").

void trackAddToCart(BuildContext context, String productId) {
  final event = AddToCartEvent(
    value: 99.99,
    productId: productId,
    quantity: 1,
    currency: 'RUB',
  );

  GravitySDK.instance.triggerEvent(
    context: context,
    events: [event],
    pageContext: PageContext(
      type: ContextType.product,
      data: [productId],
      location: 'app://product/$productId',
    ),
  );
}

# Шаг 5: Отображение inline-кампаний

Для отображения рекомендаций прямо в верстке страницы (например, блок "Персональные рекомендации") используйте виджет GravityInlineWidget.

// В методе build вашего виджета
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Inline Recommendations')),
    body: ListView(
      children: [
        // ... другие виджеты
        GravityInlineWidget(
          selector: 'homepage-recommendations',
          height: 250, // Обязательно задайте высоту для блока
          pageContext: PageContext(
            type: ContextType.homepage,
            data: [],
            location: 'app://homepage',
          ),
        ),
        // ... другие виджеты
      ],
    ),
  );
}

Виджет сам загрузит и отобразит релевантный контент по селектору homepage-recommendations.

# Инициализация и конфигурация

# initialize()

Основной метод для настройки SDK. Вызывается один раз при старте приложения.

Future<void> initialize({
  required String apiKey,
  required String section,
  ProductWidgetBuilder? productWidgetBuilder,
  GravityEventCallback? gravityEventCallback,
  LogLevel logLevel = LogLevel.info,
});
  • apiKey (required): Ваш уникальный ключ API.
  • section (required): Идентификатор секции вашего проекта.
  • productWidgetBuilder: Опциональный билдер для кастомизации виджетов продуктов. (См. раздел 8. Кастомизация UI).
  • gravityEventCallback: Опциональный колбэк для получения уведомлений о событиях SDK. (См. раздел 11. Обработка обратных вызовов (Callbacks)).
  • logLevel: Уровень детализации логов SDK. По умолчанию LogLevel.info. (См. раздел 5.3. Настройка логирования).

# setOptions()

Позволяет задать глобальные настройки для всех последующих запросов.

void setOptions({
  Options? options,
  ContentSettings? contentSettings,
  String? proxyUrl,
});
  • options: Настройки для управления поведением запросов.
  • contentSettings: Настройки для управления получаемым контентом.
  • proxyUrl: URL прокси-сервера для отправки запросов.

Пример:

GravitySDK.instance.setOptions(
  options: Options(
    isReturnUserInfo: true, // Возвращать информацию о пользователе в ответах
    isImplicitImpression: true, // Автоматически отправлять событие показа
  ),
  contentSettings: ContentSettings(
    skusOnly: false, // Возвращать полную информацию о продуктах, а не только SKU
    fields: ['name', 'price', 'imageUrl'], // Запросить конкретные поля
  ),
);

# Настройка логирования

При инициализации SDK вы можете указать уровень детализации логов с помощью параметра logLevel. Это полезно для отладки интеграции.

await GravitySDK.instance.initialize(
  apiKey: 'YOUR_API_KEY',
  section: 'YOUR_SECTION_ID',
  logLevel: LogLevel.debug, // Устанавливаем максимальный уровень детализации
);

Доступные уровни LogLevel:

  • LogLevel.none: Логирование полностью отключено.
  • LogLevel.error: Только ошибки.
  • LogLevel.warn: Ошибки и предупреждения.
  • LogLevel.info: (По умолчанию) Информационные сообщения, ошибки и предупреждения.
  • LogLevel.debug: Максимальная детализация, включая тела запросов и ответов.

# Идентификация пользователя

SDK поддерживает два подхода к идентификации: автоматический (управляется SDK) и ручной (управляется вашим приложением).

# Автоматическая идентификация (SDK-managed)

Это подход по умолчанию. При первом запросе SDK получает от сервера уникальный uid (user ID) и ses (session ID) и сохраняет их на устройстве. Все последующие запросы будут использовать эти идентификаторы.

Чтобы связать анонимный профиль с профилем авторизованного пользователя, после входа в систему отправьте LoginEvent.

// Вызывается после успешной авторизации
void onUserLoggedIn(BuildContext context, String userCustomId) {
  final loginEvent = LoginEvent(
    cuid: userCustomId,
    cuidType: 'mySystemUserId', // Укажите тип вашего идентификатора
  );

  GravitySDK.instance.triggerEvent(
    context: context,
    events: [loginEvent],
    pageContext: PageContext(
      type: ContextType.other,
      data: [],
      location: 'app://login',
    ),
  );
}

# Ручная идентификация

Если ваше приложение уже управляет ID пользователей и сессий, вы можете передавать их в SDK напрямую с помощью метода setUser.

# setUser()

Устанавливает custom (ваш ID пользователя) и ses (ваш ID сессии) для всех последующих запросов.

void setUser(String userId, String sessionId);

Пример:

// Вызывается при старте сессии, если ID уже известны
GravitySDK.instance.setUser('user-from-my-system-42', 'session-from-my-system-xyz');

В этом режиме SDK не будет использовать автоматически сгенерированный uid.

# Передача контекста

Gravity Field принимает решение о показе персонализированного контента на основе контекста страницы, который передаёт SDK.

Контекст страницы позволяет Gravity Field понять:

  • Где сейчас находится пользователь в приложении
  • Какие товары или категории он просматривает
  • В каком регионе или в каких условиях пользователь находится

# trackView(...)

Отправляет событие просмотра экрана. В ответ может прийти кампания для показа.

Future<void> trackView({
  required BuildContext context,
  required PageContext pageContext,
});
  • context: BuildContext текущего экрана.
  • pageContext: Контекст страницы, который описывает, где находится пользователь.

Пример:

// На экране продукта
GravitySDK.instance.trackView(
  context: context,
  pageContext: PageContext(
    type: ContextType.product,
    data: ['product-sku-123'], // SKU продукта
    location: 'app://product/123',
  ),
);

# PageContext

Ключевая модель для описания местоположения и контекста пользователя.

Свойство Тип Описание
type ContextType Тип страницы (homepage, product, cart и т.д.).
data List Массив строк с данными, зависящими от типа (например, SKU для product).
location String Уникальный идентификатор местоположения (URL, deeplink, название экрана).
lng String? Язык или регион.
utm Map<String, String>? UTM-метки.
attributes Map<String, Object>? Дополнительные атрибуты для таргетинга.

💡 Типы контекста и соответсвующие для них data описаны здесь: Page context

# Трекинг событий

Трекинг событий позволяет отправлять информацию о действиях пользователя — таких как покупка, добавление товара в корзину, авторизация и другие события. Эти данные используются Gravity Field для аналитики, построения сегментов и запуска кампаний, активируемых по событиям.

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

События фиксируются для того, чтобы измерить поведение пользователя в разных вариациях кампаний: например, узнать, какой из вариантов чекаута приводит к более высокой конверсии. Это позволяет анализировать эффективность персонализации и принимать решения на основе данных.

Платформа поддерживает как системные (предопределённые) события, так и произвольные пользовательские события.

  • Системные события — это события с зафиксированными type, которые платформа распознаёт и может использовать для активации кампаний или анализа (например, purchase, add_to_cart, login).
  • Пользовательские события — разработчик может отправлять любые события с кастомным type. Такие события будут использоваться для аналитики и построения собственных отчётов или логики в Gravity Field.

# triggerEvent(...)

Отправляет одно или несколько событий о действиях пользователя.

Future<void> triggerEvent({
  required BuildContext context,
  required List<TriggerEvent> events,
  required PageContext pageContext,
});
  • context: BuildContext текущего экрана.
  • events: Список событий для отправки. SDK предоставляет множество готовых классов событий (PurchaseEvent, LoginEvent и др.).
  • pageContext: Контекст страницы.

Ниже приведены примеры кода для отслеживания наиболее распространенных бизнес-событий с помощью triggerEvent().

# Покупка (PurchaseEvent)

Событие отправляется после успешного завершения заказа.

final purchaseEvent = PurchaseEvent(
  uniqueTransactionId: 'ORDER-12345', // Уникальный ID транзакции
  value: 2550.75,
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-123', quantity: 1, itemPrice: 100.50),
    CartItem(productId: 'sku-456', quantity: 1, itemPrice: 50.00),
    CartItem(productId: 'sku-abc-1', quantity: 1, itemPrice: 1500.00),
    CartItem(productId: 'sku-def-2', quantity: 2, itemPrice: 525.375),
  ],
);

GravitySDK.instance.triggerEvent(
  context: context,
  events: [purchaseEvent],
  pageContext: PageContext(
    type: ContextType.other,
    data: [],
    location: 'app://checkout/success',
  ),
);

# Добавление в корзину (AddToCartEvent)

Отправляется, когда пользователь добавляет товар в корзину.

final addToCartEvent = AddToCartEvent(
  value: 1500.00,
  productId: 'sku-abc-1',
  quantity: 1,
  currency: 'RUB',
);

GravitySDK.instance.triggerEvent(
  context: context,
  events: [addToCartEvent],
  pageContext: PageContext(
    type: ContextType.product,
    data: ['sku-abc-1'],
    location: 'app://product/sku-abc-1',
  ),
);

# Вход в систему (LoginEvent)

Отправляется после успешной аутентификации пользователя.

final loginEvent = LoginEvent(
  // Передайте один из идентификаторов
  cuid: 'customer-12345',
  cuidType: 'mySystemUserId',
  // hashedEmail: '...' // или хешированный email
);

GravitySDK.instance.triggerEvent(
  context: context,
  events: [loginEvent],
  pageContext: PageContext(
    type: ContextType.other,
    data: [],
    location: 'app://login',
  ),
);

# Кастомное событие (CustomEvent)

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

final customEvent = CustomEvent(
  type: 'survey-completed-v1', // Уникальный тип события
  name: 'Опрос пройден', // Человекочитаемое имя
  properties: {
    'surveyId': 'summer-2025-feedback',
    'rating': '5',
  },
);

GravitySDK.instance.triggerEvent(
  context: context,
  events: [customEvent],
  pageContext: PageContext(
    type: ContextType.other,
    data: [],
    location: 'app://survey/summer-2025-feedback',
  ),
);

Подробное описание всех доступных событий и их параметров — в разделе ниже.

# Передача статуса Push-уведомлений

Чтобы платформа могла таргетировать кампании на пользователей в зависимости от статуса подписки на push-уведомления, необходимо передавать этот статус в SDK.

# setNotificationPermissionStatus()

Устанавливает текущий статус разрешения на push-уведомления.

void setNotificationPermissionStatus(NotificationPermissionStatus status);

status: NotificationPermissionStatus.granted, NotificationPermissionStatus.denied или NotificationPermissionStatus.unknown.

Пример получения и установки статуса (через firebase_messaging):

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:gravity_sdk/gravity_sdk.dart';

// 1. Получаем статус из системы
Future<NotificationPermissionStatus> getNotificationStatus() async {
  final settings = await FirebaseMessaging.instance.getNotificationSettings();
  switch (settings.authorizationStatus) {
    case AuthorizationStatus.authorized:
    case AuthorizationStatus.provisional:
      return NotificationPermissionStatus.granted;
    case AuthorizationStatus.denied:
      return NotificationPermissionStatus.denied;
    default:
      return NotificationPermissionStatus.unknown;
  }
}

// 2. Передаем статус в SDK при старте приложения и после запроса разрешений
final status = await getNotificationStatus();
GravitySDK.instance.setNotificationPermissionStatus(status);

# FAQ / Troubleshooting

  1. Почему GravityInlineWidget не отображается или имеет нулевую высоту?

    • Вы не задали обязательный параметр height. Задайте высоту в конструкторе GravityInlineWidget.
  2. Почему UI кампании выглядит иначе, чем в превью?

    • SDK наследует стили из ThemeData приложения. Если в кампании не задан шрифт/цвет, используется Theme.of(context).
  3. Почему клик по кнопке в кампании не приводит к переходу?

    • Не обработаны события FollowUrlEvent/FollowDeeplinkEvent в gravityEventCallback. SDK не выполняет навигацию сам.

# Работа с контентом

# GravityInlineWidget (Автоматический рендеринг)

Виджет для встраивания кампаний (например, товарных рекомендаций) прямо в верстку вашего экрана. Для отображения нескольких виджетов из одной кампании (мультивиджет-кампании) используйте параметр placeholderId:

// Пример для мультивиджет кампании
Column(
  children: [
    GravityInlineWidget(
      selector: 'homepage-recs',
      placeholderId: 'placeholder-1',
      height: 250,
      pageContext: PageContext(
        type: ContextType.homepage,
        data: [],
        location: 'app://homepage',
      ),
    ),
    GravityInlineWidget(
      selector: 'homepage-recs',
      placeholderId: 'placeholder-2',
      height: 250,
      pageContext: PageContext(
        type: ContextType.homepage,
        data: [],
        location: 'app://homepage',
      ),
    ),
  ],
)

# Параметры виджета

Параметр Тип Описание
selector String Обязательный. Строковый идентификатор (селектор) для получения контента.
placeholderId String? ID места в интерфейсе, куда вставляется виджет, для мультивиджет кампании.
width double? Ширина виджета.
height double? Высота виджета.
pageContext PageContext Контекст страницы.

# ProductWidgetBuilder (Кастомный рендеринг товаров)

Для полного контроля над внешним видом карточек товаров в GravityInlineWidget, создайте свой класс, наследующий ProductWidgetBuilder.

// 1. Создайте свой билдер
class CustomProductWidgetBuilder extends ProductWidgetBuilder {
  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    // Ваша кастомная верстка карточки товара
    return GestureDetector(
        onTap: () {
          // При клике важно отправлять событие взаимодействия
          GravitySDK.instance.sendProductEngagement(
            ProductClickEngagement(product, content, campaign)
          );
          // ... и выполнять переход на экран товара
        },
      child: Card(
        child: Text(product.item['name'] as String? ?? ''),
      ),
    );
  }
}

// 2. Передайте его в initialize
await GravitySDK.instance.initialize(
  // ...
  productWidgetBuilder: CustomProductWidgetBuilder(),
);

⚠️ Обратите внимание: ProductWidgetBuilder отвечает за отрисовку одной карточки товара. Эта карточка будет автоматически помещена SDK в соответствующий контейнер: ListView для горизонтального скролла (row) или GridView для отображения сеткой (grid), в зависимости от настроек кампании.

# 10.2. Отображение товарных рекомендаций (products-container)

Если кампания возвращает список товаров (например, товарные рекомендации), SDK автоматически отрисовывает их внутри специального элемента products-container. Этот контейнер управляет расположением товарных карточек и может работать в двух режимах:

  • row (горизонтальный список): Товары отображаются в виде горизонтального прокручиваемого списка (ListView). Это поведение по умолчанию.

  • grid (сетка): Товары отображаются в виде сетки (GridView). Количество колонок и отступы между элементами настраиваются в шаблоне кампании.

Тип контейнера (row или grid) и его стили задаются в шаблоне кампании в интерфейсе Gravity Field. SDK автоматически применяет эти настройки при рендеринге.

Роль ProductWidgetBuilder в этом процессе — отрисовать одну карточку товара, которая затем будет помещена в соответствующий контейнер. Таким образом, вы контролируете внешний вид отдельного товара, а Gravity Field — их общее расположение.

⚠️ Обратите внимание: product.item представляет собой Map<String, dynamic>. Ключи в этой карте (например, 'name', 'price', 'imageUrl') соответствуют полям в вашем товарном фиде. Убедитесь, что вы используете правильные ключи и безопасно обрабатываете типы данных и возможные null значения.

# JSON-кампании (Полностью ручной рендеринг)

Для максимальной гибкости вы можете получать кампании в виде чистого JSON и рендерить их самостоятельно.

⚠️ Важно: При работе с JSON-кампаниями SDK лишь доставляет данные. Ваше приложение несет полную ответственность за парсер JSON-объекта, полученного из response.data, и последующий рендеринг UI на основе этих данных.

# getContentBySelector(...)

Запрашивает кампанию по селектору и возвращает ее данные.

Пример:

// Получаем контент для инлайнового блока на главной
final response = await GravitySDK.instance.getContentBySelector(
  selector: 'homepage-inline-banner',
  pageContext: PageContext(type: ContextType.homepage, data: [], location: 'app://home'),
);

// Используем полученные данные для отображения
if (response.data.isNotEmpty) {
  final campaign = response.data.first;
  // ... ваша логика отображения
}

# Событийная модель

Вы также можете получать JSON-кампании, которые активируются в ответ на события (trackView, triggerEvent). Для этого подпишитесь на gravityEventCallback и отлавливайте событие ContentLoadEvent.

await GravitySDK.instance.initialize(
  // ... другие параметры
  gravityEventCallback: (event) {
    if (event is ContentLoadEvent) {
      // Здесь ваша логика обработки и отображения данных из event.content
    }
  },
);

# Трекинг взаимодействий (engagement)

Трекинг взаимодействий (engagement) в Flutter SDK организован на двух уровнях, чтобы обеспечить гибкий сбор аналитики как по виджетам в целом, так и по отдельным товарам внутри них. Понимание этой иерархии — ключ к правильной интеграции и достоверным отчетам.

# Иерархия событий в кампании

Чтобы понять, какое событие за что отвечает, важно видеть их место в структуре данных кампании.

graph TD
    subgraph Структура Кампании
        A[Кампания / Виджет] --> B(content.events)
        B --> C["WRIMP (Показ всего виджета)"]
        B --> D["WCLICK (Клик по виджету/контенту)"]

        A --> E[products]
        E -- содержит --> F{products.slots}
        
        subgraph Уровень Товара
            F --> G{Slot 1}
            F --> H{Slot 2}
            F --> I[...]

            G --> J(products.slots.events)
            J --> K["PIMP (Показ товара)"]
            J --> L["PCLICK (Клик по товару)"]
        end
    end

# Диаграмма последовательности событий взаимодействия

sequenceDiagram
    participant User as Пользователь
    participant App as Приложение (UI)
    participant SDK as Gravity SDK
    participant Server as Gravity Field

    User->>App: Скролл до виджета / экран показан
    App->>SDK: Виджет стал видимым (>=50%)
    SDK->>Server: WRIMP (ContentVisibleImpression)
    SDK->>Server: PIMP для каждого видимого товара

    User->>App: Клик по товару
    App->>SDK: sendProductEngagement(ProductClickEngagement)
    SDK->>Server: PCLICK
    opt Дополнительно для A/B (опционально)
        App->>SDK: sendContentEngagement(ContentClickEngagement)
        SDK->>Server: WCLICK
    end

    Server-->>SDK: 200 OK

# Назначение и использование событий

Каждый тип события служит для определенных аналитических целей.

# События уровня виджета (WRIMP и WCLICK)

Эти события отслеживают взаимодействие со всей кампанией (виджетом).

  • WRIMP (Widget Real Impression): Фиксирует показ всего виджета, когда он попадает в зону видимости пользователя. Это базовое событие для отслеживания видимости кампании.
  • WCLICK (Widget Click): Используется для отслеживания кликов по не-рекомендательным виджетам, таким как баннеры, in-app, кнопки и тд.

# События уровня товара (PIMP и PCLICK)

Эти события необходимы для сбора статистики по рекомендательным кампаниям.

  • PIMP (Product Impression): Фиксирует показ отдельного товара в рекомендательном блоке, когда он становится видимым.
  • PCLICK (Product Click): Ключевое событие для аналитики рекомендаций. Оно содержит информацию о конкретном товаре и рекомендательной стратегии. Отправка PCLICK обязательна для того, чтобы в отчете по рекомендательным стратегиям появилась аналитика по кликам на товары.

💡 Продвинутый сценарий: WCLICK для A/B-тестов рекомендаций

В случае рекомендательных кампаний, разработчик может опционально отправлять событие WCLICK при клике на любую область виджета (включая клик по товару, одновременно с PCLICK).

Зачем это нужно? Это полезно для A/B-тестирования, когда требуется сравнить общий CTR (Click-Through Rate) разных рекомендательных алгоритмов. WCLICK позволяет измерить кликабельность всего виджета как единого целого, в то время как PCLICK измеряет интерес к конкретным товарам.

# Функции SDK для ручной отправки событий

Когда SDK не может автоматически отследить взаимодействие (например, в кастомном UI), вы должны отправлять события вручную. Для этого в SDK предусмотрены два метода:

GravitySDK.instance.sendContentEngagement() Используется для отслеживания взаимодействий с виджетом в целом (события WRIMP/WCLICK). По умолчанию SDK автоматически отправляет эти события для встроенных форматов. Это актуально когда вы получаете кампанию через getContentBySelector(...), где вы сами отвечаете за рендеринг.

GravitySDK.instance.sendProductEngagement() Используется для отслеживания взаимодействий с отдельными товарами внутри виджета (события PIMP/PCLICK). Отправка PCLICK обязательна при использовании ProductWidgetBuilder. PIMP отправляется автоматически.

# Сценарий: Клик по товару в кастомном ProductWidgetBuilder

Если вы реализуете свой ProductWidgetBuilder, вы обязаны вручную отправлять событие PCLICK при нажатии на товар. SDK автоматически отслеживает только показ (PIMP) кастомных виджетов.

import 'package:gravity_sdk/gravity_sdk.dart';

class CustomProductWidgetBuilder extends ProductWidgetBuilder {
  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    return GestureDetector(
      onTap: () {
        // 1. Отправляем событие клика по товару (PCLICK)
        GravitySDK.instance.sendProductEngagement(
          ProductClickEngagement(product, content, campaign),
        );

        // 2. Выполняем навигацию на страницу товара
        // your_navigation_logic(product.item['url']);
      },
      child: Card(
        // ... ваша верстка карточки товара
      ),
    );
  }
}

# Справочник по событиям (TriggerEvent)

В таблице ниже описаны основные классы событий, которые можно отправлять с помощью triggerEvent(...).

Событие Описание Ключевые параметры
AddToCartEvent Добавление товара в корзину value, productId, quantity, currency?, cart?
PurchaseEvent Успешная покупка uniqueTransactionId, value, currency?, cart?
RemoveFromCartEvent Удаление товара из корзины value, productId, quantity, currency?, cart?
SyncCartEvent Синхронизация полного состава корзины currency?, cart?
AddToWishlistEvent Добавление в список желаний productId
SignUpEvent Регистрация нового пользователя hashedEmail, cuid, cuidType
LoginEvent Вход пользователя в систему hashedEmail?, cuid?, cuidType?
CustomEvent Любое другое кастомное событие type, name, properties?

# Обработка обратных вызовов (Callbacks)

Подпишитесь на события SDK, передав функцию gravityEventCallback в initialize. Это критично для обработки навигации и запросов разрешений.

# Справочник по событиям (TrackingEvent)

Событие Описание Требует обработки?
ContentLoadEvent Контент кампании загружен. Нет
ContentImpressionEvent Контент кампании показан. Нет
ContentVisibleImpressionEvent Контент стал видимым на экране. Нет
ContentCloseEvent Пользователь закрыл In-App. Нет
CopyEvent Пользователь скопировал данные (промокод). Нет
FollowUrlEvent Пользователь нажал на внешнюю ссылку. Да
FollowDeeplinkEvent Пользователь нажал на внутреннюю ссылку. Да
RequestPushEvent Пользователь нажал кнопку запроса push-разрешений. Да
ProductImpressionEvent Карточка товара стала видимой. Нет

Пример обработки:

gravityEventCallback: (event) {
  if (event is FollowUrlEvent) {
    // Открыть URL с помощью url_launcher
  } else if (event is FollowDeeplinkEvent) {
    // Выполнить навигацию с помощью вашего роутера
  } else if (event is RequestPushEvent) {
    // Запросить разрешение на push-уведомления
  }
}

Чтобы видеть клики/навигацию в отчетах, можно дополнительно отправлять CustomEvent через triggerEvent(...) в обработчиках колбэка.

# FAQ (Часто задаваемые вопросы)

Q: В чем разница между trackView и triggerEvent?
A: Оба метода могут инициировать показ кампании. Используйте trackView для отслеживания просмотров экранов (например, главная, карточка товара, корзина). Используйте triggerEvent для отслеживания конкретных действий пользователя (например, добавление в корзину, покупка, логин).

Q: Почему мой GravityInlineWidget не отображается или имеет нулевую высоту?
A: Самая частая причина — для GravityInlineWidget не задан параметр height. Flutter требует явных ограничений по размеру для многих виджетов. Убедитесь, что вы передали конкретное значение высоты: height: 250.

Q: Как обрабатывать клики по кнопкам и ссылкам в In-App сообщениях (например, в модальном окне)?
A: SDK отправляет события FollowUrlEvent (для внешних ссылок) и FollowDeeplinkEvent (для внутренней навигации) в колбэк gravityEventCallback, который вы передаете при инициализации. Вы должны реализовать логику обработки этих событий, например, используя пакет url_launcher для открытия ссылок. SDK сам навигацию не выполняет.

Q: Что произойдет, если SDK не сможет получить кампанию от сервера (например, нет интернета)?
A: SDK обработает ошибку внутри. Если это GravityInlineWidget, он просто не будет отображен (останется пустым). Если это In-App кампания, она не будет показана. Ошибки не должны приводить к падению приложения.

Q: Почему в ProductWidgetBuilder я должен сам отправлять событие клика?
A: Потому что вы создаете полностью кастомный виджет. SDK не знает его внутреннюю структуру и на какой именно элемент пользователь должен нажать. SDK автоматически отслеживает только видимость вашей карточки, а за клик отвечает ваш код через вызов sendProductEngagement().

Q: Как мне протестировать кампанию перед запуском на всех пользователей?
A: Используйте таргетинг по CUID (Custom User ID) в настройках кампании в интерфейсе Gravity Field. Укажите свой тестовый ID, и кампания будет показываться только вам.