πŸ“¦ GravitySDK for Flutter

GravitySDK β€” инструмСнт для ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ пСрсонализированного ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°, отслСТивания взаимодСйствий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΈ ΠΏΠΎΠΊΠ°Π·Π° ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΉ Π² ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ… Flutter-прилоТСниях. SDK сам Π·Π°Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ ΠΈ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ in-app ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ (модальноС ΠΎΠΊΠ½ΠΎ, bottom sheet, полноэкранный Ρ€Π΅ΠΆΠΈΠΌ, tooltip), Π° Ρ‚Π°ΠΊΠΆΠ΅ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°Π΅Ρ‚ inline-Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Ρ‹ ΠΈ headless-Ρ€Π΅ΠΆΠΈΠΌ для ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ кастомной отрисовки.

πŸ“š ОглавлСниС

ВозмоТности

  • Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ SDK с ΠΊΠ»ΡŽΡ‡ΠΎΠΌ API ΠΈ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Π°ΠΌΠΈ сСкции
  • ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ просмотров экранов ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΡ… событий с Π°Π²Ρ‚ΠΎΠΏΠΎΠΊΠ°Π·ΠΎΠΌ in-app ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΉ
  • ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° ΠΏΠΎ сСлСктору, ID ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ ΠΈΠ»ΠΈ Π³Ρ€ΡƒΠΏΠΏΠ΅
  • АвтоматичСский Π±Π°Ρ‚Ρ‡ΠΈΠ½Π³ ΠΈ дСдупликация ΠΊΠΎΠ½ΠΊΡƒΡ€Π΅Π½Ρ‚Π½Ρ‹Ρ… choose-запросов
  • Inline-Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Ρ‹ ΠΈ tooltip-ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ (GravityAnchor)
  • Headless-Ρ€Π΅ΠΆΠΈΠΌ: сырыС Π΄Π°Π½Π½Ρ‹Π΅ ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΉ Π±Π΅Π· встроСнного UI
  • ΠœΠ½ΠΎΠ³ΠΎΡˆΠ°Π³ΠΎΠ²Ρ‹Π΅ (multi-step) ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ
  • ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° взаимодСйствий с ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ΠΎΠΌ ΠΈ ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚Π°ΠΌΠΈ
  • ВрСмСнная Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π°Π²Ρ‚ΠΎΠΏΠΎΠΊΠ°Π·Π° in-app ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° (presentation lock)

Установка

Π”ΠΎΠ±Π°Π²ΡŒ ΠΏΠ°ΠΊΠ΅Ρ‚ Π² Ρ„Π°ΠΉΠ» pubspec.yaml:

dependencies:
  gravity_sdk: ^0.20.0

Π—Π°Ρ‚Π΅ΠΌ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈ ΠΊΠΎΠΌΠ°Π½Π΄Ρƒ:

flutter pub get

И ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠΉ ΠΏΠ°ΠΊΠ΅Ρ‚ Π² своём ΠΊΠΎΠ΄Π΅:

import 'package:gravity_sdk/gravity_sdk.dart';

ВрСбуСтся Dart SDK ^3.10.0.

Для iOS добавь Π² ios/Runner/Info.plist описаниС App Tracking Transparency (SDK Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ статус Ρ‚Ρ€Π΅ΠΊΠΈΠ½Π³Π°):

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

Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ

Для Ρ€Π°Π±ΠΎΡ‚Ρ‹ SDK Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ провСсти Π±Π°Π·ΠΎΠ²ΡƒΡŽ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ, ΠΏΠ΅Ρ€Π΅Π΄Π°Π² ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ apiKey ΠΈ section. Π˜Ρ… ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΠΉΡ‚ΠΈ Π² Π»ΠΈΡ‡Π½ΠΎΠΌ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ‚Π΅.

await GravitySDK.instance.initialize(
  apiKey: 'api-key',
  section: 'section',
);

Полная сигнатура:

Future<void> initialize({
  required String apiKey,
  required String section,
  ProductWidgetBuilder? productWidgetBuilder,
  GravityEventCallback? gravityEventCallback,
  GravityContentCallback? gravityContentCallback,
  LogLevel logLevel = LogLevel.info,
});
  • productWidgetBuilder β€” кастомная отрисовка ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡Π΅ΠΊ ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚ΠΎΠ² (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅)
  • gravityEventCallback β€” колбэк событий SDK ΠΈ дСйствий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ (ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Π΅Π΅)
  • gravityContentCallback β€” колбэк с сырым ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ΠΎΠΌ ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ для headless-сцСнариСв
  • logLevel β€” ΡƒΡ€ΠΎΠ²Π΅Π½ΡŒ логирования

Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅

await GravitySDK.instance.initialize(
  apiKey: 'api-key',
  section: 'section',
  logLevel: LogLevel.debug,
);

Π£Ρ€ΠΎΠ²Π½ΠΈ: none (ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ), error, warn, info (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ), debug.

ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΈ настройки

// Ручная идСнтификация ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ
GravitySDK.instance.setUser('user-id', 'session-id');

// Бброс ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈ сСссии (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ΠΏΡ€ΠΈ logout)
await GravitySDK.instance.resetUser();

// Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Ρ‹Π΅ настройки
GravitySDK.instance.setOptions(
  options: Options(
    isReturnUserInfo: true,
    isImplicitImpression: true,
  ),
  contentSettings: ContentSettings(
    skusOnly: false,
    fields: ['name', 'price', 'imageUrl'],
  ),
  proxyUrl: 'https://your-proxy.com',
  isFetchContentOnTrack: true,
);

// Бтатус Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΡ Π½Π° push-увСдомлСния
GravitySDK.instance.setNotificationPermissionStatus(NotificationPermissionStatus.granted);
  • proxyUrl β€” ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ΠΈΠ·Π°Ρ†ΠΈΡ запросов Ρ‡Π΅Ρ€Π΅Π· прокси
  • isFetchContentOnTrack β€” автоматичСская Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° послС trackViewNoShow() / triggerEventNoShow() (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ true)
  • NotificationPermissionStatus: granted / denied / unknown

ΠžΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ ΠΈ события

