Вы видите, как сработал клик, резолвнулся intent, Android передал управление целевому приложению — и через секунду посетитель снова на вашей landing-странице, будто ссылка с самого начала открыла браузер вместо приложения. В вашем логе написано, что приложение открылось. Клиент хочет знать, почему посетитель ничего не сделал, оказавшись якобы внутри. Вы оба правы, и в этом и есть проблема: успешный переход и посетитель, всё равно оказавшийся обратно в браузере, — вещи, которые вполне могут происходить одновременно.
Тот же механизм, два результата, одно устройство
Это не гипотеза. Это то, что произошло на одном Pixel 8, Android 15, Chrome 131, когда один и тот же механизм диспетчеризации запустили дважды против двух разных приложений:
| TikTok | ||
|---|---|---|
| Диспетчеризация | intent://www.instagram.com/instagram/#Intent;scheme=https;package=com.instagram.android;end | Тот же механизм intent://, package TikTok |
| Handler activity | com.instagram.url.UrlHandlerActivity | com.ss.android.ugc.aweme.deeplink.AppLinkHandlerV2 |
| Системный лог | Task создан, приложение осталось на переднем плане | Task создан, затем закрыт с numActivities=0 |
| Что увидел посетитель | Instagram, на профиле назначения | Браузер, на исходной веб-странице |
| Почему | У приложения была сессия, чтобы отрендерить назначение | Приложение было разлогинено, и ему было нечего рендерить |
Instagram получил ссылку, открылся и остался на профиле назначения. TikTok получил такой же класс ссылки, в собственном handler’е, и закрылся немедленно — системный лог показывает, что task создан с deep link на нём, затем закрыт с numActivities=0, и посетитель оказался обратно в браузере на той же странице, с которой ушёл. Оба приложения получили ссылку корректно. Только одному из них было что показать взамен.
numActivities=0 — это собственный учёт Android только что созданного task: приложение открыло task, чтобы вместить целевой экран deep link, и этот task теперь содержит ноль activities, потому что приложение разобрало его вместо того, чтобы заполнить. Это не сбой, и нигде в отчёте кампании это не логируется как ошибка. Это осознанный выбор приложения — что делать со ссылкой, которую оно не может отрендерить, — и приложение выбирает «ничего» вместо экрана ошибки или требования залогиниться.
Это не дефект, специфичный для TikTok, и цель поставить эти два запуска рядом — не выделить одно приложение. Дело в том, что тот же режим отказа доступен любому приложению на другом конце deep link, и какой именно вы наблюдаете, зависит от состояния аккаунта посетителя, держащего телефон, а не от того, какое приложение вы таргетировали или как построили ссылку. Залогиненный аккаунт TikTok, которому передали тот же intent в тот же handler, имеет сессию, чтобы отрендерить назначение, — то же условие, что сделало успешным запуск Instagram.
Что на самом деле измеряет «открыто»
Работа уровня роутинга заканчивается на handoff: построить правильный intent://, резолвнуть правильный package и заставить Android запустить целевое приложение. Именно это видит лог кликов или воронка открытий приложения, и по этому критерию оба запуска выше были успешными — Android запустил приложение в обоих случаях. Чего уровень роутинга не видит и никогда не мог видеть — это что приложение делает со ссылкой, получив её. У Instagram была активная сессия, и оно отрендерило назначение. У TikTok — нет, и вместо того, чтобы показать ошибку или требование залогиниться, оно закрыло собственный handler и позволило посетителю упасть обратно туда, откуда тот пришёл.
Именно поэтому «открылось ли приложение» — неправильный вопрос, вокруг которого стоит строить отчёт. Это реальный вопрос с реальным ответом, и этот ответ часто «да» для посетителя, который так ничего и не увидел внутри приложения. Воронка открытий, останавливающаяся на handoff, засчитает запуск TikTok в этом сравнении ровно так же, как запуск Instagram — как успех, — потому что с точки зрения уровня роутинга это и был успех.
Две разные вещи истинны одновременно, и одна метрика не может вместить обе:
| Что измеряет | Где решается | |
|---|---|---|
| Handoff | Android запустил правильное приложение в правильном handler’е | Уровень роутинга, в момент резолва intent |
| Результат внутри приложения | Отрендерило ли приложение назначение или вернуло посетителя назад | Само приложение, в зависимости от собственного состояния сессии на момент запуска |
Дашборд кампании, в котором есть место только для одного из этих рядов, по умолчанию выберет handoff, потому что handoff — более лёгкая половина для инструментирования: это событие на стороне клика с чётким сигналом успеха, тот же лёгкий для фиксации сигнал, который подаёт pixel, независимо от того, что случится после него. У результата внутри приложения нет эквивалентного сигнала на стороне отправителя вообще — он существует только на устройстве, внутри приложения, уже после того, как контроль перешёл туда.
Ничто на стороне отправителя не может этому помешать
Здесь нет fallback URL, повтора или лучшего intent, который изменил бы то, что произошло. Уровень роутинга сделал свою работу: доставил правильное приложение, в правильный handler, с прикреплённой ссылкой. Что приложение сделало дальше — решило состояние, к которому уровень роутинга не имеет доступа и не может заранее запросить: была ли у того аккаунта TikTok валидная сессия в момент прихода ссылки. Залогиненный посетитель получает другой результат, чем незалогиненный, от абсолютно одинаковой ссылки, доставленной абсолютно одинаково. Это не ошибка роутинга, которую нужно исправлять. Это условие, которое уровень роутинга никогда не мог обнаружить.
Как отличить эти два случая при диагностике
Со стороны клика успех в стиле Instagram и откат в стиле TikTok выглядят одинаково вплоть до момента handoff — тот же intent, тот же резолвнутый package, то же запущенное приложение. Разница проявляется только после этой точки, и единственное место, где она реально видна, — то же место, где эти два запуска различили изначально: само устройство, в момент клика, с открытым системным логом. Воспроизвести клик на реальном устройстве и посмотреть, что сообщает Android — какой handler activity получил ссылку, остаётся ли результирующий task на переднем плане или закрывается с numActivities=0, — даёт окончательный ответ, удержало ли приложение посетителя или вернуло его назад. Воспроизвести тот же клик залогиненным в целевое приложение и разлогиненным — самый быстрый способ увидеть, зависит ли откат конкретного приложения от сессии, как это было с TikTok в данном случае.
Чего вы не можете сделать — это вывести разницу из чего-либо доступного на этапе построения ссылки. intent://, который вы строите, package, который таргетируете, правила fallback, которые устанавливаете, — ничто из этого не несёт информации о том, что приложение сделает, получив контроль, потому что этой информации не существует, пока приложение не оценит собственную сессию в момент запуска.
Практическая версия этой проверки, когда вы подозреваете, что конкретное приложение возвращает посетителей обратно:
- Воспроизведите клик на реальном устройстве с установленным и залогиненным целевым приложением. Подтвердите handler activity и зафиксируйте, остаётся ли результирующий task на переднем плане.
- Разлогиньтесь из того же приложения и воспроизведите идентичный клик. Если результат меняется — приложение теперь закрывает свой handler и возвращает вас в браузер, — откат зависит от сессии, а не от сломанной ссылки.
- Сравните два handler activities, которые сообщает ОС. Симптом, заметный посетителю, который с вашей стороны читается одинаково («приложение открылось, посетитель не сконвертировался»), может исходить от двух разных компонентов на стороне приложения, и только устройство говорит вам, какой именно вы видите.
Ни один из этих трёх шагов не касается самой ссылки, потому что ссылка никогда не была переменной. Переменная — то, что залогинено на телефоне, который её запускает.
Что это значит для того, что вы обещаете клиенту
«Приложение открылось» и «посетитель сконвертировался внутри приложения» — это два разных утверждения, и отчёт, сводящий их в одно число, даёт обещание, которое уровень роутинга никогда не мог выполнить. Честная версия отчёта о роутинге говорит, что ссылку успешно передали правильному приложению — это то, что реально происходит и измеряется на уровне роутинга. Остался ли посетитель там после этого — отдельный результат, определённый собственным состоянием сессии целевого приложения, и никакая настройка на стороне отправителя не меняет это число.
Конкретно: если клиент спрашивает, почему кампания с открытием приложения не конвертирует так, как предполагает уровень открытий — тот же по форме вопрос, что и про конверсию, которая так и не пришла, — ответ может быть вообще не в кампании. Может оказаться, что заметная доля посетителей открывает целевое приложение разлогиненными, и само приложение отказывается им что-либо показывать — что уровень роутинга выполнил корректно, и что ничто в таргетинге, креативе или построении ссылки изменить не может. Показывать успешный handoff и результат внутри приложения двумя отдельными строками вместо одного смешанного показателя «приложение открыто» — это разница между клиентом, который понимает, что он на самом деле видит, и клиентом, который считает, что каждое открытие достигло назначения.
Именно такое разделение DarkCore PWA Tracker удерживает на уровне роутинга: зафиксированный handoff целевому приложению, отдельно от того, что посетитель делает после того, как контроль перешёл туда, — потому что эти две вещи решают разные системы, и представление их одним числом отвечает на вопрос, который никто не задавал.