Відвідувач тапає ваше посилання, націлене на Instagram чи Telegram, і застосунок не відкривається. Замість нього відкривається Chrome, прямо на сторінці, яку ви й будували редірект, щоб обійти. Клік зарахувався. У дашборді — звичайний клік. Результат — вкладка браузера.
Це не один режим збою з одним фіксом. Це ланцюжок незалежних рішень браузера, Android і застосунку-цілі, і провал у будь-якій одній ланці цього ланцюжка дає однаковий симптом: браузер замість застосунку. Розібратися в ланцюжку — єдиний спосіб зрозуміти, яка саме ланка підвела.
Звичайне https-посилання не знає, що має відкрити застосунок
https://-URL — це просто посилання. Те, що його рендерить, саме вирішує, що з ним робити, і нічого в самій схемі https не несе інструкції передати клік нативному застосунку. Цю інструкцію треба додати навмисно. Посилання https:// саме собою щоразу рендериться як вебсторінка, незалежно від того, встановлений застосунок-ціль чи ні.
intent:// — це та сама відсутня інструкція
Форма, яка її додає:
intent://host/path#Intent;scheme=https;package=<package>;end
Вона називає пакет, якому Android має передати клік, зберігаючи при цьому базову схему https в intent для перевірки. Якщо названий пакет встановлено і зареєстровано на цей контент, Android передає клік напряму, без зупинки в браузері.
Місце, де людей найчастіше ловить пастка, — фолбек. intent://-посилання може нести S.browser_fallback_url на випадок, якщо пакет-ціль відсутній, але це поле приймає лише значення http і https. Вкажіть замість цього market://-URL — і Android просто відкине його. Фолбек, який ви ніби налаштували, ніколи не спрацює так, як обіцяє синтаксис.
Один застосунок рідко буває одним пакетом
Логіка фолбека ускладнюється в ту мить, коли ви пробуєте більш ніж один пакет, а для кількох великих застосунків доводиться саме так. Той самий застосунок постачається під різними назвами пакетів залежно від того, звідки прийшло встановлення:
| Застосунок | Пакети |
|---|---|
| Telegram | org.telegram.messenger (Play Store), org.telegram.messenger.web (APK з telegram.org), org.thunderdog.challegram (Telegram X) |
com.whatsapp, com.whatsapp.w4b (Business) | |
| TikTok | com.zhiliaoapp.musically, com.ss.android.ugc.trill |
com.instagram.android | |
| Shopee | окремий пакет для кожної країни: com.shopee.id, .br, .vn, .th, .ph, .tw, .my, .sg, .mx, .cl, .ar |
| Amazon | com.amazon.mShop.android.shopping глобально, крім Індії: in.amazon.mShop.android.shopping |
Розкол Telegram застає зненацька найбільше людей. APK, який Telegram роздає зі свого власного сайту, має інший package id, ніж збірка з Play Store, а саме org.telegram.messenger.web, підтверджено прочитанням прямо з маніфесту цього APK. Націльте ланцюжок intent лише на пакет Play Store, і кожен відвідувач, який встановив Telegram напряму з telegram.org, стає невидимим для вашого routing, невідрізненним від застосунку, якого взагалі немає.
Amazon — нагадування перевіряти, а не припускати: японський магазин Amazon використовує той самий глобальний пакет, що й усюди. Пакет jp.amazon.mShop.android трапляється на сайтах-дзеркалах APK, але це не діючий лістинг у Play. Побудова ланцюжка навколо нього ціляє в ніщо реальне.
Це не опційна деталь для routing, який націлений лише на одну збірку. Вона стає важливою в ту мить, коли ви намагаєтеся покрити всю базу встановлень застосунку, бо пакети вище — не взаємозамінні кандидати, що конкурують за одного й того ж відвідувача. Відвідувач зі збіркою з telegram.org і відвідувач зі збіркою з Play — це дві різні людини з погляду Android, і кожній потрібен власний названий пакет у ланцюжку, перш ніж хоч одна з них дістанеться застосунку.
Правило про fallback, яке майже всі розуміють навпаки
Щойно ви ланцюжите кілька пакетів (збірка Play, потім регіональна збірка, потім останній варіант), інстинкт підказує причепити fallback URL і до першої спроби теж, про всяк випадок.
Це рівно навпаки. Якщо перший кандидат несе fallback, а його пакет не встановлено, відвідувача виштовхує в браузер ще до того, як спробують другий кандидат. Ланцюжок так і не дістається пакета, який спрацював би. Жоден крок у ланцюжку з кількома пакетами не повинен нести fallback URL, включно з останнім. Фолбек належить після того, як усі кандидати вичерпані, і застосовується вже вашою власною посадковою сторінкою, коли ви точно знаєте, що жоден із них не спрацював.
Chrome не спробує перехід без тапу
Навіть правильно побудований intent://-URL, націлений на потрібний пакет без завчасного fallback, залежить від того, як його запускають. Chrome вимагає user gesture, щоб обробити intent://-навігацію. Запустіть її автоматично, при завантаженні сторінки, зі скрипта, з ланцюжка редіректів без кліку посередині, і Chrome взагалі не спробує відкрити застосунок. Він одразу піде на S.browser_fallback_url.
Ззовні сторінки цей результат невідрізненний від того, що застосунок не встановлено. Ваші логи, ваша фолбек-посадкова, все нижче за потоком виглядає однаково, чи застосунку справді немає, чи Chrome просто відмовився пробувати, бо відвідувач нічого не робив, що запустило б перехід. Це та деталь, навколо якої варто проєктувати весь флоу: автоматична спроба ненадійна не в сенсі «працює здебільшого». Це крок, який ніколи не запускається. Тап — не опційна прикраса; це передумова для всього механізму.
Це ще й та деталь, яку QA зазвичай пропускає, бо ручне тестування майже цілком складається з тапів. Тестувальник, який відкриває посилання рукою, щоразу задовольняє вимогу gesture, тож флоу, який запускає перехід автоматично посередині ланцюжка редіректів, може пройти кожен ручний тест і все одно провалитися на реальному трафіку в ту мить, коли якийсь крок у продакшні не походить із прямого тапу.
Webview — це інша кімната, а не менший Chrome
Не кожен клік потрапляє в Chrome. Посилання, відкрите у власному in-app браузері іншого застосунку, у webview, яким Instagram, Facebook чи TikTok огортають зовнішні посилання, — це окреме середовище рендерингу. Вимога gesture вище задокументована саме для Chrome. Що робить webview, отримавши той самий intent://-URL, ця сама документація не описує. Routing, перевірений лише в Chrome, тестується в одній кімнаті з кількох, через які насправді приходить трафік.
Доказ того, що успішний перехід не гарантує задоволеного відвідувача
Навіть коли все вище зроблено правильно, застосунок-ціль все одно може відправити відвідувача прямісінько назад у браузер. Перевірено на реальному пристрої Android 15 (Pixel 8, Chrome 131):
Intent-посилання в Instagram, intent://www.instagram.com/instagram/#Intent;scheme=https;package=com.instagram.android;end, розв’язалося чисто. Android передав його власному com.instagram.url.UrlHandlerActivity Instagram, і застосунок відкрився прямо на потрібному профілі й лишився там.
Intent-посилання в TikTok, побудоване так само, теж розв’язалося правильно. Android доставив його до com.ss.android.ugc.aweme.deeplink.AppLinkHandlerV2, точно як задумано. Системний лог показав, що завдання створено з прикріпленим deep link, а потім закрилося зі значенням numActivities=0, і відвідувач опинився назад у браузері, на початковій вебсторінці. TikTok був розлогінений, і йому не було що рендерити для цього посилання. Routing спрацював. Застосунок відмовився показати контент.
Успішний перехід і відвідувач, який все одно опиняється в браузері, — не взаємовиключні речі, і нічого на боці відправника цьому не завадить. Якщо ваша метрика — чи ОС передала клік застосунку, це у вашій владі. Якщо ваша метрика — чи відвідувач лишився в застосунку, це залежить від стану логіну й рішення про рендеринг усередині коду, який вам не належить.
Що контрольовано, а що ні
| Рівень | Хто вирішує | Чи можна контролювати |
|---|---|---|
| Який пакет отримує клік | Ваша побудова intent:// | Так |
| Чи спрацює fallback завчасно | Розміщення вашого fallback, лише останній крок | Так |
| Чи взагалі буде спроба переходу | Вимога gesture у Chrome | Так, запускайте її реальним тапом |
| Chrome чи in-app webview | Де відвідувач тапнув посилання | Ні |
| Чи встановлено пакет-ціль | Пристрій відвідувача | Ні |
| Що покаже застосунок після відкриття | Власний стан логіну й рендерингу застосунку | Ні |
Перші три рядки — це інженерія. Побудуйте ланцюжок правильно, тримайте fallback подалі від кожного кроку-кандидата, крім останнього, і ніколи не запускайте перехід автоматично. Стрім, побудований так, а логіка редіректу DarkCore застосовує те саме правило fallback у кінці і порядок пакетів для кожного застосунку, доводить кожного відвідувача, якого можна довести, до моменту переходу. Останні три рядки — це взагалі не проблема routing. Жоден fallback URL, жодна логіка повтору і жоден вигадливіший рядок intent:// не змінить того, що відбувається, коли ОС уже передала клік застосунку.
FAQ
Чи виправляє додавання ще пакетів у ланцюжок відкат у стилі TikTok?
Ні. Той відкат стався вже після того, як Android правильно передав клік TikTok. Застосунку не було що рендерити, бо він був розлогінений. Більше пакетів допомагає відвідувачу з іншою збіркою застосунку дістатися потрібної. Вони не змінюють того, що робить застосунок-отримувач, коли клік уже в нього.
Якщо відвідувач опиняється на фолбеку, чи означає це, що застосунок не встановлено?
Не обов’язково. Це означає або що застосунку немає, або що перехід так і не спробували, бо він запустився без user gesture. Обидва варіанти дають однакову фолбек-посадкову, і нічого у ваших логах їх не розрізняє.
Чи діє правило порядку fallback, якщо я ціляю лише в один пакет?
Ризик, від якого воно захищає, специфічний саме для ланцюжків із більш ніж одним кандидатом. З одним пакетом виштовхувати відвідувача завчасно нема від чого, бо fallback на цьому єдиному кроці спрацює лише після того, як цей пакет уже виключено. Правило стає важливим у ту мить, коли ви додаєте другого кандидата, щоб покрити іншу збірку того самого застосунку.