Оба ΠΌΠ΅Ρ‚ΠΎΠ΄Π° ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°ΡŽΡ‚ BuildContext β€” Ссли событиС Ρ‚Ρ€ΠΈΠ³Π³Π΅Ρ€ΠΈΡ‚ кампанию, SDK сам ΠΏΠΎΠΊΠ°ΠΆΠ΅Ρ‚ Π΅Ρ‘ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚.

await GravitySDK.instance.trackView(
  context: context,
  pageContext: PageContext(
    type: ContextType.homepage,
    data: [],
    location: 'app://homepage',
  ),
);

await GravitySDK.instance.triggerEvent(
  context: context,
  events: [
    AddToCartEvent(value: 99.99, productId: 'sku-123', quantity: 1, currency: 'RUB'),
  ],
  pageContext: PageContext(
    type: ContextType.product,
    data: ['sku-123'],
    location: 'app://product/sku-123',
  ),
);

ДоступныС события (TriggerEvent): AddToCartEvent, RemoveFromCartEvent, SyncCartEvent, PurchaseEvent, AddToWishlistEvent, SignUpEvent, LoginEvent, CustomEvent.

final purchase = PurchaseEvent(
  uniqueTransactionId: 'ORDER-12345',
  value: 2550.75,
  currency: 'RUB',
  cart: [
    CartItem(productId: 'sku-123', quantity: 1, itemPrice: 100.50),
  ],
);

final custom = CustomEvent(
  type: 'survey-completed-v1',
  name: 'Survey completed',
  customProps: {'surveyId': 'summer-2025', 'rating': '5'},
);

Π£ всСх событий Π΅ΡΡ‚ΡŒ Π΄Π²Π° Π½Π΅ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… поля:

  • customProps β€” Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ свойства Map<String, String> (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ строковыС значСния: числа ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°ΠΉΡ‚Π΅ строкой, Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Π΅ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ ΠΈ списки сСрвСр Π½Π΅ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚);
  • eventTime β€” ΠΊΠΎΠ³Π΄Π° событиС ΠΏΡ€ΠΎΠΈΠ·ΠΎΡˆΠ»ΠΎ (DateTime). SDK отправляСт Π΅Π³ΠΎ Π² UTC; Ссли Π½Π΅ Π·Π°Π΄Π°Π½ΠΎ, SDK сам проставляСт ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π²Ρ‹Π·ΠΎΠ²Π° (см. Β«ΠžΡ„Π»Π°ΠΉΠ½-доставка событий»), поэтому ΠΎΡ‚Π»ΠΎΠΆΠ΅Π½Π½ΠΎΠ΅ событиС сохраняСт Π²Π΅Ρ€Π½ΠΎΠ΅ врСмя.
AddToCartEvent(
  value: 99.99,
  productId: 'sku-123',
  quantity: 1,
  customProps: {'list': 'search'},
  eventTime: DateTime.now(),
);

Π£ CustomEvent Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π΅ΡΡ‚ΡŒ cuid / cuidType (ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ, ΠΊΠ°ΠΊ Ρƒ LoginEvent) ΠΈ cart (список CartItem).

ΠžΡ„Π»Π°ΠΉΠ½-доставка событий

