|
Professor Seleznov
|
У каждого второго разработчика или QA есть сервис, который:
- Написан на древней версии языка
- Не имеет авторов
- Тесты не работают
- Документация — одна страница
- Но он стабильно работает, и его все боятся трогать
А потом прилетает задача: добавить мультиязычность, или новый тип данных, или интеграцию с внешним API. И вы понимаете: либо вы его трогаете сейчас, либо он ломается сам через полгода в самый неподходящий момент. Всем привет! На связи Даша, QA команды «Платформа Web» в Иви, и Андрей, наш разработчик. Нам достался Pyro — SEO-сервис с минимумом тестов, документации и авторов. Задача: добавить мультиязычность, ничего не сломать. Рассказываем, как мы чистили мусор, писали скрипты перевода и восстанавливали пирамиду тестов. Спойлер: у нас получилось. Поэтому если вы когда-либо сталкивались с вопросами «Как тестировать и предотвращать проблемы с SEO?» или «Как воскресить легаси сервис?», то эта статья для вас! Надеемся наш опыт вам поможет! А также будем рады, если в комментариях вы поделитесь своими мнениями и идеями. Давайте обсудим вместе! Введение: что такое Pyro и почему мы его боялись Pyro— это SEO-сервис, написанный на PHP 5, который хранит и отдаёт метатеги и другие SEO-данные для всего веба Иви. SEO в вебе — это не просто «технические настройки», а философия создания понятных, структурированных и ценных веб-ресурсов, которые:
- Люди могут легко читать и использовать
- Поисковые системы могут правильно индексировать
- Помогают владельцам получать стабильный трафик и клиентов
Наша задача: сделать сервис мультиязычным. Проблема, с которой мы столкнулись: исторически специального параметра для языка не существовало. Все данные были на русском. Добавить поддержку языка, не сломав существующую сложную логику — вот главная задача. 1. Знакомство с монстром: как работает Pyro 1.1. Размеры проблемы Pyro участвовал в формировании каждой страницы сайта. Если он падал или ошибался — мы теряли значительную часть поискового трафика на затронутых страницах. Для бизнеса это прямые потери. Для нас — жёсткий SLA. Для понимания масштаба: одна страница → 5 запросов к Pyro. Всего роутов — 23, плюс параметризирующие GET-параметры. Ошибка в одном роуте — и трафик улетает. Вот как это выглядит на примере страницы /watch/...:
- /group — общие метатеги для всех карточек контента (фильмы, сериалы)
- /video — метатеги для конкретной карточки
- /special_links, /menu — данные для шапки сайта
- /meta — аналог robots.txt в head
Итого 5 запросов на одну страницу. А страниц — тысячи. Нагрузка на разработку и тестирование — колоссальная. 1.2. Логика работы сервиса в двух словах Проблема:В текущей реализации нашего SEO-сервиса не была заложена поддержка нескольких языков. Данные хранятся в виде «ключ → значение», ключ строится из url а при запросе сервис собирает все ключи от общего к частному и склеивает ответы. Это порождает три классических эффекта:
- Протекание данных — общие ключи затирают частные
- Неявный приоритет — порядок обхода жёстко зашит в коде
- Трудность с новыми параметрами — они ломают всю логику мержа
Логика работы Pyro: SEO-сервис работает по одной идее — «отдать хоть что-то и как можно больше». В нем на каждый GET-запрос выполняется рекурсивный обход базы данных. Ключ в базе данных собирается из url запрашиваемого ресурса. Например, если пользователь запросил url вида/menu?host=ivi.tv с параметром определенного хоста, то ключ в БД будет выглядеть как mask/menu/ivitv.
Пример ответа БД по ключу
Пример ответа БД по ключу - mask/menu:
{
"110_text": "DEFAULT TEXT",
"201_text": "DEFAULT TEXT",
"111_linktitle": "ONLY IVI RU TEXT",
"162_text": "DEFAULT TEXT",
...
}
Пример ответа БД по ключу - mask/menu/ivitv:
{
"110_text": "DEFAULT TEXT",
"201_text": "DEFAULT TEXT",
"162_text": "ONLY IVI TV TEXT",
...
}
Ключ делится по слешам и ищется слева-направо. Например, в HTTP запросе к /menu/?host= ivi.tv, Pyro запросит ключ — mask/menu/ivitv, сделает два запроса в БД — по ключам mask/menu и mask/menu/ivitv, смержит ответы от БД и отдаст это в ответе.
{
"110_text": "DEFAULT TEXT",
"201_text": "DEFAULT TEXT",
"111_linktitle": "ONLY IVI RU TEXT",
"162_text": "ONLY IVI TV TEXT",
...
}
1.3. Как редакторские изменения SEO-данных попадают на прод При необходимости внести какие-либо изменения в разметку страницы на сайте, редактор будет вносить ее непосредственно в файлы Pyro-updater. Pyro-updater — это «админка» для работы с данными Pyro. Представляет из себя репозиторий в гите со специальным CI. Реплицирует структуру базы данных в файловом виде. Обновляет данные внутри Pyro («прожигает») посредством PUT-запросов из CI гита. Из yaml файла берется key — это url, по которому выполнится запрос и будет собираться ключ в БД, и value — это SEO-данные для конкретной сущности. Ниже представлен пример такого файла для главной страницы Иви:

