Паблишинг приложений и реклама в яндексе

47 minute read

Сейчас будем делать сложновое. Будем связывать AppMetrica, Яндекс Метрику, RuStore, Google Play и прямое скачивание скачивание apk в одну цельную систему. Все это нужно, чтобы рекламировать наше приложение в разных магазинах с помощью одного лендинга и одной кнопки на скачивание.

Если вас заинтересует описанные мною сервисы и вы захотите их попробовать их у себя, то не стесняйтесь, пишите t.me/akovardin

Важно: обратите внимание, что Яндекс Метрика и AppMetrica это разные сервисы Яндекса и, к сожалению, они не очень дружат друг с другом.

Зачем?

Казалось бы, что сложного в рекламе мобильного приложения через Директ? Наверняка, все уже давно сделано за нас? На самом деле, это не так. Например, если вы загрузили приложение в RuStore и хотите завести рекламную кампанию в Директе, то это будет выглядеть вот так:

direct1

Яндекс Директ не собирается рекламировать приложение, он создает рекламную кампанию на сайт rustore.ru. И у вас не получится запустить рекламу с оптимизацией на установку

Надо отметить, что директ умеет работать с альтернативными магазинами приложений, но не с RuStore. Для магазина GetApps от Xiaomi у Директа даже свой лендинг есть

Для сравнения, вот так будет выглядеть рекламная кампания для любого приложения в Google Play

direct2

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

Директ умеет работать с конверсиями в Яндекс Метрике. К сожалению, она никак не связана с AppMetrica, которая используется в приложении. Эту проблему нам нужно будет решить самостоятельно.

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

Минисервисы

Небольшое лирическое отступление. Меня вдохновила статья про “самодельное ПО” - когда вы сами реализуете простые приложения под ваши нужны собирая его из готовы кусочков и/или используя ИИ. А это прям то что нужно для пет проектов. Поэтому, решил по такому принципу “готовить” свои проекты. Назову такой подход минисервисами(не путать микро)

Для рекламы приложений я написал пару минисервисов:

  • land - сервис для создания простых лендингов приложений. Бонусом сразу создаются странички terms и privacy, которые можно указать в настройках магазина приложений
  • tracker - а этот сервис сохраняет событие установки из AppMetrica в базу и потом передает их в Яндекс Метрику

Все минисервисы сделаны с помощью PocketBase. Очень рекомендую этот простой фреймворк с админкой, который работает с SQLite и с кучей готовых плюшек. Есть готовый API под flutter. Если у вас не самое большое приложение, то вам этого хватит с головой.

Принцип работы

Сначала разберемся что куда будет передаваться. Самое главное, что нам нужно сделать - это передать параметр yclid от клика в рекламном материале до трекера в Яндекс Метрике

Хорошая схема лучше абзаца текста. Постараюсь максимально понятно объяснить что мы будем делать

Картинка в большом разрешении

Трекинг ссылка в объявлении

Первое, что вам нужно сделать, после добавления в AppMetrica - создать трекинг ссылку и добавить ее в рекламное объявление

Чтобы директ передавал все нужные параметры на лендинг, настройте “Параметры в URL” для ваших баннеров. Я указывал все возможные параметры

utm_content={banner_id}&utm_medium=cpa&utm_source=yadirect&utm_campaign={campaign_id}&yclid={yclid}

Но для учета установок будет достаточно указать yclid={yclid}. Подробно про проброс параметров из Директа описано в документации

Трекинг ссылка будет вести на наш лендинг, реализованный в минисервисе land.

Пример создания трекинг ссылки для моего приложения:

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

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

Подробнее про настройку трекер ссылок и постбеков

Лендинг с Яндекс Метрикой

Для создания лендинга нам нужен минисервис land. В этом сервисе нет совершенно ничего сложного, он позволяет очень просто завести страничку приложения и подключить к ней трекер яндекс метрики

Сервис постоен на базе PocketBase - это очень простая и легкая админка. Сама логика странички приложения реализована на Go

Для добавления лендинга нужно заполнить несколько полей:

  • appname - название приложения
  • title - заголовок страницы для тега <title>
  • description - описание для тега <description>
  • keywords - ключевые слова для тега <keywords>
  • slug - URL, по которому будет доступен лендинг
  • yandex_counter - счетчик Яндекс Метрики

В поле yandex_counter нужно указать правильный идентификатор Яндекс Метрики, который нужно взять в настройках

