Compare commits
10
Commits
95599ead29
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffd467bf43 | ||
|
|
dda4f3aeb5 | ||
|
|
05e4cdea4d | ||
|
|
9d3a8053d3 | ||
|
|
1fccaf8599 | ||
|
|
247ab72d26 | ||
|
|
1fdc7be733 | ||
|
|
772a6c48c7 | ||
|
|
6c9d986114 | ||
|
|
2e42cf63ff |
@@ -0,0 +1,99 @@
|
||||
## Ответ
|
||||
|
||||
Сумма заказа живёт **на обеих сторонах**, и стороны законно расходятся:
|
||||
клиентское событие несёт объявленную сумму (то, что сайт продиктовал трекеру
|
||||
через dataLayer — до пересчёта на бэкенде), заказ бэкенда — итоговую правду
|
||||
со статусами. Правило стенда: **поведение и атрибуцию считаем по трекеру,
|
||||
деньги — по бэкенду**. Правило выучивается на конфликте: суммы у менти
|
||||
не сойдутся, и он сам раскопает почему.
|
||||
|
||||
### Клиентская сторона (широкое событие, рамка #18)
|
||||
|
||||
- Таксономия торговых событий: `pageview` + `add_to_cart` + `purchase`.
|
||||
Полный словарь Метрики (`detail`, `remove`, `impressions`) не берём:
|
||||
механика та же, объём массивов большой, новых идей нет — остаётся теорией
|
||||
в документе о реализме.
|
||||
- `purchase` несёт `purchaseID/Revenue/Currency/Coupon` + массивы `product*`
|
||||
состава заказа + сырую строку `ecommerce` (по образцу выгрузки Метрики,
|
||||
фактура — #27).
|
||||
- `add_to_cart` — массивы `product*` с одним товаром. Оживают мёртвые
|
||||
счётчики `purchases`/`add_to_carts` в `v_utm_effectiveness` без правки витрины.
|
||||
|
||||
### Сторона бэкенда (второй источник, рамка #18)
|
||||
|
||||
- **Формат выгрузки: полный ежедневный слепок заказов в пределах окна
|
||||
изменяемости K дней.** Запись = снапшот заказа: `order_id`, `user_id`,
|
||||
времена, `status` (`created`/`paid`/`cancelled`), `updated_at`, итоговые
|
||||
суммы (товары / скидка / доставка / итого) + **позиции вложенным JSON**
|
||||
(`items: [{sku, qty, price}]`) — «родной» экспорт бэкенда, а не диалект
|
||||
Метрики.
|
||||
- Обоснование слепка (а не инкремента): очень частый боевой формат;
|
||||
идемпотентный приём и самовосстановление (пропущенный день ничего не
|
||||
ломает); дедуп до последней версии становится обязательным с первого дня.
|
||||
Окно K делает объём защитимым (за окном заказ неизменяем — возить незачем)
|
||||
и даёт границу пересчёта: выручка дня D «дышит» K дней, потом замерзает.
|
||||
- Приём: дедуп через `ReplacingMergeTree`/`argMax`; разбор JSON-позиций —
|
||||
**один раз** в трансформации ODS → DDS, дальше витрины работают с плоскими
|
||||
массивами. Это единственный носитель навыка «вложенный JSON в ClickHouse»
|
||||
в стенде.
|
||||
- В `docs/generator-realism.md` — честный абзац trade-off «инкремент экономнее,
|
||||
слепок надёжнее» и сноска про compacted topic как родной Kafka-паттерн
|
||||
для состояния сущности.
|
||||
|
||||
### Сверка
|
||||
|
||||
- **Ключ: клиентский `purchaseID` = `order_id` бэкенда** (магазин знает номер
|
||||
заказа на `/confirmation` — как `actionField.id` у Метрики).
|
||||
- Конструируемые расхождения — перечислимый список из четырёх причин,
|
||||
детерминированных от seed:
|
||||
- **A. Отмена** — `purchase` есть, заказ дошёл до `status='cancelled'`
|
||||
(бесплатно из статусов);
|
||||
- **B. Потерянное событие** — заказ есть, `purchase` не доехал
|
||||
(вероятность в генераторе);
|
||||
- **C. Дельта суммы** — систематическая, по построению: клиент объявляет
|
||||
сумму товаров до промокода и доставки, бэкенд — итог; менти может вывести
|
||||
формулу связи;
|
||||
- **D. Дубль события** — повторный `purchase` от обновления
|
||||
`/confirmation` (вероятность).
|
||||
- Пятый эффект бесплатно даёт формат доставки: **опоздание** (заказ впервые
|
||||
появляется в слепке D+1/D+2) — «вчера не сходилось, сегодня сошлось».
|
||||
- Не берём: сироту-фрод (дублирует B механически) и расхождение часов
|
||||
клиент/сервер — это жители тумана «Грязь в данных», придут своим тикетом.
|
||||
|
||||
### Слои и витрины
|
||||
|
||||
- DDS: одна новая сущность `dds.order` (последняя версия заказа, позиции
|
||||
разобраны в массивы). `purchase` отдельной сущности не получает — это
|
||||
строка широкого события.
|
||||
- DM, три роли: **выручка** (`v_revenue_daily`, по категориям через `dictGet`
|
||||
каталога) — строится только от заказов; **сверка**
|
||||
(`v_purchase_vs_orders`) — FULL OUTER JOIN по ключу с классификатором
|
||||
расхождения; **атрибуция** (`v_utm_effectiveness`) — остаётся клиентской,
|
||||
плюс объявленная выручка по UTM. Точный состав колонок — в спеку (#17).
|
||||
|
||||
### Эталонный мир
|
||||
|
||||
Пересборка артефакта неизбежна и уже оплачена решением #18; этот тикет
|
||||
нагружает её смыслом: манифест расширяется контрольными числами заказной
|
||||
стороны — заказы и выручка по дням, и ровно N потерянных / M дублей /
|
||||
K отмен для самопроверки лабы сверки. Политика версионирования артефакта
|
||||
здесь не решается (пункт тумана карты).
|
||||
|
||||
### Ограничения исполнения и страховочные срезы
|
||||
|
||||
Аудитория — джун после базовой программы: расхождения — перечислимый список,
|
||||
не хаос. Если при сборке спеки (#17) суммарный объём испугает, резать в
|
||||
порядке: (1) статусы сузить до `created`/`cancelled` — урок дедупа держится
|
||||
на самом слепке; (2) расхождения вводить поэтапно — сначала A+C, потом B+D;
|
||||
(3) окно K сделать константой мира, а не параметром.
|
||||
|
||||
### Отклонено по дороге
|
||||
|
||||
Событийный лог заказов (сборка автомата — дальше от типовой работы DE),
|
||||
CDC-формат (имитация Debezium без Debezium), шапка+строки (воскрешает склейку
|
||||
по ключам), отдельный поток возвратов, полный словарь торговых событий
|
||||
Метрики, механика Sign/CollapsingMergeTree (остаётся кандидатом на потом,
|
||||
её дом — клиентская сторона, поток визитов Метрики Про, не заказы).
|
||||
|
||||
Фактура: `docs/research/2026-07-26-yandex-clickstream-format.md` (#27),
|
||||
резолюция «Модель данных» (#18).
|
||||
@@ -0,0 +1,213 @@
|
||||
# Слепок трекера на 2026-07-26 (аккаунт GitHub заблокирован)
|
||||
|
||||
Страховочная копия wayfinder-карты и ключевых резолюций из GitHub Issues.
|
||||
Снята из контекста сессии в момент блокировки аккаунта `dementev-dev`
|
||||
(сразу после закрытия #15). Если аккаунт восстановят — файл можно удалить;
|
||||
если нет — это источник для восстановления трекера на новом месте.
|
||||
|
||||
**Хвост, не доехавший до GitHub:** строка про #15 в Decisions so far карты #10
|
||||
(текст — в разделе «Карта», помечен как НЕ ОПУБЛИКОВАНО).
|
||||
|
||||
## Состояние issues (все, на момент блокировки)
|
||||
|
||||
| # | Состояние | Метки | Название |
|
||||
|---|---|---|---|
|
||||
| 27 | CLOSED | wayfinder:research | Что отдаёт Яндекс как кликстрим: форма события и выгрузка |
|
||||
| 24 | CLOSED | needs-triage | Вычистить прозаические тесты из test_world_dags_contract.py |
|
||||
| 23 | CLOSED | ready-for-agent | Сверка цифр курса на живом стенде и финальная проверка |
|
||||
| 22 | CLOSED | ready-for-agent | Лабы 07 (next-day) и 08 (continue) + метадокументы курса |
|
||||
| 21 | CLOSED | ready-for-agent | Каркас курса и переобвязка уроков 0–6 под путь import |
|
||||
| 20 | OPEN | wayfinder:task | Обновить мажорную версию airflow до версии 3 |
|
||||
| 18 | CLOSED | wayfinder:grilling | Модель данных: широкое событие и второй источник |
|
||||
| 17 | OPEN | wayfinder:task | Собрать спеку боевого реализма |
|
||||
| 16 | OPEN | wayfinder:grilling | Анонимы и identity stitching: нужно ли и сколько |
|
||||
| 15 | CLOSED (2026-07-26) | wayfinder:grilling | Purchase с выручкой: форма события и место в стенде |
|
||||
| 14 | OPEN | wayfinder:grilling | Кластер: где живёт опыт менти и какая топология |
|
||||
| 13 | CLOSED | wayfinder:research | Цена кластера для пайплайна |
|
||||
| 12 | CLOSED | wayfinder:research | Ресурсный бюджет стенда на 16 ГБ |
|
||||
| 11 | CLOSED | wayfinder:task | Реализм генератора: границы применимости стенда |
|
||||
| 10 | OPEN | wayfinder:map | Карта: боевой реализм стенда |
|
||||
| 9 | CLOSED | — | Редизайн пути менти: мир из артефакта и две ветки роста |
|
||||
| 8 | CLOSED | wontfix | Техдолг: инкрементальный ETL вместо full_refresh на каждый день |
|
||||
| 7 | CLOSED | ready-for-agent | Редизайн лаб курса под три режима менти |
|
||||
| 6 | CLOSED | ready-for-agent | Один учебный профиль: daily-wave — учебный, ci — служебный |
|
||||
| 5 | CLOSED | ready-for-agent | Инкрементальные счётчики manifest: next-day без перечитки всей Kafka |
|
||||
| 4 | CLOSED | ready-for-agent | Поверхность DAG'ов: generator_control → world_init, беспараметрный world_next_day |
|
||||
| 3 | CLOSED | ready-for-agent | Эталонный мир: 3-дневный артефакт в git и import по умолчанию |
|
||||
| 2 | CLOSED | wontfix | Airflow Grid: всплывающая JS-ошибка при авто-обновлении |
|
||||
| 1 | CLOSED | — | Быстрый разлогин в Airflow и Superset |
|
||||
|
||||
Блокировки #15 (нативные dependencies): blocked_by #11, #18 — обе закрыты.
|
||||
|
||||
## Карта #10 «Карта: боевой реализм стенда» (тело)
|
||||
|
||||
### Destination
|
||||
|
||||
Принятая спека в `docs/specs/` «Боевой реализм стенда»: где менти получает
|
||||
кластерный опыт ClickHouse и в какой топологии; какие доработки реализма
|
||||
данных генератора делаем; явные границы. Спека готова к разбиению через
|
||||
`/to-tickets`.
|
||||
|
||||
### Notes
|
||||
|
||||
- Расчёт на ноутбук менти 16 ГБ RAM (у кого 8 ГБ — VDS за счёт менти).
|
||||
- Исполнение доработок — после фичи «Редизайн пути менти» (#9); карта
|
||||
решений может идти параллельно с ней.
|
||||
- Изменения генератора тянут пересборку эталонного мира
|
||||
(`data/startup_history/reference-world.json.xz`) и «поплывшие» числа
|
||||
в лабах — учитывать в каждом решении.
|
||||
- Скиллы: `/grilling` и `/domain-modeling` для тикетов-решений,
|
||||
`/research` для тикетов-исследований.
|
||||
- Рабочая гипотеза владельца: кластер здесь, облегчённо (2 шарда без
|
||||
реплик), цель — Distributed и ON CLUSTER на живом потоке; «голый»
|
||||
clickhouse-learning-cluster этого не даёт.
|
||||
- Порядок: решение по модели данных предшествует кластерному — иначе
|
||||
межшардовые джойны четырёх топиков придётся оплатить дважды.
|
||||
|
||||
### Decisions so far
|
||||
|
||||
- [Реализм генератора: границы применимости стенда](#11) — документ
|
||||
`docs/generator-realism.md` (коммит 068d96f): честно как в бою —
|
||||
схема/воронка/сессии/волна/обвязка; упрощено — четыре топика, только
|
||||
pageview, клоны пользователей, нет «грязи», масштаб.
|
||||
- [Ресурсный бюджет стенда на 16 ГБ](#12) — полный стенд в покое ≈3,4 ГБ;
|
||||
2×1 добавляет ≈0,6–0,8 ГБ (влезает свободно), 2×2 — ≈1,7–1,9 ГБ (влезает,
|
||||
но впритык к дефолтному бюджету WSL2 ~8 ГБ); координатором брать
|
||||
clickhouse-keeper, не ZooKeeper.
|
||||
- [Цена кластера для пайплайна](#13) — объём средне-крупный (~15–20 файлов,
|
||||
тяжёлое — SQL); главная боль — JOIN поверх Distributed и TRUNCATE в
|
||||
трансформациях (риск для контрольных сумм), приём из Kafka требует одного
|
||||
консьюмера + Distributed-цели; рекомендация исследования — опциональный
|
||||
`make up-cluster`, не дефолт.
|
||||
- [Что отдаёт Яндекс как кликстрим](#27) — плоское широкое ядро (~140
|
||||
колонок) плюс параллельные массивы для многозначного, вложенного JSON
|
||||
нет (Яндекс ближе к Snowplow, чем к Segment); доставка батчем (Logs API,
|
||||
TSV, лог доформировывается ~3 дня), поток только в «Метрике Про» через
|
||||
Data Transfer с задержкой до 15 минут — Kafka у Яндекса нет, наша Kafka
|
||||
учебная замена; детали в `docs/research/2026-07-26-yandex-clickstream-format.md`
|
||||
(ветка `research/yandex-clickstream-format`, коммит `e6e34f2`).
|
||||
- [Модель данных: широкое событие и второй источник](#18) — переходим на
|
||||
одно широкое событие по образцу Яндекс Метрики (плоское ядро +
|
||||
параллельные массивы + сырое поле `ecommerce`, таксономия event_type);
|
||||
интеграционная ценность — заказы бэкенда той же Kafka, но пачками с
|
||||
опозданиями и отменами (одна труба, два режима), каталог товаров —
|
||||
словарь ClickHouse из файла; прямое чтение прод-Postgres и файловые
|
||||
источники отклонены (файлы — зона Lakehouse-стенда); Kafka — учебная
|
||||
замена батчевого Logs API, фиксируем в docs/generator-realism.md.
|
||||
- **[НЕ ОПУБЛИКОВАНО — добавить при восстановлении доступа]**
|
||||
[Purchase с выручкой: форма события и место в стенде](#15) — деньги на
|
||||
обеих сторонах (клиент объявляет через dataLayer, бэкенд — итоговая
|
||||
правда; атрибуция по трекеру, деньги по бэкенду); клиент:
|
||||
`pageview`+`add_to_cart`+`purchase`; заказы — ежедневный полный слепок
|
||||
окна изменяемости K со статусами и JSON-позициями, приём через
|
||||
ReplacingMergeTree; сверка по `purchaseID`=`order_id` с четырьмя
|
||||
конструируемыми расхождениями (отмена, потеря, дельта суммы, дубль);
|
||||
выручка в DM — только от заказов.
|
||||
|
||||
### Not yet specified
|
||||
|
||||
- «Грязь» в данных: боты, дубли событий, опоздавшие мобильные батчи,
|
||||
расхождение часов клиент/коллектор — вернуться после решений по
|
||||
purchase и identity.
|
||||
- Политика версионирования эталонного артефакта при изменениях
|
||||
генератора (когда пересобирать, как жить лабам со сменой чисел).
|
||||
- Как новые возможности лягут в лабы курса (после редизайна лаб, #7).
|
||||
|
||||
### Out of scope
|
||||
|
||||
- ~~Формат доставки событий: закрыть теорией~~ — решение отменено
|
||||
2026-07-22: исследование цены кластера показало, что четыре топика
|
||||
несовместимы с шардированием без GLOBAL JOIN; вопрос вернулся в рамку
|
||||
тикетом «Модель данных: широкое событие и второй источник».
|
||||
- Редизайн лаб курса — отдельный issue #7.
|
||||
- Инкрементальный ETL — отдельный issue #8; багфиксы — #1, #2.
|
||||
|
||||
### Комментарии карты
|
||||
|
||||
1. Решение 2026-07-23: дальнейшая работа карты пойдёт в новом
|
||||
v2-репозитории. v1 замораживается как стабильный стенд для менти
|
||||
(добить путь менти, баги #1/#2, лекции). v2 стартует пустым
|
||||
репозиторием с осознанным первым коммитом (переносим только нужное;
|
||||
генератор переписывается, переиспользуются идеи). Имя нового
|
||||
репозитория выберем из решения о нише; карта и открытые тикеты
|
||||
переедут туда после создания.
|
||||
2. Рабочий кандидат имени v2-репозитория: **clickstream-data-platform**
|
||||
(согласован 2026-07-23). Мотив: стенд перерастает классическое DWH —
|
||||
потоковый приём, оркестрация, кластер, витрины; «data platform»
|
||||
описывает целое. Финальное закрепление — при решении тикета о нише.
|
||||
|
||||
## Тикет #15 «Purchase с выручкой: форма события и место в стенде»
|
||||
|
||||
### Тело
|
||||
|
||||
Part of #10
|
||||
|
||||
**Question:** Решить форму события покупки с суммой заказа: схема и носитель
|
||||
(клиентское событие в широкой модели, источник заказов бэкенда или обе
|
||||
стороны со сверкой — зависит от решения «Модель данных» #18), как ложится
|
||||
в DDL/DM и дашборд, что делает с эталонным миром (пересборка артефакта
|
||||
и чисел лаб).
|
||||
|
||||
Комментарий владельца: решение #18 добавило вторую сущность — заказ бэкенда
|
||||
(Kafka, пачками, с опозданиями и отменами). При решении формы purchase решить
|
||||
и форму события заказа, и сюжет сверки «клиентский purchase против
|
||||
бэкенд-заказа». Фактура по ecommerce Яндекса — в
|
||||
docs/research/2026-07-26-yandex-clickstream-format.md.
|
||||
|
||||
### Резолюция (опубликована 2026-07-26, тикет закрыт)
|
||||
|
||||
Опубликованный текст: комментарий
|
||||
`issues/15#issuecomment-5084445090`. Полная копия — в
|
||||
`.scratch/backup/20260726-resolution-15.md` (соседний файл).
|
||||
|
||||
## Тикет #18 «Модель данных: широкое событие и второй источник» (резолюция)
|
||||
|
||||
**Да, переходим на широкое событие.** Четыре топика-осколка уходят; модель
|
||||
одна, целевая (двух моделей «для дефолта и для кластера» не держим). Мотив —
|
||||
реализм: менти должен узнавать в стенде тот кликстрим, с которым столкнётся
|
||||
на работе. Кластер — побочный довод, не причина.
|
||||
|
||||
**Форма события — по образцу Яндекс Метрики** (фактура — #27,
|
||||
`docs/research/2026-07-26-yandex-clickstream-format.md`): плоское широкое
|
||||
ядро плюс параллельные массивы для многозначного (товары, цели, свои
|
||||
параметры) плюс одно сырое поле-строка `ecommerce`. Вложенных объектов в
|
||||
стиле Segment/Amplitude не делаем. Таксономия `event_type` вместо «только
|
||||
pageview». Точный состав полей — в спеку (#17), опора — таблица 53 полей
|
||||
из исследования.
|
||||
|
||||
**Интеграционная учебная ценность** (взамен склейки осколков):
|
||||
|
||||
- **Второй источник — заказы бэкенда.** Вторая версия правды о покупке;
|
||||
учебный сюжет — сверка клиентского purchase против заказа, расхождения,
|
||||
отмены. Форма события заказа и сверка — тикет #15.
|
||||
- **Транспорт заказов — та же Kafka, но пачками**: бэкенд выгружает заказы
|
||||
раз в модельный день, с опозданиями и отменами. Одна труба, два режима —
|
||||
как в бою, где батчи льют в брокер из удобства. Открывает темы, которых
|
||||
у менти нет после курсовой airflow-greenplum: согласование потока и
|
||||
батча, поздние данные, кросс-источниковые проверки, сенсоры/Datasets
|
||||
(DAG слоя DDS ждёт дневной батч).
|
||||
- **Каталог товаров — словарь ClickHouse из файла** (CSV в репозитории;
|
||||
тот же файл использует генератор — расхождений нет по построению).
|
||||
Даёт `dictGet` и политику обновления словаря.
|
||||
|
||||
**Ландшафт итогом:** Kafka — единственная труба (кликстрим потоком, заказы
|
||||
пачками); Postgres остаётся только служебной базой Airflow; смешанность
|
||||
ландшафта выражена режимами и частотами, а не второй трубой.
|
||||
|
||||
**Отклонено по дороге:** прямое чтение прод-базы магазина из ClickHouse
|
||||
(анти-приём: нагрузка на прод и связность; в документе о реализме
|
||||
зафиксировать как явный учебный пункт «в бою — реплика или выгрузка»);
|
||||
файловые дропы как источник (зона Lakehouse-стенда; остаются теорией —
|
||||
«настоящий Logs API — это скачанный TSV»); HTTP-сервис заказов и CDC
|
||||
(Debezium) — цена выше учебной отдачи; каталог отдельным топиком Kafka —
|
||||
выдумка, в бою так не делают.
|
||||
|
||||
**Честность к Яндексу:** у Метрики кликстрим — батч (Logs API), потока в
|
||||
общем доступе нет; наша Kafka — учебная замена, так и называем в
|
||||
`docs/generator-realism.md`.
|
||||
|
||||
**Что это открывает дальше:** #15 (purchase и форма заказа) и #14 (кластер:
|
||||
с широким событием склейка осколков исчезает, ключ шардирования решается
|
||||
там) разблокированы; новые приёмы стенда — ARRAY JOIN, словари,
|
||||
сенсоры/Datasets, кросс-источниковый DQ; кандидат — версии записи через
|
||||
Sign (механика CollapsingMergeTree из потока Метрики Про).
|
||||
@@ -0,0 +1,92 @@
|
||||
# Handoff: трекер переехал на Gitea, карта живёт, GitHub в блоке
|
||||
|
||||
Дата: 2026-07-26. Пишу по итогам сессии, в которой закрыли тикет #15
|
||||
карты «Боевой реализм стенда» и экстренно переносили репозиторий с
|
||||
трекером на собственный Gitea после блокировки GitHub-аккаунта.
|
||||
|
||||
## Что случилось и что сделано
|
||||
|
||||
1. **Тикет #15 «Purchase с выручкой» решён и закрыт** (шесть решений,
|
||||
полная резолюция — в тикете). Гист — в Decisions so far карты #10.
|
||||
2. **GitHub-аккаунт dementev-dev заблокирован** (ToS violation, причина
|
||||
не названа) — сразу после закрытия #15. Апелляция готовится, см.
|
||||
«Хвосты» ниже.
|
||||
3. **Репозиторий и весь трекер перенесены на Gitea:**
|
||||
`https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo`
|
||||
(remote `gitea`, ветка `main` запушена). Все 27 номеров issues
|
||||
воссозданы 1:1 (на местах PR #19/#25/#26 — закрытые заглушки, чтобы
|
||||
ссылки `#NN` в текстах не разъехались).
|
||||
4. **Из транскриптов прошлых сессий субагентами восстановлены дословно:**
|
||||
тела #18, #20, #27; резолюции #12, #13, #15, #18, #27; граф блокировок.
|
||||
Тела #14, #16, #17 — реконструкции по памяти (помечены в самих тикетах).
|
||||
Резолюция #11 отдельным текстом не нашлась — её содержание есть в
|
||||
`docs/generator-realism.md`.
|
||||
|
||||
## Состояние карты (#10 на Gitea)
|
||||
|
||||
- Закрыты: #11, #12, #13, #15, #18, #27 — гисты в Decisions so far.
|
||||
- **Фронтир:** #14 «Кластер: где живёт опыт менти и какая топология»
|
||||
(все блокеры закрыты; для него готова фактура — резолюции #12 и #13)
|
||||
и #16 «Анонимы и identity stitching» (разблокирован закрытием #15).
|
||||
- #17 «Собрать спеку боевого реализма» ждёт #14 и #16.
|
||||
- Блокировки записаны строками `Blocked by:` в телах тикетов — нативные
|
||||
dependencies в этой инсталляции Gitea выключены (API отдаёт 404).
|
||||
|
||||
## Как работать с Gitea (нюансы, стоившие времени)
|
||||
|
||||
- Хост домашний — **в обход прокси**: `curl --noproxy '*'`,
|
||||
`no_proxy=git.dementev.space git push gitea ...`.
|
||||
- Токен — в `~/.git-credentials` (строка ddmitry). Скоупы:
|
||||
`write:repository`, `write:issue`, `write:package`. Создание репо через
|
||||
API недоступно (нужен `write:user`) — создавать в UI.
|
||||
- Токен не подставлять в командную строку (блокируется классификатором) —
|
||||
читать из файла в python/через конфиг curl.
|
||||
- `docs/agents/issue-tracker.md` всё ещё описывает GitHub/gh — пока
|
||||
GitHub в блоке, рабочий трекер де-факто Gitea (операции — через
|
||||
`curl`/python по API, образцы: `upload.py` и `apply_updates.py` в
|
||||
scratchpad прошлой сессии; проще написать заново по образцу из этого
|
||||
handoff).
|
||||
|
||||
## Хвосты (в порядке срочности)
|
||||
|
||||
1. **Апелляция в GitHub.** SMS на номера РФ не доходят (шлюз GitHub не
|
||||
шлёт в РФ), попытки смены номера упёрлись в rate limit. План: спустя
|
||||
~сутки — казахстанский номер (в выпадашке стран выбрать Kazakhstan);
|
||||
параллельно тикет через «I can't sign in» на support.github.com
|
||||
(без SMS, только почтовый код) с почты аккаунта. Черновик письма —
|
||||
в прошлой сессии; суть: спросить причину, описать легитимное
|
||||
использование (учебные репо, менти, Pages), упомянуть всплеск
|
||||
API-активности через gh CLI как возможный триггер. Один тред, не
|
||||
плодить дубли. Если ответят «multiple free accounts» — стандартный
|
||||
выход: конвертировать менторскую учётку в организацию.
|
||||
2. **При восстановлении GitHub:** донести в карту #10 строку про #15
|
||||
(на Gitea она уже есть, на GitHub — нет), затем решить, какой трекер
|
||||
основной, и синхронизировать/заморозить второй.
|
||||
3. **Решение о доме трекера и v2.** Подозрение владельца: собственный
|
||||
git-сервер для менторской работы — не такая плохая идея (независимость
|
||||
от блокировок). Против: доступность для менти (публичные ссылки из
|
||||
роадмапа, GitHub Pages де-факто витрина), привычность GitHub в резюме
|
||||
менти. Это решение стоит принять осознанно — возможно, грилингом,
|
||||
и оно связано с запланированным v2-репозиторием (комментарии к карте
|
||||
#10: v2 стартует пустым, кандидат имени clickstream-data-platform).
|
||||
4. **Резервные копии:** `.scratch/backup/20260726-*.md` — слепок трекера
|
||||
и резолюция #15 на момент блокировки. После стабилизации (GitHub или
|
||||
окончательный переезд) — можно удалить. Этот handoff и backup пока
|
||||
не закоммичены.
|
||||
|
||||
## Suggested skills
|
||||
|
||||
- `/wayfinder #10 #14` или `/wayfinder #10 #16` — продолжать карту
|
||||
(следующий тикет по выбору владельца; для #14 фактура уже собрана
|
||||
в резолюциях #12/#13).
|
||||
- `/grilling` + `/domain-modeling` — внутри тикетов-решений; владелец
|
||||
просил на развилках сначала веер гипотез (дивергенцию), потом
|
||||
конвергенцию с рекомендацией.
|
||||
- `/conventional-commits` — при коммите backup/handoff.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Трекер: https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo/issues
|
||||
- Карта: issue #10 там же; резолюция #15 — комментарий в issue #15.
|
||||
- Исследование формата Яндекса: `docs/research/2026-07-26-yandex-clickstream-format.md`.
|
||||
- Границы реализма генератора: `docs/generator-realism.md`.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Handoff: трекер настроен на Gitea и tea, граф блокировок восстановлен
|
||||
|
||||
Дата: 2026-07-29. Сессия была узкой: перевести контракт работы с задачами с
|
||||
GitHub на Gitea, поставить CLI `tea` и убедиться, что всё это живое.
|
||||
|
||||
## Что сделано
|
||||
|
||||
1. **`tea` 0.15.0 поставлен** в `~/.local/bin/tea` (бинарник с
|
||||
`dl.gitea.com`, sha256 сверена). Логин `git.dementev.space` назначен
|
||||
логином по умолчанию — `tea` работает из любого каталога.
|
||||
2. **Контракт переписан** — коммит `bc62b94` в ветке
|
||||
`chore/gitea-tracker-config` (`docs/agents/issue-tracker.md`,
|
||||
`docs/agents/triage-labels.md`, блок «Agent skills» в `AGENTS.md`).
|
||||
Ветка **не влита и не запушена**.
|
||||
3. **Нативные зависимости Gitea оказались рабочими.** Прошлый вывод «API
|
||||
отдаёт 404» был следствием нехватки прав у старого токена. Граф
|
||||
блокировок карты #10 собран заново нативными связями, текстовые строки
|
||||
`Blocked by:` из тел #14, #16, #17 убраны — источник истины теперь один.
|
||||
Что построено: #17 ← #14, #15, #16; #14 ← #12, #13, #18; #16 ← #15.
|
||||
4. **Метка `wayfinder:prototype`** заведена (не хватало; остальные
|
||||
`wayfinder:*` и все пять меток триажа уже были).
|
||||
5. **Прокси.** `~/dotfiles` домен покрывал, расхождение было только в
|
||||
`~/.t3/userdata/settings.json` — владелец поправил сам. Короткая рабочая
|
||||
форма, если переменная не подхватилась: `NO_PROXY='*' tea ...`.
|
||||
6. **Старый токен отозван** владельцем. Он оставался открытым текстом в
|
||||
записях разрешений `.claude/settings.local.json` — файл стоит подчистить
|
||||
при случае, хотя токен уже мёртвый.
|
||||
|
||||
Подробности по командам, скоупам и граблям — в самом
|
||||
`docs/agents/issue-tracker.md`, здесь не дублирую.
|
||||
|
||||
## Состояние карты «Боевой реализм стенда» (#10)
|
||||
|
||||
Не менялось за эту сессию, только уточнилось представление блокировок.
|
||||
|
||||
- Закрыты: #11, #12, #13, #15, #18, #27.
|
||||
- **Фронтир:** #14 «Кластер: где живёт опыт менти и какая топология» и
|
||||
#16 «Анонимы и identity stitching» — оба открыты и разблокированы.
|
||||
- #17 «Собрать спеку боевого реализма» ждёт #14 и #16.
|
||||
- Вне карты: #20 «Обновить мажорную версию airflow до версии 3».
|
||||
|
||||
## Хвосты
|
||||
|
||||
1. **Ветка `chore/gitea-tracker-config`** — влить в `main` (PR в Gitea или
|
||||
merge локально) и запушить.
|
||||
2. **Ветка `chore/gitea-migration`** — тоже не влита. В ней слепок трекера
|
||||
на момент блокировки GitHub и handoff предыдущей сессии
|
||||
(`.scratch/backup/`, `.scratch/handoffs/20260726-2201-*`). Решить:
|
||||
влить или удалить как отработавшую.
|
||||
3. **Переписка с GitHub** идёт, затянулась. Владелец считает, что основную
|
||||
работу в любом случае ведём в Gitea. Открытым остаётся вопрос, что делать
|
||||
с GitHub-зеркалом, когда (и если) аккаунт вернут.
|
||||
4. **Решение о доме трекера и v2-репозитории** — не принято. Против Gitea:
|
||||
доступность публичных ссылок для менти, привычность GitHub в их резюме.
|
||||
За: независимость от блокировок. Связано с планом v2 (кандидат имени
|
||||
`clickstream-data-platform`, стартует пустым).
|
||||
5. **`docs/adr/0001`** упоминает GitHub Issues как отклонённый вариант — это
|
||||
ADR своего времени, трогать не надо, но при чтении может сбивать.
|
||||
|
||||
## Suggested skills
|
||||
|
||||
- `/wayfinder #10 #14` или `/wayfinder #10 #16` — продолжить карту; какой
|
||||
из двух, выбирает владелец. Для #14 фактура уже собрана в резолюциях
|
||||
#12 и #13.
|
||||
- `/grilling` внутри тикетов-решений: владелец просил на развилках сначала
|
||||
веер гипотез (дивергенцию), потом конвергенцию с рекомендацией.
|
||||
- `/conventional-commits` — при любом коммите в этом репозитории.
|
||||
- `/github` — если дойдёт до вливания веток через PR.
|
||||
|
||||
## Ссылки
|
||||
|
||||
- Трекер: https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo/issues
|
||||
- Карта — issue #10 там же.
|
||||
- Контракт трекера: `docs/agents/issue-tracker.md`.
|
||||
- Исследование формата Яндекса: `docs/research/2026-07-26-yandex-clickstream-format.md`.
|
||||
- Границы реализма генератора: `docs/generator-realism.md`.
|
||||
@@ -41,11 +41,11 @@
|
||||
|
||||
### Issue tracker
|
||||
|
||||
GitHub Issues (через CLI `gh`). Спека фичи — файлом в `docs/specs/` (источник истины), корневой issue — тонкий, со ссылкой на спеку и чек-листом дочерних issues. См. `docs/agents/issue-tracker.md`.
|
||||
Gitea на `git.dementev.space` (через CLI `tea`). Спека фичи — файлом в `docs/specs/` (источник истины), корневой issue — тонкий, со ссылкой на спеку и чек-листом дочерних issues. См. `docs/agents/issue-tracker.md`.
|
||||
|
||||
### Triage labels
|
||||
|
||||
Пять канонических ролей как метки GitHub, имена совпадают (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). См. `docs/agents/triage-labels.md`.
|
||||
Пять канонических ролей как метки Gitea, имена совпадают (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). См. `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
[](./docker-compose.yml)
|
||||
[](./docs/ARCHITECTURE.md)
|
||||
|
||||
> **Стенд заморожен для новых фич.** Он остаётся стабильным учебным стендом:
|
||||
> что здесь работает, то работает и дальше — курс и лабы живут тут.
|
||||
> Развитие переехало в
|
||||
> [clickstream-data-platform](https://git.dementev.space/ddmitry/clickstream-data-platform):
|
||||
> там одно широкое событие кликстрима вместо четырёх топиков, заказы бэкенда
|
||||
> вторым источником и ClickHouse кластером.
|
||||
|
||||
Живой стек для работы с кликстримом: Kafka, ClickHouse, Airflow, Superset и мониторинг
|
||||
(Prometheus с Grafana) поднимаются в Docker одной командой. На этом стенде можно учиться
|
||||
по курсу или просто поднять его у себя и поэкспериментировать с потоковой загрузкой и
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# ADR-0007: Дом разработки — свой Gitea, GitHub — зеркало
|
||||
|
||||
Принято: 2026-07-29
|
||||
Статус: accepted
|
||||
|
||||
## Решение
|
||||
|
||||
Активную разработку — код, issues, PR, ревью — ведём в собственном Gitea
|
||||
(`git.dementev.space`). Он остаётся домом независимо от того, чем закончится
|
||||
история с блокировкой GitHub-аккаунта. GitHub держим как **публичное зеркало**
|
||||
кода: туда уходит `main`, чтобы у менти и внешнего читателя была привычная
|
||||
публичная ссылка.
|
||||
|
||||
## Контекст
|
||||
|
||||
Поводом стала блокировка аккаунта `dementev-dev` (2026-07-26): работа встала,
|
||||
пока не подняли Gitea. Но решение принято не «назло» блокировке — своя площадка
|
||||
выигрывает и по существу:
|
||||
|
||||
- **Свой API — без лимитов.** Агентские скиллы (wayfinder, триаж, конвейер)
|
||||
дёргают трекер интенсивно. На чужом хостинге это упирается в квоты, на своём —
|
||||
нет.
|
||||
- **Ещё одна резервная копия.** Репозиторий физически лежит на своём железе, а
|
||||
не только у поставщика услуги.
|
||||
- **Ролевые игры с менти.** Форк репозитория, ветка, PR, ревью, обсуждение в
|
||||
issue — весь учебный цикл «как в настоящей команде» Gitea даёт целиком, и
|
||||
удобнее GitLab. Заводить менти учебные аккаунты на своём хосте можно свободно.
|
||||
|
||||
Против Gitea был один довод: публичность ссылок и привычность GitHub в резюме
|
||||
менти. Зеркало его снимает.
|
||||
|
||||
## Последствия
|
||||
|
||||
- **Трекер один — Gitea.** Контракт уже переписан на `tea`
|
||||
(`docs/agents/issue-tracker.md`). Зеркало на GitHub — только код; issues туда
|
||||
не едут, и это не потеря: issue — вещь короткоживущая, а всё, что должно
|
||||
пережить задачу, лежит в самом репозитории — спеки (`docs/specs/`), ADR и
|
||||
история коммитов. Зеркало кода несёт эту часть целиком.
|
||||
- **Зеркало настраиваем встроенным push-mirror Gitea**, направление одно:
|
||||
Gitea → GitHub. Обратной синхронизации нет, чтобы не было двух источников
|
||||
истины. Настройка отложена до разблокировки аккаунта.
|
||||
- **Репозиторий v2** (кандидат имени `clickstream-data-platform`) создаём сразу
|
||||
в Gitea, с тем же зеркалированием.
|
||||
- В ADR-0001 GitHub Issues значится как отклонённый вариант — это запись своего
|
||||
времени (тогда поток держали файлами в `.scratch/`), переписывать её не нужно.
|
||||
@@ -1,16 +1,52 @@
|
||||
# Issue tracker: GitHub
|
||||
# Issue tracker: Gitea
|
||||
|
||||
Задачи этого репозитория живут в GitHub Issues. Все операции — через CLI `gh`;
|
||||
репозиторий `gh` определяет сам по `git remote`.
|
||||
Задачи этого репозитория живут в Gitea на `git.dementev.space`
|
||||
(`ddmitry/clickstream-ch-kafka-superset-demo`, это remote `origin`). Все
|
||||
операции — через CLI [`tea`](https://gitea.com/gitea/tea), официальный клиент
|
||||
Gitea; по устройству он близок к `gh` и `glab`. Логин и репозиторий `tea`
|
||||
определяет сам по git remote в текущем каталоге.
|
||||
|
||||
- **Создать issue**: `gh issue create --title "..." --body "..."` (многострочное
|
||||
тело — heredoc'ом).
|
||||
- **Прочитать issue**: `gh issue view <номер> --comments`.
|
||||
- **Список**: `gh issue list --state open --json number,title,labels` с нужными
|
||||
фильтрами `--label` / `--state`.
|
||||
- **Комментарий**: `gh issue comment <номер> --body "..."`.
|
||||
- **Метки**: `gh issue edit <номер> --add-label "..."` / `--remove-label "..."`.
|
||||
- **Закрыть**: `gh issue close <номер> --comment "..."`.
|
||||
## Перед первым запуском
|
||||
|
||||
- **Бинарник.** Скачивается с `https://dl.gitea.com/tea/<версия>/` (файл
|
||||
`tea-<версия>-linux-amd64` и `.sha256` рядом), кладётся в `~/.local/bin/tea`.
|
||||
Проверка: `tea --version`.
|
||||
- **Вход.** `tea logins add --name git.dementev.space --url
|
||||
https://git.dementev.space`, токен передаётся переменной
|
||||
`GITEA_SERVER_TOKEN` (не аргументом командной строки — он попадёт в историю
|
||||
оболочки). Логин уже добавлен и назначен по умолчанию, так что `tea` работает
|
||||
из любого каталога.
|
||||
- **Скоупы токена:** `read:user` (без него `tea` откажется добавлять логин),
|
||||
`write:issue`, `write:repository`. Токен выпускается в UI: Settings →
|
||||
Applications. Нехватка скоупа выглядит не как «нет прав», а как невнятная
|
||||
ошибка или пустой ответ — на этом уже один раз потеряли нативные блокировки
|
||||
(решили, что их нет в установке).
|
||||
- **Прокси.** Домен `dementev.space` должен быть в `NO_PROXY`, иначе запросы
|
||||
уходят в прокси и виснут. В обычной оболочке это делает `proxy-client` из
|
||||
`~/dotfiles`; для агента в t3 — блок `environment` в
|
||||
`~/.t3/userdata/settings.json`. Если переменная не подхватилась, короткий
|
||||
разовый префикс: `NO_PROXY='*' tea ...`.
|
||||
|
||||
## Команды
|
||||
|
||||
- **Создать issue:** `tea issues create --title "..." --description "..."`.
|
||||
Многострочное тело удобнее собрать heredoc'ом в переменную и подставить
|
||||
как `--description "$BODY"`.
|
||||
- **Прочитать issue:** `tea issues <номер> --comments`.
|
||||
- **Список:** `tea issues list --state open --output json --fields
|
||||
index,title,labels,assignees`. Фильтры: `--labels`, `--assignee`,
|
||||
`--keyword`.
|
||||
- **Комментарий:** `tea comments add <номер> -d "..."`.
|
||||
- **Метки:** `tea issues edit <номер> --add-labels "..."` / `--remove-labels
|
||||
"..."`. Список меток репозитория — `tea labels list`, создать новую —
|
||||
`tea labels create --name "..." --color "..."`.
|
||||
- **Закрыть:** `tea issues close <номер>`. Комментария при закрытии команда не
|
||||
принимает — сначала `tea comments add`, потом `close`.
|
||||
- **Взять в работу:** `tea issues edit <номер> --add-assignees ddmitry`.
|
||||
Сокращения вида `@me` в `tea` нет, имя пишется целиком.
|
||||
- **Чего нет в CLI** — через `tea api <path>`: команда ходит в REST API Gitea
|
||||
уже с сохранённым токеном, например
|
||||
`tea api repos/ddmitry/clickstream-ch-kafka-superset-demo/issues/17`.
|
||||
|
||||
## Спека — источник истины
|
||||
|
||||
@@ -25,39 +61,65 @@
|
||||
|
||||
## Когда скилл говорит «опубликовать в issue tracker»
|
||||
|
||||
Создать GitHub issue.
|
||||
Создать issue в Gitea: `tea issues create ...`.
|
||||
|
||||
## Когда скилл говорит «достать тикет»
|
||||
|
||||
`gh issue view <номер> --comments`.
|
||||
`tea issues <номер> --comments`.
|
||||
|
||||
## PR как поверхность триажа
|
||||
|
||||
**Нет** — одиночный учебный репозиторий, внешних PR не ждём. (Если включить —
|
||||
`/triage` начнёт гонять PR через те же метки и состояния командами `gh pr ...`.)
|
||||
`/triage` начнёт гонять PR через те же метки и состояния командами
|
||||
`tea pulls ...`.)
|
||||
|
||||
## Wayfinding-операции
|
||||
|
||||
Используются `/wayfinder`. Карта — один issue, тикеты — дочерние issues.
|
||||
|
||||
- **Карта**: issue с меткой `wayfinder:map` (Notes / Decisions-so-far / Fog в теле).
|
||||
- **Дочерний тикет**: sub-issue карты (`gh api` на endpoint sub-issues); если
|
||||
sub-issues недоступны — пункт task-list в теле карты + `Part of #<map>` в
|
||||
начале тела тикета. Метки: `wayfinder:<type>` (`research`/`prototype`/
|
||||
`grilling`/`task`).
|
||||
- **Блокировки**: нативные issue dependencies —
|
||||
`gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<db-id блокера>`
|
||||
(`<db-id>` — числовой database id: `gh api repos/<owner>/<repo>/issues/<n> --jq .id`,
|
||||
не `#номер`). Fallback — строка `Blocked by: #<n>` в начале тела. Тикет
|
||||
разблокирован, когда все блокеры закрыты.
|
||||
- **Карта**: issue с меткой `wayfinder:map` (Notes / Decisions-so-far / Fog
|
||||
в теле).
|
||||
- **Дочерний тикет**: вложенных issues в Gitea нет, поэтому связь держится
|
||||
двумя ссылками — пункт списка `- [ ] #NN` в теле карты и строка
|
||||
`Part of #<карта>` в начале тела тикета. Метки: `wayfinder:<тип>`
|
||||
(`research` / `prototype` / `grilling` / `task`).
|
||||
- **Блокировки**: нативные зависимости Gitea — единственный источник истины,
|
||||
текстовых строк `Blocked by:` в телах тикетов больше нет. В CLI их команд
|
||||
нет, работаем через `tea api` (`{owner}` и `{repo}` подставляются из текущего
|
||||
репозитория):
|
||||
- добавить блокер: `tea api repos/{owner}/{repo}/issues/<n>/dependencies
|
||||
-F index=<блокер> -f owner=ddmitry -f repo=clickstream-ch-kafka-superset-demo`
|
||||
— поля `owner` и `repo` обязательны, без них API отвечает
|
||||
«repository does not exist»;
|
||||
- кто блокирует тикет: `GET .../issues/<n>/dependencies`;
|
||||
- кого блокирует тикет: `GET .../issues/<n>/blocks`;
|
||||
- снять блокировку: тот же путь методом `DELETE` с тем же телом.
|
||||
|
||||
Тикет разблокирован, когда у всех блокеров `state == "closed"`.
|
||||
- **Фронтир**: открытые дети карты минус заблокированные и назначенные; первый
|
||||
в порядке карты.
|
||||
- **Взять в работу**: `gh issue edit <n> --add-assignee @me`.
|
||||
- **Закрыть**: комментарий с ответом, `gh issue close`, указатель на контекст —
|
||||
в Decisions-so-far карты.
|
||||
в порядке карты. Блокеры проверяются запросом `dependencies` по каждому
|
||||
кандидату.
|
||||
- **Взять в работу**: `tea issues edit <n> --add-assignees ddmitry` — первая
|
||||
запись за сессию.
|
||||
- **Закрыть**: `tea comments add <n> -d "<ответ>"`, затем `tea issues close
|
||||
<n>`, затем указатель на контекст (суть + ссылка) в Decisions-so-far карты.
|
||||
|
||||
## Что проверено и когда
|
||||
|
||||
2026-07-29: Gitea 1.27.0, `tea` 0.15.0. Список команд и флагов снят с
|
||||
`tea <команда> --help` установленного бинарника, а не из документации в вебе.
|
||||
При обновлении `tea` стоит перечитать `--help`: набор флагов между версиями
|
||||
менялся. Нативные зависимости и правка тел тикетов через `tea api` проверены
|
||||
живыми запросами: граф блокировок карты #10 в тот день собран заново
|
||||
(#17 ← #14, #15, #16; #14 ← #12, #13, #18; #16 ← #15).
|
||||
|
||||
## Архив
|
||||
|
||||
До 2026-07-19 задачи велись markdown-файлами в `.scratch/<feature>/issues/`
|
||||
(фичи `data-generator` и `generator-model-time-startup-history`, задачи 01–21).
|
||||
Не мигрированы; доступны в истории git — срез `0e312b3`.
|
||||
- До 2026-07-26 трекер жил в GitHub Issues (`dementev-dev/…`). Аккаунт
|
||||
заблокирован, remote `github` заморожен; все 27 номеров issues воссозданы в
|
||||
Gitea один в один. Слепок трекера на момент блокировки —
|
||||
`.scratch/backup/20260726-tracker-snapshot.md` (ветка
|
||||
`chore/gitea-migration`).
|
||||
- До 2026-07-19 задачи велись markdown-файлами в `.scratch/<feature>/issues/`
|
||||
(фичи `data-generator` и `generator-model-time-startup-history`, задачи
|
||||
01–21). Не мигрированы; доступны в истории git — срез `0e312b3`.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Triage labels
|
||||
|
||||
Скиллы оперируют пятью каноническими ролями триажа. Здесь они сопоставлены с
|
||||
метками GitHub Issues этого репозитория.
|
||||
метками issues этого репозитория в Gitea.
|
||||
|
||||
| Роль в mattpocock/skills | Метка GitHub | Значение |
|
||||
| Роль в mattpocock/skills | Метка Gitea | Значение |
|
||||
| ------------------------ | ----------------- | ---------------------------------------------- |
|
||||
| `needs-triage` | `needs-triage` | Мейнтейнеру нужно оценить задачу |
|
||||
| `needs-info` | `needs-info` | Ждём от репортёра дополнительную информацию |
|
||||
@@ -12,4 +12,5 @@
|
||||
| `wontfix` | `wontfix` | Не будет сделано |
|
||||
|
||||
Правый столбец можно поменять под свою лексику. Сейчас — дефолт (метка = имя
|
||||
роли); метки созданы в репозитории GitHub.
|
||||
роли); все пять меток заведены в репозитории Gitea. Посмотреть текущий список —
|
||||
`tea labels list`.
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
# Боевой реализм стенда (v2): широкое событие, заказы, кластер, анонимность
|
||||
|
||||
Статус: Accepted (2026-07-30). Три помеченных отступления подтверждены
|
||||
владельцем на приёмке: порядок страховочных срезов (раздел 9), `Sign` как
|
||||
колонка без механики (раздел 1.1), `VisitID` как эталон самопроверки (1.2).
|
||||
Дата: 2026-07-30. Тикет: #17 (сборка карты #10).
|
||||
Источник истины переехал в v2:
|
||||
https://git.dementev.space/ddmitry/clickstream-data-platform/src/branch/main/docs/specs/2026-07-30-stand-v2-realism.md
|
||||
Источники: резолюции #18 (модель данных), #15 (`purchase` и заказы),
|
||||
#14 (кластер), #13 (цена кластера), #16 (анонимность и склейка); исследование
|
||||
[формата кликстрима Яндекса](../research/2026-07-26-yandex-clickstream-format.md);
|
||||
[docs/generator-realism.md](../generator-realism.md).
|
||||
|
||||
## Зачем
|
||||
|
||||
Менти должен узнавать в стенде тот кликстрим и тот дата-контур, с которыми
|
||||
столкнётся на работе. Сейчас стенд упрощён в четырёх местах: событие разрезано
|
||||
на четыре топика, есть только просмотры страниц (нет денег), весь трафик
|
||||
идентифицирован по email, ClickHouse — одна нода. Карта #10 приняла четыре
|
||||
решения, которые эти упрощения снимают. Эта спека собирает их в одну целевую
|
||||
картину и оценивает объём исполнения.
|
||||
|
||||
Исполнение — **новый репозиторий**, не переработка этого (решение карты #10
|
||||
от 2026-07-23): v1 замораживается как стабильный стенд для менти, v2 стартует
|
||||
пустым с осознанным первым коммитом — переносим только нужное, генератор
|
||||
переписывается, переиспользуются идеи. Рабочее имя — `clickstream-data-platform`
|
||||
(финальное закрепление — при решении тикета о нише). Карта и открытые тикеты
|
||||
переедут туда после создания. Предусловие — задача «Редизайн пути менти»
|
||||
(#9) — выполнено, задача закрыта.
|
||||
|
||||
## Целевая картина одним взглядом
|
||||
|
||||
- **Одно широкое событие** по образцу Яндекс Метрики: плоское ядро,
|
||||
параллельные массивы, сырое поле `ecommerce`. Таксономия `EventType`:
|
||||
`pageview`, `add_to_cart`, `purchase`. Четыре топика уходят.
|
||||
- **Второй источник — заказы бэкенда**: та же Kafka, но ежедневный полный
|
||||
слепок окна изменяемости, со статусами и JSON-позициями. Одна труба,
|
||||
два режима.
|
||||
- **Каталог товаров** — словарь ClickHouse из CSV в репозитории.
|
||||
- **Сверка** клиентского `purchase` против заказа: четыре конструируемых
|
||||
расхождения плюс опоздание. Деньги в витринах — только по бэкенду.
|
||||
- **Кликстрим анонимный**: у события только `ClientID` (кука). Склейка
|
||||
идентичностей — через мост `purchase`↔заказ; часть покупателей — с двух кук.
|
||||
- **Кластер единственным режимом**: 2 шарда × 1 реплика + clickhouse-keeper,
|
||||
`make up` поднимает сразу кластер. Superset — на ноду 2.
|
||||
|
||||
## 1. Широкое событие кликстрима
|
||||
|
||||
Форма — хит Метрики из облачной выгрузки: одно событие = одна строка,
|
||||
многозначное — в параллельных массивах одной длины, плюс одно сырое
|
||||
JSON-поле `ecommerce`. Сессий в потоке нет — их менти собирает сам в DDS.
|
||||
|
||||
### 1.1 Решения по именам и типам
|
||||
|
||||
- **Имена колонок — как в облачной выгрузке Метрики** (`ClientID`,
|
||||
`UTCEventTime`, `purchaseID`…). Сырой слой хранит имена источника; свои
|
||||
snake_case-имена появляются в DDS/DM. Это учебный пункт: у каждого
|
||||
источника — свой стиль, нормализует его склад, а не трекер.
|
||||
- **Идентификаторы — числовые UInt64** (`WatchID`, `VisitID`, `ClientID`),
|
||||
UUID уходят. Исследование советовало UUID не трогать, но тот совет исходил
|
||||
из цены переделки текущего генератора; v2 пишет генератор заново, цена
|
||||
нулевая, а числовые id — самая узнаваемая черта формата Метрики. Генератор
|
||||
держит значения id ниже 2^53: выше этой границы double-числа в JSON (jq,
|
||||
консоль браузера) искажают id при округлении. Настоящая Метрика так не
|
||||
делает — её id длиннее.
|
||||
- **Убираем наши выдумки**: `geo_latitude`, `geo_longitude` — координат в
|
||||
выгрузке Метрики нет (гео — регион и его числовой id). `browser_user_agent`
|
||||
тоже не берём, но это наш выбор, а не запрет источника: исследование
|
||||
запрещало только выдавать это поле за формат Яндекса, оставить разрешало.
|
||||
Разбор строки user agent — не урок этого стенда.
|
||||
- **`Sign` берём как колонку формата, без механики** (решение владельца на
|
||||
приёмке спеки): генератор всегда пишет `Sign = 1`, исправлений записей не
|
||||
шлёт — движки и запросы не меняются. Сама механика версий
|
||||
(CollapsingMergeTree, пара `HitVersion`) — кандидат на потом, по #15.
|
||||
Честность: комментарий в DDL и абзац в документе о реализме («в бою здесь
|
||||
бывают −1/+1, считают через `sum(Sign)`»); в лекции — крючок про
|
||||
CollapsingMergeTree (частый вопрос на собеседованиях).
|
||||
- **Не берём** `ClientEventTime` (в выгрузке Метрики нет клиентской метки;
|
||||
расхождение часов — тема тумана «грязь»), `Params` (второй сырой JSON не
|
||||
нужен: этот навык уже несут заказы), `LastSearchEngineRoot`, `IsPageView`,
|
||||
`NotBounce`, `HTTPError`, `pageViewID`, `CounterUserIDHash`, Openstat
|
||||
и соцдем-поля (см. «чего не воспроизводить» в исследовании).
|
||||
- **Отступление по типу**: `DeviceCategory` берём как UInt8, у Метрики это
|
||||
String; коды те же (1–4).
|
||||
- **Наша честная добавка** — `EventType`: у Метрики такого поля нет
|
||||
(там `isPageView` + `productEventType`), стенду таксономия нужна явно.
|
||||
|
||||
### 1.2 Состав полей (47 колонок)
|
||||
|
||||
Идентификаторы и время:
|
||||
|
||||
| Колонка | Тип | Комментарий |
|
||||
|---|---|---|
|
||||
| `WatchID` | UInt64 | id события (хита) |
|
||||
| `VisitID` | UInt64 | id визита от генератора — эталон самопроверки лабы сессий («собери сам, потом сравни») |
|
||||
| `ClientID` | UInt64 | анонимный id браузера (кука) — ключ шардирования |
|
||||
| `CounterID` | UInt32 | константа стенда (один сайт) |
|
||||
| `EventDate` | Date | дата события |
|
||||
| `UTCEventTime` | DateTime | единственная метка времени, как у Метрики |
|
||||
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
|
||||
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
|
||||
| `Sign` | Int8 | всегда 1: колонка формата, механика исправлений не реализована (см. 1.1) |
|
||||
|
||||
Правила резки визитов в генераторе документируются и совпадают с лабной
|
||||
логикой (30-минутный таймаут).
|
||||
|
||||
Страница и атрибуция: `URL`, `Referer`, `Title`, `UTMSource`, `UTMMedium`,
|
||||
`UTMCampaign`, `UTMContent`, `UTMTerm`, `LastTrafficSource`, `HasGCLID`
|
||||
(UInt8), `YCLID` (UInt64) — 11 колонок: все String, кроме `HasGCLID`
|
||||
(UInt8) и `YCLID` (UInt64).
|
||||
|
||||
Браузер, устройство, гео: `Browser`, `BrowserMajorVersion` (UInt16),
|
||||
`BrowserLanguage`, `OperatingSystem`, `OperatingSystemRoot`, `DeviceCategory`
|
||||
(UInt8, коды 1–4 как у Метрики), `MobilePhoneModel`, `ScreenWidth`,
|
||||
`ScreenHeight` (UInt16), `IPAddress`, `RegionCountry`, `RegionCity`,
|
||||
`RegionCountryID`, `RegionCityID` (UInt32) — 14 колонок.
|
||||
|
||||
Массивы и параметры: `GoalsReached` Array(UInt32) (две цели: корзина и
|
||||
покупка — цели в бою дублируют события, это нормально), `ParsedParamsKey1`
|
||||
Array(String) (свои параметры сайта, один уровень, например вариант
|
||||
A/B-теста; Key2..10 не берём).
|
||||
|
||||
Ecommerce (заполнены только у торговых событий):
|
||||
|
||||
| Колонка | Тип |
|
||||
|---|---|
|
||||
| `purchaseID` | Array(String) |
|
||||
| `purchaseRevenue` | Array(Float64) |
|
||||
| `purchaseCurrency` | Array(String) |
|
||||
| `purchaseCoupon` | Array(String) |
|
||||
| `productID`, `productName`, `productCategory` | Array(String) |
|
||||
| `productPrice` | Array(Int64) |
|
||||
| `productQuantity` | Array(UInt64) |
|
||||
| `productEventType` | Array(String) |
|
||||
| `ecommerce` | String — сырой JSON события, как отдаёт Метрика (кандидат будущей лабы: сырое против разобранного) |
|
||||
|
||||
`add_to_cart` несёт массивы `product*` с одним товаром; `purchase` — состав
|
||||
заказа и блок `purchase*`. Выручка у клиента — во Float64, как у Метрики:
|
||||
это не недосмотр, а часть урока о расхождениях (см. раздел 4).
|
||||
|
||||
### 1.3 Ключи и движки
|
||||
|
||||
- Партиции — **по дням** (`PARTITION BY EventDate`): дневная партиция —
|
||||
единица переобработки (решение #14). Отступление от Метрики (там месяц) —
|
||||
зафиксировать комментарием в DDL.
|
||||
- `ORDER BY (CounterID, EventDate, intHash32(ClientID), WatchID)` — ключ под
|
||||
запросы «по сайту за период по посетителю», хвост `WatchID` даёт
|
||||
дедупликацию в ReplacingMergeTree. Точную форму проверить при исполнении.
|
||||
`SAMPLE BY intHash32(ClientID)` — семплирование по тому же выражению;
|
||||
учебный вопрос к лабе: почему `SAMPLE 0.1` не портит uniq-метрики.
|
||||
- В DDL комментарием зафиксировать вырождение ключа как учебный факт:
|
||||
`CounterID` — константа стенда (один сайт), `EventDate` — константа внутри
|
||||
дневной партиции; реальная сортировка идёт по посетителю и событию
|
||||
(`intHash32(ClientID)`, `WatchID`).
|
||||
- Шардирование — **по `cityHash64(ClientID)`, не по сырому `ClientID`**:
|
||||
структурированный числовой id перекашивает остаток по модулю числа шардов,
|
||||
хеш — нет. Сессионизация, склейка идентичностей и uniq-метрики остаются
|
||||
локальными на шарде. У анонимов кука есть — перекоса в NULL нет.
|
||||
- Предупреждение-урок из v1: колонка версии не должна попадать ни в
|
||||
партицию, ни в ключ сортировки ReplacingMergeTree — иначе версии одной
|
||||
строки никогда не окажутся рядом и не склеятся при мерже.
|
||||
|
||||
### 1.4 Схема как контракт
|
||||
|
||||
Одно машинное описание схемы события (python-модуль или YAML) — источник
|
||||
истины: из него выводятся DDL и валидация генератора, а не наоборот. 47
|
||||
колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью,
|
||||
трансформации, витрины, манифест, доки) — без контракта они расходятся
|
||||
молча. Заодно это учебный артефакт: менти видит на живом примере, что такое
|
||||
«схема как контракт».
|
||||
|
||||
## 2. Заказы бэкенда
|
||||
|
||||
Второй источник и вторая версия правды о покупке. Транспорт — та же Kafka
|
||||
(топик `orders`), но **пачками**: раз в модельный день бэкенд выгружает
|
||||
**полный слепок заказов окна изменяемости K дней**.
|
||||
|
||||
- **K = 7 модельных дней, константа мира** (страховочный срез 3 из #15
|
||||
применён — см. раздел 9). За окном заказ неизменяем, возить его незачем;
|
||||
выручка дня D «дышит» K дней, потом замерзает. Боевой аналог окна есть и у
|
||||
трекеров: лог Метрики «доформировывается» ещё около трёх дней.
|
||||
- Запись слепка — состояние заказа на момент выгрузки, «родной» экспорт
|
||||
бэкенда в snake_case:
|
||||
|
||||
| Поле | Тип | Комментарий |
|
||||
|---|---|---|
|
||||
| `order_id` | String | номер заказа; равен клиентскому `purchaseID` |
|
||||
| `user_id` | UInt64 | пользователь магазина — мост к склейке |
|
||||
| `status` | String | `created` → `paid` → `cancelled` |
|
||||
| `created_at`, `updated_at` | DateTime | |
|
||||
| `items_total`, `discount`, `delivery`, `total` | Decimal(18,2) | деньги бэкенда — в Decimal |
|
||||
| `items` | String | позиции вложенным JSON: `[{sku, qty, price}]` |
|
||||
| `snapshot_date` | Date | дата слепка (день выгрузки) |
|
||||
|
||||
- Приём идемпотентный, но дедуп расщеплён на два слоя:
|
||||
- `ods.order_snapshot` — партиция по `snapshot_date`, **без дедупа**,
|
||||
хранит «как приехало»; идемпотентность повторного прогона — заменой
|
||||
партиции дня слепка, а не ReplacingMergeTree.
|
||||
- Дедуп до последней версии — **argMax** в трансформации при сборке
|
||||
`dds.order`. `dds.order` — единственная дедуплицированная таблица:
|
||||
партиция по дню заказа (`toDate(created_at)`),
|
||||
ReplacingMergeTree(`updated_at`), `ORDER BY order_id` — заказ всегда
|
||||
лежит в одной партиции, дедуп работает.
|
||||
|
||||
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
|
||||
- Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше
|
||||
витрины работают с плоскими массивами `dds.order`: `item_sku`
|
||||
Array(String), `item_qty` Array(UInt64), `item_price` Array(Decimal(18,2))
|
||||
— одной длины, порядок как в JSON. Это единственный носитель навыка
|
||||
«вложенный JSON в ClickHouse» на стенде.
|
||||
- Статусы держим все три: смена `created` → `paid` и есть причина «дыхания»
|
||||
выручки внутри окна; сужение до двух — резервный срез 1.
|
||||
|
||||
## 3. Каталог товаров
|
||||
|
||||
CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `category`,
|
||||
`brand`, `price`) — **словарь ClickHouse** из файла. Тот же файл использует
|
||||
генератор — расхождений нет по построению. Даёт `dictGet` в витринах и
|
||||
разговор о политике обновления словаря. На кластере файл монтируется в обе
|
||||
ноды, словарь создаётся ON CLUSTER.
|
||||
|
||||
## 4. Сверка `purchase` против заказов
|
||||
|
||||
Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер
|
||||
заказа на `/confirmation`). У события `purchase` массив `purchaseID` несёт
|
||||
ровно один элемент (одно подтверждение — один заказ), сверка соединяет по
|
||||
`purchaseID[1]`; правило зафиксировать комментарием в SQL сверки.
|
||||
Расхождения — перечислимый список,
|
||||
детерминированный от seed, не хаос:
|
||||
|
||||
| | Расхождение | Механика в генераторе | Ориентир доли |
|
||||
|---|---|---|---|
|
||||
| A | Отмена | заказ дошёл до `cancelled`, `purchase` остался | ~5% заказов |
|
||||
| B | Потерянное событие | заказ есть, `purchase` не доехал | ~3% |
|
||||
| C | Дельта суммы | сверка приведена к сравнимой базе (`items_total`, не `total`); `amount_delta` — только необъяснённый остаток после этого: округления Float64, вероятность в генераторе | ~1–2% |
|
||||
| D | Дубль события | повторный `purchase` от обновления `/confirmation`: новый `WatchID` с тем же `purchaseID` — бизнес-дубль, не технический; дедуп ReplacingMergeTree его не съедает и не должен | ~2% |
|
||||
|
||||
Классы пересекаются — приоритет: `cancelled` > `lost_event` >
|
||||
`duplicate_event` > `amount_delta` > `match`.
|
||||
|
||||
Пятое — **опоздание** — бесплатно даёт формат доставки: часть заказов
|
||||
впервые появляется в слепке D+1/D+2 («вчера не сходилось, сегодня сошлось»),
|
||||
ориентир ~10%. Точные доли фиксируются при пересборке эталонного мира;
|
||||
манифест хранит точные счётчики по каждому классу расхождений (отмены,
|
||||
потери, дубли).
|
||||
|
||||
Не берём: сироту-фрод (`purchase` есть, а заказа не будет никогда) —
|
||||
механически дублирует B.
|
||||
|
||||
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
|
||||
бэкенду**. Единственное разрешённое исключение — клиентская оценка выручки
|
||||
под именем `declared_*` там, где атрибуция без трекера невозможна (UTM);
|
||||
слово `declared` в имени — сигнал «это заявка клиента, не деньги
|
||||
отчётности». Оно выучивается на конфликте: суммы не сойдутся, менти сам
|
||||
раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
|
||||
|
||||
## 5. Анонимность и склейка идентичностей
|
||||
|
||||
- Email из кликстрима исчезает полностью: у события только `ClientID`.
|
||||
Это честно к Logs API Метрики (UserID не выгружается). Отдельная «доля
|
||||
анонимов» не нужна: мы знаем ровно тех, кто купил, — это сам урок.
|
||||
- **Карта соответствий кука↔пользователь** строится трансформацией из уже
|
||||
существующего моста: `purchase`-событие (`ClientID`, `purchaseID`) ↔ заказ
|
||||
(`order_id`, `user_id`). Ни новых полей, ни нового транспорта.
|
||||
- Форма и место карты: таблица `dds.identity_map`
|
||||
(`client_id` UInt64, `user_id` UInt64, `first_matched_at` DateTime),
|
||||
ReplacingMergeTree, `ORDER BY (client_id, user_id)`, шардирование по
|
||||
`cityHash64(client_id)` — тем же выражением, что события (иначе ко-локации
|
||||
нет); ко-локация делает обогащение витрин локальным;
|
||||
заказы при сборке карты подтягиваются через GLOBAL JOIN.
|
||||
- **N:1**: часть покупателей покупает с двух кук («телефон и ноутбук») —
|
||||
параметр мира; значение фиксирует эта спека: 15%, детерминировано
|
||||
от seed. Ядро лабы:
|
||||
`uniq(посетителей) > uniq(людей)`, менти выводит расхождение сам.
|
||||
Константа мира: каждый двухкуковый покупатель делает минимум по одному
|
||||
заказу с каждой куки — иначе вторая кука не попадает в карту соответствий
|
||||
(она строится только из покупок) и лаба не воспроизводится. Манифест
|
||||
хранит число именно таких пар.
|
||||
- Витрины разводят имена честно: **«посетители»** (`uniq(ClientID)`) и
|
||||
**«известные пользователи»** (после склейки) — оба числа рядом в дашборде.
|
||||
|
||||
## 6. Кластер
|
||||
|
||||
Соседний `clickhouse-learning-cluster` остаётся разминкой при курсе: там
|
||||
концепции, здесь жизнь — забыть ON CLUSTER, получить ошибку, починить.
|
||||
|
||||
Топология и режим — по резолюции #14:
|
||||
|
||||
- **2 шарда × 1 реплика + отдельный clickhouse-keeper**, единственный режим:
|
||||
`make up` поднимает сразу кластер, выключателя нет. Страховка от «слишком
|
||||
сложно» — отсутствие реплик и runbook, а не профиль без кластера.
|
||||
- Движки локальных таблиц — `Replicated*` (макросы `{shard}`/`{replica}`,
|
||||
пути keeper, готовность к будущей реплике). Без HAProxy — балансировать
|
||||
нечего; в доках абзац «в бою здесь LB».
|
||||
- Роли нод: нода 1 — инициатор DDL и подключение Airflow; **Superset — на
|
||||
ноду 2**. Это осознанная ловушка правильных ошибок: забытый ON CLUSTER или
|
||||
VIEW поверх локальной таблицы проявляются в дашборде сами.
|
||||
- **Приём Kafka**: Kafka-таблицы и MV — на обеих нодах, одна consumer group,
|
||||
2 партиции на топик; MV пишут в Distributed-цели. Раскладку решает ключ:
|
||||
события — по `cityHash64(ClientID)` (см. 1.3), заказы —
|
||||
`cityHash64(order_id)`, сырьё STG —
|
||||
`cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между
|
||||
прогонами, куда легли данные — нет».
|
||||
- **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные
|
||||
поля — без значений по умолчанию. Контракт присутствия: генератор выдаёт
|
||||
**все 47 полей в каждом событии**; «пусто» — пустой массив, пустая строка
|
||||
или 0, а не отсутствие ключа в JSON. Так строгий приём уживается с
|
||||
полями, пустыми по смыслу (ecommerce у `pageview`, UTM у прямого захода). Несовпадение имени поля — громкая
|
||||
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
|
||||
опечатка иначе не падает.
|
||||
- **Политика соединений**: по ключу ко-локации — обычное соединение с
|
||||
комментарием, почему локальный результат корректен; по любому другому ключу
|
||||
— только явный GLOBAL; `NOT IN` — только `GLOBAL NOT IN`. Сверка
|
||||
`purchase`↔заказ —
|
||||
легитимная GLOBAL-витрина (заказы малы).
|
||||
- **Конвейер без TRUNCATE**: поток — append-only в ReplacingMergeTree (дедуп
|
||||
через argMax); батчевая переобработка — по дневным партициям
|
||||
(`DROP/REPLACE PARTITION ON CLUSTER`); `TRUNCATE ... ON CLUSTER` остаётся
|
||||
только в `make reset`. `DROP/REPLACE PARTITION` работает только по
|
||||
**локальным** таблицам ON CLUSTER, не по Distributed; замена через
|
||||
DROP+INSERT неатомарна — дашборд в середине прогона честно моргает (это
|
||||
осознанная цена, не баг).
|
||||
- **Поздние заказы поглощает только ODS** (`ods.order_snapshot` — новая
|
||||
партиция дня слепка, без переделки старого); материализованное ниже —
|
||||
нет. Каждый прогон ETL перестраивает партиции последних K+1 дней у
|
||||
заказозависимых объектов (`dds.order` и производные, `dm.dq_summary`).
|
||||
Сессии перестраиваются только за текущий день: правило мира — сессия
|
||||
режется по границе модельных суток, дневная партиция самодостаточна.
|
||||
- Для ETL-вставок — `distributed_foreground_insert = 1` (раньше называлась
|
||||
`insert_distributed_sync`), иначе проверки видят неполные данные.
|
||||
- Все контрольные суммы и dq-проверки считают через `argMax`/`GROUP BY`/
|
||||
`FINAL` — голый `count()` по ReplacingMergeTree зависит от того, сколько
|
||||
мержей уже прошло.
|
||||
- **Проверки и контрольные суммы — только по Distributed-таблицам**:
|
||||
агрегаты от раскладки не зависят; раскладка по шардам нигде не фиксируется,
|
||||
пошардовые наблюдения — исследовательские, в лабах.
|
||||
|
||||
### Ресурсный бюджет (#10)
|
||||
|
||||
Расчёт на ноутбук менти с 16 ГБ памяти; у кого 8 ГБ — берёт VDS за свой счёт.
|
||||
Полный стенд в покое ≈3,4 ГБ. Топология 2×1 добавляет ≈0,6–0,8 ГБ — влезает
|
||||
свободно. Топология 2×2 добавила бы ≈1,7–1,9 ГБ и упёрлась бы в дефолтный
|
||||
бюджет WSL2 (~8 ГБ) — это второй довод против реплик, рядом с главным
|
||||
(репликационная эксплуатация — отдельный операционный домен). Координатор —
|
||||
clickhouse-keeper, а не ZooKeeper, в том числе из-за этого бюджета.
|
||||
|
||||
## 7. Слои: карта таблиц v2
|
||||
|
||||
| Слой | Объект | Что это |
|
||||
|---|---|---|
|
||||
| Kafka | `hits`, `orders` | два топика, по 2 партиции |
|
||||
| STG | `stg.kafka_hits`, `stg.hits_raw` + MV; то же для orders | сырые строки, Kafka Engine на обеих нодах |
|
||||
| ODS | `ods.event` (+`_errors`) | типизированное широкое событие, ReplacingMergeTree |
|
||||
| ODS | `ods.order_snapshot` (+`_errors`) | слепки заказов как приехали, партиция по `snapshot_date`, без дедупа |
|
||||
| DDS | `dds.session` | сборка сессий из событий (наследник `dds.click`) |
|
||||
| DDS | `dds.v_event` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
|
||||
| DDS | `dds.order` | единственная дедуплицированная таблица заказа: партиция по дню заказа (`toDate(created_at)`), ReplacingMergeTree(`updated_at`), `ORDER BY order_id`, дедуп до последней версии — argMax в трансформации при сборке |
|
||||
| DDS | `dds.identity_map` | карта кука↔пользователь |
|
||||
| DDS | словарь `products` | каталог из CSV |
|
||||
| DM | витрины `dm.v_*`, `dm.dq_summary` | см. ниже |
|
||||
|
||||
Служебные колонки: `ods.event` и `ods.order_snapshot` получают метку приёма
|
||||
`_ingested_at`; у `ods.event` та же колонка — колонка версии
|
||||
ReplacingMergeTree. Таблицы `stg.*_raw` хранят виртуальные колонки Kafka
|
||||
(`_topic`, `_partition`, `_offset`, `_timestamp`) — без них урок «какая нода
|
||||
читала топик» ненаблюдаем. `stg.hits_raw` дополнительно хранит извлечённый
|
||||
`event_date` — им кормится переобработка дня X (при исчерпании retention
|
||||
Kafka переобработка возможна только из эталонного артефакта).
|
||||
|
||||
`dds.v_event` — первый на стенде пример правила «слой — это контракт, а не
|
||||
обязательно копия данных».
|
||||
|
||||
Событие в DDS не дублируется: склейки четырёх источников больше нет, ODS уже
|
||||
широкий и типизированный; DDS хранит бизнес-сущности (сессия, заказ,
|
||||
идентичность). «Грязные» записи по-прежнему уходят в `*_errors`, не валят
|
||||
пайплайн.
|
||||
|
||||
### Витрины DM
|
||||
|
||||
- **`v_revenue_daily`** (выручка, только от заказов): `report_date`,
|
||||
`product_category` (через `dictGet` каталога + ARRAY JOIN позиций),
|
||||
`orders`, `units`, `revenue`, `aov`. Считается по заказам в статусе
|
||||
`paid`; внутри окна K число дня «дышит».
|
||||
- **`v_purchase_vs_orders`** (сверка): FULL OUTER GLOBAL JOIN по
|
||||
`purchaseID = order_id`; колонки: `order_day`, `order_id`,
|
||||
`declared_revenue` (клиент), `items_total` (бэкенд, сравнимая база — не
|
||||
`total`: промокод и доставка клиенту не видны), `status`, `mismatch_class`
|
||||
(`match` / `cancelled` / `lost_event` / `duplicate_event` / `amount_delta`,
|
||||
в порядке приоритета — классы пересекаются, побеждает более ранний).
|
||||
`match` — большинство строк; `amount_delta` — только необъяснённый остаток
|
||||
после приведения к сравнимой базе (округления Float64, ~1–2% заказов).
|
||||
Строка «`purchase` без заказа» внутри живого окна — опоздание, ждущее
|
||||
слепка, а не расхождение: она получает служебный класс `awaiting_order`
|
||||
(шестое значение `mismatch_class`, вне приоритетов расхождений). После
|
||||
закрытия окна K таких строк не остаётся — сироты исключены построением
|
||||
(раздел 4).
|
||||
- **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру);
|
||||
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
|
||||
`declared_revenue` по UTM.
|
||||
- **`v_daily_traffic`** — расширяется парой «посетители» / «известные
|
||||
пользователи» (обогащение через `dds.identity_map`, локальное соединение
|
||||
по ключу ко-локации).
|
||||
- `v_events_enriched`, `v_top_pages_daily`, `v_session_overview`,
|
||||
`v_dq_errors_daily` — переезжают на новую модель без смены роли: источник —
|
||||
`dds.v_event`, не `ods.event`.
|
||||
- `dm.dq_summary` переводится с TRUNCATE+INSERT на партиционную замену
|
||||
(политика «без TRUNCATE»).
|
||||
|
||||
Дашборд Superset получает три новых сюжета: выручка по дням и категориям,
|
||||
таблица сверки с классами расхождений, пара посетители/известные.
|
||||
|
||||
Ландшафт итогом: Kafka — единственная труба (кликстрим потоком, заказы
|
||||
пачками), Postgres остаётся только служебной базой Airflow. Смешанность
|
||||
ландшафта выражена режимами и частотами, а не второй трубой.
|
||||
|
||||
## 8. Эталонный мир и манифест
|
||||
|
||||
Пересборка артефакта `data/startup_history/` неизбежна и оплачена решением
|
||||
#18 один раз — все изменения генератора съезжаются в одну пересборку.
|
||||
Манифест расширяется контрольными числами:
|
||||
|
||||
- заказная сторона: заказы и выручка по дням; манифест хранит точные
|
||||
счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
|
||||
самопроверка лабы сверки;
|
||||
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых
|
||||
покупателей — лаба склейки получает самопроверку.
|
||||
|
||||
Артефакт вырастет (ecommerce-массивы, заказы) — размер проверить при
|
||||
пересборке. Политика версионирования артефакта здесь не решается (туман
|
||||
карты #10).
|
||||
|
||||
## 9. Оценка объёма исполнения
|
||||
|
||||
v2 стартует пустым, поэтому объём ниже — это новый код, а не правка на
|
||||
месте; v1 служит источником идей и образцов (масштаб оценён по нему).
|
||||
|
||||
| Направление | Что строим | Объём |
|
||||
|---|---|---|
|
||||
| Генератор | с нуля: модель v1 не переносится (другая модель данных, плюс известные проблемы производительности v1); широкое событие, таксономия, анонимность, N:1, заказы слепками, расхождения A–D, каталог; масштаб — ~4–5 тыс. строк с тестами | L |
|
||||
| Инфраструктура | compose: 2 ноды CH + keeper + остальной стенд; конфиги кластера, макросы; make/скрипты | M — ~10–12 файлов |
|
||||
| SQL | 5 DDL-файлов (ON CLUSTER, Replicated*, Distributed) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность |
|
||||
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка, ожидание дневного батча заказов — сенсор/Datasets), world_init/next_day, helpers | M — ~5–6 файлов |
|
||||
| Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла |
|
||||
| Эталонный мир | сборка артефакта v2, манифест-счётчики, чек-скрипты | M–L |
|
||||
| Мониторинг | Prometheus/Grafana: цели двух нод и keeper | S — 2–4 конфига |
|
||||
| Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR |
|
||||
|
||||
Итого ~80–110 файлов нового репозитория (посчитаны конфиги по нодам,
|
||||
документация, экспорт Superset — дерево YAML, CI и артефакты данных);
|
||||
тяжёлое — генератор и SQL. Это крупный релиз, но он режется на этапы с
|
||||
работающим стендом после каждого. Согласуется с оценкой исследования #13
|
||||
(~15–20 файлов только на кластерную часть).
|
||||
|
||||
### Решение по страховочным срезам (#15)
|
||||
|
||||
- **Срез 3 применён**: окно K — константа мира (7 дней), не параметр.
|
||||
- **Срез 2 применён как порядок, не как отказ**: расхождения A+C входят в
|
||||
этап сверки, B+D — отдельным следующим этапом.
|
||||
- **Срез 1 в резерве**: статусы держим все три (`created`/`paid`/
|
||||
`cancelled`) — на статусе `paid` стоит «дыхание» выручки; сужение до пары
|
||||
`created`/`cancelled` — запасной ход, если генератор заказов окажется дороже
|
||||
ожиданий. Связка: если срез 1 сработает, определение выручки в
|
||||
`v_revenue_daily` придётся сменить с «заказы в статусе `paid`» на «все
|
||||
неотменённые заказы».
|
||||
- **Отступление от порядка #15**: резолюция предписывала резать в порядке
|
||||
1 → 2 → 3, спека применяет 3 и 2, а 1 держит в резерве. Довод: срезы 3 и 2
|
||||
ничего не отнимают у уроков (окно и так одно, расхождения и так вводятся
|
||||
этапами), а срез 1 убирает статус `paid` — вместе с ним ушло бы «дыхание»
|
||||
выручки.
|
||||
|
||||
### Этапы для /to-tickets (черновик)
|
||||
|
||||
0. Рождение v2: создать репозиторий (рабочее имя
|
||||
`clickstream-data-platform`), осознанный первый коммит (скелет доков,
|
||||
AGENTS.md, лицензия), переезд карты #10 и открытых тикетов.
|
||||
1. Каркас стенда: кластерный compose (2×CH + keeper + Kafka, Airflow,
|
||||
Superset, мониторинг), конфиги, `make up`, smoke-проверка ON CLUSTER.
|
||||
2. DDL и генератор (слиты в один этап — DDL проверяется только настоящими
|
||||
данными): базы и таблицы событий ON CLUSTER, приём `hits` обеими нодами;
|
||||
широкое событие, таксономия, анонимность, N:1 (клиентская сторона
|
||||
целиком). В конце этапа фиксируется маленький «зерновой» мир для
|
||||
стабильных приёмок следующих этапов (полная пересборка эталонного мира —
|
||||
отдельный этап 7).
|
||||
3. Заказы и каталог: генератор слепков, STG/ODS/DDS заказа, словарь.
|
||||
4. Трансформации и витрины: сессии, identity_map, выручка, сверка A+C.
|
||||
5. Airflow: `etl_pipeline` (партиционная переобработка, ожидание дневного
|
||||
батча заказов — сенсор/Datasets).
|
||||
6. Расхождения B+D и опоздания; счётчики манифеста.
|
||||
7. Эталонный мир: пересборка артефакта, чек-скрипты.
|
||||
8. Superset-дашборд v2.
|
||||
9. Мониторинг и runbook «keeper упал / DDL повис в очереди».
|
||||
|
||||
Критерий приёмки этапа — честный: `make up` работает и проходят
|
||||
smoke-проверки, а не «дашборд зелёный». Это минимальная планка; свои
|
||||
наблюдаемые критерии каждый этап получает при разбиении в /to-tickets. Документация правится в PR этапа
|
||||
(правило AGENTS.md).
|
||||
|
||||
## 10. Границы: чего не делаем
|
||||
|
||||
- «Грязь» в данных: боты, дубли на транспорте, опоздавшие мобильные батчи,
|
||||
расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом.
|
||||
- Политика версионирования эталонного артефакта — туман карты.
|
||||
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
|
||||
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
|
||||
переносится. Спека даёт будущим лабам только опорные точки — контрольные
|
||||
числа манифеста (сверка, идентичность). Явное следствие: после этапа 9
|
||||
стенд работает, но учебного пути на нём ещё нет.
|
||||
- Инкрементальный ETL (#8) — свой issue.
|
||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||
- Полный словарь торговых событий Метрики (detail, remove, impressions),
|
||||
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
|
||||
- Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
|
||||
визитов Метрики Про); сама колонка `Sign` уже в схеме, статикой (см. 1.1).
|
||||
- Событийный лог заказов, CDC/Debezium, HTTP-сервис заказов, шапка+строки,
|
||||
отдельный поток возвратов — отклонены в #15/#18.
|
||||
- Файловые дропы как источник — зона следующего стенда (Lakehouse), у нас
|
||||
остаются теорией; каталог отдельным топиком Kafka — в бою так не делают
|
||||
(оба отклонения — из #18).
|
||||
- Вероятностная склейка, identity graph, кука 1:N («семейный планшет») —
|
||||
тема лекции, не лабы.
|
||||
- Эмуляция `setUserID` и сюжет «логин посреди сессии» через свои параметры —
|
||||
отклонены в #16: нечестно к формату выгрузки Метрики.
|
||||
|
||||
## 11. Проверить при исполнении
|
||||
|
||||
- Поведение соединения двух Distributed-таблиц и `distributed_product_mode` —
|
||||
эмпирически на стенде (хвост #14).
|
||||
- Kafka Engine на двух нодах в одной consumer group: ребаланс партиций между
|
||||
прогонами, отсутствие дублей при штатной работе.
|
||||
- Точная форма `ORDER BY` ODS-таблиц (выражение `intHash32` в ключе
|
||||
ReplacingMergeTree).
|
||||
- Размер артефакта эталонного мира после пересборки.
|
||||
- Спорные API (Airflow Datasets/сенсоры, ClickHouse DDL) — перед кодом
|
||||
сверять через MCP Context7 (правило AGENTS.md).
|
||||
- Генератор: рабочее решение — Python с производительной архитектурой
|
||||
(батчевая генерация вместо посточной, быстрая JSON-сериализация,
|
||||
распараллеливание по модельным дням). Читаемость генератора для менти —
|
||||
не довод при выборе языка: он в любом случае сложнее уровня DE-джуна.
|
||||
До этапа 3 зафиксировать требования производительности (пересборка
|
||||
эталонного мира, живой поток ×60); переход на компилируемый язык
|
||||
(Rust/Go) — только если замеры покажут, что Python приемлемой скорости
|
||||
не даёт.
|
||||
|
||||
## 12. Влияние на документацию
|
||||
|
||||
Состав и структура доков v2 проектируются заново: набор документов v1
|
||||
сложился исторически и не копируется. Какие документы нужны v2 — решение
|
||||
этапа 0. Обязательный минимум по содержанию (не по списку файлов):
|
||||
быстрый старт, архитектура слоёв, операционка с runbook keeper/DDL,
|
||||
словарь терминов (широкое событие, слепок, окно изменяемости,
|
||||
посетители/известные пользователи, ко-локация).
|
||||
|
||||
Документ о реализме (`generator-realism.md`) переезжает в v2 и получает
|
||||
крупное обновление: Kafka — учебная замена батчевого Logs API («настоящий
|
||||
Logs API — это скачанный TSV»); trade-off «инкремент экономнее, слепок
|
||||
надёжнее» + сноска про compacted topic; identity stitching перестаёт быть
|
||||
чистой теорией; «прямое чтение прод-базы — анти-приём, в бою — реплика или
|
||||
выгрузка»; колонка `Sign` без механики — честное ограничение стенда
|
||||
(в бою −1/+1 и `sum(Sign)`); полный словарь торговых событий — теория.
|
||||
|
||||
В v1 при заморозке — указатель на v2 в README (форму решить при рождении
|
||||
v2, этап 0).
|
||||
|
||||
### Опорные точки для будущих лекций и лаб
|
||||
|
||||
Лабы и курс — вне скоупа спеки (см. раздел 10). Список нужен только затем,
|
||||
чтобы хвосты резолюций не потерялись:
|
||||
|
||||
- анти-паттерны ключа шардирования (`rand()`, `toDate`) — как отрицательные
|
||||
примеры;
|
||||
- «как выбирают топологии в бою»: часто 1 шард × N реплик, шардирование — про
|
||||
рост;
|
||||
- сцена «разные consumer groups → дубли»;
|
||||
- словарь регионов из CSV той же машинерией, что каталог товаров (оживляет
|
||||
`RegionCityID`);
|
||||
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
|
||||
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
|
||||
синтетическая постановка — осознанный приём;
|
||||
- лекция «`Sign` и CollapsingMergeTree»: почему на стенде `sum(Sign)` =
|
||||
`count()`, а в бою — нет; частый вопрос на собеседованиях;
|
||||
- лекция про идентичность «как в бою»: `setUserID` и first-party id,
|
||||
детерминированная против вероятностной склейки, identity graph,
|
||||
кросс-девайс, CDP — с рамкой «мы склеили через транзакции, потому что трекер
|
||||
user id не отдаёт».
|
||||
Reference in New Issue
Block a user