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

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

Тап по мобільному посиланню розгалужується на два шляхи: у вкладку браузера і в нативний застосунок

Відвідувач тапає ваше посилання, націлене на 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 просто відкине його. Фолбек, який ви ніби налаштували, ніколи не спрацює так, як обіцяє синтаксис.

Один застосунок рідко буває одним пакетом

Логіка фолбека ускладнюється в ту мить, коли ви пробуєте більш ніж один пакет, а для кількох великих застосунків доводиться саме так. Той самий застосунок постачається під різними назвами пакетів залежно від того, звідки прийшло встановлення:

ЗастосунокПакети
Telegramorg.telegram.messenger (Play Store), org.telegram.messenger.web (APK з telegram.org), org.thunderdog.challegram (Telegram X)
WhatsAppcom.whatsapp, com.whatsapp.w4b (Business)
TikTokcom.zhiliaoapp.musically, com.ss.android.ugc.trill
Instagramcom.instagram.android
Shopeeокремий пакет для кожної країни: com.shopee.id, .br, .vn, .th, .ph, .tw, .my, .sg, .mx, .cl, .ar
Amazoncom.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 на цьому єдиному кроці спрацює лише після того, як цей пакет уже виключено. Правило стає важливим у ту мить, коли ви додаєте другого кандидата, щоб покрити іншу збірку того самого застосунку.

  • link opens browser instead of app
  • deep link opens browser
  • open app from link android
  • intent url android