Если ваш публичный проект может похвастаться внушительной аудиторией, вашими пользователями точно заинтересуются рекламные системы. Бизнес рано или поздно это замечает и закономерно пытается извлечь некоторые профиты.

По стечению обстоятельств большую часть времени я занимался разработкой интерфейсов для рекламных сервисов в hh.ru. В этой статье вы не найдете описания каких-то прорывных технологий, данный материал скорее о том, как построить взаимодействие с подрядчиками (в контексте внутренней рекламной системы) и не посыпаться кукушечкой.

Как всё устроено

Итак, заказчик приходит к команде и произносит заветное слово "баннеры". Тимлид ищет время в своем календаре для очередной встречи, и начинают формироваться требования.

Баннер в web можно охарактеризовать как "независимый блок верстки рекламного характера", что звучит довольно размыто. Проще говоря, в блок может "прилететь" что угодно, и надо быть к этому готовым. Конечно, вы можете формализовать сущность баннера до состояния url/image/description. Мы так и сделали, но практика показала, что этого мало, поэтому в крутилке появился тип баннера iframe.

Sounds like a plan
Sounds like a plan

Пока backend-разработчик думал как держать большой RPS, frontend занимался внедрением новой логики в web-приложение. Время шло, WoW изменился до неузнаваемости, а реклама выделилась в отдельную команду Технического департамента. Так со временем мы пришли к следующей архитектуре:

Страница приложения представляет собой совокупность экранов для разных вьюпортов. В случае hh возможно наличие до 4-х экранов (условно обозначим экраны как xs/s/m/l). Брейкпоинты нас сейчас не интересуют, важен только факт наличия сетки на CSS.

Главная hh.ru
Главная hh.ru

В свою очередь, баннеры на разных экранах (на одной странице) могут быть разные, так как заказчик хочет детализировать трафик. Причем детализировать трафик нужно еще и в контексте пользовательской роли (очередное требование заказчика).

Изначально баннеры просто описали в шаблонах, но позже мы пошли в сторону декларативности, чтобы поддержка логики не причиняла боль и страдания. В итоге каждое баннерное место на экране получило свой уникальный идентификатор. В теории по неймингу можно понять, где находится место. К примеру, serp_first_l указывает на поисковую выдачу на L экране (serp - search engine result page).

Каждому месту был поставлен в соответствие идентификатор баннера, по которому и приходит статика из баннерной системы. Вся вышеописанная логика была спрятана в препроцессор страницы и снабжена конфигом.

banners.add(
    pages=['vacancy_search_result'],
    banner_ids={
        BannerPlaces.SERP_FIRST_L: [1],
        BannerPlaces.SERP_FIRST_M: [2],
        BannerPlaces.SERP_FIRST_S: [3],
        BannerPlaces.SERP_FIRST_XS: [4],
        BannerPlaces.SERP_SECOND_L: [5],
        BannerPlaces.SERP_SECOND_M: [6],
        BannerPlaces.SERP_SECOND_S: [7],
        BannerPlaces.SERP_SECOND_XS: [8],
    },
)

Причем, конфиг — это единственное место, в котором указаны id баннеров, в коде мы оперируем только идентификаторами мест (пример выше, serp_first_l). Данная условность необходима, чтобы при смене id баннера не тратить время на разбор кода, а легким движением руки обновить конфигурацию.

В результате механизм работает следующим образом: пользователь заходит на страницу, препроцессор обрабатывает конфиг и пушит в global store баннерный setup, а на основе setup-а идем в баннерную систему и отрисовываем на клиенте ответ.

Как вы могли заметить, интерфейсная сетка на CSS. То есть ноды с баннерами для всех четырех экранов страницы будут находится в dom дереве сразу, что искажает статистику просмотров. А это уже проблема. Нужно явным образом учитывать видимость баннерного места и запрашивать баннер только в случае, если пользователь видит нужную ноду.

Проблема решается достаточно просто: подписываемся на ресайз окна и проверяем размеры нужного элемента (как вариант, callback можно задебаунсить, чтобы не срабатывал слишком часто). Если размеры элемента не нулевые, вызываем callback, в котором идем AJAX-ом за статикой баннера.

const visibilityWatcher = ({ element, onVisible }) => {
    let visible = false;
    const checkVisible = () => {
        if (visible) {
            return;
        }
        visible = 
            element.offsetWidth || 
            element.offsetHeight || 
            element.getClientRects().length;
        if (visible) {
            onVisible();
            window.removeEventListener('resize', checkVisible);
            element.dataset.listenersSetted = 'false';
        }
    };
    if (element.dataset.listenersSetted !== 'true') {
        window.addEventListener('resize', checkVisible);
        element.dataset.listenersSetted = 'true';
    }
    checkVisible();
};

