У каждого второго разработчика или 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 а при запросе сервис собирает все ключи от общего к частному и склеивает ответы. Это порождает три классических эффекта:

  1. Протекание данных — общие ключи затирают частные

  2. Неявный приоритет — порядок обхода жёстко зашит в коде

  3. Трудность с новыми параметрами — они ломают всю логику мержа

Логика работы 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 данными
Рис.1. Файл с SEO данными

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

Рис.2. Silex-диаграмма работы сервиса со стороны редактора SEO-данных.
Рис.2. Silex-диаграмма работы сервиса со стороны редактора SEO-данных.
Рис.3. Silex-диаграмма работы сервиса со стороны пользователя Иви.
Рис.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. Мы написали скрипты для выполнения следующих задач:

  1. Подготовка данных для загрузки в SmartCat. Скрипт рекурсивно собирает большой объём глубоко вложенных файлов в единую структуру, делит их на мелкие чанки для загрузки в SmartCat (мы не можем загнать огромный объем данных за раз)

  2. Подготовка полученных от SmartCat переводов. Скрипт соединяет разделенные ранее блоки в единый файл и добавляет параметр языка переведенным данным

  3. Скрипт, сравниващий структуры данных в разных языковых директориях и копирующий недостающие файлы. Так мы решили проблему ручного ввода SEO-данных админом

  4. Проверка отсутствия кириллицы в финальных переводах для анализа корректности проделанной работы

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)

На что смотреть:

  • Тип разметки соответствует содержимому страницы (MovieTVSeriesPerson и т.д.)

  • Обязательные поля (nameurlimage) не пустые

  • Нет синтаксических ошибок в 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

Такая иерархия решает сразу несколько проблем в покрытии:

  1. Удобство прохождения ручных проверок и понятная параметризация в каждом сценарии. Теперь все кейсы, где неважен роут, а важен GET-параметр, вынесены в один блок.

  2. Только важные пользовательские сценарии внутри директорий по роутам. Что сокращает количество сценариев, а значит ускоряет регресс.

  3. Понятное распределение кейсов по уровням пирамиды и полноценное понимание уровня покрытия сервиса.

Такой комплексный подход в тестировании помогает опираться не только на знания QA, но и на реальные данные, а соответственно строить более устойчивое тестовое покрытие.

Автоматизация рутинных проверок

Автоматизация — одно из прекраснейших созданий разработки. Поэтому не пренебрегайте ею. Вместо ручной проверки метатегов расширением, мы написали скрипт, который:

  1. Ходит по списку важных страниц

  2. Запрашивает данные из Pyro для разных языков

  3. Сравнивает с эталонными шаблонами

  4. Формирует отчёт в 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. Кейс для итеграционного теста выше
Рис.4. Кейс для итеграционного теста выше

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

Рис.5. Пример интеграционного теста
Рис.5. Пример интеграционного теста

5. Заключение

Количественные изменения:

  1. Тестовое покрытие — восстановили пирамиду тестирования (см. таблицу ниже)

  2. Мусор в Pyro-updater — больше нет дублирующих и неиспользуемых файлов. Вес снизился с ~550 до ~400 МБ

Качественные изменения:

  1. Pyro теперь мультиязычный — поддерживает ru, uz и готов к быстрому добавлению новых языков

  2. Понимание сервиса — у команды появилась документация и тестовое покрытие

  3. Стабильность — добавили в CI тесты, релизы перестали быть русской рулеткой благодаря тестовому покрытию

Наш главный неочевидный совет, который мы поняли к концу проекта:

Не пытайтесь понять весь легаси-сервис целиком. Вместо этого:

  1. Найдите одну самую важную страницу/ручку, которую сервис обслуживает (у нас — карточка контента)

  2. Напишите один сквозной тест на неё (запрос → ответ → отображение)

  3. Зафиксируйте результат как эталон

  4. Только потом расширяйтесь на другие роуты

Мы потратили 3 недели на полный разбор всех роутов и поняли, что 70% времени ушло на то, что почти не используется. Если бы начали с карточки контента, то сократили бы исследование в 2 раза.

Как итог: архив знаний по Pyro был воскрешён. Сервис заговорил на узбекском, а в перспективе — и на других языках. Теперь команда не боится его, готова к активному развитию и обновлению сервиса. Теперь у нас есть тесты, документация и CI. Сервис больше не чёрный ящик. Это история о том, что даже с самым забытым legacy-сервисом можно подружиться. Нужно только пройти через боль, гнев и тонны тестов. И помните: если у вас есть сервис, который работает, но его все боятся трогать, то велика вероятность, что однажды это придется сделать именно вам.

А теперь вопрос к вам: какой самый древний язык/фреймворк вы оживляли? Сколько лет было сервису? И главное — вы всё переписали или оставили как есть? Жду в комментариях ваши истории и стадии принятия. В общем, попрощаемся, как обычно: не стойте на месте, с удовольствием изучайте новое и улучшайте себя! До новых встреч на Хабре!

Комментарии (1)


  1. Dhwtj
    04.05.2026 14:32

    1. Найдите одну самую важную страницу/ручку, которую сервис обслуживает (у нас — карточка контента)

    2. Напишите один сквозной тест на неё (запрос → ответ → отображение)

    3. Зафиксируйте результат как эталон

    4. Только потом расширяйтесь на другие роуты

    Это прямо по Физерсу

    Эффективная работа с унаследованным кодом