Рис.1. Файл с SEO данными Когда пользователь попадет на страницу, где менялась СЕО-разметка, сайт отправит GET-запрос на url указанный как ключ в файле Pyro-updater'а, и отдана информация обновленная из ранее описанного PUT-запроса. В целом, диаграмма работы Иви с сервисом выглядит следующим образом:

Рис.2. Silex-диаграмма работы сервиса со стороны редактора SEO-данных.

Рис.3. Silex-диаграмма работы сервиса со стороны пользователя Иви. 2. План спасения Вместо одной большой задачи «Добавить мультиязычность в Pyro» мы создали эпик с чёткими шагами: 1. Исследование
- Разработчик: разобраться в логике работы, поднять локально
- QA: провести аудит тестового покрытия, изучить баги за год, собрать список роутов Pyro по приоритету 2. Чистка данных
- Проверить на наличие неиспользуемых файлов 3. Автоматизация процессов
- Подготовить скрипты для машинного перевода, поиска в базе файлов дубликатов и поиска отличий внутри языковых директорий 4. Добавление url-параметра языка в запросы сервиса — lang
- Изменить логику формирования ключей 3. Разработка 3.1. Чистка данных: удаляем мусор Репозиторий Pyro-updater (админка для SEO-данных) представлял из себя ~550 МБ чистого текста. Многие данные дублировались или не использовались. Существовали специальные параметры, дробящие пользователей на группы, например,authorized— деление на авторизованных и не авторизованных. Что вырезали:
- Файлы, содержащие в url устаревшие GET-параметры для авторизованных подписчиков и не подписчиков, что сильно увеличивало объем.
- Устаревшие маски-плейсхолдеры для удалённых разделов сайта
- Дублирующиеся JSON-LD разметки — для этого был написан анализирующий скрипт, выдающий список файлов, где и что дублируется Итог: минус ~150 МБ «мусора», упрощение структуры. 3.2. Добавление параметра языка lang: осторожно рекурсия Главная особенность Pyro — рекурсивный обход + мерж данных. Если не осторожничать, русские данные «протекают» в другие языки через общие маски. Наше ключевое решение: параметр lang не участвует в рекурсии. Почему это было больно? Покажу на примере карточки контента. Как работало раньше (без языков):
- PUT в /group/{content}/ → {"title": "Общий заголовок"}
- GET на /video/{id}/ → получает тот же заголовок (через мерж)
Как стало (с ?lang=uz):
- PUT в /group/{content}/?lang=uz → ок
- GET на /video/{id}/?lang=uz → пусто. Мержа для разных языков нет.
Последствия: 4 самых приоритетных роута Pyro начали отдавать заглушки вместо реальных данных. Редактор SEO был не в восторге — пришлось бы вручную заполнять теги для каждого контента. Что дальше? Мы думали вернуть мерж обратно. Но поняли: переписывать логику мержа — значит рисковать всеми роутами. Этого мы себе позволить не могли, поэтому выбрали другой путь — разработали скрипты для массового перевода и контроля качества. 3.3. Инструменты: скрипты для массового перевода и финального контроля качества Для машинного перевода был использован уже знакомый команде сервис SmartCat. Мы написали скрипты для выполнения следующих задач:
- Подготовка данных для загрузки в SmartCat. Скрипт рекурсивно собирает большой объём глубоко вложенных файлов в единую структуру, делит их на мелкие чанки для загрузки в SmartCat (мы не можем загнать огромный объем данных за раз)
- Подготовка полученных от SmartCat переводов. Скрипт соединяет разделенные ранее блоки в единый файл и добавляет параметр языка переведенным данным
- Скрипт, сравниващий структуры данных в разных языковых директориях и копирующий недостающие файлы. Так мы решили проблему ручного ввода SEO-данных админом
- Проверка отсутствия кириллицы в финальных переводах для анализа корректности проделанной работы
4. Тестирование: восстанавливаем пирамиду для SEO-сервиса Для начала поговорим о проверках, которые могут быть не очевидны для тех, кто раньше не работал с SEO. 4.1. Базовые проверки SEO для чайников Кейс 1. Глазами бота Зачем: если бот получит 500 или 404 — страница выпадет из индекса. Теряем трафик. Как проверить:
- Chrome DevTools → Network conditions → User agent → выбрать Googlebot Smartphone (или YandexBot)
- Обновить страницу
На что смотреть:
- Страница открылась
- Метатеги на месте
- Нет 500, нет 404
- Нет редиректа (если хотите скрыть раздел — лучше 404)
Кейс 2. Сервис упал — что видит пользователь Зачем: убедиться, что при падении Pyro страница не рассыпается. Как проверить: заблокировать запрос к Pyro (сниффер), обновить страницу. На что смотреть: страница открыта, метатеги заменены на статику (не пустые). Кейс 3. Сервис упал — что видит бот Зачем: бот должен получить 503 (временная проблема), а не 500. Это сохранит позиции в индексе. Как проверить:
- Заблокировать запрос к Pyro
- Подставить User-Agent бота (Googlebot / YandexBot)
- Обновить страницу
На что смотреть: страница отдаёт 503, а не 500 Кейс 4. Данные из Pyro дошли без искажений Зачем: E2E-проверка, что фронтенд не перебил ответ сервиса. Как проверить: для ускорения тестирования мы используем расширение SEO META in 1 CLICK, которое позволяет увидеть заполненные теги на странице. Также для проверки на мобильных устройствах мы используем собственное расширение, так как SEO META in 1 CLICK не позволяет проверить данные на телефоне. На что смотреть: метатеги на странице = ответу Pyro. Кейс 5. Проверка данных в JSON-LD Зачем: поисковые системы используют JSON-LD для понимания структуры страницы (фильм, сериал, персона, отзыв). Ошибки в разметке → неправильный сниппет в выдаче → падение кликов. Как проверить:
- Chrome DevTools → Elements → найти
- Или расширение SEO META in 1 CLICK (вкладка Structured Data)
- Скопировать JSON и проверить валидатором (например, validator.schema.org)
На что смотреть:
- Тип разметки соответствует содержимому страницы (Movie, TVSeries, Person и т.д.)
- Обязательные поля (name, url, image) не пустые
- Нет синтаксических ошибок в JSON
Кейс 6. Наличие атрибута lang в HTML в соответствии с языком страницы Зачем: поисковики учитывают lang при ранжировании для конкретного языка. Скринридеры используют его для выбора правильного произношения. Как проверить:
- Chrome DevTools → Elements → найти или
- Либо вручную посмотреть исходный код страницы
На что смотреть:
- Атрибут lang присутствует и соответствует языку страницы (ru, uz, en и т.д.)
- Если страница мультиязычная — lang меняется при переключении языка
- og:locale синхронизирован с lang
4.2. Подготовка к тестированию В Иви подготовка к тестированию включает в себя множество этапов: аудит покрытия, изучение багов, изучение пользовательких сценариев, построение графиков по посещениям на страницах с сервисом, архитектура покрытия на основе полученных данных и т.д. Предлагаю бегло пройтись по этапам. Аудит покрытия (шокирующие цифры) После небольшого экскурса в проверки SEO хотелось бы обсудить само тестовое покрытие, варианты его оптимизации. Понимаю, что перевернутой пирамидой тестирования никого не удивить, но хотелось бы этот пункт посвятить именно ей. У нас были написаны тест-кейсы на проверки для SEO, о которых я рассказала выше в пункте 4.1. Часть кейсов была покрыта автотестами, но достаточно ли нам этого? Как оказалось, нет, и вот почему:
- Масштаб сервиса и важность передаваемой им информации (см.пункт 1.1) требуют покрыть все роуты хотя бы базовыми проверками. Например, проверка GET и PUT запросов, проверка мержа данных и т.д.
- Как выяснилось долгое время в CI не запускались написанные ранее API и Unit-тесты, и это лишь часть проблемы
- Уровень найденных авто, API и Unit тестов был совсем «базовый минимум». «Роскошного максимума» никто и не хотел, но важно было сократить количество ручных проверок:
- Многие GET-параметры не были учтены, что явно добавляло ручных проверок с учетом параметризации
- Мерж данных между разными роутами вообще не проверялся, хотя является одной из важнейших фич этого сервиса
- Проверки рекурсивного обхода также не было
- UI-тесты редко содержали в себе проверки полученных данных из Pyro. Часто падали, требуя ручной перепроверки
- В ручных тест-кейсах проверялись лишь сценарии из пункта 4.1. То есть количество кейсов здесь не равно качественной проверке, т.к. напрямую сам сервис не проверялся
Состояние «до»:
- Кейсы проходимые вручную: 57
- UI-тесты: 93
- API-тесты: 70
- Unit-тесты: 131
Главная проблема: Не соблюдается принцип пирамиды тестирования. Скорее у нас были хрень какая-то песочные часы. Много хрупких UI-тестов, мало стабильных API и Unit-тестов. Построение графиков по посещениям страниц с сервисом Для построения графиков использовали инструмент Grafana. Нашей целью было узнать:
- На каких страницах больше всего пользователей
- На какой платформе больше всего нагрузки
- Как часто и какие боты к нам приходят
После получения данных мы можем точно понять, какие роуты нуждаются в большем внимании при написании кейсов и каковы будут потери при пропуске каких-либо сценариев. В нашем случае самой важной оказалась карточка контента, а именно сериалы, на desktop версии сайта. Архитектура тестового покрытия Изучили баги, собрали сценарии от бизнеса. Тест-кейсы разделили на API (unit-тесты) и e2e (UI-автотесты). Примерная иерархия покрытия тест-кейсами:
- Pyro
- Общие проверки — проверки, подходящие любому роуту
- GET-параметры — проверки не привязанные к роуту. Проверяются на списке роутов
- Негативные проверки — общие негативные проверки, подходящие любому роуту
- Роуты — кейсы учитывающие особенности одного отдельно взятого роута или его слияние данных с другим роутом
Такая иерархия решает сразу несколько проблем в покрытии:
- Удобство прохождения ручных проверок и понятная параметризация в каждом сценарии. Теперь все кейсы, где неважен роут, а важен GET-параметр, вынесены в один блок.
- Только важные пользовательские сценарии внутри директорий по роутам. Что сокращает количество сценариев, а значит ускоряет регресс.
- Понятное распределение кейсов по уровням пирамиды и полноценное понимание уровня покрытия сервиса.
Такой комплексный подход в тестировании помогает опираться не только на знания QA, но и на реальные данные, а соответственно строить более устойчивое тестовое покрытие. Автоматизация рутинных проверок Автоматизация — одно из прекраснейших созданий разработки. Поэтому не пренебрегайте ею. Вместо ручной проверки метатегов расширением, мы написали скрипт, который:
- Ходит по списку важных страниц
- Запрашивает данные из Pyro для разных языков
- Сравнивает с эталонными шаблонами
- Формирует отчёт в CI
Итог всей проделанной работы:
- Кейсы проходимые вручную: 37
- UI‑тесты: 93 → 95 (удалили дублирующиеся, добавили важное)
- API‑тесты: 70 → 164
- Unit‑тесты: 131 → 315
Вывод: сместили фокус на низкие уровни, регресс ускорили в 3 раза. Также мы получили выхлоп в виде сокращения ЧЧ QA в 10 раз. Т.к. инженерам по обеспечению качества оставалось только проверить приоритетные роуты в Pyro. 5. Интересные нюансы разработки и тестирования для SEO Особенность нашего приложения — BFF Фронтенд Иви имеет полностью самописный SSR с клиентскими переходами. Переходы на клиенте сделаны при помощи BFF, отдающим данные для запрашиваемой страницы. BFF запрос может не получить информацию о языке и отдать данные на русском. А это может подпортить пользовательский опыт из-за частичного перевода некоторых блоков на страницах. Решение: Добавили язык в роут для всех внутренних запросов к BFF. Проблемы с машинным переводом Переводя большие объемы данных на новый язык мы столкнулись с проблемой, что SmartCat, умеющий работать с латиницей в обычных названиях и ссылках, не корректно считывал поле, содержащее список ссылок подряд. Он переводил его вместе с основным текстом, что приводило к ошибке 404 при переходах на клиенте. Решение: Проконсультировавшись с руководителем SEO было принято решение вырезать это поле во всех переводах. Дополнительно нам помог скрипт анализа кириллицы в финальных переводах из пункта 3.3. Пример покрытия для тестирование рекурсивного обхода и слияния данных Рассмотрим на примере одного запроса: /group/category/{category}. Для начала пишем кейсы на основе данных, багов и пользовательских сценариев. Главные правила:Сначала постарайтесь ответить на вопрос "на какой уровень будет тест, если я его напишу?" и выбирайте уровень как можно ниже.

