У каждого второго разработчика или 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-данные для конкретной сущности. Ниже представлен пример такого файла для главной страницы Иви:

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


2. План спасения
Вместо одной большой задачи «Добавить мультиязычность в Pyro» мы создали эпик с чёткими шагами:
-
Исследование
Разработчик: разобраться в логике работы, поднять локально
QA: провести аудит тестового покрытия, изучить баги за год, собрать список роутов Pyro по приоритету
-
Чистка данных
Проверить на наличие неиспользуемых файлов
-
Автоматизация процессов
Подготовить скрипты для машинного перевода, поиска в базе файлов дубликатов и поиска отличий внутри языковых директорий
-
Добавление 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 → найти
<script type="application/ld+json">Или расширение 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 → найти
<html lang="...">или<meta property="og:locale">Либо вручную посмотреть исходный код страницы
На что смотреть:
Атрибут
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-параметры — проверки не привязанные к роуту. Проверяются на списке роутов
Негативные проверки — общие негативные проверки, подходящие любому роуту
-
Роуты — кейсы учитывающие особенности одного отдельно взятого роута или его слияние данных с другим роутом
/route_1
/route_2
-
Такая иерархия решает сразу несколько проблем в покрытии:
Удобство прохождения ручных проверок и понятная параметризация в каждом сценарии. Теперь все кейсы, где неважен роут, а важен 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}. Для начала пишем кейсы на основе данных, багов и пользовательских сценариев. Главные правила: Сначала постарайтесь ответить на вопрос "на какой уровень будет тест, если я его напишу?" и выбирайте уровень как можно ниже.

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

5. Заключение
Количественные изменения:
Тестовое покрытие — восстановили пирамиду тестирования (см. таблицу ниже)
Мусор в Pyro-updater — больше нет дублирующих и неиспользуемых файлов. Вес снизился с ~550 до ~400 МБ
Качественные изменения:
Pyro теперь мультиязычный — поддерживает ru, uz и готов к быстрому добавлению новых языков
Понимание сервиса — у команды появилась документация и тестовое покрытие
Стабильность — добавили в CI тесты, релизы перестали быть русской рулеткой благодаря тестовому покрытию

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