Adv.Cake

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

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

Интеграция позволяет корректно учитывать заказы из мобильного приложения в CPA-канале:

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

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

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

Содержание:

  1. Добавление приложения;
  2. Интеграция SDK AppsFlyer;
  3. Настройка отправляемых из приложения событий;
  4. Настройка панели администратора AppsFlyer;
  5. Настройка брендированных доменов;
  6. Проведение тестов.

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

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

Перейдите на страницу добавления нового приложения и заполните необходимую информацию.

Подробнее: https://support.appsflyer.com/hc/en-us/articles/207377436-Adding-an-app-to-AppsFlyer

Шаг 2. Интеграция SDK AppsFlyer

Интегрируйте SDK AppsFlyer в ваше приложение, чтобы начать получать статистику о приложении и его пользователях.

Подробнее: https://support.appsflyer.com/hc/en-us/sections/6551164458257-Integrate-the-AppsFlyer-SDK

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

Определите имя события для отправки. В нашем примере событием является тап по кнопке «Старт», поэтому мы назвали его "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

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

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, приходят с крайне ограниченным набором данных. Из нетрекинговых данных (отправляемых через logEvent()) нам доступно только "af_revenue". Остальные нестандартные поля, включая "orderId", недоступны.
  2. Постбек события установки приложения (первого запуска) не содержит 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

  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:
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())
});

Шаг 4. Настройка панели администратора AppsFlyer

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

  1. Выберите ваше приложение и перейдите в раздел ConfigurationApp Settings;
  2. Выставите окно реатрибуции (рекомендуемое значение – 3 месяца);
  3. Установите галочку на «Re-engagement attribution»;
  4. Выставьте время между разными привязками (рекомендуемое значение – none).

-


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

OneLink-шаблон нужен для того, чтобы пользователь при переходе по ссылке попадал сразу в приложение и при этом работал ретаргетинг.

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

-


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

-


Этот домен будет использоваться для создания ссылок.

  1. Выберите поведение ссылки:

    В разделе «When app isn't installed» (если приложение не установлено) укажите:

    Если у вас нет веб-версии, только приложение, используйте перенаправление в магазин:

    • 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» (если на ссылку кликнули на десктопе) укажите:

Вместо 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».

  1. Внесите правки в приложение:

Для Android

После заполнения SHA256 для Android вы получите возможность скопировать код, который необходимо добавить в AndroidManifest.xml:

-


Скопируйте код нажатием на кнопку и добавьте его в файл AndroidManifest.xml в блок, описывающий main activity.

Скопированный код можно расположить перед закрывающим тегом &lt;activity&gt;.

Ваш 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

  1. Перейдите в Xcode;
  2. Нажмите на свой проект;
  3. Нажмите на таргет проекта;
  4. Перейдите на вкладку Capabilities;
  5. Активируйте переключатель Associated domains;
  6. Добавьте в список доменов ссылку, полученную после добавления поддомена. К ней нужно добавить префикс applinks, после чего ссылка будет выглядеть так: applinks:subdomain.onelink.me:

-


  1. Вернитесь в файл AppDelegate.swift к методу UIApplicationDelegate:application:continue:restorationHandler: и замените поддомен yourapp в «yourapp.onelink.me» на ваш новый поддомен.

Сообщите нам название созданного OneLink-шаблона — это упростит тестирование с нашей стороны.

Шаблон и поведение ссылок настроены. Теперь добавим Adv.Cake как партнёра.

Добавление 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» (настраивали ранее) и активируйте галочку «Retargeting Settings» в нижней части страницы:

-

3.5 Перейдите на вкладку Permissions, установите галочки на указанных пунктах и нажмите Save Permissions в нижней части страницы:

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

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

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

-

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

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

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

-


Для чего нужны брендированные домены?

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

Брендированный домен связывается с доменом AppsFlyer через стандартные параметры DNS. Потребуется также внести небольшие правки в приложение и привязать домен в панели AppsFlyer.

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

Настройка DNS

-


  1. Выберите полное доменное имя, например click.abcdef.com, где abcdef.com — название вашего бренда. Поддомен (например, click) можно настроить по своему усмотрению;
  2. Попросите администратора DNS создать полный домен (хост);
  3. Попросите администратора DNS настроить запись CNAME, чтобы полный домен (домен бренда) указывал на нужный URL-адрес (хост 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 и запрашивать данные о конверсиях.

Для 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

  1. Откройте файл AndroidManifest.xml;
  2. Найдите блок intent-filter, содержащий <action android:name=android.intent.action.VIEW /> (если такого нет, скопируйте его из примера ниже и отредактируйте под свои ссылки);
  3. Добавьте объект <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

  1. Перейдите в Xcode;
  2. Нажмите на свой проект;
  3. Нажмите на таргет проекта (показано на скриншоте снизу);
  4. Перейдите на вкладку Capabilities;
  5. Активируйте переключатель Associated domains;
  6. Добавьте в список доменов ссылку с брендированным доменом с префиксом applinks, например: applinks:click.your-domain.me:

-


  1. Вернитесь в файл 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")
}

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

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

  1. В панели администратора перейдите в раздел CollaborateActive integrations → выберите партнёра Adv.CakeAttribution linkUse OneLink;
  2. Выберите созданный ранее шаблон и скопируйте ссылку из поля «Click attribution link» в нижней части страницы.

- Ссылка будет выглядеть так:

https://your-domain.onelink.me/BQt3?pid=advcake_int&c=affiliate&af_siteid=testweb&is_retargeting=true&af_reengagement_window=30d&af_click_lookback=7d

На данном этапе замените домен на брендированный. Ссылка из примера выше изменится на:

https://click.your-domain.me/BQt3?pid=advcake_int&c=affiliate&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, ретаргетинг настроен правильно.

-

Вам помогла эта страница?

Последнее изменение: 2026-01-23