- Зачем:
- клиентская сторона мира становится целой: без add_to_cart и purchase
в данных нет ни таксономии событий, ни вложенного JSON, ни денег,
а метка «покупатель» из плана состава ни на что не влияла (#40).
- Что:
- добавлен модуль commerce: корзина шире заказа, номер заказа вида
ГГГГММДД-NNNN, промокод без скидки в сумме, сырой ecommerce через orjson;
- метка покупателя получила два рычага — долгую жизнь куки в плане и
свою воронку в дне; CART_PERCENT опущен с 8 до 6, чтобы конверсия
мира осталась около 2%;
- часть цен каталога получила копейки: productPrice округляется форматом,
purchaseRevenue несёт точную сумму — расхождение живёт внутри события;
- граница суток забирает страницу подтверждения вместе с её покупкой:
потерь на клиентской стороне этот этап не заводит;
- решения и перемеренные числа мира записаны в спеку генератора, §9.
- Проверка:
- make lint && make typecheck && make test — 383 passed (было 353).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
4.9 KiB
Python
89 lines
4.9 KiB
Python
"""Сборка «описания выгрузки» — публичной документации формата события.
|
|
|
|
Аналог документации Метрики: по нему пишется сторона хранилища (DDL, матвью,
|
|
витрины), поэтому документ должен читаться сам по себе, без чтения кода. Всё
|
|
содержание берётся из контракта (`schema`), правится только там; свежесть
|
|
документа сторожит тест.
|
|
|
|
Запуск — из корня репозитория целью `make docs`.
|
|
"""
|
|
|
|
import argparse
|
|
from itertools import groupby
|
|
from pathlib import Path
|
|
|
|
from clickstream_generator.schema import COLUMNS, Column
|
|
|
|
PREAMBLE = """# Описание выгрузки: событие кликстрима
|
|
|
|
Документ собран из контракта схемы генератора
|
|
(`generator/src/clickstream_generator/schema.py`). Руками не править —
|
|
пересобрать: `make docs`.
|
|
|
|
Одно событие — одна строка: хит по образцу облачной выгрузки Яндекс Метрики.
|
|
Многозначное лежит в параллельных массивах, плюс одно сырое JSON-поле
|
|
`ecommerce`. Длина у массивов общая **внутри группы**, а не по всему
|
|
событию: `purchase*` — по элементу на заказ (у нас всегда один), `product*` —
|
|
по элементу на товар. Отдельной сущности «визит» в выгрузке нет — визиты
|
|
собирают на стороне хранилища, а `VisitID` дан как эталон для самопроверки.
|
|
|
|
Имена и типы колонок — стороны источника. Хранилище принимает их как есть и
|
|
нормализует у себя: то же имя в нашем стиле ждёт в столбце «Нормализованное
|
|
имя». Это имя источника, приведённое к snake_case, а не имя атрибута в
|
|
модели данных: слой DDS складывает свою модель и называет атрибуты по ней.
|
|
Столбец «Тип numpy» показывает, чем колонка представлена внутри генератора;
|
|
у массивов это тип элемента. Номер — место колонки в выгрузке: порядок задан
|
|
контрактом.
|
|
|
|
Колонки группы «Ecommerce» заполнены только у торговых событий:
|
|
`add_to_cart` несёт один товар, `purchase` — состав заказа и блок
|
|
`purchase*`. У остальных событий они пусты.
|
|
|
|
Деньги: `productPrice` — целые рубли, округление формата. Точная сумма
|
|
заказа живёт в `purchaseRevenue` и в сыром `ecommerce`, поэтому пересчитать
|
|
выручку по разобранным массивам нельзя — цены каталога бывают с копейками.
|
|
|
|
Всего колонок: {count}."""
|
|
|
|
TABLE_HEADER = (
|
|
"| № | Колонка | Тип ClickHouse | Тип numpy | Нормализованное имя | Комментарий |",
|
|
"|---|---|---|---|---|---|",
|
|
)
|
|
|
|
|
|
def render() -> str:
|
|
"""Собирает документ целиком: преамбула и таблица колонок по группам."""
|
|
lines = PREAMBLE.format(count=len(COLUMNS)).splitlines()
|
|
numbers = iter(range(1, len(COLUMNS) + 1))
|
|
for group, columns_of_group in groupby(COLUMNS, key=lambda column: column.group):
|
|
lines += ["", f"## {group.value}", "", *TABLE_HEADER]
|
|
lines += [table_row(next(numbers), column) for column in columns_of_group]
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def table_row(number: int, column: Column) -> str:
|
|
"""Строка таблицы колонок; номер — место колонки в порядке выгрузки."""
|
|
cells = (
|
|
str(number),
|
|
f"`{column.name}`",
|
|
f"`{column.clickhouse_type}`",
|
|
f"`{column.numpy_dtype}`",
|
|
f"`{column.normalized_name}`",
|
|
column.comment,
|
|
)
|
|
return "| " + " | ".join(cells) + " |"
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Собирает описание выгрузки из контракта схемы события."
|
|
)
|
|
parser.add_argument("output", type=Path, help="путь к файлу описания")
|
|
output = parser.parse_args().output
|
|
output.write_text(render(), encoding="utf-8")
|
|
print(f"Описание выгрузки собрано: {output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|