Инструкция по настройке AppsFlyer (постбеки)
Эта инструкция поможет вам настроить интеграцию мобильных приложений через AppsFlyer — платформы для мобильной атрибуции и маркетинговой аналитики.
Данный тип интеграции настраивается для того, чтобы заказы, созданные через мобильное приложение, корректно учитывались в канале CPA:
Если у пользователя установлено мобильное приложение — мы будем отправлять его в приложение, после чего наша система будет атрибуцировать все его покупки.
Если у пользователя не установлено мобильное приложение — он будет как прежде совершать заказы на сайте по CPA-модели.
Важно понимать, что рекламодатель платит не за установки, а за конкретные покупки.
Мы приводим и атрибуцируем каждую покупку за нужным партнером и вебмастером.
Содержание:
Шаг 1. Добавление приложения (общее)
Примечание
Если у вас еще нет аккаунта AppsFlyer, зарегистрируйте его здесь.
После регистрации аккаунта следует добавить ваше приложение в AppsFlyer:
Перейдите на страницу приложений;
Нажмите + Add app и введите данные приложения;
В пункте Enter app details выберите платформу (Android/iOS) и статус приложения (опубликовано / в разработке);
Если приложение уже опубликовано, вставьте ссылку из маркетплейса на приложение в поле App Store URL для iOS или Google Play URL для Android. Если приложение еще не опубликовано или находится на рассмотрении – введите Apple App ID для iOS или Android package name для Android;
В пункте Additional Information введите дополнительные данные – валюту, часовой пояс и укажите, ориентировано ли приложение на детей;
Нажмите Add my app.

Подсказка
Где взять Apple App ID и Android package name?
Apple App ID: Вы можете скопировать этот параметр из URL приложения в App Store, например
https://apps.apple.com/ru/app/microsoft-word/id586447913
, где586447913
и есть ID приложения,Android package name: Вы можете скопировать это название из URL приложения в Google Play, например
play.google.com/store/apps/details?id=com.example.app123
, гдеcom.example.app123
и есть название пакета.
Шаг 2. Установка SDK AppsFlyer
Чтобы работать через AppsFlyer необходимо интегрировать SDK AppsFlyer в приложение.
Для iOS
Установить SDK AppsFlyer для iOS можно несколькими способами:
Через Cocoapods
Установите Сocoapods;
Добавьте зависимости – необходимо добавить последнюю версию
AppsFlyerFramework
в Podfile проекта с помощью команды:
pod 'AppsFlyerFramework'
Установите зависимости с помощью команды:
pod 'install'
После установки необходимо использовать
.xcworkspace
файл для открытия проекта, вместо.xcodeproj
.
Через SwiftPackageManager (рекомендуется)
В Xcode перейдите по пути: File → Swift Packages → Add Package Dependency:

