Схема API описывает намерение вендора, а не поведение его клиента
Симптом
Пользователь нажимает в чате кнопку бота, которая должна открыть мини-приложение VK. Вместо приложения появляется фирменная страница VK «Page Not Found». Если нажать на неё, страница исчезает и приложение открывается нормально. Поведение одинаково воспроизводилось в вебе и мобильном клиенте.
Контекст
VK позволяет ботам добавлять в клавиатуру кнопку типа open_app. По
официальной схеме messages_keyboard_button_action_open_app
у действия четыре обязательных поля — type, app_id, owner_id, label — и
два опциональных: payload и hash. Схема показывает, что hash становится
фрагментом ссылки вида vk.com/app{app_id}_{owner_id}#{hash}. Это штатный канал,
через который кнопка сообщает приложению, куда открываться.
Бэкенд использовал его по назначению: в уведомлении о новой беседе передавал в
hash ключ вида conversation=<id>, а в обычном ответе бота — метку
from_bot. Фронтенд читал ключ и открывал нужную беседу.
Коллега, исследовавший проблему со стороны фронтенда, сообщил причину: кнопка
якобы отправляется как обычная ссылка open_link, поэтому её надо заменить на
open_app. Это утверждение было выведено из симптома, а не измерено.
Корневая причина
VK в этом месте — кривая херотень.
В проверенных клиентах VK отвечал своей страницей «Page Not Found» на ссылку
приложения с #fragment. Приложение загружалось только после того, как
пользователь прокликивал эту ошибку. Содержимое фрагмента не влияло на симптом.
Я измерил это вручную сравнением трёх ссылок в браузере: ссылка приложения без
фрагмента открывалась чисто, а варианты с #from_bot и
#conversation=<id> оба давали 404.
То есть наблюдаемый дефект находился на стороне VK: платформа документирует
hash как механизм передачи параметров запуска, но её клиенты ломали переход с
этим параметром. Из нашего кода это можно было обойти только отказом от
фрагмента.
Ошибочное предположение
Я исходил из того, что документация VK актуальна и точно описывает поведение
клиентов. Фактически она описывала намерение: hash был документирован как
рабочий канал, поэтому мы его и использовали.
То же допущение породило ложный диагноз. Единственное расхождение нашей кнопки
со схемой было в payload: библиотека-обёртка безусловно записывала
payload: null, хотя схема типизирует payload строкой. Из этого появилась
гипотеза: клиент не разбирает действие и падает в веб-фолбэк. Исправление
внедрили и выкатили, но симптом не изменился.
Оба вывода — и диагноз, и несостоявшийся фикс — были сделаны из спецификации. Ни один не измерили до внедрения.
Обнаружение
Причину показали три ручных открытия: без фрагмента и с двумя разными фрагментами.
Наш код автоматически этого не видит. Страницу 404 рисует клиент чужой системы, а нам не приходит ни код ответа, ни событие. Без автоматизации самих клиентов ранний сигнал здесь один: человек открывает ссылку и видит результат.
Исправление
hash полностью убран из кнопки.
Вместе с ним параметр беседы вырезан по всей цепочке: из функции сборки клавиатуры, функции отправки уведомления, протокола нотификатора и места вызова в цикле материализации тредов.
Диплинк в конкретную беседу потерян: теперь приложение открывается на списке. Для открытия нужного треда придётся делать другой механизм.
Проверка
Я добавил юнит-тест, который сравнивает весь набор ключей действия кнопки с
эталонным словарём, а не отдельные поля. Прежние тесты точечно проверяли hash
и app_id, поэтому не замечали ни лишний payload, ни сам факт отправки
фрагмента. Новый тест краснеет при любом лишнем ключе, включая возврат hash.
Сам дефект на стороне клиента VK этим тестом не закрывается.
Общий вывод
Спецификация чужой системы описывает её намерение, а не гарантирует поведение. Когда переход через границу сломан, разницу между работающим и неработающим вызовом надо измерять, а не выводить: найти ближайший работающий путь и сокращать отличия до одного.
Признак ложного следа — две подряд правдоподобные гипотезы из документации, которые не воспроизводятся. После второй надо менять метод, а не гипотезу.
И ещё: причинное утверждение от человека, исследовавшего проблему, остаётся
гипотезой, пока не сказано, чем оно измерено. Здесь диагноз «отправляется
open_link» за пару минут опровергался чтением исходников библиотеки-обёртки:
она эмитила open_app с самого первого коммита.