Бобытия (triggerEvent, triggerEventNoShow, ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ Ρ„ΠΎΡ€ΠΌ) Π½Π΅ Ρ‚Π΅Ρ€ΡΡŽΡ‚ΡΡ Π±Π΅Π· сСти:

  1. ΠšΠΎΡ€ΠΎΡ‚ΠΊΠΈΠΉ ΠΏΠΎΠ²Ρ‚ΠΎΡ€. Π›ΡŽΠ±ΠΎΠΉ запрос SDK ΠΏΡ€ΠΈ сСтСвой ошибкС ΠΈΠ»ΠΈ ΠΎΡ‚Π²Π΅Ρ‚Π΅ 408/429/5xx повторяСтся Π΄ΠΎ Ρ‚Ρ€Ρ‘Ρ… Ρ€Π°Π· с ΠΏΠ°ΡƒΠ·Π°ΠΌΠΈ 1 β†’ 2 β†’ 4 с, ΠΏΠΎΠΊΠ° Π½Π΅ истёк staleContentTimeout (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ 10 с). Ошибки Π²ΠΈΠ΄Π° 4xx Π½Π΅ ΠΏΠΎΠ²Ρ‚ΠΎΡ€ΡΡŽΡ‚ΡΡ.
  2. ΠžΡ‡Π΅Ρ€Π΅Π΄ΡŒ Π½Π° дискС. Π’Π΅Π»ΠΎ POST /event записываСтся Π² SharedPreferences Π΄ΠΎ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ ΠΈ удаляСтся послС ΠΎΡ‚Π²Π΅Ρ‚Π° сСрвСра, поэтому событиС ΠΏΠ΅Ρ€Π΅ΠΆΠΈΠ²Π°Π΅Ρ‚ Π΄Π°ΠΆΠ΅ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΠ΅ прилоТСния Π²ΠΎ врСмя зависшСго запроса. Если запрос Ρ‚Π°ΠΊ ΠΈ Π½Π΅ ΡƒΡˆΡ‘Π», событиС отправляСтся ΠΏΠΎΠ·ΠΆΠ΅ β€” ΠΏΡ€ΠΈ запускС прилоТСния, Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π΅ Π½Π° ΠΏΠ΅Ρ€Π΅Π΄Π½ΠΈΠΉ ΠΏΠ»Π°Π½, послС любого ΡƒΠ΄Π°Ρ‡Π½ΠΎΠ³ΠΎ запроса SDK, ΠΏΠΎ Ρ‚Π°ΠΉΠΌΠ΅Ρ€Ρƒ с Π½Π°Ρ€Π°ΡΡ‚Π°ΡŽΡ‰Π΅ΠΉ ΠΏΠ°ΡƒΠ·ΠΎΠΉ (5 с … 15 ΠΌΠΈΠ½), ΠΏΡ€ΠΈ Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ Ρ‡Π΅Ρ€Π΅Π· setOptions ΠΈΠ»ΠΈ ΠΏΠΎ Π²Ρ‹Π·ΠΎΠ²Ρƒ flushQueue(). ΠŸΠ°ΡƒΠ·Π° послС ΠΎΡ‚Π²Π΅Ρ‚Π° сСрвСра 408/429/5xx Π½Π΅ прСрываСтся ΡƒΠ΄Π°Ρ‡Π½Ρ‹ΠΌΠΈ запросами ΠΊ Π΄Ρ€ΡƒΠ³ΠΈΠΌ адрСсам β€” Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π°ΠΉΠΌΠ΅Ρ€ΠΎΠΌ, flushQueue() ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‚ΠΎΠΌ Π½Π° ΠΏΠ΅Ρ€Π΅Π΄Π½ΠΈΠΉ ΠΏΠ»Π°Π½. Кампании ΠΈΠ· ΠΎΡ‚Π²Π΅Ρ‚Π° Π½Π° ΠΎΡ‚Π»ΠΎΠΆΠ΅Π½Π½ΠΎΠ΅ событиС Π½Π΅ Π·Π°ΠΏΡ€Π°ΡˆΠΈΠ²Π°ΡŽΡ‚ΡΡ ΠΈ Π½Π΅ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°ΡŽΡ‚ΡΡ.
  3. eventTime. SDK проставляСт врСмя события (UTC, RFC 3339) Π² ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π²Ρ‹Π·ΠΎΠ²Π° β€” Π΅Ρ‰Ρ‘ Π΄ΠΎ оТидания сСссии, поэтому ΠΎΡ‚Π»ΠΎΠΆΠ΅Π½Π½ΠΎΠ΅ событиС лоТится Π² Π°Π½Π°Π»ΠΈΡ‚ΠΈΠΊΡƒ с Π²Π΅Ρ€Π½Ρ‹ΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½Π΅ΠΌ. Если событиС ΡƒΠΆΠ΅ содСрТит eventTime, ΠΎΠ½ΠΎ Π½Π΅ пСрСзаписываСтся.
  4. Π˜Π΄Π΅Π½Ρ‚ΠΈΡ‡Π½ΠΎΡΡ‚ΡŒ. Π—Π°ΠΏΠΈΡΡŒ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ Ρ…Ρ€Π°Π½ΠΈΡ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π½Π° ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π²Ρ‹Π·ΠΎΠ²Π°, поэтому событиС, ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½Π½ΠΎΠ΅ послС resetUser() ΠΈΠ»ΠΈ смСны ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ, ΡƒΡ…ΠΎΠ΄ΠΈΡ‚ ΠΎΡ‚ Ρ‚ΠΎΠ³ΠΎ, ΠΊΡ‚ΠΎ Π΅Π³ΠΎ ΡΠΎΠ²Π΅Ρ€ΡˆΠΈΠ». Π‘ΠΎΠ±Ρ‹Ρ‚ΠΈΠ΅, поставлСнноС Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ Π΄ΠΎ ΠΏΠ΅Ρ€Π²ΠΎΠΉ сСссии (свСТая установка Π±Π΅Π· сСти), ΠΏΡ€ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ΅ ΠΏΡ€ΠΎΡ…ΠΎΠ΄ΠΈΡ‚ Ρ‚ΠΎΡ‚ ΠΆΠ΅ ΠΏΡƒΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΈ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ запрос: ΠΆΠ΄Ρ‘Ρ‚ ΡƒΠΆΠ΅ ΠΈΠ΄ΡƒΡ‰ΡƒΡŽ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ сСссии ΠΈΠ»ΠΈ создаёт Π΅Ρ‘ сам ΠΈ сохраняСт uid, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²Π΅Ρ€Π½ΡƒΠ» сСрвСр. ΠŸΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ запросы прилоТСния ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ этот ΠΆΠ΅ uid.

Π§Ρ‚ΠΎ Π½Π΅ ΠΏΠΎΠΏΠ°Π΄Π°Π΅Ρ‚ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ: /visit (просмотр экрана), /choose (запрос ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°), engagement-ΡƒΡ€Π»Ρ‹ (ΠΏΠΎΠΊΠ°Π·Ρ‹/ΠΊΠ»ΠΈΠΊΠΈ) β€” ΠΎΠ½ΠΈ ΠΏΠΎΠ²Ρ‚ΠΎΡ€ΡΡŽΡ‚ΡΡ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΈΠΌ ΠΏΠΎΠ²Ρ‚ΠΎΡ€ΠΎΠΌ.

GravitySDK.instance.setOptions(
  offlineQueue: const OfflineQueueSettings(
    enabled: true,                 // ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ Π²ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΎ
    maxEntries: 500,               // старыС записи Π²Ρ‹Ρ‚Π΅ΡΠ½ΡΡŽΡ‚ΡΡ
    maxAge: Duration(days: 7),     // ΡΡ‚Π°Ρ€ΡˆΠ΅ β€” Π½Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ΡΡ
  ),
  staleContentTimeout: const Duration(seconds: 10),
);

// Π‘Π²ΠΎΠΉ Π΄Π΅Ρ‚Π΅ΠΊΡ‚ΠΎΡ€ сСти? Π”Ρ‘Ρ€Π½ΠΈΡ‚Π΅ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ, ΠΊΠΎΠ³Π΄Π° соСдинСниС появилось:
await GravitySDK.instance.flushQueue();

// Π’Ρ‹Ρ…ΠΎΠ΄ ΠΈΠ· Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚Π° ΠΏΠΎ Ρ‚Ρ€Π΅Π±ΠΎΠ²Π°Π½ΠΈΡŽ ΠΏΠΎΠ»ΠΈΡ‚ΠΈΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ…, ΠΎΠ±Ρ‰Π΅Π΅ устройство,
// смСна окруТСния: ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ всё, Ρ‡Ρ‚ΠΎ Π΅Ρ‰Ρ‘ Π½Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ. resetUser() ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ
// Π½Π΅ Ρ‚Ρ€ΠΎΠ³Π°Π΅Ρ‚ β€” события, ΡΠΎΠ²Π΅Ρ€ΡˆΡ‘Π½Π½Ρ‹Π΅ Π΄ΠΎ Π²Ρ‹Ρ…ΠΎΠ΄Π°, Π΄ΠΎΡΡ‚Π°Π²Π»ΡΡŽΡ‚ΡΡ ΠΎΡ‚ Ρ‚ΠΎΠ³ΠΎ
// ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΈΡ… ΡΠΎΠ²Π΅Ρ€ΡˆΠΈΠ».
await GravitySDK.instance.clearQueue();