Добавьте IOS SDK репозиторий AppsFlyer (https://github.com/AppsFlyerSDK/AppsFlyerFramework):

Выберите версию SDK – Exact (текущая):

Добавьте
AppsFlyerLib
к нужному таргету:

Для Android
Установить SDK AppsFlyer для Android можно также несколькими способами:
Установка с помощью Gradle
Добавьте репозиторий
mavenCentral()
в файлbuild.gradle
:

Добавьте зависимости в файл
build.gradle
– здесь можно сразу добавить зависимостьinstallreferer
, поскольку она понадобится нам в дальнейшем:
implementation 'com.android.installreferrer:installreferrer:2.2'
implementation 'com.appsflyer:af-android-sdk:6.3.2'

Установка без Gradle
Переведите структуру проекта из Android в Project:

Скачайте SDK;
Переместите скачанный SDK в
app/libs
:

Правой кнопкой мыши кликните по
jar
-файлу и нажмите + Add as Library, после чего кликните повторно и нажмите Refactor;После установки необходимо добавить разрешения в файл
AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
Предупреждение
Если установка была выполнена без Gradle, необходимо добавить зависимость ниже (для установки с помощью Gradle мы добавили ее ранее) в build.gradle
:
implementation 'com.android.installreferrer:installreferrer:2.2'
Далее перейдем к интеграции SDK в приложение.
Шаг 3. Интеграция SDK AppsFlyer
Примечание
Перед началом интеграции SDK AppsFlyer необходимо узнать ключ разработчика (в панели AppsFlyer перейдите в раздел Configuration → App Settings → поле «Dev key») и Apple App ID (в панели AppStore Connect перейдите в раздел Мои приложения → выберите ваше приложение → Информация о приложении → поле «Apple ID»).
Swift
Импортируйте
AppsFlyerLib
:
import AppsFlyerLib
Инициализируйте SDK AppsFlyer:
AppsFlyerLib.shared().appsFlyerDevKey="your-key"
AppsFlyerLib.shared().appleAppID="your-id"
Вызовите
AppsFlyerLib.shared().start
;
Пример:

После этого, добавляем поддержку AppTrackingTransparency
.
Перед вызовом
start()
, добавьтеwaitForATTUserAuthorization(timeoutInterval: 60)
вdidFinishLaunchingWithOptions
:

Запросите одобрение пользователя в
applicationDidBecomeActive()
:

Kotlin
Импортируйте
AppsFlyerLib
:
import com.appsflyer.AppsFlyerLib
Инициализируйте SDK AppsFlyer:
AppsFlyerLib.getInstance().init("your-key", null, this)
Запустите Android SDK:
AppsFlyerLib.getInstance().start(this)
Пример:

React Native
Импортируйте
appsFlyer
изreact-native-appsflyer
:
import appsFlyer from 'react-native-appsflyer';
import appsFlyer from 'react-native-appsflyer';
Инициализируйте SDK AppsFlyer:
appsFlyer.initSdk({
devKey: 'K2***********99',
isDebug: false,
appId: '41*****44',
onInstallConversionDataListener: true, //Optional
onDeepLinkListener: true, //Optional
timeToWaitForATTUserAuthorization: 10 //for iOS 14.5
});
appsFlyer.initSdk({
devKey: 'K2***********99',
isDebug: false,
appId: '41*****44',
onInstallConversionDataListener: true, //Optional
onDeepLinkListener: true, //Optional
timeToWaitForATTUserAuthorization: 10 //for iOS 14.5
});
После интеграции SDK в приложение перейдем к настройке событий.
Шаг 4. Настройка отправляемых из приложения событий
Для начала необходимо определить имя события для отправки. В нашем примере событием является тап по кнопке «Старт», поэтому мы назвали его «start_button_tap». В реальном приложении события могут называться по разному, например: «purchase»,» level_up», «bet» и т.д.
После того, как вы определитесь с именем события для отправки, необходимо подготовить массив (map/dictionary) withValues
, который будет содержать следующие данные о событии:
Сумма заказа/доход. В качестве ключа используется константа
AFInAppEventParameterName.REVENUE
(Kotlin) илиAFEventParamRevenue
(Swift), либо строкаaf_revenue
(React Native). Опционально для Android.ID заказа или события в системе рекламодателя. Ключ «order_id» или любой другой (по согласованию). Обязательно для передачи!
Другие данные, которые вы готовы нам предоставить для атрибутирования заказов, например, категория заказа («category»), тип клиента («clientType») или промокод («coupon»). Опционально.
Далее, необходимо вызвать функцию logEvent()
из AppsFlyerLib
, передав ей имя события и массив с данными.
Предупреждение
Реализация интеграции для iOS на этом не заканчивается – смотрите раздел Реализация на iOS (Swift, React Native).
Реализация на Kotlin
Пример реализации:
val eventData = HashMap<String, Any>()
eventData.put("orderId", orderId)
eventData.put(AFInAppEventParameterName.REVENUE, revenue)
eventData.put("category", category)
eventData.put("clientType", clientType)
eventData.put("coupon", coupon)
AppsFlyerLib.getInstance().logEvent(context, "start_button_tap", eventData)
Реализация на React Native
Пример реализации:
appsFlyer.logEvent("purchase", {
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
});
appsFlyer.logEvent("purchase", {
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
});
Реализация на iOS (Swift, React Native)
Реализовать интеграцию AppsFlyer на платформе iOS значительно сложнее, поскольку есть две проблемы:
Постбеки событий, отправленных с iOS (независимо от использованной реализации AppsFlyer SDK), приходят с крайне ограниченным количеством данных, а именно: из нетрекинговых данных (данные, которые отправляются через
logEvent()
мы получаем только «af_revenue». Остальные нестандартные поля нам недоступны (в том числе «orderId»)).Постбек события установки приложения (первого запуска приложения) не содержит AppsFlyerID – случайный ID, генерируемый SDK при первом запуске приложения. Из-за этого мы не сможем связывать покупки (или другие действия) с событием установки (нам важны трекинговые метки события установки).
Однако эти препятствия можно обойти.
Отправка события покупки
Как мы уже писали, нам доступно исключительно поле «af_revenue» – строковое поле, в котором AppsFlyer ожидает числовое значение.
Поле передаётся в постбеке без изменений, а при обработке числа на своей стороне AppsFlyer обрезает его до 5 знаков после запятой. Благодаря этому мы можем использовать это поле для хранения данных, если предварительно закодировать их в числовую последовательность.
Для этих целей мы разработали алгоритм, реализацию которого вы можете увидеть в примерах ниже.
class NumSeqEncoder {
/// Encodes byte array into one large numeric string.
///
/// - Parameter bytes: Byte array with binary data.
/// - Returns: String with a long number.
static func encode(_ bytes: [UInt8]) -> String {
let size = bytes.count
var string: String = ""
string.reserveCapacity(Int(ceil(Double(size) * 8 / 3)))
var buffer: UInt32 = 0
var bufferSize = 0
var pos = 0
while pos < size {
let pickBytes = min(size - pos, bufferSize != 0 ? 3 : 4)
buffer = buffer << (pickBytes * 8)
for y in 0..<pickBytes {
buffer |= ((UInt32(bytes[pos + y]) & ((1 << 8) - 1)) << ((pickBytes - y - 1) * 8))
}
bufferSize += pickBytes * 8
pos += pickBytes
while bufferSize >= 3 {
if bufferSize >= 4 {
let quad = UInt(buffer >> (bufferSize - 4) & ((1 << 4) - 1))
if quad == 0b1000 || quad == 0b1001 {
string += String(quad)
bufferSize -= 4
buffer &= (1 << bufferSize) - 1
continue
}
}
let triple = UInt(buffer >> (bufferSize - 3) & ((1 << 3) - 1))
string += String(triple)
bufferSize -= 3
buffer &= (1 << bufferSize) - 1
}
}
if bufferSize > 0 {
string += String(buffer << (3 - bufferSize))
}
return string
}
/// Encodes order data into a string as a long float number.
///
/// - Parameters:
/// - data: Data to encode. Allowed up to 1441 bytes (not characters!).
/// - Returns: String with a long float number; up to 3851 characters/bytes.
static func encodeToRevenue(_ data: String) -> String {
let dataBytes = Array(data.utf8)
// 3852 is max known length for Appsflyer's revenue field (it's probably greater though).
if Int(ceil(Double(dataBytes.count) * 8 / 3)) > 3843 {
return "0.000000"
}
return "0.000000" + encode(dataBytes)
}
/// Decodes binary data from a string with a long number.
///
/// - Parameter string: String with a long number.
/// - Returns: Byte array with binary data.
static func decode(_ string: String) -> [UInt8] {
let bytes: [UInt8] = Array(string.utf8)
var data = [UInt8](repeating: 0, count: bytes.count * 4 / 8)
var bitIndex = 0
for i in 0..<bytes.count {
let code = UInt8(bytes[i])
if code < 48 || code > 57 {
return data
}
let number = code - 48
let len = number >= 8 ? 4 : 3
for y in stride(from: len - 1, through: 0, by: -1) {
let byteIndex: Int = bitIndex / 8
let byteBitIndex: Int = bitIndex % 8
data[byteIndex] |= (number >> y & 1) << (7 - byteBitIndex)
bitIndex += 1
}
}
let resultSize = Int((Double(bitIndex) / 8).rounded(.down)) - 1
return Array(data[0...resultSize])
}
/// Parses a string of data encoded as a long float number.
///
/// - Parameter string: String with a long float number.
/// - Returns: Data as String?.
static func decodeFromRevenue(_ string: String) -> String? {
let range = string.range(of: ".")
if range == nil {
return nil
}
let dataEncoded = String(string[string.index(range!.lowerBound, offsetBy: 7)...])
let dataDecoded = decode(dataEncoded)
return String(bytes: dataDecoded, encoding: .utf8)
}
}
class NumSeqEncoder {
/**
* Encodes binary string into one large numeric string.
* @param {string} data String with binary data.
* @returns {string} String with a long number.
*/
static encode(data: string): string {
const bytes = (new TextEncoder().encode(data));
const size = bytes.length;
let string = "";
let buffer = 0;
let bufferSize = 0;
let pos = 0;
while (pos < size) {
let pickBytes = Math.min(size - pos, bufferSize != 0 ? 3 : 4);
buffer = buffer << (pickBytes * 8);
for (let i = 0; i < pickBytes; ++i) {
buffer |= (bytes[pos + i] & ((1 << 8) - 1)) << ((pickBytes - i - 1) * 8);
}
bufferSize += pickBytes * 8;
pos += pickBytes;
while (bufferSize >= 3) {
if (bufferSize >= 4) {
const quad = buffer >> (bufferSize - 4) & ((1 << 4) - 1);
if (quad === 0b1000 || quad === 0b1001) {
string = string.concat(quad.toString());
bufferSize -= 4;
buffer &= (1 << bufferSize) - 1;
continue;
}
}
const triple = buffer >> (bufferSize - 3) & ((1 << 3) - 1);
string = string.concat(triple.toString());
bufferSize -= 3;
buffer &= (1 << bufferSize) - 1;
}
}
if (bufferSize > 0) {
string = string.concat((buffer << (3 - bufferSize)).toString());
}
return string;
}
/**
* Encodes order data into a string as a long float number.
* @param {string} data String to encode. Allowed up to 1441 bytes (not characters!).
* @returns {string} String with a long float number; up to 3851 characters/bytes.
*/
static encodeToRevenue(data: string): string {
const size = new TextEncoder().encode(data).length;
// 3852 is max known length for Appsflyer's revenue field (it's probably greater though).
if (Math.ceil(size * 8 / 3) > 3843) {
return "0.000000";
}
return "0.000000" + NumSeqEncoder.encode(data);
}
/**
* Decodes binary data from a string with a long number.
* @param {string} string String with a long number.
* @returns {string} Decoded binary string.
*/
static decode(string: string): string {
const bytes = (new TextEncoder().encode(string));
const size = bytes.length;
let data = new Uint8Array(size * 4 / 8);
let bitIndex = 0;
for (let i = 0; i < size; ++i) {
if (bytes[i] < 48 || bytes[i] > 57) {
break;
}
const number = bytes[i] - 48;
let len = number >= 8 ? 4 : 3;
for (let y = len - 1; y >= 0; --y) {
const byteIndex = ~~(bitIndex / 8);
const byteBitIndex = bitIndex % 8;
data[byteIndex] = (data[byteIndex] || 0) | ((number >> y & 1) << (7 - byteBitIndex));
++bitIndex;
}
}
return new TextDecoder().decode(data.slice(0, Math.floor(bitIndex / 8)));
}
/**
* Parses a string of data encoded as a long float number.
* @param {string} string String with a long float number.
* @returns {string|null} Decoded binary string. Null if string is invalid.
*/
static decodeFromRevenue(string: string): string | null {
const pos = string.indexOf('.');
if (pos === -1) {
return null;
}
return NumSeqEncoder.decode(string.substring(pos + 7));
}
}
"use strict";
class NumSeqEncoder {
/**
* Encodes binary string into one large numeric string.
* @param {string} data String with binary data.
* @returns {string} String with a long number.
*/
static encode(data) {
const bytes = (new TextEncoder().encode(data));
const size = bytes.length;
let string = "";
let buffer = 0;
let bufferSize = 0;
let pos = 0;
while (pos < size) {
let pickBytes = Math.min(size - pos, bufferSize != 0 ? 3 : 4);
buffer = buffer << (pickBytes * 8);
for (let i = 0; i < pickBytes; ++i) {
buffer |= (bytes[pos + i] & ((1 << 8) - 1)) << ((pickBytes - i - 1) * 8);
}
bufferSize += pickBytes * 8;
pos += pickBytes;
while (bufferSize >= 3) {
if (bufferSize >= 4) {
const quad = buffer >> (bufferSize - 4) & ((1 << 4) - 1);
if (quad === 0b1000 || quad === 0b1001) {
string = string.concat(quad.toString());
bufferSize -= 4;
buffer &= (1 << bufferSize) - 1;
continue;
}
}
const triple = buffer >> (bufferSize - 3) & ((1 << 3) - 1);
string = string.concat(triple.toString());
bufferSize -= 3;
buffer &= (1 << bufferSize) - 1;
}
}
if (bufferSize > 0) {
string = string.concat((buffer << (3 - bufferSize)).toString());
}
return string;
}
/**
* Encodes order data into a string as a long float number.
* @param {string} data String to encode. Allowed up to 1441 bytes (not characters!).
* @returns {string} String with a long float number; up to 3851 characters/bytes.
*/
static encodeToRevenue(data) {
const size = new TextEncoder().encode(data).length;
// 3852 is max known length for Appsflyer's revenue field (it's probably greater though).
if (Math.ceil(size * 8 / 3) > 3843) {
return "0.000000";
}
return "0.000000" + NumSeqEncoder.encode(data);
}
/**
* Decodes binary data from a string with a long number.
* @param {string} string String with a long number.
* @returns {string} Decoded binary string.
*/
static decode(string) {
const bytes = (new TextEncoder().encode(string));
const size = bytes.length;
let data = new Uint8Array(size * 4 / 8);
let bitIndex = 0;
for (let i = 0; i < size; ++i) {
if (bytes[i] < 48 || bytes[i] > 57) {
break;
}
const number = bytes[i] - 48;
let len = number >= 8 ? 4 : 3;
for (let y = len - 1; y >= 0; --y) {
const byteIndex = ~~(bitIndex / 8);
const byteBitIndex = bitIndex % 8;
data[byteIndex] = (data[byteIndex] || 0) | ((number >> y & 1) << (7 - byteBitIndex));
++bitIndex;
}
}
return new TextDecoder().decode(data.slice(0, Math.floor(bitIndex / 8)));
}
/**
* Parses a string of data encoded as a long float number.
* @param {string} string String with a long float number.
* @returns {string|null} Decoded binary string. Null if string is invalid.
*/
static decodeFromRevenue(string) {
const pos = string.indexOf('.');
if (pos === -1) {
return null;
}
return NumSeqEncoder.decode(string.substring(pos + 7));
}
}
Необходимо отправлять два события: «purchase» и «advcake_purchase». Оба должны содержать данные о заказе, но второй должен содержать закодированное «revenue» вместо настоящей суммы дохода/цены.
Включите в своё приложение класс NumSeqEncoder
. При создании пользователем заказа сформируйте массив с данными о заказе и дополнительно добавьте в него поле «appsflyerId» со значением функции AppsFlyerLib.shared().getAppsFlyerUID()
(appsFlyer.getAppsFlyerUID()
). Затем сформируйте такой же массив (либо переиспользуйте старый) и замените в нём поле AFEventParamRevenue
(af_revenue
) на результат функции NumSeqEncoder.encodeToRevenue(json)
(строка), передав ему в качестве аргумента этот же массив с данными о заказе, закодированный в JSON-строку.
Смотрите пример реализации ниже.
Примечание
Обратите внимание, что размер JSON-строки с данными не должен превышать 1441 байт (не символов), что равняется <=3851 байт в закодированном виде. Это ограничение AppsFlyer, выявленное путём проведения крайне ограниченного числа тестов. Вполне вероятно, что допустимы строки большего размера. В ближайшее время информация будет уточнена, а инструкция и код обновлены.
Если переданная в функцию encodeToRevenue()
строка будет превышать описанное ограничение, данные закодированы не будут (функция вернёт «0.000000»).
Поэтому перед добавлением в JSON очередного поля убедитесь, что суммарная максимальная длина значений всех полей, а также длина ключей и токенов не превышает описанного ограничения.
Пример реализации:
struct OrderInfo: Codable {
let appsflyerId: String
let orderId: String
let revenue: Double
let category: String
let clientType: String
let coupon: String?
var dictionary: [String: Any?] {
return [
"appsflyerId": appsflyerId,
"orderId": orderId,
"revenue": revenue,
"category": category,
"clientType": clientType,
"coupon": coupon,
]
}
}
//...
let orderInfo = OrderInfo(
appsflyerId: AppsFlyerLib.shared().getAppsFlyerUID(),
orderId: orderId,
revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon
)
var afEventValues = orderInfo.dictionary
afEventValues[AFEventParamRevenue] = afEventValues["revenue"]
AppsFlyerLib.shared().logEvent("purchase", withValues: afEventValues)
let orderInfoJsonData = try JSONEncoder().encode(orderInfo)
let orderInfoJsonStr = String(data: orderInfoJsonData, encoding: .utf8) ?? ""
afEventValues[AFEventParamRevenue] = NumSeqEncoder.encodeToRevenue(orderInfoJsonStr)
AppsFlyerLib.shared().logEvent("advcake_purchase", withValues: afEventValues)
const eventValues = {
appsflyerId: appsFlyer.getAppsFlyerUID(),
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
};
const eventValuesAdvcake = {...eventValues};
eventValuesAdvcake.af_revenue = NumSeqEncoder.encodeToRevenue(JSON.stringify(eventValues));
appsFlyer.logEvent("purchase", eventValues);
appsFlyer.logEvent("advcake_purchase", eventValuesAdvcake);
const eventValues = {
appsflyerId: appsFlyer.getAppsFlyerUID(),
orderId: orderId,
af_revenue: revenue,
category: category,
clientType: clientType,
coupon: coupon,
};
const eventValuesAdvcake = {...eventValues};
eventValuesAdvcake.af_revenue = NumSeqEncoder.encodeToRevenue(JSON.stringify(eventValues));
appsFlyer.logEvent("purchase", eventValues);
appsFlyer.logEvent("advcake_purchase", eventValuesAdvcake);
Отправка дополнительного события установки
Дополнительное событие установки необходимо отправлять при первом запуске приложения, а также если пользователь попал в приложение через Universal Links.
Это событие никак не повлияет на статистику в AppsFlyer.
Swift
Включите в своё приложение класс
NumSeqEncoder
, как это описано в предыдущем разделе;Реализуйте в своём приложении выполнение следующего кода при каждом запуске приложения на устройстве пользователя:
Имплементируйте метод
UlApplicationDelegate: application:continue: restorationHandler:
. Этот метод будет вызываться каждый раз при открытии приложения через deeplink. Проверьте, что у вас имеется параметрpid
=advcake_int
и хост совпадает с хостом вашей ссылки:func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { /// Если приложение открыто через диплинк и содержит параметр pid=advcake_int let hosts = [ "yourapp.onelink.me" ] if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL, let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true), let params = components.queryItems, let host = components.host, hosts.contains(host), params.first(where: { $0.name == "pid" })?.value == "advcake_int" { UserDefaults.standard.set(true, forKey: "advcakeDeeplink") } return true }
Примечание
Вместо «yourapp» в yourapp.onelink.com укажите поддомен вашего OneLink-шаблона. Если у вас его ещё нет, мы создадим его в 5 шаге – Настройка панели администратора Appsflyer.
Добавьте следующий код в функцию
applicationDidBecomeActive()
:/// Если приложение не было установлено ранее, либо произошел переход через Universal Links - отправляем событие advcake_install if !UserDefaults.standard.bool(forKey: "launchedBefore") || UserDefaults.standard.bool(forKey: "advcakeDeeplink") { let values = [ AFEventParamRevenue: NumSeqEncoder().enrichRevenue(0, AppsFlyerLib.shared().getAppsFlyerUID()) ] AppsFlyerLib.shared().logEvent("advcake_install", withValues: values as [AnyHashable: Any]) UserDefaults.standard.set(false, forKey: "advcakeDeeplink") } /// После необходимых действий, устанавливаем ключ true для lanuchedBefore UserDefaults.standard.set(true, forKey: "launchedBefore")
React Native
Включите в своё приложение класс
NumSeqEncoder
, как это описано в предыдущем разделе;Реализуйте в своём приложении выполнение следующего кода при первом запуске на устройстве пользователя или открытии через deeplink:
appsFlyer.initSdk({
devKey: 'K2***********99',
isDebug: false,
appId: '41*****44',
onInstallConversionDataListener: true, //Optional
onDeepLinkListener: true, //Optional
timeToWaitForATTUserAuthorization: 10 //for iOS 14.5
});
appsFlyer.logEvent("advcake_install", {
af_revenue: NumSeqEncoder.encodeToRevenue(appsFlyer.getAppsFlyerUID())
});
appsFlyer.initSdk({
devKey: 'K2***********99',
isDebug: false,
appId: '41*****44',
onInstallConversionDataListener: true, //Optional
onDeepLinkListener: true, //Optional
timeToWaitForATTUserAuthorization: 10 //for iOS 14.5
});
appsFlyer.logEvent("advcake_install", {
af_revenue: NumSeqEncoder.encodeToRevenue(appsFlyer.getAppsFlyerUID())
});
Шаг 5. Настройка панели администратора AppsFlyer
Включение ретаргетинга в настройках приложения
Выберите ваше приложение и перейдите в раздел Configuration → App Settings;
Выставите окно реатрибуции (рекомендуемое значение – 3 месяца);
Установите галочку на «Re-engagement attribution»;
Выставьте время между разными привязками (рекомендуемое значение – none).

Ретаргетинг включен! Перейдем к настройке OneLink-шаблона.
Настройка OneLink-шаблона
Настройка OneLink-шаблона нужна для того, чтобы пользователь при переходе по ссылке попадал сразу в приложение. При этом, будет работать ретаргетинг.
Примечание
Если у пользователя установлено мобильное приложение — мы будем отправлять его в приложение, после чего наша система будет атрибуцировать все его покупки,
Если у пользователя не установлено мобильное приложение — он будет как прежде совершать заказы на сайте по CPA-модели.
В панели администратора AppsFlyer перейдите в раздел Engage → Experiences & Deep Linking;
Нажмите кнопку ⁝ и выберите пункт New OneLink template (или Edit OneLink template если шаблон уже имеется):

Задайте произвольный поддомен:

Примечание
Позже этот домен будет использоваться для создания самих ссылок.
Выберите поведение ссылки:
В разделе «When app isn’t installed» (если приложение не установлено) в соответствующих полях укажите:
iOS: Redirect users to your domain: https://your-domain.ru/;
Android: Redirect users to your domain: https://your-domain.ru/;
Если у вас нет веб-версии, а только приложение, то вместо перенаправления на веб, используйте перенаправление в магазин:
iOS: Redirect users to AppStore;
Android: Redirect users to Google Play.
В разделе «When app is installed» (если приложение установлено) в соответствующих полях укажите:
iOS: Launching the app using Universal links;
Android: Launching the app using Android App links;
Android and iOS fallback: No fallback is set (стоит по умолчанию).
А также «Apple Team ID» (для iOS) и SHA256 (для Android) по кнопке ✎ (о том, что это такое и где найти, смотрите в подсказке ниже).
В разделе «When link is clicked on desktop» (если на ссылку кликнули на десктопе) укажите:
Desktop: Redirect users to your domain: https://your-domain.ru/.
Примечание
Вместо https://your-domain.ru/ укажите ссылку на свой основной лендинг.

Подсказка
Где найти «Apple Team ID» для iOS и SHA256 для Android?
Apple Team ID: Перейдите на https://developer.apple.com/account/resources/certificates/list и авторизируйтесь под учетной записью разработчика – в верхней части страницы (там, где ваши Ф.И.) «Имя компании» и будет означать «Apple Team ID».
SHA256: Перейдите на https://play.google.com/console/, затем, в меню слева, выберите «Настройка» → «Целостность приложения» → раздел «Сертификат ключа для подписи приложения». SHA256 будет указан в поле «Цифровой отпечаток сертификата SHA-256».
Внесите правки в приложение:
Для Android
После заполнения SHA256 для Android вы получите возможность скопировать код, который необходимо добавить в AndroidManifest.xml
:

Скопируйте код нажатием на кнопку и добавьте его в файл AndroidManifest.xml
в блок, описывающий main activity
.
Скопированный код можно расположить перед закрывающим тегом </activity>
.
Ваш AndroidManifest.xml
будет выглядеть так:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.advcake.getluckyapp">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GetLuckyApp">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GetLuckyApp.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Начало скопированного кода -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="subdomain.onelink.me"
android:pathPrefix="/ZBZ8" />
</intent-filter>
<!-- Конец скопированного кода -->
</activity>
</application>
</manifest>
Для iOS
Перейдите в Xcode;
Нажмите на свой проект;
Нажмите на таргет проекта;
Перейдите на вкладку Capabilities;
Активируйте переключатель Associated domains;
Добавьте в список доменов ту ссылку, которую вы получили после добавления поддомена. К ней нужно добавить префикс
applinks
, после чего ссылка будет выглядеть так –applinks:subdomain.onelink.me
:

Вернитесь в файл
AppDelegate.swift
к методуUIApplicationDelegate:application:continue:restorationHandler:
и замените поддомен yourapp в «yourapp.onelink.me» на ваш новый поддомен.
Примечание
Пожалуйста, сообщите нам название созданного вами OneLink-шаблона – так нам будет проще понять какой тестировать.
Шаблон и поведение ссылок настроены, теперь добавим Adv.Cake как партнера.
Добавление Adv.Cake
В панели администратора AppsFlyer перейдите в раздел Collaborate → Active integrations:

Впишите название «Adv.Cake» в строку поиска, после чего вы увидите двух партнеров – рекламную сеть и агентство, для которых необходимо предоставить доступы:

Чтобы предоставить доступ для рекламной сети Adv.Cake:
3.1 Выберите партнера с пометкой «Ad network»,
3.2 Нажмите Set up integration и на открывшейся вкладке Integration установите галочки «Activate partner» и «In-app event postbacks»:
3.3 Добавьте события, которые отправляются из нашей сети (которые мы настраивали ранее): для этого, нажмите Add event и добавьте нужное событие: в «Partner event identifier» можно указать название события (например,
start_button_tapped
), в «Send revenue» – values & revenue.Предупреждение
Обратите внимание, что в постбеках для iOS не будут приходить «values» – для этого, мы настраивали «revenue».
3.4 Перейдите на вкладку Attribution link, выберите шаблон в поле «Select OneLink template» (настраивали pанее) и активируйте галочку «Retargeting Settings» в нижней части страницы:
3.5 Перейдите на вкладку Permissions и установите галочки на указанных пунктах – это позволит самостоятельно настраивать ссылки. После чего, в нижней части страницы, нажмите Save Permissions:
Чтобы предоставить доступ для агентства Adv.Cake:
4.1 Выберите партнера с пометкой «Agency»,
4.2 Нажмите Set up integration и на открывшейся вкладке Permissions и установите галочки на указанных пунктах:
4.3 В нижней части страницы нажмите Save Permissions.
Шаг 6. Настройка брендированных доменов
Брендированные домены позволяют указывать в ссылках атрибуции название бренда и домен.

Многие блокировщики рекламы и VPN-сервисы блокируют доступ к некоторым доменам, среди которых – onelink.me.
Пользователь не может перейти по ссылке и попросту теряется, однако эту проблему можно решить с помощью брендированных доменов.
Брендированный домен связывается с доменом AppsFlyer с помощью стандартных параметров DNS.
Вам также потребуется внести небольшие правки в приложение и привязать домен в панели AppsFlyer.
Чтобы подключить брендированный домен, необходимо выполнить следующие шаги:
Настройка DNS

Выберите полное доменное имя, например,
click.abcdef.com
, гдеabcdef.com
— название вашего бренда. Поддомен (например, click) можно также настроить;Попросите администратора DNS создать полный домен (хост);
Попросите администратора DNS настроить запись CNAME, чтобы полный домен (домен бренда) указывал на заданный URL-адрес (хост AppsFlyer), как это показано на схеме выше – брендированная ссылка указывает на серверы AppsFlyer.
Привязка домена к AppsFlyer
В панели администратора AppsFlyer перейдите в раздел Engage → Experiences & Deep Linking → Branded Domains;
Нажмите Get started;
В поле «Brand Domain» введите полный домен, как в записи DNS (например,
click.abcdef.com
);Выберите поддомен OneLink из открывшегося списка:

Нажмите Verify.
Предупреждение
Если диплинкинг выполняется с использованием брендированных доменов, SDK AppsFlyer не может получить данные о конверсиях по установкам и диплинкингу.
Настройка мобильного приложения
Необходимо использовать методы setOneLinkCustomDomain
и oneLinkCustomDomains
для Android и iOS, соответственно.
Так производится отправка запроса по брендированной ссылке, возвращается ссылка OneLink, которой она сопоставлена, после чего направляется запрос OneLink для получения данных о конверсиях.
Для Android
public class AFApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
AppsFlyerConversionListener conversionListener = new AppsFlyerConversionListener() {
}
AppsFlyerLib.getInstance().setOneLinkCustomDomain("promotion.greatapp.com");
AppsFlyerLib.getInstance().init(AF_DEV_KEY, conversionListener, this);
AppsFlyerLib.getInstance().start(this, AF_DEV_KEY);
}
}
Если у вас несколько брендированных доменов, передайте их все в API, чтобы всегда получать данные о конверсиях и иметь возможность использовать диплинки:
AppsFlyerLib.getInstance().setOneLinkCustomDomain(
"promotion.greatapp.com",
"click.greatapp.com",
"deals.greatapp.com");
Для iOS
Настройте брендированную ссылку, чтобы она получала данные о конверсии, используя приведенный ниже метод:
func application(_ application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
AppsFlyerLib.shared().appsFlyerDevKey = "6CQi4Be6Zs9oNLsCusPbUL"
AppsFlyerLib.shared().appleAppID = "340954504"
AppsFlyerLib.shared().oneLinkCustomDomains = ["promotion.greatapp.com"]
//...
//...
}
Подсказка
Если у вас несколько брендированных доменов, их можно передать в API как массив строк. Так, независимо от того, какая именно брендированная ссылка используется, вы всегда будете получать данные о конверсиях и сможете использовать диплинки:
AppsFlyerLib.shared().oneLinkCustomDomains = ["promotion.greatapp.com", "click.greatapp.com"]
Теперь, ссылку с брендированным доменом необходимо «зашить» в приложение.
Для Android
Откройте файл
AndroidManifest.xml
;Найдите блок
intent-filter
, содержащий в себе<action android:name=android.intent.action.VIEW />
(если такого нет, вы можете скопировать его из примера ниже и отредактировать под свои ссылки);Добавьте в него объект
<data />
, описывающий ваш брендированный домен. Например, он может быть таким:<data android:scheme=https android:host=click.yourdomain.me />
;
Пример файла AndroidManifest.xml
после правок:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.advcake.getluckyapp">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GetLuckyApp">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.GetLuckyApp.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="subdomain.onelink.me"
android:pathPrefix="/ZBZ8" />
<!-- Начало добавленного кода -->
<data android:scheme="https"
android:host="click.your-domain.me" />
<!-- Конец добавленного кода -->
</intent-filter>
</activity>
</application>
</manifest>
Для iOS
Перейдите в Xcode;
Нажмите на свой проект;
Нажмите на таргет проекта (показано на скриншоте снизу);
Перейдите на вкладку Capabilities;
Активируйте переключатель Associated domains;
Добавьте в список доменов новую ссылку с брендированным доменом. К ней нужно добавить префикс
applinks
, после чего ссылка будет выглядеть так –applinks:click.your-domain.me
:

Вернитесь в файл
AppDelegate.swift
к методуUIApplicationDelegate:application:continue:restorationHandler:
и добавьте ваш брендированный домен в массивhosts
:
/// Если приложение открыто через диплинк и содержит параметр pid=advcake_int
let hosts = [
"yourapp.onelink.me",
"app.yourbrand.com", // Ваш брендированный домен
]
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL,
let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let params = components.queryItems,
let host = components.host,
hosts.contains(host),
params.first(where: { $0.name == "pid" })?.value == "advcake_int"
{
UserDefaults.standard.set(true, forKey: "advcakeDeeplink")
}
Шаг 7. Проведение тестов
После настройки поведения ссылок, добавления Adv.Cake как рекламной сети и настройки брендированных доменов необходимо создать тестовую ссылку в панели администратора AppsFlyer.
В панели администратора перейдите в раздел Collaborate → Active integrations → выберите партнера Adv.Cake → Attribution link → Use OneLink;
Выберите созданный pанее шаблон и внизу страницы скопируйте ссылку из поля «Click attribution link».
Ссылка будет выглядеть так:
https://your-domain.onelink.me/BQt3?pid=advcake_int&c=affilate&af_siteid=testweb&is_retargeting=true&af_reengagement_window=30d&af_click_lookback=7d
Предупреждение
На данном этапе необходимо подменить домен на брендированный! Таким образом, ссылка, из примера выше, изменится на:
https://click.your-domain.me/BQt3?pid=advcake_int&c=affilate&af_siteid=testweb&is_retargeting=true&af_reengagement_window=30d&af_click_lookback=7d
Лишние GET-параметры можно удалить, так как для теста они не понадобятся.
Подсказка
Подробнее о настройке брендированных доменов читайте в этом разделе.
Перейдите по ссылке с ПК – должна открыться веб-версия вашего приложения. Если этого не произошло – проверьте настройки OneLink-шаблона.
Отправьте ссылку на мобильные устройства для тестирования;
Удалите приложения с мобильных устройств и перейдите по ссылке. Если после перехода открылась веб-версия вашего приложения – это значит, что всё настроено правильно. В противном случае, перепроверьте настройки OneLink-шаблона;
Установите приложения на мобильные устройства и перейдите по ссылке. Ваши приложения должны перехватывать ссылки и открывать их изнутри. Если этого не происходит – проверьте, все ли правки из предыдущего раздела вы внесли в приложение;
Перейдите по новой ссылке и совершите тестовую покупку в приложении. Должен выполниться код, который вы настраивали ранее.
Чтобы посмотреть события – перейдите в раздел Analyze → Events (обычно события подгружаются в течение часа):
Если события не появились – возможно, вы неправильно настроили отправление событий из приложения. Обратите внимание на источник, за которым закрепилось событие.
Если событие пришло как organic
это значит, что вы не настроили ретаргетинг.
Если источник – advcake_int
это значит, что ретаргетинг настроен правильно.
