Инструкция по настройке AppsFlyer (постбеки)

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

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

  1. Если у пользователя установлено мобильное приложение — мы будем отправлять его в приложение, после чего наша система будет атрибуцировать все его покупки.

  2. Если у пользователя не установлено мобильное приложение — он будет как прежде совершать заказы на сайте по CPA-модели.

Важно понимать, что рекламодатель платит не за установки, а за конкретные покупки.

Мы приводим и атрибуцируем каждую покупку за нужным партнером и вебмастером.

Содержание:

  1. Добавление приложения;

  2. Установка SDK AppsFlyer;

  3. Интеграция SDK AppsFlyer;

  4. Настройка отправляемых из приложения событий;

  5. Настройка панели администратора AppsFlyer;

  6. Настройка брендированных доменов;

  7. Проведение тестов.

Шаг 1. Добавление приложения (общее)

Примечание

Если у вас еще нет аккаунта AppsFlyer, зарегистрируйте его здесь.

После регистрации аккаунта следует добавить ваше приложение в AppsFlyer:

  1. Перейдите на страницу приложений;

  2. Нажмите + Add app и введите данные приложения;

  3. В пункте Enter app details выберите платформу (Android/iOS) и статус приложения (опубликовано / в разработке);

    • Если приложение уже опубликовано, вставьте ссылку из маркетплейса на приложение в поле App Store URL для iOS или Google Play URL для Android. Если приложение еще не опубликовано или находится на рассмотрении – введите Apple App ID для iOS или Android package name для Android;

  4. В пункте Additional Information введите дополнительные данные – валюту, часовой пояс и укажите, ориентировано ли приложение на детей;

  5. Нажмите Add my app.

-

Подсказка

Где взять Apple App ID и Android package name?


  • Apple App ID: В панели AppStore Connect перейдите в раздел Мои приложения → выберите ваше приложение → Информация о приложении → поле Apple 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

  1. Установите Сocoapods;

  2. Добавьте зависимости – необходимо добавить последнюю версию AppsFlyerFramework в Podfile проекта с помощью команды:

pod 'AppsFlyerFramework'
  1. Установите зависимости с помощью команды:

pod 'install'
  1. После установки необходимо использовать .xcworkspace файл для открытия проекта, вместо .xcodeproj.

Через SwiftPackageManager (рекомендуется)

  1. В Xcode перейдите по пути: FileSwift PackagesAdd Package Dependency:

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

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

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

-

Для Android

Установить SDK AppsFlyer для Android можно также несколькими способами:

Установка с помощью Gradle

  1. Добавьте репозиторий mavenCentral() в файл build.gradle:

-
  1. Добавьте зависимости в файл build.gradle – здесь можно сразу добавить зависимость installreferer, поскольку она понадобится нам в дальнейшем:

implementation 'com.android.installreferrer:installreferrer:2.2'
implementation 'com.appsflyer:af-android-sdk:6.3.2'
-

Установка без Gradle

  1. Переведите структуру проекта из Android в Project:

-
  1. Скачайте SDK;

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

-
  1. Правой кнопкой мыши кликните по jar-файлу и нажмите + Add as Library, после чего кликните повторно и нажмите Refactor;

  2. После установки необходимо добавить разрешения в файл 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 перейдите в раздел ConfigurationApp Settings → поле «Dev key») и Apple App ID (в панели AppStore Connect перейдите в раздел Мои приложения → выберите ваше приложение → Информация о приложении → поле «Apple ID»).

Swift

  1. Импортируйте AppsFlyerLib:

import AppsFlyerLib
  1. Инициализируйте SDK AppsFlyer:

AppsFlyerLib.shared().appsFlyerDevKey="your-key"
AppsFlyerLib.shared().appleAppID="your-id"
  1. Вызовите AppsFlyerLib.shared().start;

Пример:

-

После этого, добавляем поддержку AppTrackingTransparency.

  1. Перед вызовом start(), добавьте waitForATTUserAuthorization(timeoutInterval: 60) в didFinishLaunchingWithOptions:

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

-

Kotlin

  1. Импортируйте AppsFlyerLib:

import com.appsflyer.AppsFlyerLib
  1. Инициализируйте SDK AppsFlyer:

AppsFlyerLib.getInstance().init("your-key", null, this)
  1. Запустите Android SDK:

AppsFlyerLib.getInstance().start(this)

Пример:

-

React Native

  1. Импортируйте appsFlyer из react-native-appsflyer:

  • TypeScript
  • JavaScript