// Диагностика:
final pending = await GravitySDK.instance.pendingDeliveries;

staleContentTimeout Ρ‚Π°ΠΊΠΆΠ΅ ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ ΠΏΠΎΠΊΠ°Π·: Ссли ΠΎΡ‚Π²Π΅Ρ‚ /visit ΠΈΠ»ΠΈ /event ΠΏΡ€ΠΈΡˆΡ‘Π» ΠΏΠΎΠ·ΠΆΠ΅ этого Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚Π° (ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ, скорСС всСго, ΡƒΠΆΠ΅ Π½Π° Π΄Ρ€ΡƒΠ³ΠΎΠΌ экранС), кампания Π½Π΅ Π·Π°ΠΏΡ€Π°ΡˆΠΈΠ²Π°Π΅Ρ‚ΡΡ β€” Π² headless-Ρ€Π΅ΠΆΠΈΠΌΠ΅ ΠΌΠ΅Ρ‚ΠΎΠ΄ Π²Π΅Ρ€Π½Ρ‘Ρ‚ null. Вторая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° β€” ΡƒΠΆΠ΅ ΠΏΠ΅Ρ€Π΅Π΄ самим ΠΏΠΎΠΊΠ°Π·ΠΎΠΌ, послС ΠΏΠ°ΡƒΠ·Ρ‹ delayTime ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ β€” считаСт всё врСмя Π²Ρ‹Π·ΠΎΠ²Π° ΠΎΡ‚ Π΅Π³ΠΎ Π½Π°Ρ‡Π°Π»Π°, Π° Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΠ°ΡƒΠ·Ρƒ: Ссли /visit ΠΎΡ‚Π²Π΅Ρ‚ΠΈΠ» быстро, Π° запрос ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° затянулся, ΠΏΠΎΠΊΠ°Π· Ρ‚ΠΎΠΆΠ΅ отмСняСтся.

ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΡ: событиС, запрос ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ Π΄ΠΎΡˆΡ‘Π» Π΄ΠΎ сСрвСра, Π½ΠΎ ΠΎΡ‚Π²Π΅Ρ‚ потСрялся, ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ доставлСно Π΄Π²Π°ΠΆΠ΄Ρ‹ (ΠΏΠΎΠΊΡƒΠΏΠΊΠΈ Π΄Π΅Π΄ΡƒΠΏΠ»ΠΈΡ†ΠΈΡ€ΡƒΡŽΡ‚ΡΡ ΠΏΠΎ uniqueTransactionId); ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ° происходит Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΏΠΎΠΊΠ° ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π·Π°ΠΏΡƒΡ‰Π΅Π½ΠΎ; flushQueue() ΠΏΡ€ΠΈ Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½Π½ΠΎΠΉ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ Π΄Π΅Π»Π°Π΅Ρ‚; Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ Π½Π΅ отмСняСт ΡƒΠΆΠ΅ Π½Π°Ρ‡Π°Ρ‚ΡƒΡŽ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΡƒ; pendingDeliveries ΡƒΡ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ ΠΈ событиС, запрос ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ ΠΈΠ΄Ρ‘Ρ‚ прямо сСйчас; Π² headless-ΠΌΠ΅Ρ‚ΠΎΠ΄Π°Ρ… (*NoShow) Ρ‚Π°ΠΉΠΌΠ°ΡƒΡ‚ свСТСсти ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΡ‚Π²Π΅Ρ‚ /visit//event β€” ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚, ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹ΠΉ послС Π½Π΅Π³ΠΎ, возвращаСтся Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‰Π΅ΠΌΡƒ, ΠΈ Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅ ΠΎ ΠΏΠΎΠΊΠ°Π·Π΅ остаётся Π·Π° ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ΠΌ.

ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° колбэков

gravityEventCallback ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ события ΠΆΠΈΠ·Π½Π΅Π½Π½ΠΎΠ³ΠΎ Ρ†ΠΈΠΊΠ»Π° ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° ΠΈ дСйствий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ (TrackingEvent). Π‘ΠΎΠ»ΡŒΡˆΠΈΠ½ΡΡ‚Π²ΠΎ β€” ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅; ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠΉ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π½Π° сторонС прилоТСния Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ FollowUrlEvent, FollowDeeplinkEvent ΠΈ RequestPushEvent. FollowUrlEvent.type (FollowUrlType.browser / FollowUrlType.webview) подсказываСт, Π³Π΄Π΅ кампания просит ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ ссылку; Ссли Π² ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ Ρ‚ΠΈΠΏ Π½Π΅ Π·Π°Π΄Π°Π½, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ browser:

await GravitySDK.instance.initialize(
  apiKey: 'api-key',
  section: 'section',
  gravityEventCallback: (event) {
    if (event is FollowUrlEvent) {
      if (event.type == FollowUrlType.webview) {
        // ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ event.url Π²ΠΎ Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½Π΅ΠΌ webview прилоТСния
      } else {
        launchUrl(Uri.parse(event.url), mode: LaunchMode.externalApplication);
      }
    }
    if (event is FollowDeeplinkEvent) {
      // навигация ΠΏΠΎ Π΄ΠΈΠΏΠ»ΠΈΠ½ΠΊΡƒ прилоТСния
    }
    if (event is RequestPushEvent) {
      // запрос Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΡ Π½Π° push-увСдомлСния
    }
  },
);

Π˜Π½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΎΠ½Π½Ρ‹Π΅ события: ContentLoadEvent, ContentImpressionEvent, ContentVisibleImpressionEvent, ContentCloseEvent, CopyEvent, CancelEvent, ProductImpressionEvent.

Π‘Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° ΠΏΠΎΠΊΠ°Π·Π° (presentation lock)

ΠŸΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎ Π·Π°ΠΏΡ€Π΅Ρ‚ΠΈΡ‚ΡŒ Π°Π²Ρ‚ΠΎΠΏΠΎΠΊΠ°Π· in-app ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΉ Gravity β€” Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ΠΏΠΎΠΊΠ° ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ собствСнный Π΄ΠΈΠ°Π»ΠΎΠ³, ΠΎΠ½Π±ΠΎΡ€Π΄ΠΈΠ½Π³ ΠΈΠ»ΠΈ paywall:

GravitySDK.instance.lockPresentation();

// ... ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚Π½Ρ‹ΠΉ UI прилоТСния ...