Рис.4. Кейс для итеграционного теста выше После всех мучений с кейсами создаётся задача на покрытие в коде, и если вы готовы писать тесты и умеете это делать, то не стесняясь приступайте! В нашем случае тесты написаны с использованием фреймворка phpunit.

Рис.5. Пример интеграционного теста 5. Заключение Количественные изменения:
- Тестовое покрытие — восстановили пирамиду тестирования (см. таблицу ниже)
- Мусор в Pyro-updater — больше нет дублирующих и неиспользуемых файлов. Вес снизился с ~550 до ~400 МБ
Качественные изменения:
- Pyro теперь мультиязычный — поддерживает ru, uz и готов к быстрому добавлению новых языков
- Понимание сервиса — у команды появилась документация и тестовое покрытие
- Стабильность — добавили в CI тесты, релизы перестали быть русской рулеткой благодаря тестовому покрытию
 Наш главный неочевидный совет, который мы поняли к концу проекта: Не пытайтесь понять весь легаси-сервис целиком. Вместо этого:
- Найдите одну самую важную страницу/ручку, которую сервис обслуживает (у нас — карточка контента)
- Напишите один сквозной тест на неё (запрос → ответ → отображение)
- Зафиксируйте результат как эталон
- Только потом расширяйтесь на другие роуты
Мы потратили 3 недели на полный разбор всех роутов и поняли, что 70% времени ушло на то, что почти не используется. Если бы начали с карточки контента, то сократили бы исследование в 2 раза. Как итог: архив знаний по Pyro был воскрешён. Сервис заговорил на узбекском, а в перспективе — и на других языках. Теперь команда не боится его, готова к активному развитию и обновлению сервиса. Теперь у нас есть тесты, документация и CI. Сервис больше не чёрный ящик. Это история о том, что даже с самым забытым legacy-сервисом можно подружиться. Нужно только пройти через боль, гнев и тонны тестов. И помните: если у вас есть сервис, который работает, но его все боятся трогать, то велика вероятность, что однажды это придется сделать именно вам. А теперь вопрос к вам: какой самый древний язык/фреймворк вы оживляли? Сколько лет было сервису? И главное — вы всё переписали или оставили как есть? Жду в комментариях ваши истории и стадии принятия. В общем, попрощаемся, как обычно: не стойте на месте, с удовольствием изучайте новое и улучшайте себя! До новых встреч на Хабре!-Источник
|