import appsFlyer from 'react-native-appsflyer';
import appsFlyer from 'react-native-appsflyer';
  1. Инициализируйте SDK AppsFlyer:

  • TypeScript
  • JavaScript
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, который будет содержать следующие данные о событии:

  1. Сумма заказа/доход. В качестве ключа используется константа AFInAppEventParameterName.REVENUE (Kotlin) или AFEventParamRevenue (Swift), либо строка af_revenue (React Native). Опционально для Android.

  2. ID заказа или события в системе рекламодателя. Ключ «order_id» или любой другой (по согласованию). Обязательно для передачи!

  3. Другие данные, которые вы готовы нам предоставить для атрибутирования заказов, например, категория заказа («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

Пример реализации:

  • TypeScript
  • JavaScript
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 значительно сложнее, поскольку есть две проблемы:

  1. Постбеки событий, отправленных с iOS (независимо от использованной реализации AppsFlyer SDK), приходят с крайне ограниченным количеством данных, а именно: из нетрекинговых данных (данные, которые отправляются через logEvent() мы получаем только «af_revenue». Остальные нестандартные поля нам недоступны (в том числе «orderId»)).

  2. Постбек события установки приложения (первого запуска приложения) не содержит AppsFlyerID – случайный ID, генерируемый SDK при первом запуске приложения. Из-за этого мы не сможем связывать покупки (или другие действия) с событием установки (нам важны трекинговые метки события установки).

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


Отправка события покупки

Как мы уже писали, нам доступно исключительно поле «af_revenue» – строковое поле, в котором AppsFlyer ожидает числовое значение.

Поле передаётся в постбеке без изменений, а при обработке числа на своей стороне AppsFlyer обрезает его до 5 знаков после запятой. Благодаря этому мы можем использовать это поле для хранения данных, если предварительно закодировать их в числовую последовательность.

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

  • Swift
  • TypeScript
  • JavaScript
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 очередного поля убедитесь, что суммарная максимальная длина значений всех полей, а также длина ключей и токенов не превышает описанного ограничения.

Пример реализации:

  • Swift
  • TypeScript
  • JavaScript
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

  1. Включите в своё приложение класс NumSeqEncoder, как это описано в предыдущем разделе;

  2. Реализуйте в своём приложении выполнение следующего кода при каждом запуске приложения на устройстве пользователя:

    • Имплементируйте метод 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

  1. Включите в своё приложение класс NumSeqEncoder, как это описано в предыдущем разделе;

  2. Реализуйте в своём приложении выполнение следующего кода при первом запуске на устройстве пользователя или открытии через deeplink:

  • JavaScript
  • TypeScript
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

Включение ретаргетинга в настройках приложения

  1. Выберите ваше приложение и перейдите в раздел ConfigurationApp Settings;

  2. Выставите окно реатрибуции (рекомендуемое значение – 3 месяца);

  3. Установите галочку на «Re-engagement attribution»;

  4. Выставьте время между разными привязками (рекомендуемое значение – none).

-

Ретаргетинг включен! Перейдем к настройке OneLink-шаблона.

Добавление Adv.Cake

  1. В панели администратора AppsFlyer перейдите в раздел CollaborateActive integrations:

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

-
  1. Чтобы предоставить доступ для рекламной сети 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:

    -
  2. Чтобы предоставить доступ для агентства Adv.Cake:

    4.1 Выберите партнера с пометкой «Agency»,

    4.2 Нажмите Set up integration и на открывшейся вкладке Permissions и установите галочки на указанных пунктах:

    -

    4.3 В нижней части страницы нажмите Save Permissions.

Шаг 6. Настройка брендированных доменов

Брендированные домены позволяют указывать в ссылках атрибуции название бренда и домен.

-

Многие блокировщики рекламы и VPN-сервисы блокируют доступ к некоторым доменам, среди которых – onelink.me.

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

Брендированный домен связывается с доменом AppsFlyer с помощью стандартных параметров DNS.

Вам также потребуется внести небольшие правки в приложение и привязать домен в панели AppsFlyer.

Чтобы подключить брендированный домен, необходимо выполнить следующие шаги:

Настройка DNS

-
  1. Выберите полное доменное имя, например, click.abcdef.com, где abcdef.com — название вашего бренда. Поддомен (например, click) можно также настроить;

  2. Попросите администратора DNS создать полный домен (хост);

  3. Попросите администратора DNS настроить запись CNAME, чтобы полный домен (домен бренда) указывал на заданный URL-адрес (хост AppsFlyer), как это показано на схеме выше – брендированная ссылка указывает на серверы AppsFlyer.

Привязка домена к AppsFlyer

  1. В панели администратора AppsFlyer перейдите в раздел Engage → Experiences & Deep Linking → Branded Domains;

  2. Нажмите Get started;

  3. В поле «Brand Domain» введите полный домен, как в записи DNS (например, click.abcdef.com);

  4. Выберите поддомен OneLink из открывшегося списка:

-
  1. Нажмите 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"]

Теперь, ссылку с брендированным доменом необходимо «зашить» в приложение.

Шаг 7. Проведение тестов

После настройки поведения ссылок, добавления Adv.Cake как рекламной сети и настройки брендированных доменов необходимо создать тестовую ссылку в панели администратора AppsFlyer.

  1. В панели администратора перейдите в раздел CollaborateActive integrations → выберите партнера Adv.CakeAttribution linkUse OneLink;

  2. Выберите созданный 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-параметры можно удалить, так как для теста они не понадобятся.

Подсказка

Подробнее о настройке брендированных доменов читайте в этом разделе.

  1. Перейдите по ссылке с ПК – должна открыться веб-версия вашего приложения. Если этого не произошло – проверьте настройки OneLink-шаблона.

  2. Отправьте ссылку на мобильные устройства для тестирования;

  3. Удалите приложения с мобильных устройств и перейдите по ссылке. Если после перехода открылась веб-версия вашего приложения – это значит, что всё настроено правильно. В противном случае, перепроверьте настройки OneLink-шаблона;

  4. Установите приложения на мобильные устройства и перейдите по ссылке. Ваши приложения должны перехватывать ссылки и открывать их изнутри. Если этого не происходит – проверьте, все ли правки из предыдущего раздела вы внесли в приложение;

  5. Перейдите по новой ссылке и совершите тестовую покупку в приложении. Должен выполниться код, который вы настраивали ранее.

Чтобы посмотреть события – перейдите в раздел AnalyzeEvents (обычно события подгружаются в течение часа):

Если события не появились – возможно, вы неправильно настроили отправление событий из приложения. Обратите внимание на источник, за которым закрепилось событие.

Если событие пришло как organic это значит, что вы не настроили ретаргетинг.

Если источник – advcake_int это значит, что ретаргетинг настроен правильно.

-