GravitySDK.instance.unlockPresentation();

Подписка Π½Π° ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ состояния Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ:

GravitySDK.instance.setPresentationLockListener((locked) {
  debugPrint('Gravity presentation locked: $locked');
});

// отписка
GravitySDK.instance.setPresentationLockListener(null);

Π‘Π΅ΠΌΠ°Π½Ρ‚ΠΈΠΊΠ° (совпадаСт с iOS/Android SDK):

  • ΠΏΠΎΠΊΠ° Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠ° Π°ΠΊΡ‚ΠΈΠ²Π½Π°, trackView ΠΈ triggerEvent Π·Π°Π³Ρ€ΡƒΠΆΠ°ΡŽΡ‚ ΠΈ рСзолвят ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚, Π½ΠΎ Π½Π΅ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°ΡŽΡ‚ in-app UI β€” Π² Π»ΠΎΠ³ ΠΏΠΈΡˆΠ΅Ρ‚ΡΡ Presentation is locked, skipped content for campaign <campaignId>;
  • Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΏΠΎΠ΄ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΎΠΉ всё Ρ€Π°Π²Π½ΠΎ загруТаСтся (уходят запрос choose ΠΈ события contentLoaded), сСрвСрныС Π»ΠΈΠΌΠΈΡ‚Ρ‹ ΠΏΠΎΠΊΠ°Π·ΠΎΠ² ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ (frequency cap, one-shot) Ρ€Π°ΡΡ…ΠΎΠ΄ΡƒΡŽΡ‚ΡΡ Π΄Π°ΠΆΠ΅ Π±Π΅Π· ΠΏΠΎΠΊΠ°Π·Π°;
  • блокируСтся Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π°Π²Ρ‚ΠΎΠΏΠΎΠΊΠ°Π·: fetchAnchorContent (Π² Ρ‚ΠΎΠΌ числС Π°Π²Ρ‚ΠΎΠ·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Ρ‡Π΅Ρ€Π΅Π· GravityAnchor), step-навигация ΡƒΠΆΠ΅ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎΠ³ΠΎ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°, trackViewNoShow/triggerEventNoShow ΠΈ getContentBy* ΠΏΠΎΠ΄ Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΡƒ Π½Π΅ ΠΏΠΎΠΏΠ°Π΄Π°ΡŽΡ‚;
  • ΡƒΠΆΠ΅ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Ρ‹ΠΉ in-app ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΏΡ€ΠΈ lockPresentation() Π½Π΅ закрываСтся;
  • послС unlockPresentation() ΠΏΠΎΠΊΠ°Π·Ρ‹ снова Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½Ρ‹ для ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΡ… trackView/triggerEvent; кампания, пропущСнная Π²ΠΎ врСмя Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ, Ρ€Π΅Ρ‚Ρ€ΠΎΠ°ΠΊΡ‚ΠΈΠ²Π½ΠΎ Π½Π΅ показываСтся;
  • ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Π΅ Π²Ρ‹Π·ΠΎΠ²Ρ‹ lock/unlock бСзопасны; listener вызываСтся ΠΏΡ€ΠΈ ΠΊΠ°ΠΆΠ΄ΠΎΠΌ Π²Ρ‹Π·ΠΎΠ²Π΅;
  • состояниС Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ ΠΆΠΈΠ²Ρ‘Ρ‚ Π² памяти ΠΈ сбрасываСтся ΠΏΡ€ΠΈ пСрСзапускС прилоТСния β€” Ссли ΠΎΠ½ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΠΏΠ΅Ρ€Π΅ΠΆΠΈΠ²Π°Ρ‚ΡŒ рСстарт, сохраняйтС ΠΈ восстанавливайтС Π΅Π³ΠΎ Π½Π° сторонС прилоТСния (ΠΏΡ€ΠΈΠΌΠ΅Ρ€ β€” PresentationLockPrefs Π² example/);
  • Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ состояниС доступно Ρ‡Π΅Ρ€Π΅Π· GravitySDK.instance.isPresentationLocked.

ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°

ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΌΠΎΠΆΠ½ΠΎ Π·Π°ΠΏΡ€ΠΎΡΠΈΡ‚ΡŒ Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ, Π±Π΅Π· Π°Π²Ρ‚ΠΎΠΏΠΎΠΊΠ°Π·Π°:

final bySelector = await GravitySDK.instance.getContentBySelector(
  selector: 'homepage-recommendations',
  pageContext: pageContext,
  rules: rules, // ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ, List<RtRule>
);

final byCampaign = await GravitySDK.instance.getContentByCampaignId(
  campaignId: 'campaign-id',
  pageContext: pageContext,
);

final byGroup = await GravitySDK.instance.getContentByGroup(
  group: 'homepage-group',
  pageContext: pageContext,
);

ΠžΡ‚Π²Π΅Ρ‚ β€” ContentResponse с кампаниями (data), ΠΈΡ… вариациями (payload) ΠΈ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ΠΎΠΌ (contents):

final campaign = bySelector.data.firstOrNull;
final variation = campaign?.payload.firstOrNull;
final content = variation?.contents.firstOrNull;

Π‘Π°Ρ‚Ρ‡ΠΈΠ½Π³ ΠΈ дСдупликация

ΠšΠΎΠ½ΠΊΡƒΡ€Π΅Π½Ρ‚Π½Ρ‹Π΅ Π²Ρ‹Π·ΠΎΠ²Ρ‹ getContentBySelector / getContentByCampaignId автоматичСски ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΡƒΡŽΡ‚ΡΡ Π² ΠΏΡ€Π΅Π΄Π΅Π»Π°Ρ… ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΎΠ³ΠΎ ΠΎΠΊΠ½Π° (10 мс):

  • ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ‡Π½Ρ‹Π΅ запросы Π΄Π΅Π΄ΡƒΠΏΠ»ΠΈΡ†ΠΈΡ€ΡƒΡŽΡ‚ΡΡ β€” ΡƒΡ…ΠΎΠ΄ΠΈΡ‚ ΠΎΠ΄ΠΈΠ½ сСтСвой Π²Ρ‹Π·ΠΎΠ², Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‰ΠΈΠ΅ дСлят ΠΎΠ΄ΠΈΠ½ ΠΎΡ‚Π²Π΅Ρ‚;
  • Ρ€Π°Π·Π½Ρ‹Π΅ запросы с ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹ΠΌ ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅ΠΌ (ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ, PageContext, Options) ΠΎΠ±ΡŠΠ΅Π΄ΠΈΠ½ΡΡŽΡ‚ΡΡ Π² ΠΎΠ΄ΠΈΠ½ POST /choose с нСсколькими элСмСнтами data[].

