
Я продолжаю развивать свой небольшой проект рекламного сервиса ads.coffee. Сегодня я хочу добавить в сервис логику самой простой медиации из всех возможных — можно будет настраивать, какая часть трафика будет уходить на мои рекламные объявления, а какая — на Яндекс.Рекламу.
Медиация позволит запускать как рекламу из сторонних рекламных сетей, так и мои прямые рекламные объявления из ads.coffee. Получится AdFox на минималках. Кстати, AdFox стал платным, поэтому если захотите создать свой стартап и сделать "убийцу" AdFox — напишите мне.
Всё это будет работать на сайте kodikapusta.ru. Я не хочу усложнять использование этой логики: чтобы подключить медиацию на сайт, достаточно добавить небольшой JS-код:
<script>
window.adsCoffeeMediationCb = window.adsCoffeeMediationCb || [];
</script>
<script src="https://ads.coffee/static/js/mediation.js"></script>
<div id="coffee-block"></div>
<script>
window.adsCoffeeMediationCb.push(() => {
window.adsCoffeeMediation.render({
block: 'u8bhev85pd3xh65',
renderTo: 'coffee-block'
});
});
</script>
В этом небольшом скрипте мы сначала подключаем на страницу рекламный код, указываем div
для отображения рекламы и вызываем функцию, которая отобразит рекламу в указанном блоке.
Скрипт
Внутри скрипт реализован достаточно просто. Сейчас моя медиация поддерживает только мою собственную рекламу и рекламу от Яндекса, поэтому нужно подключить соответствующие скрипты:
loadScript('https://yandex.ru/ads/system/context.js');
loadScript('https://ads.coffee/static/js/coffee.js');
// Инициализируем глобальные объекты и очереди колбэков
window.yaContextCb = window.yaContextCb || [];
window.adsCoffeeCb = window.adsCoffeeCb || [];
После этого отправляем запрос на бэкенд, чтобы определить, какую сеть использовать для показа рекламы:
fetch(`https://ads.coffee/mediation/${options.block}`)
.then(response => {})
С бэкенда приходит JSON, в котором указаны рекламная сеть (network
) и идентификатор блока (data.block
):
{
"unit": "3qj9qddp1n4e08q",
"network": "yandex",
"data": {
"block": "R-A-14476736-1"
}
}
unit
— это сущность, в которой хранятся настройки блока из рекламной сети. То есть для одного блока можно настроить несколько юнитов, и в каждом указать данные для отображения рекламы. Позже я покажу, как юниты добавляются в рекламный блок медиации.
Теперь можно вызвать код отрисовки рекламы из той сети, которую вернул бэкенд:
if (data.network === 'yandex') {
window.yaContextCb.push(() => {
Ya.Context.AdvManager.render({
blockId: data.data.block,
renderTo: options.renderTo
});
});
} else if (data.network === 'coffee') {
window.adsCoffeeCb.push(() => {
window.adsCoffee.render({
template: "horizontal",
renderTo: options.renderTo,
blockId: data.data.block
});
});
}
Таким образом можно подключать сколько угодно рекламных сетей, которые будут участвовать в медиации.
Давайте разберёмся, как устроен выбор сети на бэкенде.
Модель данных и выбор сети
Основная рабочая структура — это блок. В нём указываются название, сайт, пользователь, создавший блок, и список юнитов:
type Block struct {
Id string
Name string
Site string
User string
Units []Unit
}
Самый важный элемент — список юнитов, которые могут выиграть в "аукционе" на этом блоке.
Структура юнита выглядит так:
type Unit struct {
Id string
Name string
User string
Site string
Data map[string]interface{}
Network string // рекламная сеть
Weight int // вес для распределения трафика
}
Помимо названия, сайта и пользователя, здесь есть три ключевых параметра:
Data
— словарь с настройками для работы рекламной сети.Network
— название рекламной сети (например,yandex
илиcoffee
).Weight
— вес юнита, который используется в алгоритме распределения трафика.
Выбор юнита
Сейчас реализована простейшая логика выбора — взвешенный рандом:
import "github.com/mroth/weightedrand/v2"
// ...
block, err := h.blocks.One(id)
if err != nil {
h.app.App.Logger().Warn("error on found block", "block", id, "err", err)
return err
}
elements := []weightedrand.Choice[models.Unit, int]{}
for _, unit := range block.Units {
elements = append(elements, weightedrand.NewChoice(unit, unit.Weight))
}
chooser, err := weightedrand.NewChooser(elements...)
if err != nil {
return err
}
winner := chooser.Pick()
Я использую библиотеку weightedrand, в которой уже реализован алгоритм взвешенного выбора. Остаётся только вернуть JSON для mediation.js
:
type MediationResponse struct {
Unit string `json:"unit"` // ID юнита
Network string `json:"network"` // рекламная сеть
Data map[string]interface{} `json:"data"` // данные для рендеринга
}
return e.JSON(http.StatusOK, MediationResponse{
Unit: winner.Id,
Network: winner.Network,
Data: winner.Data,
})
На этом основная логика медиации готова. Нужно добавить интерфейс для сервиса ads.coffee, но три часа подошли к концу — интерфейсы будут в следующей серии.
Планы по доработке
В формате трёх часов я успел не всё, но у меня есть идеи для улучшения:
Интерфейс
Сейчас блоки и юниты настраиваются через админку, но нужен удобный UI в самом сервисе.
Аналитика
Хочется собирать статистику не только по своей рекламе, но и по сторонним сетям. Все данные — в ClickHouse, с графиками в интерфейсе.
Таргетинг
Сейчас трафик распределяется случайно, но можно добавить правила: например, показывать Яндекс.Рекламу только пользователям Android.
Оптимизация показов
Чтобы максимизировать прибыль, нужно показывать самую дорогую рекламу. Пока можно начать с простого расчёта eCPM на основе данных, выгружаемых по API из рекламных сеток.
Заключение
Это были интересные три часа. Надеюсь, вам тоже было познавательно. Посмотреть на работу сервиса можно на ads.coffee или прямо на этой странице.