Перейти до вмісту
Усі статті
8 хв читання

Помилка S.browser_fallback_url, яка непомітно забирає app open

Схема послідовних спроб Android intent:// для різних пакетів, де fallback URL стоїть лише в самому кінці

intent://-посилання виглядають поблажливо. Ви додаєте S.browser_fallback_url, перевіряєте один раз зі свого телефону, бачите, що застосунок відкривається, і викочуєте це в продакшн. Потім кампанія запускається, і app open rate на Android опиняється рівно там, де був би, якби посилання одразу вело в браузер. Жодних помилок. Жодних крашів у консолі. Посилання зробило рівно те, що ви йому сказали, — проблема саме в тому, що ви йому сказали.

Що насправді каже intent URL

Android intent-посилання має такий вигляд:

intent://host/path#Intent;scheme=https;package=<package>;S.browser_fallback_url=<url>;end

Кожен сегмент — окрема інструкція для Android, а не прикраса:

СегментЩо це каже Android
intent://host/pathціль, виражена як opaque intent замість звичайного URL
scheme=httpsяку схему застосунок-отримувач має використати для розв’язання цілі
package=<package>закріплює intent за одним конкретним застосунком; без цього поля Android може показати chooser або не показати нічого
S.browser_fallback_url=<url>куди піде відвідувач, якщо package не встановлено, щоб перехопити intent
endзакриває блок intent

Якщо читати окремо, це виглядає як повна інструкція: спробувати застосунок, у разі невдачі — впасти в браузер. Для одного пакета так воно і є. Але це перестає бути так у ту мить, коли пакетів стає більше одного.

Випадок з одним пакетом поблажливий — саме тому баг непомітний

Перше intent-посилання в більшості випадків націлене рівно на один застосунок: один пакет, один fallback, готово. Воно працює, його викочують у продакшн, і воно вчить неправильного висновку, бо режим збою, про який ця стаття, виникає лише тоді, коли в ланцюжку з’являється другий кандидат.

Перевірка кількох пакетів по черзі — це не крайній випадок, а звичайна ситуація. Посилання в Telegram може потребувати послідовної перевірки Play-збірки і Telegram X. Посилання в WhatsApp може потребувати перевірки звичайного застосунку і Business-версії. Кожен кандидат отримує власний блок intent://, який запускається по черзі, доки один із них не спрацює. Природний інстинкт — залишити той самий S.browser_fallback_url, що й у версії з одним пакетом, і просто повторити блок для кожного пакета. Саме цей інстинкт ламає ланцюжок.

Чому fallback на першому кроці вбиває всі наступні

Поставте fallback URL на перший блок — і ви вже вирішили, що станеться, коли саме цей перший пакет відсутній: Chrome одразу відправляє відвідувача на S.browser_fallback_url, у вкладку браузера, ще до того, як другий блок intent:// взагалі запуститься. Не має значення, що другий пакет стоїть на пристрої, встановлений і готовий. Його ніколи не запитають, бо ланцюжок уже завершився.

✗ Неправильно — fallback на першому кроці завершує ланцюжок завчасно
intent://…#Intent;scheme=https;package=com.whatsapp;S.browser_fallback_url=https://example.com;end
intent://…#Intent;scheme=https;package=com.whatsapp.w4b;end

✓ Правильно — жоден крок не несе fallback, включно з останнім
intent://…#Intent;scheme=https;package=com.whatsapp;end
intent://…#Intent;scheme=https;package=com.whatsapp.w4b;end

Це та контрінтуїтивна частина, над якою варто зупинитися: у правильному ланцюжку немає fallback узагалі, навіть на останньому кандидаті. Прив’язати fallback до останнього пакета виглядало б коректно ізольовано — цей крок справді останній шанс для застосунку перехопити intent, — але це привчає думати про fallback як про щось, що належить окремому кроку. Це не так. Fallback належить ланцюжку в цілому, застосовується рівно один раз, після того як усі кандидати вже перевірені й жоден не спрацював. І це рішення повинна ухвалювати ваша власна сторінка, а не якийсь окремий блок intent://.

Чому market:// тут не рятує

Очевидний обхідний шлях — направити fallback першого кроку на сторінку Play Store саме для цього пакета, щоб відсутній застосунок хоча б відправляв відвідувача кудись доречне, а не на загальну вебсторінку. Це теж не працює: S.browser_fallback_url приймає лише значення http і https. Значення market:// у цьому полі Android просто відкидає — магазин не те що не відкривається, поле від самого початку не приймає таку схему. Усе, що ви вкажете в S.browser_fallback_url, має бути реальним http- або https-посиланням, а це знову повертає вас у ту саму пастку: будь-який URL у цьому полі завершує ланцюжок на першому відсутньому пакеті — незалежно від того, веде він у стор чи ні.

Правило про gesture перетворює провалений перехід на примарне «застосунок не встановлено»

Поверх проблеми з порядком накладається ще одна умова, і саме вона робить першу проблему складною для діагностики ззовні. Chrome вимагає user gesture, щоб обробити навігацію intent://, — тап, а не редірект, який спрацьовує при завантаженні сторінки, і не таймер, що спрацьовує сам собою. Запустіть те саме посилання без gesture позаду нього — і Chrome взагалі не намагатиметься запустити застосунок. Він одразу йде на S.browser_fallback_url, так ніби пакета не було.