И отдельно нужно настроить ссылки на магазины приложений для универсальной кнопки на главной странице самого лендинга

  • title - текст, который будет отображаться на странице лендинга
  • description - текст, который будет отображаться на странице лендинга
  • huawei_link - ссылка на приложение в магазине huawei
  • android_link - ссылка на приложение в Google Play или на скачивание прямой apk
  • rustore_link - ссылка на приложение в RuStore
  • cta - текст на кнопке для скачивания приложения
  • image - картинка, которая будет отображаться на лендинге

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

Как работает универсальная кнопка? Используется несколько важных уловок. Через User Agent определяем на каком девайсе пользователь смотрит лендинг: huawei или обычный android:

1if (/huawei/i.test(userAgent)) {
2    document.getElementById("download-button").href = huaweiLink;
3} else if (/android/i.test(userAgent)) {
4    document.getElementById("download-button").href = androidLink;
5} else {
6    document.getElementById("download-button").href = rustoreLink;
7}

Если не смогли определить тип девайса, то просто отправляем пользователя на установку приложения из RuStore

Второй важный принцип - использование ссылки на интент и параметра S.browser_fallback_url=

1<a href="intent://apps.rustore.ru/app/com.roblox.client#Intent;scheme=rustore;package=ru.vk.store;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.roblox.client%26hl%3Den;end">Скачать приложение</a>

Выглядит сложно, но давайте разберемся. Сначала нужно нырнуть в доку по chrome: developer.chrome.com/docs/android/intents. Тут нас учат как строить ссылки, которые на андроиде будут открывать интент в приложении. Чтобы понять, как формировать ссылку на экран нужного приложения в RuStore - читаем доку www.rustore.ru/help/sdk/rustore-deeplinks/. Этих двух док достаточно, чтобы научиться правильно строить ссылки на свое приложение

Теперь переходим к фоллбеку S.browser_fallback_url. Сюда нужно указать заэнкоженную ссылку на приложение в Google Play или AppGallery, для примера это https://www.rustore.ru/catalog/app/com.roblox.client которая энкодиться с помощью www.urlencoder.org и вставляется как https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.roblox.client%26hl%3Den

Но это еще не все. Бегло погуглив про наших братьев huawei, обнаружил что там это тоже, вероятно, работает - цинк. Должно работать даже в дефолтном браузере хуавея и в любом на базе webkit

1var rustoreLink = "{{.rustoreLink}}?referrerId=" + escape(referrer)
2var bundleName = "{{.bundleName}}" // application package
3
4var intent = "intent://apps.rustore.ru/app/" + bundleName + "?referrerId=" + escape(referrer) + "#Intent;scheme=rustore;package=ru.vk.store;"
5var androidFallback = "S.browser_fallback_url=" + escape("{{.androidLink}}?referrer="+escape(referrer)) + ";end"
6var huaweiFallback = "S.browser_fallback_url=" + escape("{{.huaweiLink}}?referrer="+escape(referrer))  + ";end"
7
8var androidLink = intent + androidFallback
9var huaweiLink = intent + androidFallback

Таким образом, мы отправляем пользователя в RuStore, но если магазин не установлен на девайсе, то будет срабатывать S.browser_fallback_url и, в зависимости от того на каком девайсе пользователь смотрит ленд, он отправится в Google Play или AppGallery.

Вместо ссылки на Google Play можно указать ссылку на скачивание apk и это тоже будет работать. Очень удобно если вашего приложения нет в Google Play

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

 1function setCookie(name, value, days){
 2    var date = new Date();
 3    date.setTime(date.getTime() + (days*24*60*60*1000)); 
 4    var expires = "; expires=" + date.toGMTString();
 5    document.cookie = name + "=" + value + expires + ";path=/";
 6}
 7
 8function getParam(p){
 9    var match = RegExp('[?&]' + p + '=([^&]*)').exec(window.location.search);
10    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
11}
12
13var yclid = getParam('yclid');
14var rbclickid = getParam('rb_clickid');
15
16if(yclid){
17    setCookie('yclid', yclid, 90);
18}
19
20if(rbclickid){
21    setCookie('rbclickid', rbclickid, 90);
22}
23
24function readCookie(name) { 
25    var n = name + "="; 
26    var cookie = document.cookie.split(';'); 
27    for(var i=0;i < cookie.length;i++) {      
28        var c = cookie[i];      
29        while (c.charAt(0)==' '){c = c.substring(1,c.length);}      
30        if (c.indexOf(n) == 0){return c.substring(n.length,c.length);} 
31    } 
32    return ""; 
33}

Если вас смущает параметр rb_clickid, то пока не обращайте на него внимания. Он нужен для учета конверсий от ВК Рекламы, но про это будет в отдельной статье

