Цифра в дашборде не берётся из боевой базы — у неё свой путь. Боевая база (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, и молча менять схему события нельзя.