коротко

Цифра в дашборде не берётся из боевой базы — у неё свой путь. Боевая база (OLTP) заточена под мелкие быстрые операции (создать заказ, списать деньги), и крутить на ней тяжёлую аналитику нельзя: один отчёт «продажи за год по регионам» положит оформление заказов. Поэтому данные переливают в отдельное аналитическое хранилище (DWH/OLAP), заточенное под чтение огромных объёмов. Раньше данные чистили до загрузки (ETL), с облачными хранилищами победил обратный порядок — сначала залить как есть, потом считать на месте (ELT). Отдельный поток — продуктовые события: приложение шлёт «пользователь нажал кнопку», события едут через очередь в сырой слой, из сырого слоя строят витрины, из витрин считают метрики. Поток бывает батчевый (раз в час/сутки, просто, но с задержкой) и стриминговый (почти в реальном времени, сложнее). Роль аналитика — описать событие до релиза: имя, свойства, когда шлётся (это tracking plan / data contract) и что считается метрикой.

Однажды я гордо открыл на ретро дашборд с выручкой — и финансовый директор сказал, что цифра не сходится с его отчётом на 8%. Мы сели сверять. Оказалось, продуктовая аналитика считала «оплату» по событию payment_succeeded, которое приложение шлёт сразу после ответа платёжного шлюза, а финансы считали по фактически зачисленным деньгам — за вычетом возвратов, которые приходят на следующий день, и без тестовых платежей сотрудников, которые мы забыли отфильтровать. Обе цифры были «правильные» — просто мерили разное. Я тогда понял: между «пользователь нажал кнопку» и «число в отчёте» лежит целый конвейер, и если не знаешь, как он устроен, будешь спорить о цифрах, которые в принципе не обязаны совпадать. Эта запись — про этот конвейер: как событие в приложении превращается в метрику, и где аналитик обязан вмешаться, чтобы цифра была честной.

Сразу разведу с соседней темой, чтобы не путать. Есть наблюдаемость (логи, метрики, трейсы) — это про здоровье системы: жив ли сервис, какая задержка, сколько ошибок. А есть продуктовая аналитика — это про поведение бизнеса и пользователей: сколько людей дошли до оплаты, какой retention, выросла ли выручка. Технически они похожи (там тоже события и числа), но это разные потоки, разные хранилища и разные потребители: observability смотрит дежурный инженер ночью, продуктовую аналитику — продакт и финансы на ретро. Здесь речь про второй поток.

Боевая база против аналитической: почему нельзя считать на проде

Начнём с фундамента, который многие пропускают. Боевая база вашего приложения — это OLTP (online transaction processing, «обработка транзакций»). Она заточена под то, что делает приложение каждую секунду: создать один заказ, прочитать один профиль, списать деньги с одного счёта. Это мелкие операции, которые трогают одну-две строки, но их очень много и они должны быть быстрыми. Про устройство такой базы — таблицы, ключи, транзакции — есть отдельная запись про базы данных для аналитика.

А теперь представьте запрос для отчёта: «суммарная выручка по всем регионам за год с разбивкой по месяцам и категориям товаров». Этот запрос не трогает одну строку — он перемалывает миллионы строк за всю историю. На OLTP-базе он будет делать две неприятные вещи: во-первых, выполняться долго (минуты, иногда часы), во-вторых, блокировать ресурсы и мешать обычным операциям. Пока ваш отчёт читает таблицу заказов целиком, оформление новых заказов начинает тормозить. Один аналитик с тяжёлым SELECT может уронить продакшен — я это видел.

Поэтому для аналитики заводят отдельную базу — аналитическое хранилище данных (DWH, data warehouse), работающее в режиме OLAP (online analytical processing, «аналитическая обработка»). Это другая база, на других серверах, заточенная ровно под обратное: не мелкие частые записи, а редкие, но гигантские чтения с агрегацией. Данные туда регулярно переливают из боевой базы. Главное правило, которое надо усвоить раз и навсегда: аналитику считают на копии данных в DWH, а не на боевой базе.

OLTP (боевая база)OLAP / DWH (аналитическая)
Под что заточенамного мелких операцийредкие тяжёлые чтения
Типичный запрос«дай заказ №5012»«выручка по регионам за год»
Трогает строкодну-двемиллионы
Кто пользуетсяприложение, пользователианалитики, дашборды, отчёты
Цена ошибкиуронить отчётуронить продакшен
Как хранитпострочно (row)часто по колонкам (columnar)