Весь код нужно поместить в коллбек Яндекс Метрики getClientID в котором у нас есть доступ к clientid. Это не обязательно, конверсии могут учитываться и по clientid, и по yclid. В будущем добавлю запись статы по показу ленда с учетом параметра clientid:

 1ym({{.yandexCounter}}, "getClientID", function(clientid) {
 2    var yclid = readCookie('yclid'); // реклама yandex
 3    var rbclickid = readCookie('rbclickid'); // реклама vk
 4
 5    var clickid = "";
 6    if (yclid != "") {
 7        clickid = yclid;
 8    } else if (rbclickid != "") {
 9        clickid = rbclickid;
10    }
11
12    var userAgent = navigator.userAgent || navigator.vendor || window.opera;
13    var referrer = "client_id=" + clientid + "&yclid=" + yclid + "&rb_clickid=" + rbclickid + "&click_id=" + clickid
14
15    var rustoreLink = "{{.rustoreLink}}?referrerId=" + escape(referrer)
16    var bundleName = "{{.bundleName}}" // application package
17
18    var intent = "intent://apps.rustore.ru/app/" + bundleName + "?referrerId=" + escape(referrer) + "#Intent;scheme=rustore;package=ru.vk.store;"
19    var androidFallback = "S.browser_fallback_url=" + escape("{{.androidLink}}?referrer="+escape(referrer)) + ";end"
20    var huaweiFallback = "S.browser_fallback_url=" + escape("{{.huaweiLink}}?referrer="+escape(referrer))  + ";end"
21
22    var androidLink = intent + androidFallback
23    var huaweiLink = intent + androidFallback
24
25    if (/huawei/i.test(userAgent)) {
26        document.getElementById("download-button").href = huaweiLink;
27    } else if (/android/i.test(userAgent)) {
28        document.getElementById("download-button").href = androidLink;
29    } else {
30        document.getElementById("download-button").href = rustoreLink;
31    }
32});

Пример моего лендинга можно посмотреть по ссылке land.kovardin.ru/l/downloader

Цели

На этом шаге важно добавить новую цель в Яндекс Метрику. Переходим в цели, жмакаем добавить новую цель и указываем настройки

Настройки цели:

  • Название - указываем какое хоти, но оно нам понадобиться при загрузке конверсий. В моем случае это app_install
  • Тип условия - выбираем JavaScript-событие

Подробнее про цели в документации Яндекс Метрики

После добавления цели для счетчика, эта цель станет доступной в настройках рекламной кампании в Директе. Это именно то, что нам нужно

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

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

Интеграция AppMetrica в приложение

Это самый простой и понятный шаг. Делаем все по инструкции

1class YourApplication : Application() {
2    override fun onCreate() {
3        super.onCreate()
4        // Creating an extended library configuration.
5        val config = AppMetricaConfig.newConfigBuilder(API_KEY).build()
6        // Initializing the AppMetrica SDK.
7        AppMetrica.activate(this, config)
8    }
9}

После добавления SDK к нам начнут прилетать постбеки. Из этих постбеков нужно выцепить параметр yclid

Обработка постбеков

Я уже писал выше про настройку постбека, теперь посмотрим как его обработать. AppMetrica будет отправлять GET запрос на указанный нами URL https://tracker.kovardin.ru/downloader/fire

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

  • name - название трекера
  • network - название рекламной сетки с которой работаем (пока только yandex)
  • yaurl - это URL по которому мы будем загружать конверсии в Яндекс Метрику
  • yatoken - токен авторизации для загрузки конверсий, который можно получить в настройках

Сначала разберемся как мы обрабатываем постбек. AppMetrica присылает постбеки через GET запрос. Например, вот такой:

https://tracker.kovardin.ru/downloader/fire?client_id={client_id}&yclid={yclid}&install_timestamp={install_timestamp}&appmetrica_device_id={appmetrica_device_id}&click_id={click_id}&transaction_id={transaction_id}&match_type={match_type}&tracker=appmetrica_821509867285037527&rb_clickid={rb_clickid}

В сервисе реализована ручка /{:name}/fire, в которой сохраняются постбеки в коллекцию conversions

 1yclid := c.QueryParam("yclid")
 2rbclickid := c.QueryParam("rb_clickid")
 3key := yclid + rbclickid
 4
 5if key == "" {
 6    return nil
 7}
 8
 9network := networkVK
10if yclid != "" {
11    network = networkYandex
12}
13
14record.Set("yclid", yclid)
15record.Set("rb_clickid", rbclickid)
16record.Set("key", key)
17record.Set("uploaded", false)
18record.Set("network", network)
19record.Set("tracker", tracker.Id)
20
21if err := h.app.Dao().SaveRecord(record); err != nil {
22    h.app.Logger().Error("error on save conversions", "error", err)
23}