Π­Ρ‚ΠΎ ΠΏΡ€ΠΎΠ·Ρ€Π°Ρ‡Π½ΠΎ для Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‰Π΅Π³ΠΎ ΠΊΠΎΠ΄Π° ΠΈ Π½Π΅ Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ настройки. getContentByGroup Π² Π±Π°Ρ‚Ρ‡ΠΈΠ½Π³Π΅ Π½Π΅ участвуСт.

ΠŸΡ€ΠΈΠΌΠ΅Ρ€: Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΉ ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌΠΈ

final rules = [
  RtRule(
    type: 'filter',
    conditions: [
      RtRuleCondition(
        field: 'category',
        arguments: [RtRuleArgument(action: 'in', value: ['shoes'])],
      ),
    ],
  ),
];

Headless-Ρ€Π΅ΠΆΠΈΠΌ

Для ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ кастомной отрисовки Π΅ΡΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ с сырым JSON ΠΈ Β«Π±Π΅Π·Π·Π²ΡƒΡ‡Π½Ρ‹Π΅Β» Π°Π½Π°Π»ΠΎΠ³ΠΈ Ρ‚Ρ€Π΅ΠΊΠΈΠ½Π³Π° (ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ возвращаСтся, Π½ΠΎ Π½Π΅ показываСтся):

// модСль + сырой JSON
final GravityDataResponse<ContentResponse> details =
    await GravitySDK.instance.getContentBySelectorWithDetails(
  selector: 'homepage-banner',
  pageContext: pageContext,
);

// Ρ‚Ρ€Π΅ΠΊΠΈΠ½Π³ Π±Π΅Π· ΠΏΠΎΠΊΠ°Π·Π°: null, Ссли кампания Π½Π΅ сработала, ΠΎΡ‚Π²Π΅Ρ‚ ΠΏΡ€ΠΈΡˆΡ‘Π» ΠΏΠΎΠ·ΠΆΠ΅ staleContentTimeout ΠΈΠ»ΠΈ событиС ΡƒΡˆΠ»ΠΎ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ
final GravityDataResponse<ContentResponse>? triggered =
    await GravitySDK.instance.trackViewNoShow(pageContext: pageContext);

final GravityDataResponse<ContentResponse>? byEvent =
    await GravitySDK.instance.triggerEventNoShow(
  events: [event],
  pageContext: pageContext,
);

trackViewNoShow / triggerEventNoShow Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‚ gravityContentCallback ΠΈ ΠΏΠΎΠ΄Ρ‡ΠΈΠ½ΡΡŽΡ‚ΡΡ Ρ„Π»Π°Π³Ρƒ isFetchContentOnTrack.

ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ»ΡŒΠ½Ρ‹Π΅ ΠΊΠ»ΡŽΡ‡ΠΈ variables

ΠšΡ€ΠΎΠΌΠ΅ Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… ΠΏΠΎΠ»Π΅ΠΉ (title, elements, frameUI, …) ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ variables ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Π»ΡŽΠ±Ρ‹Π΅ ΠΊΠ»ΡŽΡ‡ΠΈ, Π·Π°Π΄Π°Π½Π½Ρ‹Π΅ Π² Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π΅ Gravity. Они доступны Π±Π΅Π· Ρ€Π°Π·Π±ΠΎΡ€Π° сырого JSON:

  • content.rawVariables β€” вСсь ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ variables ΠΊΠ°ΠΊ Map<String, dynamic>;
  • content.variables['<ΠΊΠ»ΡŽΡ‡>'] β€” Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎ ΠΊΠ»ΡŽΡ‡Ρƒ (Object?; null ΠΈ Ссли ΠΊΠ»ΡŽΡ‡Π° Π½Π΅Ρ‚, ΠΈ Ссли Π² Π½Ρ‘ΠΌ записан null β€” Ρ€Π°Π·Π»ΠΈΡ‡ΠΈΡ‚ΡŒ ΠΌΠΎΠΆΠ½ΠΎ Ρ‡Π΅Ρ€Π΅Π· content.rawVariables.containsKey('<ΠΊΠ»ΡŽΡ‡>'));
  • content.variables.valueOf<T>('<ΠΊΠ»ΡŽΡ‡>') β€” Ρ‚ΠΎ ΠΆΠ΅, Π½ΠΎ Π²Π΅Ρ€Π½Ρ‘Ρ‚ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅, Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ссли ΠΎΠ½ΠΎ ΠΈΠΌΠ΅Π΅Ρ‚ Ρ‚ΠΈΠΏ T, ΠΈΠ½Π°Ρ‡Π΅ null. ЗначСния приходят ΠΈΠ· jsonDecode, поэтому T Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ JSON-Ρ‚ΠΈΠΏΠΎΠΌ: ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ β€” Map<String, dynamic>, массивы β€” List<dynamic> (Π½Π΅ List<String>), числа β€” num (Ρ†Π΅Π»ΠΎΠ΅ 15 Π½Π΅ являСтся double, Π° 1.5 β€” int), строки β€” String, Ρ„Π»Π°Π³ΠΈ β€” bool.

rawVariables Π΅ΡΡ‚ΡŒ Ρƒ любого CampaignContent, ΠΎΡ‚ΠΊΡƒΠ΄Π° Π±Ρ‹ ΠΎΠ½ Π½ΠΈ ΠΏΡ€ΠΈΡˆΡ‘Π»: getContentBySelector, …WithDetails, gravityContentCallback, ContentLoadEvent.content, inline-Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Ρ‹.

final content = details.data.data.first.payload.first.contents.first;

// A/B-вариация, описанная Π² ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ»ΡŒΠ½Ρ‹ΠΌ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠΌ
final variant = content.variables.valueOf<Map<String, dynamic>>('inline_banner');
final label = variant?['variant'] as String?;

// вСсь ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Ρ†Π΅Π»ΠΈΠΊΠΎΠΌ
final all = content.rawVariables;