Вследствие внедрения данного кода появилась небольшая задержка в отрисовке рекламы (так как javascript рассчитывает видимость). И да, внимательный читатель заметит недостаток: происходит только расчет размеров элемента, но вообще пользователь может так и не доскролить до объявления.

Как уже упоминалось выше, верстка баннеров была отдана на аутсорс, либо клиенты сами могут присылать готовые баннеры. В связи с чем по соображениям безопасности вся статика из баннерки добавляется на hh.ru в iframe фиксированных размеров и предварительно проходит модерацию у отдела размещения.

Вышеописанная реализация работала достаточно долго и новых требований не поступало. Однако появлялись новые инструменты и заказчик пушил изменения.

Google Ad Manager

Чтобы был понятен контекст, Ad Manager — весьма развитый и распространенный инструмент для продажи трафика. Описывать преимущества площадки Google перед самописной системой, я думаю, бессмысленно (система аукционов, отчеты и т.д.).  После пилотного проекта DFP (устаревшее название Ad Manager) показал себя неплохо, и было решено развивать данное направление силами команды рекламы.

Особых проблем подключения GPT не было (GPT или Google Publisher Tags —  javascript библиотека для работы с Ad Manager). Конфиг баннеров немного расширили и в него добавили новый параметр, так называемый ad unit (или рекламный блок).

Из интересного, в старой баннерной системе уже был настроен водопад и переносить в DFP его хотели постепенно, а откладывать запуск в наши планы не входило. Решение было найдено достаточно быстро, в GPT реализовано событие отрисовки статики и среди параметров был признак того, что слот не заполнен. А если слот не заполнен, нужно просто сходить в старую "крутилку".

Результаты внедрения Ad Manager были впечатляющие: выручка с программатик-рекламы на целевой странице увеличилась почти на 20%. Внедрение DFP было самым большим обновлением view слоя hh.ru с точки зрения рекламы за последние несколько лет. Далее на очереди пара интересных случаев из практики сопровождения.

Где мой CPU, Chrome?!

Все началось с обращения в техническую поддержку. Исходя из туманного описания было понятно, что у пользователя сайт работает непозволительно медленно. Более того, воспроизвести проблему если и удавалось, то в несвязанных местах и в случайных клиентах. Даже har файл не помог…

Однако позже стало понятно, объединяет проблемные места только одно обстоятельство — наличие рекламы. Проблема была в коде одной из внешних площадок. Javascript съедал весь CPU в силу отсутствия оптимизации. Код проблемных баннеров снесли, а разработчикам отправили репорт с проблемой.

Ждать очередного обращения мы не собирались, и во избежание дальнейших проблем с производительностью на свет появился observer. Длительные макро задачи отслеживались, а события отправлялись в нашу аналитику. На основании событий был собран график в okmeter и настроены alerts, если такие проблемы и возникают, на них реагируют быстрее.

Блокировщик, что ты делаешь, ахаха, прекрати

Те же, там же:  всё началось с пользовательской жалобы. Соискатель не мог отправить отклик на вакансию. На этот раз ситуацию прояснил архив har. Блокировщик рекламы (опустим его название) внедрял свой код и блокировал pop-up на странице, а отклик как раз работал через всплывающее окно.

Вообще, если вы разрабатываете админку для рекламной системы (к примеру), данный кейс не самый редкий, так как верстка вашего клиента условному adblock не понравится. Однако этот случай выделяется тем, что блокировщик реагировал на слово pop-up. Просить пользователей что-то отключать на клиенте — дело гиблое, поэтому проще было поправить свой клиент.

Так ли мешает реклама?

Отношение к баннерному шуму у большинства пользователей скорее негативное (те же блокировщики появились не просто так). В свое время были проведены эксперименты с отключением рекламы на разных страницах. Несмотря на увеличение скорости рендера основного контента, метрики оставались серыми, изменения значений оставались в рамках погрешности.

Итог

На текущий момент основным инструментом продажи трафика является GAM (Google Ad Manager), внутренняя баннерная система используется для нестандартных продуктов, причем за март на нашу крутилку пришлось около полутора миллиардов запросов. Реклама прочно обосновалась в основном прайсе, за месяц отдел размещения обрабатывает порядка 300 обращений от клиентов. А это значит, что баннеры никуда не денутся и работа у команды рекламы будет всегда.