- Зачем:
- линия постановки: тест инвариантов обещал ловить дрейф колонок, но
переименование Referer или перенос колонки в другую группу проходили
все проверки; линия стандартов: докстринг говорил о contract-тесте
как о существующем и не нёс следа сверки API через Context7.
- Что:
- тест состава по разделу 1.2 мастер-спеки: группа, имя и тип всех 47
колонок записаны независимо от контракта, поэтому молчаливое
переименование или перестановка краснеют — проверено правкой
Referer → Referrer.
- контракт: contract-тест переведён в будущее время со ссылкой на
спеку; записана сверка записи типов ClickHouse (Context7 и запрос
к узлу стенда 26.3.17.56 — параметры входят в имя типа целиком).
- описание выгрузки самодостаточнее: расшифрованы коды
DeviceCategory, домен LastTrafficSource честно назван неполным,
«идентификатор» сведён к «id» ради одного слова на одну вещь.
- schema_doc: убраны неиспользуемые параметры render и main,
row → table_row; тест строки сверяет свойство, а не форму.
- Проверка:
- make test (249 тестов), make lint;
- make docs, затем git diff --exit-code docs/ — пусто.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
444 lines
17 KiB
Python
444 lines
17 KiB
Python
"""Контракт схемы события: чистые данные о колонках выгрузки, никакой логики.
|
|
|
|
Состав, имена и типы решены мастер-спекой «Боевой реализм стенда» (разделы
|
|
1.1–1.2) и здесь не переоткрываются — модуль записывает их машинно-читаемо.
|
|
Контракт принадлежит генератору и кормит трёх потребителей: сам генератор,
|
|
его валидацию и «описание выгрузки» в доках (`schema_doc`). Хранилище
|
|
строится по описанию, а не по модулю; границу будет сторожить contract-тест,
|
|
сверяющий `system.columns` поднятого стенда с этим контрактом, — он придёт
|
|
вместе с типизированным ODS (спека генератора, раздел 3).
|
|
|
|
Что несёт описатель колонки:
|
|
|
|
- `name` — имя источника, как в облачной выгрузке Метрики; сырой слой хранит
|
|
его без изменений.
|
|
- `clickhouse_type` — тип в хранилище ровно в той записи, в какой его вернёт
|
|
`system.columns`: параметры входят в имя типа целиком, без сокращений
|
|
(`LowCardinality(String)`, `Array(Float64)`). Сверено 2026-08-01 —
|
|
по документации ClickHouse через Context7 и запросом к узлу стенда
|
|
(26.3.17.56); от этой записи зависит будущий contract-тест.
|
|
- `numpy_dtype` — чем колонка представлена внутри генератора; у массивов это
|
|
тип элемента. Строки живут в `object`-массивах: numpy-строки фиксированной
|
|
длины стенду ничего не дают.
|
|
- `dds_name` — наше snake_case-имя, под которым колонка появится в DDS.
|
|
Перевод механический, акроним идёт одним куском (`utm_source`, `has_gclid`,
|
|
`ip_address`); единственное исключение — `client_timezone`: «timezone»
|
|
пишем одним словом.
|
|
- `group` — раздел описания выгрузки; колонки одной группы идут подряд.
|
|
- `comment` — строка описания для менти, попадает в документ как есть.
|
|
|
|
Порядок колонок несёт сам кортеж `COLUMNS` — он и есть порядок выгрузки, по
|
|
нему считается нумерация в документе. Отдельного поля с номером намеренно
|
|
нет: два источника порядка разъезжаются при первой же вставке колонки
|
|
в середину.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
|
|
class ColumnGroup(Enum):
|
|
"""Разделы описания выгрузки; порядок объявления — порядок в документе."""
|
|
|
|
IDENTIFIERS = "Идентификаторы и время"
|
|
PAGE = "Страница и атрибуция"
|
|
CLIENT = "Браузер, устройство, гео"
|
|
PARAMS = "Массивы и параметры"
|
|
ECOMMERCE = "Ecommerce"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Column:
|
|
"""Описатель одной колонки выгрузки."""
|
|
|
|
name: str
|
|
clickhouse_type: str
|
|
numpy_dtype: str
|
|
dds_name: str
|
|
group: ColumnGroup
|
|
comment: str
|
|
|
|
|
|
COLUMNS: tuple[Column, ...] = (
|
|
Column(
|
|
name="WatchID",
|
|
clickhouse_type="UInt64",
|
|
numpy_dtype="uint64",
|
|
dds_name="watch_id",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="id события — хита; держится ниже 2^53, выше числа в JSON округляются",
|
|
),
|
|
Column(
|
|
name="VisitID",
|
|
clickhouse_type="UInt64",
|
|
numpy_dtype="uint64",
|
|
dds_name="visit_id",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="id визита от генератора — эталон лабы: собери сессии сам и сравни",
|
|
),
|
|
Column(
|
|
name="ClientID",
|
|
clickhouse_type="UInt64",
|
|
numpy_dtype="uint64",
|
|
dds_name="client_id",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="анонимный id браузера — кука; по хешу от неё таблица шардируется",
|
|
),
|
|
Column(
|
|
name="CounterID",
|
|
clickhouse_type="UInt32",
|
|
numpy_dtype="uint32",
|
|
dds_name="counter_id",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="id счётчика: на стенде константа, сайт один",
|
|
),
|
|
Column(
|
|
name="EventDate",
|
|
clickhouse_type="Date",
|
|
numpy_dtype="datetime64[D]",
|
|
dds_name="event_date",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="дата события; по ней режется партиция",
|
|
),
|
|
Column(
|
|
name="UTCEventTime",
|
|
clickhouse_type="DateTime",
|
|
numpy_dtype="datetime64[s]",
|
|
dds_name="utc_event_time",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="время события в UTC — единственная метка времени, как у Метрики",
|
|
),
|
|
Column(
|
|
name="ClientTimeZone",
|
|
clickhouse_type="Int16",
|
|
numpy_dtype="int16",
|
|
dds_name="client_timezone",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="смещение часового пояса клиента от UTC, в минутах",
|
|
),
|
|
Column(
|
|
name="EventType",
|
|
clickhouse_type="LowCardinality(String)",
|
|
numpy_dtype="object",
|
|
dds_name="event_type",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="тип события: pageview, add_to_cart, purchase — добавка стенда,"
|
|
" у Метрики такого поля нет",
|
|
),
|
|
Column(
|
|
name="Sign",
|
|
clickhouse_type="Int8",
|
|
numpy_dtype="int8",
|
|
dds_name="sign",
|
|
group=ColumnGroup.IDENTIFIERS,
|
|
comment="всегда 1: колонка формата, исправлений записей генератор не шлёт",
|
|
),
|
|
Column(
|
|
name="URL",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="url",
|
|
group=ColumnGroup.PAGE,
|
|
comment="адрес страницы события",
|
|
),
|
|
Column(
|
|
name="Referer",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="referer",
|
|
group=ColumnGroup.PAGE,
|
|
comment="адрес, с которого посетитель пришёл на страницу",
|
|
),
|
|
Column(
|
|
name="Title",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="title",
|
|
group=ColumnGroup.PAGE,
|
|
comment="заголовок страницы",
|
|
),
|
|
Column(
|
|
name="UTMSource",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="utm_source",
|
|
group=ColumnGroup.PAGE,
|
|
comment="метка utm_source: площадка перехода",
|
|
),
|
|
Column(
|
|
name="UTMMedium",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="utm_medium",
|
|
group=ColumnGroup.PAGE,
|
|
comment="метка utm_medium: тип трафика",
|
|
),
|
|
Column(
|
|
name="UTMCampaign",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="utm_campaign",
|
|
group=ColumnGroup.PAGE,
|
|
comment="метка utm_campaign: рекламная кампания",
|
|
),
|
|
Column(
|
|
name="UTMContent",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="utm_content",
|
|
group=ColumnGroup.PAGE,
|
|
comment="метка utm_content: что различает объявления одной кампании",
|
|
),
|
|
Column(
|
|
name="UTMTerm",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="utm_term",
|
|
group=ColumnGroup.PAGE,
|
|
comment="метка utm_term: ключевое слово перехода",
|
|
),
|
|
Column(
|
|
name="LastTrafficSource",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="last_traffic_source",
|
|
group=ColumnGroup.PAGE,
|
|
comment="последний источник трафика: organic, direct, ad и подобные",
|
|
),
|
|
Column(
|
|
name="HasGCLID",
|
|
clickhouse_type="UInt8",
|
|
numpy_dtype="uint8",
|
|
dds_name="has_gclid",
|
|
group=ColumnGroup.PAGE,
|
|
comment="1, если в адресе была метка Google Ads",
|
|
),
|
|
Column(
|
|
name="YCLID",
|
|
clickhouse_type="UInt64",
|
|
numpy_dtype="uint64",
|
|
dds_name="yclid",
|
|
group=ColumnGroup.PAGE,
|
|
comment="id клика Яндекс Директа; без метки — 0",
|
|
),
|
|
Column(
|
|
name="Browser",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="browser",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="браузер посетителя",
|
|
),
|
|
Column(
|
|
name="BrowserMajorVersion",
|
|
clickhouse_type="UInt16",
|
|
numpy_dtype="uint16",
|
|
dds_name="browser_major_version",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="старшая версия браузера",
|
|
),
|
|
Column(
|
|
name="BrowserLanguage",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="browser_language",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="язык браузера",
|
|
),
|
|
Column(
|
|
name="OperatingSystem",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="operating_system",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="операционная система с версией",
|
|
),
|
|
Column(
|
|
name="OperatingSystemRoot",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="operating_system_root",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="семейство операционной системы, без версии",
|
|
),
|
|
Column(
|
|
name="DeviceCategory",
|
|
clickhouse_type="UInt8",
|
|
numpy_dtype="uint8",
|
|
dds_name="device_category",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="тип устройства кодами Метрики: 1 — десктоп, 2 — телефон,"
|
|
" 3 — планшет, 4 — телевизор; у Метрики это строка, у нас число",
|
|
),
|
|
Column(
|
|
name="MobilePhoneModel",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="mobile_phone_model",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="модель телефона; на десктопе пусто",
|
|
),
|
|
Column(
|
|
name="ScreenWidth",
|
|
clickhouse_type="UInt16",
|
|
numpy_dtype="uint16",
|
|
dds_name="screen_width",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="ширина экрана в пикселях",
|
|
),
|
|
Column(
|
|
name="ScreenHeight",
|
|
clickhouse_type="UInt16",
|
|
numpy_dtype="uint16",
|
|
dds_name="screen_height",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="высота экрана в пикселях",
|
|
),
|
|
Column(
|
|
name="IPAddress",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="ip_address",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="IP-адрес посетителя",
|
|
),
|
|
Column(
|
|
name="RegionCountry",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="region_country",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="страна кодом ISO",
|
|
),
|
|
Column(
|
|
name="RegionCity",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="region_city",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="город, название по-английски",
|
|
),
|
|
Column(
|
|
name="RegionCountryID",
|
|
clickhouse_type="UInt32",
|
|
numpy_dtype="uint32",
|
|
dds_name="region_country_id",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="числовой id страны в справочнике регионов Яндекса",
|
|
),
|
|
Column(
|
|
name="RegionCityID",
|
|
clickhouse_type="UInt32",
|
|
numpy_dtype="uint32",
|
|
dds_name="region_city_id",
|
|
group=ColumnGroup.CLIENT,
|
|
comment="числовой id города в том же справочнике",
|
|
),
|
|
Column(
|
|
name="GoalsReached",
|
|
clickhouse_type="Array(UInt32)",
|
|
numpy_dtype="uint32",
|
|
dds_name="goals_reached",
|
|
group=ColumnGroup.PARAMS,
|
|
comment="id достигнутых целей; на стенде их две — корзина и покупка",
|
|
),
|
|
Column(
|
|
name="ParsedParamsKey1",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="parsed_params_key1",
|
|
group=ColumnGroup.PARAMS,
|
|
comment="свои параметры сайта, один уровень — например вариант A/B-теста",
|
|
),
|
|
Column(
|
|
name="purchaseID",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="purchase_id",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="номер заказа; у события purchase — один элемент",
|
|
),
|
|
Column(
|
|
name="purchaseRevenue",
|
|
clickhouse_type="Array(Float64)",
|
|
numpy_dtype="float64",
|
|
dds_name="purchase_revenue",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="выручка заказа глазами клиента; Float64, как у Метрики —"
|
|
" на этом держится урок о расхождениях с бэкендом",
|
|
),
|
|
Column(
|
|
name="purchaseCurrency",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="purchase_currency",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="валюта заказа",
|
|
),
|
|
Column(
|
|
name="purchaseCoupon",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="purchase_coupon",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="купон заказа, если был применён",
|
|
),
|
|
Column(
|
|
name="productID",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="product_id",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="id товаров события",
|
|
),
|
|
Column(
|
|
name="productName",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="product_name",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="названия тех же товаров",
|
|
),
|
|
Column(
|
|
name="productCategory",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="product_category",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="категории тех же товаров",
|
|
),
|
|
Column(
|
|
name="productPrice",
|
|
clickhouse_type="Array(Int64)",
|
|
numpy_dtype="int64",
|
|
dds_name="product_price",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="цена за штуку целым числом: деньги генератор считает целыми",
|
|
),
|
|
Column(
|
|
name="productQuantity",
|
|
clickhouse_type="Array(UInt64)",
|
|
numpy_dtype="uint64",
|
|
dds_name="product_quantity",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="количество штук каждого товара",
|
|
),
|
|
Column(
|
|
name="productEventType",
|
|
clickhouse_type="Array(String)",
|
|
numpy_dtype="object",
|
|
dds_name="product_event_type",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="действие с товаром: detail, add, remove, purchase",
|
|
),
|
|
Column(
|
|
name="ecommerce",
|
|
clickhouse_type="String",
|
|
numpy_dtype="object",
|
|
dds_name="ecommerce",
|
|
group=ColumnGroup.ECOMMERCE,
|
|
comment="сырой JSON события, как отдаёт Метрика — материал лабы"
|
|
" про разбор JSON внутри колонки",
|
|
),
|
|
)
|