Π’Π°ΠΆΠ½ΠΎ:

  • rawVariables β€” ΠΏΠΎΠ»Π½Ρ‹ΠΉ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ variables, Π²ΠΊΠ»ΡŽΡ‡Π°Ρ Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΊΠ»ΡŽΡ‡ΠΈ; Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ поля (content.variables.title, content.variables.elements, …) ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°ΡŽΡ‚ Ρ€Π°Π±ΠΎΡ‚Π°Ρ‚ΡŒ ΠΊΠ°ΠΊ Ρ€Π°Π½ΡŒΡˆΠ΅;
  • ΠΊΠ°Ρ€Ρ‚Π° нСизмСняСмая Π½Π° Π²Π΅Ρ€Ρ…Π½Π΅ΠΌ ΡƒΡ€ΠΎΠ²Π½Π΅ (ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠ° записи бросит UnsupportedError), Π½ΠΎ Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Π΅ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ ΠΈ списки β€” Ρ‚Π΅ ΠΆΠ΅ экзСмпляры, Ρ‡Ρ‚ΠΎ ΠΈ Π² GravityDataResponse.json: Π½Π΅ ΠΌΡƒΡ‚ΠΈΡ€ΡƒΠΉΡ‚Π΅ ΠΈΡ…;
  • Π½Π°Π±ΠΎΡ€ ΠΈ структура ΠΊΠ»ΡŽΡ‡Π΅ΠΉ ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΡΡŽΡ‚ΡΡ настройками ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ Π½Π° сторонС Gravity β€” SDK ΠΈΡ… Π½Π΅ Π²Π°Π»ΠΈΠ΄ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΈ Π½ΠΈΡ‡Π΅Π³ΠΎ ΠΎ Π½ΠΈΡ… Π½Π΅ ΠΏΡ€Π΅Π΄ΠΏΠΎΠ»Π°Π³Π°Π΅Ρ‚.

ΠžΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°

In-app ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ (модальноС ΠΎΠΊΠ½ΠΎ, bottom sheet, полноэкранный Ρ€Π΅ΠΆΠΈΠΌ, tooltip) SDK ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ автоматичСски ΠΈΠ· trackView / triggerEvent β€” Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ задаётся настройками ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ Π½Π° сторонС Gravity. ΠœΠ½ΠΎΠ³ΠΎΡˆΠ°Π³ΠΎΠ²Ρ‹Π΅ ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ (ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Ρ‹ ΠΌΠ΅ΠΆΠ΄Ρƒ шагами ΠΏΠΎ ΠΊΠ½ΠΎΠΏΠΊΠ°ΠΌ) ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°ΡŽΡ‚ΡΡ встроСнным Ρ€Π΅Π½Π΄Π΅Ρ€Π΅Ρ€ΠΎΠΌ.

In-app Ρ„ΠΎΡ€ΠΌΡ‹ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, опрос с ΠΎΡ†Π΅Π½ΠΊΠΎΠΉ прилоТСния) Ρ‚ΠΎΠΆΠ΅ рСндСрятся автоматичСски: Π²Ρ‹Π±ΠΎΡ€ ΠΎΡ†Π΅Π½ΠΊΠΈ, тСкстовый Π²Π²ΠΎΠ΄ с ограничСниями Π΄Π»ΠΈΠ½Ρ‹, условная Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ элСмСнтов Π² зависимости ΠΎΡ‚ ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². Кнопка ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ Π½Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Π°, ΠΏΠΎΠΊΠ° ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ поля Π½Π΅ Π·Π°ΠΏΠΎΠ»Π½Π΅Π½Ρ‹; Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ отправляСтся Π² Gravity Π±Π΅Π· участия прилоТСния. ΠžΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠΉ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π½Π΅ трСбуСтся β€” ΠΊΡ€ΠΎΠΌΠ΅ FollowUrlEvent, Ссли кампания послС ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ Π²Π΅Π΄Ρ‘Ρ‚ Π½Π° внСшнюю ссылку (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, Π² ΠΌΠ°Π³Π°Π·ΠΈΠ½ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ).

Inline-Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Ρ‹

ВстраиваниС ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ Π² вёрстку экрана:

GravityInlineWidget(
  selector: 'homepage-recommendations',
  height: 250,
  pageContext: pageContext,
);

GravityInlineListWidget(
  group: 'homepage-group',
  height: 250,
  pageContext: pageContext,
);

GravityInlineWidget Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ ΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅Ρ‚ placeholderId, width, showLoading, loadingWidget, backgroundColor, onLoaded, rules; GravityInlineListWidget β€” showLoading, loadingWidget, showIndicator, indicatorActiveColor, indicatorInactiveColor.

Tooltip-ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ: GravityAnchor

GravityAnchor ΠΏΠΎΠΌΠ΅Ρ‡Π°Π΅Ρ‚ Π²ΠΈΠ΄ΠΆΠ΅Ρ‚ ΠΊΠ°ΠΊ ΡΠΊΠΎΡ€ΡŒ для tooltip-ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ ΠΈ сам Π·Π°Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ Π΅Ρ‘ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚:

GravityAnchor(
  selector: 'profile_tooltip',
  pageContext: pageContext,
  builder: (context, onReady) {
    return GravityInlineWidget(
      selector: 'profile_inline',
      height: 120,
      pageContext: pageContext,
      onLoaded: onReady,
    );
  },
);

Π—Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ якорный ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΌΠΎΠΆΠ½ΠΎ ΠΈ Π²Ρ€ΡƒΡ‡Π½ΡƒΡŽ:

await GravitySDK.instance.fetchAnchorContent(
  context: context,
  selector: 'profile_tooltip',
  pageContext: pageContext, // ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ
);

ProductWidgetBuilder β€” кастомизация отобраТСния ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚ΠΎΠ²

НСкоторыС ΠΊΠ°ΠΌΠΏΠ°Π½ΠΈΠΈ содСрТат ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚Ρ‹ (Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄Π°Ρ†ΠΈΠΈ, Π°ΠΊΡ†ΠΈΠΈ). Π§Ρ‚ΠΎΠ±Ρ‹ ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡ΠΊΠΈ Ρ‚ΠΎΠ²Π°Ρ€ΠΎΠ² Π²ΠΏΠΈΡΡ‹Π²Π°Π»ΠΈΡΡŒ Π² ΡΡ‚ΠΈΠ»ΡŒ прилоТСния, ΠΏΠ΅Ρ€Π΅Π΄Π°ΠΉΡ‚Π΅ свою Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ ProductWidgetBuilder ΠΏΡ€ΠΈ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ; ΠΈΠ½Π°Ρ‡Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ DefaultProductWidgetBuilder.