Почему DWH хранит данные «по колонкам»

Маленькая, но важная деталь, объясняющая всё остальное. OLTP хранит данные построчно: весь заказ (id, клиент, сумма, дата) лежит рядом — удобно достать одну строку целиком. OLAP-хранилища часто хранят данные по колонкам: все суммы заказов лежат вместе, отдельно от дат. Для запроса «средняя сумма заказа за год» базе нужна только колонка «сумма» — она читает один компактный кусок, не трогая остальное. Отсюда колоссальная разница в скорости агрегаций. Это не магия — это разная физическая раскладка данных под разный тип нагрузки. Знать детали не обязательно, но понимать, что DWH — это другой инструмент, а не «такая же база, только побольше», полезно.

Тут же напрашивается мысль: а зачем вообще реляционка для аналитики, может NoSQL? Нет, это ортогональный выбор — про разницу есть запись про SQL и NoSQL. DWH почти всегда говорит на SQL (Snowflake, BigQuery, ClickHouse), просто это SQL поверх колоночного движка, заточенного под аналитику. «SQL против NoSQL» — про модель данных приложения; «OLTP против OLAP» — про назначение базы. Это разные оси.

ETL против ELT: почему порядок букв поменялся

Данные надо как-то перенести из боевой базы в хранилище. Исторически этот процесс называли ETL — три шага в строгом порядке:

  • E — Extract (извлечь): вытащить данные из источника (боевой базы, внешнего API, файла).
  • T — Transform (преобразовать): почистить, привести к нужному виду, посчитать агрегаты — на отдельном промежуточном сервере.
  • L — Load (загрузить): положить уже готовые, причёсанные данные в хранилище.

Логика ETL родилась в эпоху, когда хранилище было дорогим и медленным: каждый гигабайт на диске стоил денег, а вычислительная мощность хранилища была ограничена. Поэтому данные чистили и сжимали до загрузки — чтобы в дорогое хранилище попадало только нужное и уже готовое. Преобразование делал отдельный ETL-сервер.

С приходом облачных хранилищ (Snowflake, BigQuery) экономика перевернулась. Хранилище стало дешёвым, а его вычислительная мощность — огромной и эластичной. И порядок букв поменялся на ELT: сначала Load — залить сырые данные в хранилище как есть, ничего не чистя, а потом Transform — преобразовать их уже внутри хранилища его же силами, средствами SQL.

