Files
ddadmin 20071c565f feat(dm): добавлена сверка покупок с заказами
- Зачем:
  - показать расхождения трекера и бэкенда на зерне заказа
  - сохранить отложенные покупки наблюдаемыми до следующего слепка
- Что:
  - добавлены таблицы, представление и пересборка purchase_vs_orders
  - сумма покупки нормализована в Decimal на границе ODS
  - обновлены договоры и указатели на механику пересборки
- Проверка:
  - `make config-test`, `make lint`
  - `cd generator && make docs lint typecheck test`
  - `make rebuild-storage`, явные `dds_transform` и `dm_transform`
  - `make smoke`, `make check-clickhouse`, `make check-services`
2026-08-27 04:21:15 -04:00

141 lines
11 KiB
Markdown

# Контракт события в DDS
Учебный результат: менти видит первый на стенде пример правила «слой — это
контракт, а не обязательно копия данных». Заказ и сессия — физические
таблицы, а событие в DDS не хранится вовсе: `dds.event_v` — представление,
которое переводит событие на язык бизнес-модели, не копируя ни строки.
Витрины DM читают его, а не ODS напрямую.
## Форма
Обычное представление над `ods.event_v` — вторым уровнем, так что `FINAL`
остаётся спрятанным в один слой, внутри контракта ODS. Единственное
сомнение против такой формы — «представление материализуется до фильтра» —
снято исследованием [проталкивания
условий](../../research/2026-08-22-view-predicate-pushdown.md): условия
внешних запросов доезжают до партиций и первичного ключа сквозь два уровня
представлений и переименование колонок.
Две оговорки исследования соблюдены контрактом. Ключевая для срезов колонка
`event_date` отдаётся переименованием `EventDate`, не пересчётом — иначе
индекс перестал бы её анализировать. Цена `FINAL` — отключённый
автоматический `PREWHERE` — свойство ODS, представление её не усугубляет.
Как публичный договор DDS, представление перечисляет колонки явно и следует
общей [конвенции чтения](../storage.md), «Имена объектов».
## Язык модели
Имена атрибутов — имена модели, а не нормализованные имена источника
([CONTEXT.md](../../../CONTEXT.md), «Нормализованное имя»). Для большинства
колонок они совпадают: атрибут переезжает под нормализованным именем из
[описания выгрузки](../../formats/clickstream-event.md). Расхождения два,
и каждое — решение модели:
- **`WatchID` → `event_id`**: модель зовёт сущность событием, «watch» —
жаргон выгрузки Метрики.
- **`UTCEventTime` → `event_time` в поясе счётчика** (`Europe/Samara`):
DDS обслуживает человека с дашбордом, время лежит местным ([дока
хранилища](../storage.md), «Часовые пояса»). Пересчёт — смена линзы, байты
те же, проталкиванию не мешает. Второй метки времени контракт не отдаёт:
выбор линзы слой сделал за читателя, UTC остаётся языком ODS.
`ClientID` остаётся `client_id`: это устоявшееся имя куки во всей
экосистеме Метрики, по нему шардирование и мост к `dds.identity_map_v`;
своё имя здесь создало бы третий синоним. `client_timezone` тоже остаётся —
это атрибут посетителя, а не линза времени.
`purchase_revenue` сохраняет смысл поля источника. Тип денег нормализуется в
ODS; точный тип и проверка описаны в разделе
[«Актуальные события»](../storage.md#актуальные-события). DDS только
переименовывает уже нормализованную колонку.
## Расшифровки
Одна: `device_category` — коды 1–4 в строки `desktop` / `mobile` /
`tablet` / `tv`. У настоящей Метрики этот атрибут и есть строка (выгрузка
стенда несёт число — принятое отступление: спека, раздел 1.1), контракт возвращает
ему боевой вид.
Значения `device_category` 1–4 получают имена, остальные сохраняются
десятичной строкой. Новый код остаётся видимым, пока у него не появится имя в
договоре.
Остальные кодированные атрибуты не расшифровываются. Для `goals_reached`
справочника целей на стенде нет, id и есть язык. Регионы в расшифровке не
нуждаются: выгрузка несёт названия рядом с id (`region_city`,
`region_country`), словарь ничего бы не раскрыл. Идея дать менти собрать
справочник регионов самому ушла в лабы — issue #143.
## Что скрыто
- **`VisitID`** — чужой суррогат сессии: свою сессию модель чеканит сама
(`session_id`, [модель сессии](session.md)), а эталон — инструмент
проверки, не атрибут модели. DQ-сверка нарезки идёт за эталоном в
`ods.event_v`: она по построению сопоставляет наш слой с чужим чеканом —
честно, что за чужим она и идёт в его слой.
- **`Sign`** — колонка формата без механики: генератор исправлений не шлёт,
и в модели события колонке ответить нечего. Честный рассказ о ней живёт в
DDL ODS и лекции.
- **`ecommerce`** — сырой JSON: сырьё, а не атрибут. Разобранные массивы
`purchase_*` и `product_*` в контракте есть; лаба про разбор JSON внутри
колонки читает ODS.
- **`_load_ts`** — у представления нет собственной загрузки, а чужие
координаты слой не переносит (правило слоя — [модель
заказа](order.md), «Служебные колонки»); расследование спрашивает ODS по
ключу события.
Остальные колонки отдаются целиком, включая `ip_address` и оба
`region_*_id`: контракт — не цензор, он прячет только то, чему в модели
нечего сказать.
## Читатели
- Сборка сессий: ключи нарезки (`client_id`, `event_time`,
`event_date`), тип события, страница, UTM, паспорт куки — категория
устройства приходит уже расшифрованной.
- Витрины `dm.events_enriched_v`, `dm.top_pages_daily_v` и
`dm.utm_effectiveness_v` (`declared_revenue` берется из `purchase_revenue`).
- Событийная сторона сверки `dm.purchase_vs_orders_v`: `purchase_id`,
`purchase_revenue`, `event_date`.
- DQ-сверка сессий с эталоном читает не его, а `ods.event_v` —
см. «Что скрыто».
## Не входит
- Форма витрин DM — #135.
- Лаба со справочником регионов — issue #143, проектирование лаб.
## Отклонённые варианты
- **Материализация вместо представления.** Дублировала бы широкий ODS ради
переименований, и урок «слой — контракт, а не копия» пропал бы;
единственный довод за — сомнение в проталкивании условий — снят
исследованием #128.
- **`utc_event_time` второй меткой рядом с `event_time`.** Контракт вернул
бы читателю выбор линзы, который слой обязан делать сам; расхождение двух
часов — урок ODS, не DDS.
- **`visit_id` в контракте.** Сверка и лаба читали бы одну поверхность, но
чужой чекан встал бы атрибутом модели — тем же доводом модель сессии не
пустила его в свою строку. Решение владельца: DQ-опыт показательнее,
когда эталон приходит из другого слоя.
- **`cookie_id` вместо `client_id`.** Честное имя модели, но третий синоним
куки на ровном месте: проза говорит «кука», экосистема — `ClientID`.
- **Словарь регионов на стенде.** Повторил бы урок `dictGet` каталога
вторым разом, а раскрывать нечего — названия уже в строке. Идея «сделать
руками» ушла в лабы (issue #143).
- **Прочие расшифровки** (`goals_reached` в имена целей, `has_gclid` в
Bool). Справочника целей нет; косметика без читателя.
## Связанные решения
- [Проталкивание условий сквозь
представления](../../research/2026-08-22-view-predicate-pushdown.md) —
резолюция #128: форма контракта и обе её оговорки.
- [Модель сессии](session.md) — `session_id` и довод против чужого чекана;
сюда же передан вопрос о судьбе `VisitID`.
- [Модель заказа](order.md) — правило служебных колонок слоя.
- Мастер-спека, раздел 7 — два контракта: `ods.event_v` хранит язык
источника, `dds.event_v` говорит языком модели.
- [CONTEXT.md](../../../CONTEXT.md) — «Нормализованное имя», «Хранилище».