class MyProductWidgetBuilder extends ProductWidgetBuilder {
  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    final item = product.item; // Map<String, dynamic> β€” Π°Ρ‚Ρ€ΠΈΠ±ΡƒΡ‚Ρ‹ Ρ‚ΠΎΠ²Π°Ρ€Π° ΠΈΠ· Ρ„ΠΈΠ΄Π°
    final imageUrl = item['imageUrl'] as String?;

    return GestureDetector(
      onTap: () {
        GravitySDK.instance.sendProductEngagement(
          ProductClickEngagement(product, content, campaign),
        );
      },
      child: Card(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (imageUrl != null) Image.network(imageUrl, height: 160, fit: BoxFit.cover),
            Text(item['name'] as String? ?? '', style: const TextStyle(fontWeight: FontWeight.bold)),
            Text('${item['price'] ?? ''}'),
          ],
        ),
      ),
    );
  }
}

await GravitySDK.instance.initialize(
  apiKey: 'api-key',
  section: 'section',
  productWidgetBuilder: MyProductWidgetBuilder(),
);

ВзаимодСйствия (engagement)

ΠŸΡ€ΠΈ кастомной отрисовкС ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π° отправляйтС взаимодСйствия Π²Ρ€ΡƒΡ‡Π½ΡƒΡŽ:

// ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚
GravitySDK.instance.sendContentEngagement(ContentImpressionEngagement(content, campaign));
GravitySDK.instance.sendContentEngagement(ContentVisibleImpressionEngagement(content, campaign));
GravitySDK.instance.sendContentEngagement(ContentCloseEngagement(content, campaign));
GravitySDK.instance.sendContentEngagement(ContentClickEngagement(content, campaign));

// ΠΏΡ€ΠΎΠ΄ΡƒΠΊΡ‚Ρ‹
GravitySDK.instance.sendProductEngagement(ProductClickEngagement(slot, content, campaign));
GravitySDK.instance.sendProductEngagement(ProductVisibleImpressionEngagement(slot, content, campaign));

ContentClickEngagement β€” ΠΊΠ»ΠΈΠΊ ΠΏΠΎ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Ρƒ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ отрисовало само (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, A/B-вариация Π² headless-Ρ€Π΅ΠΆΠΈΠΌΠ΅). SDK Π±Π΅Ρ€Ρ‘Ρ‚ URL ΠΈΠ· content.events с Ρ‚ΠΈΠΏΠΎΠΌ click; Ссли сСрвСр Ρ‚Π°ΠΊΠΎΠ³ΠΎ события Π½Π΅ прислал, Π²Ρ‹Π·ΠΎΠ² Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ отправляСт. Для ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ рисуСт сам SDK, ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ Π΅Π³ΠΎ Π½Π΅ Π½ΡƒΠΆΠ½ΠΎ: ΠΊΠ½ΠΎΠΏΠΊΠΈ follow_url, follow_deeplink ΠΈ request_push ΡƒΠΆΠ΅ Ρ„ΠΈΠΊΡΠΈΡ€ΡƒΡŽΡ‚ ΠΊΠ»ΠΈΠΊ своими дСйствиями, ΠΈ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹ΠΉ Π²Ρ‹Π·ΠΎΠ² ΡƒΠ΄Π²ΠΎΠΈΡ‚ Π΅Π³ΠΎ Π² статистикС.

ContentEngagement β€” sealed-класс: Ссли Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Π΅ΡΡ‚ΡŒ ΠΈΡΡ‡Π΅Ρ€ΠΏΡ‹Π²Π°ΡŽΡ‰ΠΈΠΉ switch ΠΏΠΎ Π΅Π³ΠΎ ΠΏΠΎΠ΄Ρ‚ΠΈΠΏΠ°ΠΌ, послС обновлСния Π΄ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ Π²Π΅Ρ‚ΠΊΡƒ для ContentClickEngagement.

ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок

ΠŸΠ΅Ρ€Π΅Π΄ Π²Ρ‹Π·ΠΎΠ²Π°ΠΌΠΈ SDK Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΡƒΠ±Π΅Π΄ΠΈΡ‚ΡŒΡΡ, Ρ‡Ρ‚ΠΎ ΠΎΠ½ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½. Π’ ΠΏΡ€ΠΎΡ‚ΠΈΠ²Π½ΠΎΠΌ случаС Π±ΡƒΠ΄Π΅Ρ‚ Π²Ρ‹Π±Ρ€ΠΎΡˆΠ΅Π½ΠΎ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅:

GravitySDK is not initialized. Call initialize() first.

Π‘Π΅Ρ‚Π΅Π²Ρ‹Π΅ ошибки getContentBy* ΠΏΡ€ΠΎΠ±Ρ€Π°ΡΡ‹Π²Π°ΡŽΡ‚ΡΡ Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‰Π΅ΠΌΡƒ ΠΊΠΎΠ΄Ρƒ (DioException); trackView / triggerEvent ΠΈ inline-Π²ΠΈΠ΄ΠΆΠ΅Ρ‚Ρ‹ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°ΡŽΡ‚ свои ошибки ΡΠ°ΠΌΠΎΡΡ‚ΠΎΡΡ‚Π΅Π»ΡŒΠ½ΠΎ ΠΈ Π½Π΅ Ρ€ΠΎΠ½ΡΡŽΡ‚ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅. Π‘Π΅Ρ‚Π΅Π²Ρ‹Π΅ сбои ΠΏΡ€ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ΅ событий ΠΏΠΎΠΊΡ€Ρ‹Ρ‚Ρ‹ ΠΏΠΎΠ²Ρ‚ΠΎΡ€ΠΎΠΌ ΠΈ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒΡŽ Π½Π° дискС β€” см. ΠžΡ„Π»Π°ΠΉΠ½-доставка событий.

Libraries

gravity_sdk