flowchart LR
  subgraph ETL["ETL — старый порядок"]
    E1["Источник"] --> T1["Преобразование
на отдельном сервере"] --> L1["Хранилище
(только чистое)"] end subgraph ELT["ELT — победил с облаком"] E2["Источник"] --> L2["Хранилище
(сырьё как есть)"] --> T2["Преобразование
внутри хранилища (SQL)"] end

На схеме два конвейера. Сверху ETL: данные преобразуются на отдельном сервере и только потом ложатся в хранилище — туда попадает лишь готовое. Снизу ELT: данные сначала льются в хранилище сырыми, а преобразование происходит уже внутри него. Разница не косметическая. В ELT сырые данные сохраняются навсегда, и если завтра понадобится посчитать метрику по-новому — это можно сделать на исторических данных, потому что они никуда не делись. В ETL то, что не положили в хранилище при загрузке, потеряно: чтобы пересчитать иначе, нужно перезаливать всё с нуля из источника.

Почему ELT выиграл — одной фразой

В ETL вы решаете, что вам понадобится, заранее (и выбрасываете остальное при загрузке). В ELT вы сохраняете всё сырьё и решаете, как его считать, потом — столько раз, сколько надо. Когда хранилище стало дешёвым, «сохранить всё на всякий случай» перестало быть роскошью, и гибкость ELT победила. Цена ELT — в хранилище лежит гора сырых данных, в том числе мусора, и порядок в преобразованиях надо наводить дисциплиной, а не самим фактом загрузки.

От события к метрике: четыре слоя

Теперь главное — как из действия пользователя рождается цифра. Перелив из боевой базы (выше) — это один источник данных для аналитики. Но самый ценный для продукта источник — продуктовые события: трекинг того, что пользователь делает. Это отдельный поток, и устроен он как конвейер из четырёх слоёв.

flowchart LR
  U["Пользователь
нажал кнопку"] --> SDK["SDK / трекер
шлёт событие"] SDK --> Q["Очередь
(транспорт)"] Q --> RAW["Сырой слой
(события как есть)"] RAW --> DM["Витрины
(агрегаты под отчёты)"] DM --> M["Метрики
дашборд, A/B"]

Разберём схему слева направо, потому что каждый слой — это отдельная зона ответственности.

1. Сбор (SDK / события). Пользователь нажал «Оформить заказ». В код приложения встроен трекер — SDK продуктовой аналитики (Amplitude, тот же self-hosted сборщик или внутренний). Он формирует событие: имя (checkout_started), свойства (сумма корзины, число товаров, способ оплаты) и метаданные (кто, когда, с какого устройства). Это и есть тот самый момент, где работа аналитика начинается, — событие надо описать до того, как разработчик его зашлёт. Про природу событий как фактов о прошлом — в записи про событийную архитектуру.

2. Транспорт (очередь). События не пишут напрямую в хранилище — их складывают в очередь (Kafka и подобные). Зачем посредник: событий в пике могут быть миллионы в секунду, и если каждое сразу синхронно писать в базу, она захлебнётся. Очередь принимает поток мгновенно и отдаёт его потребителям в их темпе — это классическая асинхронная развязка, разобранная в записи про синхронную и асинхронную интеграцию. Тот же приём, что развязывает сервисы, здесь развязывает «приложение генерит события» и «хранилище их переваривает».

3. Сырой слой (raw). Из очереди события льются в хранилище в первозданном виде — таблица «как пришло, так и записали», без причёсывания. Это нижний слой DWH. Тут события дублируются, лежат вперемешку, могут быть кривыми — и это нормально, сырой слой и должен хранить правду как есть. Именно здесь живёт принцип ELT: сначала сохрани всё, потом разбирайся.

4. Витрины (data marts). Поверх сырого слоя строят витрины — причёсанные таблицы под конкретные вопросы. «Воронка оформления заказа по дням», «выручка по регионам», «retention по когортам». Витрина — это результат преобразования (тот самый T из ELT): из миллионов сырых событий посчитан компактный агрегат, по которому дашборд отвечает мгновенно. Метрика, которую видит продакт, — это чтение из витрины, а не пересчёт сырья на лету.

И только в самом конце — метрика: DAU, конверсия, retention, результат A/B-теста. Всё это разобрано в записи про продуктовые метрики, воронки и A/B — она про потребителя этого потока. Метрики не существуют сами по себе: за каждой цифрой на дашборде стоит цепочка событие → очередь → сырой слой → витрина. Когда метрика «сломалась», чинить идут вверх по этой цепочке.

Метрика наследует структуру модели

Витрины — это не «магия дата-инженеров», это моделирование данных под чтение, ровно как описано в записи про уровни модели данных, только на аналитической стороне. Самая частая раскладка DWH — «звезда»: в центре таблица фактов (событий-измерений: каждая строка — одна оплата с суммой), вокруг таблицы измерений (кто клиент, какой товар, какой регион). Если факт и измерения смоделированы криво — метрика будет криво считаться, и никакой красивый дашборд это не спасёт. Структура данных первична, цифра вторична.

Батч против стриминга: свежесть против простоты

Главный архитектурный выбор в дата-потоке — как часто данные едут по конвейеру.

Батч (пакетная обработка) — данные обрабатывают порциями по расписанию: раз в час, раз в ночь. Накопили события за период, разом прогнали через конвейер, обновили витрины. Это просто, дёшево и надёжно: упал ночной расчёт — перезапустил, никто не заметил. Цена — задержка: метрика на дашборде отражает реальность не «сейчас», а на момент последнего расчёта. Утром вы видите вчерашние цифры. Для большинства продуктовых отчётов это абсолютно нормально — выручку за месяц не надо знать с точностью до секунды.

Стриминг (потоковая обработка) — события обрабатываются по мере поступления, метрика обновляется почти в реальном времени. Дашборд показывает, что происходит прямо сейчас. Цена — сложность: потоковый конвейер труднее построить, отладить и удержать в рабочем состоянии; обработка «на лету» порождает кучу краевых случаев (что делать с опоздавшим событием, как считать «окно» по времени). Это дороже и в разработке, и в эксплуатации. И вторая, менее очевидная цена: потоковая метрика приближённа — она считается по временным окнам, и опоздавшие события сдвигают уже показанные цифры. То, что вы видели минуту назад, может задним числом измениться: батчевая цифра «вчера к утру» окончательна, потоковая «прямо сейчас» — всегда черновик.

БатчСтриминг
Когда считаетсяпо расписанию (час/сутки)по мере поступления
Свежесть метрикис задержкойпочти в реальном времени
Сложностьнизкаявысокая
Когда уместенотчёты, дашборды, BIантифрод, мониторинг, лента «прямо сейчас»

Не платите за стриминг, если не нужна свежесть

Типичная ошибка — захотеть «реал-тайм дашборд», потому что звучит круто. Спросите: что изменится в решении, если цифра обновится не сейчас, а через час? Если ничего (а для квартального отчёта о выручке — ничего) — берите батч, он в разы проще и дешевле. Стриминг оправдан там, где задержка реально стоит денег: антифрод (мошенника надо поймать до того, как он увёл деньги), динамические лимиты, биржевые данные. Свежесть данных — это нефункциональное требование с числом: не «реал-тайм», а «метрика должна отставать не более чем на N минут». Без числа этот спор не закрыть.

Грязная правда дата-потока: дубли, опоздания, расхождения

Теперь честно про то, о чём не пишут в маркетинговых статьях про «современный data stack». Дата-поток — это распределённая асинхронная система, и она наследует все её болезни.

Дубли событий. Очередь (Kafka и почти все остальные) гарантирует доставку «хотя бы один раз» (at-least-once) — то есть одно и то же событие может прийти дважды. Сеть моргнула, SDK не получил подтверждение и переслал; консьюмер обработал событие, упал до того, как отметил его прочитанным, и при перезапуске обработал снова. Если наивно посчитать «число оплат = число событий payment_succeeded», вы получите завышенную цифру. Лекарство то же, что в платежах, — идемпотентность и дедупликация: у каждого события должен быть уникальный event_id, и при построении витрины дубли по этому id схлопываются. Идемпотентность нужна не только в боевой логике (про неё — в записи про идемпотентные платежи), но и в аналитике: посчитать событие ровно один раз — это та же задача, что списать деньги ровно один раз. Важная оговорка: дедупликация спасает, только если event_id стабилен — то есть сгенерён на клиенте один раз и не меняется при переотправке. Если SDK на каждый ретрай выписывает новый id, дубли станут неотличимы от разных событий, и схлопывать будет нечем — поэтому «генерь id заранее, до отправки» это тоже требование аналитика к событию.

Без event_id метрики врут в плюс

Я видел дашборд, где конверсия в оплату скакала на 15% день ото дня без всякой причины — оказалось, при сбоях консьюмер переобрабатывал пачки событий, и в витрину попадали дубли. Цифра была не «примерно правильной с шумом», а систематически завышенной в непредсказуемые дни. Правило: любое продуктовое событие обязано нести уникальный идентификатор, по которому его можно дедуплицировать. Это требование к событию, и закладывает его аналитик в tracking plan, а не дата-инженер постфактум.

Опоздавшие события (late-arriving data). Пользователь оформил заказ в мобильном приложении в метро без сети — событие отлежалось в офлайн-буфере SDK и доехало через два часа, уже задним числом. Для батча это вопрос: пересчитывать ли вчерашнюю витрину, когда «дозрели» вчерашние события? Обычно да — поэтому вчерашние цифры могут слегка измениться сегодня, и это не баг. Чтобы не пересчитывать историю бесконечно, вводят watermark — границу «данные до этого момента считаем окончательными»; всё, что опоздало за неё, либо отбрасывают, либо собирают в отдельный поздний пересчёт. А сама возможность переиграть вчерашнюю витрину держится на том, что сырой слой и очередь переигрываемы: в Kafka событие не исчезает после чтения, и поток можно перечитать с нужного места (по offset). Это же свойство — почему в ELT не страшно «залить сырьё»: нашли ошибку в логике расчёта — перестроили витрину из сырого слоя заново, не теряя данных.

Расхождение продуктовых и финансовых цифр. Та самая боль из начала. Продуктовая аналитика и финансовый/бухгалтерский учёт почти всегда дают разные числа по «одному и тому же» — и это нормально, потому что они мерят разное:

  • Продуктовое payment_succeeded срабатывает в момент ответа шлюза; финансы считают деньги после фактического зачисления и сверки.
  • Продукт может не вычитать возвраты и чарджбэки, которые приходят позже; финансы вычитают.
  • Продукт ловит тестовые платежи сотрудников, фрод и отменённые транзакции; финансовый контур их фильтрует.
  • Часовые пояса: продукт режет сутки по UTC, финансы — по местному времени.

Вывод не «кто-то ошибся», а «это два разных источника истины для двух разных задач». Финансовая цифра — для отчётности и налогов, она должна быть до копейки и юридически точной. Продуктовая — для понимания поведения и трендов, ей важнее скорость и полнота сигнала, чем копеечная точность. Аналитик обязан знать, какой контур для какого вопроса авторитетен, и не пытаться свести их в одну цифру там, где они принципиально разные.

Что меняется для аналитика: tracking plan и data contract

Самая частая иллюзия джуна — что события «само собой логируются», а метрики «потом посчитают». Нет. Если событие не описано и не зашито в код до релиза — данных не будет, и посчитать будет нечего, как в истории из записи про метрики, где фичу выкатили без сбора событий. Описать событие — прямая работа аналитика, и у неё есть конкретный артефакт.

Tracking plan (план трекинга) — это документ-реестр всех продуктовых событий. По каждому событию аналитик фиксирует:

  • Имя — в прошедшем времени, как факт: checkout_started, payment_succeeded. Имя — это публичный контракт, менять его потом больно (сломаются все витрины и исторические отчёты, которые на него опираются).
  • Свойства (properties) — какие поля несёт событие и какого они типа: сумма (число), способ оплаты (строка из фиксированного списка), число товаров (целое). Это схема события (event schema).
  • Когда шлётся — точный момент срабатывания. «payment_succeeded — это когда нажали кнопку, когда шлюз ответил ОК или когда деньги зачислены?» От ответа зависит вся метрика. Неоднозначность здесь и есть корень расхождения цифр.
  • Что считается метрикой — как из этого события (или нескольких) получается число на дашборде. «Конверсия в оплату = уникальные пользователи с payment_succeeded / уникальные с checkout_started за тот же день».

Когда tracking plan становится формальным соглашением между теми, кто шлёт события (разработчики), и теми, кто их потребляет (аналитики), его называют data contract (контракт на данные). Идея ровно та же, что у контракта API: продюсер обещает слать событие с такой-то схемой, потребитель на это полагается, и сломать схему молча — значит сломать всё, что вниз по потоку. Событие — это такой же контракт, как эндпоинт REST, просто его потребитель не другой сервис, а аналитический конвейер.

Как это спрашивают на собесе

Джуну достаточно развести два понятия и не перепутать базы. Сильный ответ: «Аналитику нельзя считать на боевой базе — она OLTP, заточена под мелкие операции, тяжёлый отчёт её положит; для этого есть отдельное хранилище DWH/OLAP. Данные туда переливают, а продуктовые события собирают через SDK и очередь». Если добавите «а ETL — это когда чистим до загрузки, ELT — заливаем сырьё и считаем внутри хранилища» — отлично.

Мидла гоняют по сквозному пути и по граблям. Спросят: «Опишите, как клик пользователя становится цифрой в дашборде» — ждут цепочку SDK → очередь → сырой слой → витрина → метрика. Дальше ловушки: «События приходят дважды, как считать оплаты?» (ответ — event_id и дедупликация, очередь даёт at-least-once). «Почему ваша выручка не сходится с финансовой?» (разные источники истины: момент срабатывания события, возвраты, тестовые платежи, часовые пояса — и это нормально, а не баг). «Реал-тайм или батч?» — сильный ответ начинается с вопроса «а нужна ли свежесть для этого решения», а не с «конечно стриминг». «Событие приехало с опозданием на два часа — что с метрикой?» (опоздавшие данные: вчерашнюю витрину пересчитывают, а границу окончательности задаёт watermark — поэтому вчерашние цифры могут задним числом измениться, и это не баг). И финальное: «Кто описывает события?» — аналитик, в tracking plan / data contract, до релиза.

Откуда это взялось

Концепцию хранилища данных в конце 1980-х оформил Билл Инмон («отец DWH»), а Ральф Кимбалл в 1996 году предложил размерное моделирование — те самые «звёзды» с таблицами фактов и измерений, на которых DWH стоит до сих пор. Десятилетиями царил ETL с тяжёлыми коробочными инструментами (Informatica), потому что хранилища были дорогими. Перелом случился в 2010-х с облаком: Amazon Redshift (2012), затем Snowflake и Google BigQuery сделали хранилище дешёвым и эластичным — и порядок перевернулся на ELT. Тогда же выстрелил dbt (data build tool), превративший «T» в дисциплину поверх SQL. Параллельно выросла продуктовая аналитика как отдельный мир: Mixpanel и Amplitude (начало 2010-х) приучили команды думать событиями и tracking plan'ами, а Kafka (открыта LinkedIn в 2011-м) стала стандартным транспортом для потоков событий. Свежая волна — data contracts (около 2022–2023): попытка применить к данным ту же контрактную дисциплину, что давно есть у API, потому что молча сломанная схема события — это такой же инцидент, как сломанный эндпоинт.

Частые вопросы

Почему нельзя считать аналитику прямо на боевой базе?

Боевая база (OLTP) заточена под много мелких быстрых операций — создать заказ, прочитать профиль. Аналитический запрос («выручка по регионам за год») перемалывает миллионы строк, выполняется минутами и блокирует ресурсы, мешая обычным операциям. Один такой SELECT может уронить продакшен. Поэтому данные переливают в отдельное аналитическое хранилище (DWH/OLAP), заточенное под редкие, но тяжёлые чтения, и считают аналитику там — на копии, а не на боевой базе.

Чем ETL отличается от ELT и почему ELT победил?

В ETL данные сначала преобразуют (чистят, агрегируют) на отдельном сервере и только потом загружают в хранилище — туда попадает лишь готовое. В ELT данные сначала загружают в хранилище сырыми, а преобразуют уже внутри него средствами SQL. ELT выиграл, когда облачные хранилища (Snowflake, BigQuery) стали дешёвыми и мощными: стало выгодно хранить всё сырьё и пересчитывать его как угодно потом, а не выбрасывать данные при загрузке. Цена ELT — в хранилище копится гора сырых данных, и порядок наводят дисциплиной преобразований.

Как клик пользователя превращается в цифру на дашборде?

Через конвейер из четырёх слоёв. Сбор: SDK в приложении формирует событие (имя, свойства, время) и отправляет его. Транспорт: событие попадает в очередь (Kafka), которая разгружает поток. Сырой слой: события льются в хранилище как есть, без чистки. Витрины: поверх сырья строят причёсанные агрегаты под конкретные вопросы (воронка, выручка по дням). И только из витрины читается метрика — DAU, конверсия, retention. Когда метрика «сломалась», причину ищут вверх по этой цепочке.

Почему продуктовая выручка не сходится с финансовой?

Потому что это два разных источника истины, которые мерят разное. Продуктовое событие срабатывает в момент ответа платёжного шлюза; финансы считают деньги после фактического зачисления и сверки, вычитают возвраты и чарджбэки, фильтруют тестовые платежи сотрудников и фрод, могут резать сутки по другому часовому поясу. Финансовая цифра — для отчётности, она юридически точная; продуктовая — для понимания поведения, ей важнее полнота и скорость сигнала. Это не баг: аналитик должен знать, какой контур авторитетен для какого вопроса, и не сводить их в одно число там, где они принципиально разные.

Батч или стриминг — что выбрать?

Спросите, что изменится в решении, если метрика обновится не сейчас, а через час. Если ничего — берите батч: обработка порциями по расписанию проще, дешевле и надёжнее, а задержка для большинства отчётов несущественна. Стриминг (обработка почти в реальном времени) оправдан только там, где задержка реально стоит денег: антифрод, динамические лимиты, мониторинг «прямо сейчас». Свежесть данных — это нефункциональное требование с числом: формулируйте не «реал-тайм», а «метрика отстаёт не более чем на N минут».

Что аналитику закладывать в требования про события?

Описать каждое событие в tracking plan (плане трекинга) до релиза: имя в прошедшем времени (checkout_started), свойства и их типы (схема события), точный момент срабатывания (когда именно считается «оплата прошла») и как из события получается метрика. Обязательно — уникальный event_id для дедупликации, потому что очередь доставляет события «хотя бы один раз» и дубли завышают цифры. Когда этот план становится соглашением между разработчиками (шлют события) и аналитиками (потребляют), его называют data contract — это такой же контракт, как эндпоинт API, и молча менять схему события нельзя.