Ззовні кліку — у дашборді звітності, у записі екрана редіректу, будь-де, крім інструментування самої сторінки, — цей результат невідрізнимий від того, що застосунок справді не встановлено. Обидва випадки приземляються на той самий fallback URL тим самим шляхом. На боці отримувача немає сигналу, який сказав би вам «Chrome відмовив, бо не було gesture», окрім «Android спробував, і пакета справді не було». Розрізнити їх можна лише знаючи на своєму боці, який саме з ваших кроків редіректу реально спрацював від тапу.

Саме тому ланцюжок, який ідеально проходить ручне тестування, усе одно може провалитися в продакшні. Ручний тест — це послідовність тапів: ви тапаєте по посиланню, тапаєте далі по всьому, що йде після, і вимога gesture виконується на кожному кроці непомітно для вас, бо ви навіть не помічаєте, що це вимога. У ту мить, коли будь-яка частина цієї послідовності на вашому боці автоматизується — а весь упорядкований ланцюжок має простежуватися до того самого початкового тапу, — app open може зникнути без жодного запису в логах, який пояснив би чому.

Де насправді місце fallback

Складіть правила разом, і рішення перестає бути питанням смаку:

ПравилоНаслідок
Жоден крок intent:// не несе S.browser_fallback_urlвідсутній пакет переходить до наступного кандидата, а не виходить у браузер
S.browser_fallback_url приймає лише http/httpsнемає трюку зі схемою, який дав би fallback у середині ланцюжка подвійно слугувати ще й посиланням на стор
Chrome потребує user gesture для навігаціїувесь упорядкований ланцюжок має простежуватися до одного тапу, а не до наступного редіректу
Ланцюжок має десь закінчитисясаме ваша сторінка, а не якийсь блок intent://, вирішує, коли всі кандидати вичерпано

Останній рядок і є справжньою роботою: побудувати впорядкований список спроб intent:// без поля fallback у жодній з них і дозволити своїй сторінці застосувати store URLhttps://play.google.com/store/apps/details?id=<package> на Android — лише після того, як увесь ланцюжок відпрацював і жоден пакет не перехопив intent. Саме цю задачу маршрутизації й вирішують трекери, побудовані для застосункового трафіку. Ланцюжки редіректів DarkCore запускають список пакетів на боці сервера і застосовують store fallback рівно один раз, після останнього кандидата, замість того щоб зашивати fallback у крок, де він завчасно й непомітно обриває ланцюжок.

Як це виглядає у вашій воронці

Жоден із двох режимів збою не заявляє про себе. Відвідувач, чий пакет першого вибору відсутній і потрапляє на fallback уже на першому кроці, виглядає у вашій воронці рівно так само, як відвідувач, на пристрої якого справді немає жодного з кандидатів. Відвідувач, у якого редірект посеред шляху втратив user gesture, виглядає так само. Усі три випадки приземляються на той самий fallback URL, у тій самій точці флоу, з тією самою відсутністю помилки. Змінюється одне агреговане число — app open rate, — і воно падає рівномірно, по популяції, яка включає чимало людей із застосунком просто на головному екрані.

Саме це робить баг настільки складним для виявлення за дашбордами: немає сегмента, який можна виділити. «Застосунок не встановлено» — не група відвідувачів, яку можна відфільтрувати, бо такого поля просто не існує: кожен із цих результатів записує ідентичну fallback-подію. Єдиний спосіб відділити збої порядку від збоїв gesture від справжньої відсутності застосунку — свідомо перевірити кожну умову окремо: запустити ланцюжок вручну з усіма кандидатами встановленими, переконатися, що кожен спрацьовує по черзі, а потім повторити з тим самим тригером, яким ланцюжок реально запускається в продакшні — та сама сторінка, той самий тригер, той самий шлях редіректу, — і подивитися, чи доживає тап до останнього кроку.

Перед тим як викотити наступний ланцюжок

  • Приберіть S.browser_fallback_url з кожного кроку в ланцюжку з кількома пакетами, включно з останнім. Рішення про fallback належить ланцюжку, а не окремому блоку intent://.
  • Ніколи не пишіть market:// у S.browser_fallback_url. Android тихо відкидає це значення — приймаються лише http і https, тож fallback зі схемою стору — не обхідний шлях, а значення, яке поле просто не читає.
  • Запускайте весь ланцюжок від одного user gesture. Усе, що передає естафету посеред ланцюжка без свіжого тапу, може загубити app open без жодного пояснення в логах.
  • Ставтеся до «приземлення на fallback URL» як до неоднозначного результату за замовчуванням. Це означає або «жоден пакет у ланцюжку не перехопив», або «умова gesture не виконана», і лише інструментування на вашій власній сторінці — а не сама fallback-подія — може сказати, що саме сталося.
  • Застосовуйте store fallback один раз, на своїй сторінці, після того як упорядкований список кандидатів повністю відпрацював. Це єдина точка флоу, де твердження «нічого не перехопило» справді правдиве.
  • intent url android
  • s.browser_fallback_url
  • схема intent url в android
  • fallback url для intent