uploaded - этот параметр нужен для отметки, что конверсии уже загружены в Яндекс Метрику. Поэтому устанавливаем его в false и сохраняем

rb_clickid - задел на работу с ВК Рекламой, но пока это не важно

Чтобы протестировать постбек, нужно на девайсе перейти по вашей трекинг ссылке https://redirect.appmetrica.yandex.com/serve/xxxxx?yclid=123 yclid указываем любой. Устанавливаем приложение через лендинг и ждем пока к нам прилетит постбек:

Jul 18 09:03:30 land[510662]: /downloader/fire?client_id=&yclid=123&install_timestamp=1721291374&appmetrica_device_id=11918280336705624214&click_id=&transaction_id=cpi17188142678303156033&match_type=fingerprint&tracker=appmetrica_821509867285037527&rb_clickid=

Все постбеки сохраняются в коллекцию conversions

Загрузка конверсий

Чтобы передать конверсии в Яндекс Метрику, нужно подготовить csv файл, указать там все конверсии и загрузить его на специальный URL https://api-metrika.yandex.net/management/v1/counter/97880624/offline_conversions/upload?client_id_type=YCLID

  • 97880624 - идентификатор счетчика Яндекс Метрики, который используется на лендинге
  • client_id_type=YCLID - параметр, который указывает как Метрика будет привязывать конверсии. В моем случае - по параметру yclid

Для загрузки конверсий в сервисе реализована периодическая задача Uploader. Он получает список конверсий, формирует csv и отправляет его в Яндекс Метрику

 1func (t *Task) Do() {
 2	// upload fired conversions
 3	t.app.Logger().Info("start upload conversions")
 4
 5	trackers, err := t.app.Dao().FindRecordsByFilter(
 6		"tracker",
 7		"enabled = true && network = 'yandex'",
 8		"-created",
 9		100,
10		0,
11	)
12
13	if err != nil {
14		t.app.Logger().Error("error on get trackers")
15
16		return
17	}
18
19	for _, tracker := range trackers {
20		if err := t.yandex(tracker); err != nil {
21			t.app.Logger().Error("error on upload yandex conversions", "error", err)
22		}
23
24		if err := t.vk(tracker); err != nil {
25			t.app.Logger().Error("error on upload vk conversions", "error", err)
26		}
27	}
28
29	t.app.Logger().Info("finish upload conversions")
30}

В методе yandex создается csv файл, который отправим в Метрику

 1data := &bytes.Buffer{}
 2file := csv.NewWriter(data)
 3if err := file.Write([]string{
 4    //"ClientId",
 5    "Yclid",
 6    "Target",
 7    "DateTime",
 8}); err != nil {
 9    return err
10}
11
12for _, record := range records {
13    if err := file.Write([]string{
14        record.GetString("yclid"),
15        "app_install",
16        strconv.Itoa(int(record.Created.Time().In(t.loc).Unix())),
17    }); err != nil {
18        t.app.Logger().Warn("error on write csv row", "error", err)
19        continue
20    }
21}
22
23file.Flush()
24
25body := &bytes.Buffer{}
26writer := multipart.NewWriter(body)
27
28part, _ := writer.CreateFormFile("file", "file.csv")
29if _, err := io.Copy(part, data); err != nil {
30    return err
31}
32if err := writer.Close(); err != nil {
33    return err
34}

В этом csv файле в отдельном столбце Target мы указываем значение app_install. Это значение должно совпадать с названием цели, которое указали на шаге настройки лендинга

Осталось только загрузить csv файл на нужный URL. Подробнее про загрузку конверсий читаем в документации

1request, _ := http.NewRequest("POST", yaurl, body) // yaurl - url указанный в настройках
2request.Header.Add("Authorization", "OAuth "+yatoken) // yatoken - токен авторизации указанный в настройках
3request.Header.Add("Content-Type", writer.FormDataContentType())
4
5resp, err := t.client.Do(request)
6if err != nil {
7    return err
8}

На этом круг замкнулся. Конверсии передаются в Яндекс Метрику и их можно увидеть в интерфейсе Яндекс Метрики

Таким образом можно запускать кампании не только на RuStore или другие альтернативные магазины, но и на прямое скачивание apk. Вся магия в трекинг ссылках AppMetrica, которые каким-то хитрым образом могут матчить переход по ссылке с установкой приложения. Подробнее про это расписано в документации Яндекса

Че по ВК Рекламе

Обязательно запущу и протестирую работу с ВК Рекламой, но в другой раз. Там все чуть проще - конверсии можно передавать а риалтайме

Ссылки