Compare commits

..
4 Commits
Author SHA1 Message Date
ddadminandClaude Fable 5 ffd467bf43 chore(scratch): убран отработавший handoff сессии спеки v2
- Зачем:
  - handoff одноразовый (ADR-0003), работа сессии завершена: спека принята,
    v2-репозиторий рождён.
- Что:
  - удалён .scratch/handoffs/20260730-1003-spec-v2-realism-review.md.
- Проверка:
  - git status чистый.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:01:38 +03:00
ddadminandClaude Fable 5 dda4f3aeb5 docs(readme): стенд заморожен, развитие переехало в clickstream-data-platform
- Зачем:
  - исполнение спеки «Боевой реализм стенда» идёт в новом репозитории;
    читатель v1 должен сразу видеть, где продолжение, а спека — где её
    актуальная версия.
- Что:
  - README: блок-указатель у начала — стенд заморожен для новых фич,
    остаётся учебным, развитие в clickstream-data-platform.
  - docs/specs/2026-07-30-stand-v2-realism.md: строка «Источник истины
    переехал в v2» со ссылкой на копию спеки в новом репозитории.
- Проверка:
  - ссылки открываются, содержание спеки не менялось.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:00:14 +03:00
ddadminandClaude Fable 5 05e4cdea4d docs(specs): спека «Боевой реализм v2» принята владельцем (#17)
- Зачем:
  - зафиксировать вердикт владельца по трём помеченным отступлениям и
    закрыть приёмку спеки.
- Что:
  - статус Draft -> Accepted; подтверждены порядок страховочных срезов
    и VisitID как эталон самопроверки.
  - Sign взят как колонка формата без механики (всегда 1, 47-я колонка
    события); честность — комментарий в DDL, абзац в документе о
    реализме, лекционный крючок про CollapsingMergeTree.
  - в опорные точки добавлена рамка лабы сессий: сначала собрать самому,
    потом рассказ про VisitID в хитах с октября 2025.
- Проверка:
  - вычитка разделов 1.1, 1.2, 10, 12 и шапки статуса.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:24:50 +03:00
ddadminandClaude Fable 5 9d3a8053d3 docs(specs): закрыты находки внешнего ревью спеки v2 (#17)
- Зачем:
  - слепое ревью постановки нашло места, где исполнителю пришлось бы
    молча изобретать решение.
- Что:
  - зафиксированы: правило соединения purchaseID[1]=order_id, исключение
    declared_* из правила «деньги по бэкенду», имена массивов позиций
    dds.order, контракт присутствия всех 46 полей, класс awaiting_order
    в сверке, счётчик дельт сумм в манифесте.
  - шардирование в разделе 6 и dds.identity_map приведено к
    cityHash64 — буквально тем же выражением, что в 1.3.
- Проверка:
  - сверка правок с индексом находок ревью (SOL-1..SOL-8, кроме
    отклонённого SOL-7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:04:41 +03:00
3 changed files with 54 additions and 96 deletions
@@ -1,78 +0,0 @@
# Handoff: спека «Боевой реализм стенда (v2)» — ревью и приёмка (#17)
Дата: 2026-07-30 10:03. Сессия работала над issue #17 (Gitea, CLI `tea`):
сборка спеки из резолюций карты #10.
## Состояние
- Спека: `docs/specs/2026-07-30-stand-v2-realism.md` (Draft).
**Файл untracked** — при сбое машины git его не защитит; владельцу
предложен WIP-коммит.
- Источники истины: резолюции в комментариях issues #14, #15, #16, #18;
карта #10 (там же решение: v2 — новый пустой репозиторий, рабочее имя
`clickstream-data-platform`, v1 замораживается). Фактура:
`docs/research/2026-07-26-yandex-clickstream-format.md`.
## Что сделано
1. Спека собрана и прошла четыре волны правок (всё уже в файле):
владельца (v2 — новый репозиторий; доки переосмыслить, не копировать;
лабы с нуля, вне скоупа; генератор с нуля), вычитки (термины, `K` только
как окно), сверки с резолюциями (12 правок: бюджет 16 ГБ, «ландшафт
итогом», потерянные отклонения, помеченные отступления) и грилинга
(17 решений: расщеплённый дедуп заказов, пересборка K+1 партиций,
сессии по границе суток, обязательный заказ с каждой куки двухкукового,
сверка по сравнимой базе, строгий приём JSON, `dds.v_event`,
`SAMPLE BY`, «схема как контракт», объём ~80–110 файлов, этапы слиты
и дополнены этапом Airflow).
2. Решение владельца по генератору: Python с производительной
архитектурой; компилируемый язык — только если замеры не пройдут
(в спеке, раздел 11).
## Открытые вопросы владельцу (спека их пометила как отступления)
- Порядок страховочных срезов: #15 предписывал 1→2→3, спека применяет
3 и 2, срез 1 (статусы) держит в резерве.
- `Sign` на хитах: исследование советовало взять колонку, спека не берёт.
- `VisitID` оставлен в событии как эталон самопроверки лабы сессий.
## В полёте на момент записки
- Sonnet-агент довносил 17 правок грилинга: маркеры всех правок в файле
уже есть; свежей сессии — убедиться, что файл цел (маркеры:
`skip_unknown_fields`, `_ingested_at`, `dds.v_event`, `SAMPLE BY`,
«сравнимой базе», «80–110», «Схема как контракт», «каждой куки»).
- Слепое ревью спеки Кодексом подготовлено, НЕ запущено. Промпт собран в
`$SCRATCH/spec-review/review-prompt.md` (скретчпад сессии в /tmp — мог
не пережить сбой). Пересборка: параметрический блок (роль: ревью
постановки, не реализации; префикс находок SOL; вне скоупа — стиль и
вкусовщина) + `references/prompts/review-boilerplate.md` из скилла
`claude-subagent-playbook`. Запуск — по ветке «ревьюер»
`references/codex-exec.md`: `codex exec`, модель `gpt-5.6-sol`, effort
high, фоновым Bash, вердикт в `-o`-файл. Песочница проверена — жива.
Предупреждение владельца: у Сола плохой вкус и тяга к деталям
реализации раньше времени — рамка в промпте это прижимает, остаток
фильтровать на триаже.
## Дальше (по порядку)
1. Проверить целостность 17 правок; финальное сквозное чтение спеки.
2. Запустить ревью Сола, триаж находок (вкусовые — отклонять), правки.
3. Вердикт владельца по трём открытым вопросам → статус Accepted.
4. Коммит (`/conventional-commits`), комментарий-резолюция в #17.
5. Разбиение через /to-tickets — уже отдельным заходом (спека и карта
переедут в v2-репозиторий после его создания).
## Грабли сессии
- Дважды 529 Overloaded на Opus-субагентах: правки терялись ДО записи.
Рецепт: проверять маркеры в файле, затем возобновлять агента
SendMessage'ем; после второго падения — свежий Sonnet-агент с
самодостаточным списком правок (сработало).
## Suggested skills
- `/claude-subagent-playbook` — продолжить конвейер ревью (этапы 4–6).
- `/conventional-commits` — при коммите спеки.
- `/grilling` — если владелец захочет добить три открытых вопроса.
- Контракт трекера — `docs/agents/issue-tracker.md` (Gitea, `tea`).
+7
View File
@@ -3,6 +3,13 @@
[![Stack](https://img.shields.io/badge/stack-Kafka%20%7C%20ClickHouse%20%7C%20Airflow%20%7C%20Superset%20%7C%20Prometheus%2FGrafana-blue)](./docker-compose.yml) [![Stack](https://img.shields.io/badge/stack-Kafka%20%7C%20ClickHouse%20%7C%20Airflow%20%7C%20Superset%20%7C%20Prometheus%2FGrafana-blue)](./docker-compose.yml)
[![Layers](https://img.shields.io/badge/layers-STG%20→%20ODS%20→%20DDS%20→%20DM-green)](./docs/ARCHITECTURE.md) [![Layers](https://img.shields.io/badge/layers-STG%20→%20ODS%20→%20DDS%20→%20DM-green)](./docs/ARCHITECTURE.md)
> **Стенд заморожен для новых фич.** Он остаётся стабильным учебным стендом:
> что здесь работает, то работает и дальше — курс и лабы живут тут.
> Развитие переехало в
> [clickstream-data-platform](https://git.dementev.space/ddmitry/clickstream-data-platform):
> там одно широкое событие кликстрима вместо четырёх топиков, заказы бэкенда
> вторым источником и ClickHouse кластером.
Живой стек для работы с кликстримом: Kafka, ClickHouse, Airflow, Superset и мониторинг Живой стек для работы с кликстримом: Kafka, ClickHouse, Airflow, Superset и мониторинг
(Prometheus с Grafana) поднимаются в Docker одной командой. На этом стенде можно учиться (Prometheus с Grafana) поднимаются в Docker одной командой. На этом стенде можно учиться
по курсу или просто поднять его у себя и поэкспериментировать с потоковой загрузкой и по курсу или просто поднять его у себя и поэкспериментировать с потоковой загрузкой и
+47 -18
View File
@@ -1,7 +1,11 @@
# Боевой реализм стенда (v2): широкое событие, заказы, кластер, анонимность # Боевой реализм стенда (v2): широкое событие, заказы, кластер, анонимность
Статус: Draft — на приёмку владельцу. Статус: Accepted (2026-07-30). Три помеченных отступления подтверждены
владельцем на приёмке: порядок страховочных срезов (раздел 9), `Sign` как
колонка без механики (раздел 1.1), `VisitID` как эталон самопроверки (1.2).
Дата: 2026-07-30. Тикет: #17 (сборка карты #10). Дата: 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` и заказы), Источники: резолюции #18 (модель данных), #15 (`purchase` и заказы),
#14 (кластер), #13 (цена кластера), #16 (анонимность и склейка); исследование #14 (кластер), #13 (цена кластера), #16 (анонимность и склейка); исследование
[формата кликстрима Яндекса](../research/2026-07-26-yandex-clickstream-format.md); [формата кликстрима Яндекса](../research/2026-07-26-yandex-clickstream-format.md);
@@ -64,10 +68,13 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент
тоже не берём, но это наш выбор, а не запрет источника: исследование тоже не берём, но это наш выбор, а не запрет источника: исследование
запрещало только выдавать это поле за формат Яндекса, оставить разрешало. запрещало только выдавать это поле за формат Яндекса, оставить разрешало.
Разбор строки user agent — не урок этого стенда. Разбор строки user agent — не урок этого стенда.
- **Механику версий записи (`Sign`/`HitVersion`) не берём сейчас**: по #15 это - **`Sign` берём как колонку формата, без механики** (решение владельца на
кандидат на потом, и её дом — клиентская сторона (поток визитов Метрики приёмке спеки): генератор всегда пишет `Sign = 1`, исправлений записей не
Про). Это помеченное отступление от рекомендации исследования («`Sign` на шлёт — движки и запросы не меняются. Сама механика версий
хитах полезен»): колонка без механики — мёртвый вес. (CollapsingMergeTree, пара `HitVersion`) — кандидат на потом, по #15.
Честность: комментарий в DDL и абзац в документе о реализме («в бою здесь
бывают −1/+1, считают через `sum(Sign)`»); в лекции — крючок про
CollapsingMergeTree (частый вопрос на собеседованиях).
- **Не берём** `ClientEventTime` (в выгрузке Метрики нет клиентской метки; - **Не берём** `ClientEventTime` (в выгрузке Метрики нет клиентской метки;
расхождение часов — тема тумана «грязь»), `Params` (второй сырой JSON не расхождение часов — тема тумана «грязь»), `Params` (второй сырой JSON не
нужен: этот навык уже несут заказы), `LastSearchEngineRoot`, `IsPageView`, нужен: этот навык уже несут заказы), `LastSearchEngineRoot`, `IsPageView`,
@@ -78,7 +85,7 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент
- **Наша честная добавка** — `EventType`: у Метрики такого поля нет - **Наша честная добавка** — `EventType`: у Метрики такого поля нет
(там `isPageView` + `productEventType`), стенду таксономия нужна явно. (там `isPageView` + `productEventType`), стенду таксономия нужна явно.
### 1.2 Состав полей (46 колонок) ### 1.2 Состав полей (47 колонок)
Идентификаторы и время: Идентификаторы и время:
@@ -92,6 +99,7 @@ JSON-поле `ecommerce`. Сессий в потоке нет — их мент
| `UTCEventTime` | DateTime | единственная метка времени, как у Метрики | | `UTCEventTime` | DateTime | единственная метка времени, как у Метрики |
| `ClientTimeZone` | Int16 | смещение пояса клиента в минутах | | `ClientTimeZone` | Int16 | смещение пояса клиента в минутах |
| `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` | | `EventType` | LowCardinality(String) | `pageview` / `add_to_cart` / `purchase` |
| `Sign` | Int8 | всегда 1: колонка формата, механика исправлений не реализована (см. 1.1) |
Правила резки визитов в генераторе документируются и совпадают с лабной Правила резки визитов в генераторе документируются и совпадают с лабной
логикой (30-минутный таймаут). логикой (30-минутный таймаут).
@@ -155,7 +163,7 @@ Ecommerce (заполнены только у торговых событий):
### 1.4 Схема как контракт ### 1.4 Схема как контракт
Одно машинное описание схемы события (python-модуль или YAML) — источник Одно машинное описание схемы события (python-модуль или YAML) — источник
истины: из него выводятся DDL и валидация генератора, а не наоборот. 46 истины: из него выводятся DDL и валидация генератора, а не наоборот. 47
колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью, колонок повторяются примерно в семи местах (генератор, DDL, SELECT матвью,
трансформации, витрины, манифест, доки) — без контракта они расходятся трансформации, витрины, манифест, доки) — без контракта они расходятся
молча. Заодно это учебный артефакт: менти видит на живом примере, что такое молча. Заодно это учебный артефакт: менти видит на живом примере, что такое
@@ -196,7 +204,9 @@ Ecommerce (заполнены только у торговых событий):
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает. Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
- Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше - Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше
витрины работают с плоскими массивами. Это единственный носитель навыка витрины работают с плоскими массивами `dds.order`: `item_sku`
Array(String), `item_qty` Array(UInt64), `item_price` Array(Decimal(18,2))
— одной длины, порядок как в JSON. Это единственный носитель навыка
«вложенный JSON в ClickHouse» на стенде. «вложенный JSON в ClickHouse» на стенде.
- Статусы держим все три: смена `created``paid` и есть причина «дыхания» - Статусы держим все три: смена `created``paid` и есть причина «дыхания»
выручки внутри окна; сужение до двух — резервный срез 1. выручки внутри окна; сужение до двух — резервный срез 1.
@@ -212,7 +222,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
## 4. Сверка `purchase` против заказов ## 4. Сверка `purchase` против заказов
Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер
заказа на `/confirmation`). Расхождения — перечислимый список, заказа на `/confirmation`). У события `purchase` массив `purchaseID` несёт
ровно один элемент (одно подтверждение — один заказ), сверка соединяет по
`purchaseID[1]`; правило зафиксировать комментарием в SQL сверки.
Расхождения — перечислимый список,
детерминированный от seed, не хаос: детерминированный от seed, не хаос:
| | Расхождение | Механика в генераторе | Ориентир доли | | | Расхождение | Механика в генераторе | Ориентир доли |
@@ -235,7 +248,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
механически дублирует B. механически дублирует B.
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
бэкенду**. Оно выучивается на конфликте: суммы не сойдутся, менти сам бэкенду**. Единственное разрешённое исключение — клиентская оценка выручки
под именем `declared_*` там, где атрибуция без трекера невозможна (UTM);
слово `declared` в имени — сигнал «это заявка клиента, не деньги
отчётности». Оно выучивается на конфликте: суммы не сойдутся, менти сам
раскопает почему (Float64 против Decimal, промокод, доставка, отмены). раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
## 5. Анонимность и склейка идентичностей ## 5. Анонимность и склейка идентичностей
@@ -286,7 +302,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
`cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между `cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между
прогонами, куда легли данные — нет». прогонами, куда легли данные — нет».
- **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные - **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные
поля — без значений по умолчанию. Несовпадение имени поля — громкая поля — без значений по умолчанию. Контракт присутствия: генератор выдаёт
**все 47 полей в каждом событии**; «пусто» — пустой массив, пустая строка
или 0, а не отсутствие ключа в JSON. Так строгий приём уживается с
полями, пустыми по смыслу (ecommerce у `pageview`, UTM у прямого захода). Несовпадение имени поля — громкая
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы, ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
опечатка иначе не падает. опечатка иначе не падает.
- **Политика соединений**: по ключу ко-локации — обычное соединение с - **Политика соединений**: по ключу ко-локации — обычное соединение с
@@ -370,8 +389,11 @@ Kafka переобработка возможна только из эталон
в порядке приоритета — классы пересекаются, побеждает более ранний). в порядке приоритета — классы пересекаются, побеждает более ранний).
`match` — большинство строк; `amount_delta` — только необъяснённый остаток `match` — большинство строк; `amount_delta` — только необъяснённый остаток
после приведения к сравнимой базе (округления Float64, ~1–2% заказов). после приведения к сравнимой базе (округления Float64, ~1–2% заказов).
Строка «`purchase` без заказа» внутри живого окна — это опоздание, ждущее Строка «`purchase` без заказа» внутри живого окна — опоздание, ждущее
слепка, а не отдельный класс расхождения. слепка, а не расхождение: она получает служебный класс `awaiting_order`
(шестое значение `mismatch_class`, вне приоритетов расхождений). После
закрытия окна K таких строк не остаётся — сироты исключены построением
(раздел 4).
- **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру); - **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру);
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
`declared_revenue` по UTM. `declared_revenue` по UTM.
@@ -398,7 +420,7 @@ Kafka переобработка возможна только из эталон
Манифест расширяется контрольными числами: Манифест расширяется контрольными числами:
- заказная сторона: заказы и выручка по дням; манифест хранит точные - заказная сторона: заказы и выручка по дням; манифест хранит точные
счётчики по каждому классу расхождений (отмены, потери, дубли) — счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
самопроверка лабы сверки; самопроверка лабы сверки;
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых - идентичность: uniq кук, uniq известных пользователей, число двухкуковых
покупателей — лаба склейки получает самопроверку. покупателей — лаба склейки получает самопроверку.
@@ -469,7 +491,8 @@ v2 стартует пустым, поэтому объём ниже — это
9. Мониторинг и runbook «keeper упал / DDL повис в очереди». 9. Мониторинг и runbook «keeper упал / DDL повис в очереди».
Критерий приёмки этапа — честный: `make up` работает и проходят Критерий приёмки этапа — честный: `make up` работает и проходят
smoke-проверки, а не «дашборд зелёный». Документация правится в PR этапа smoke-проверки, а не «дашборд зелёный». Это минимальная планка; свои
наблюдаемые критерии каждый этап получает при разбиении в /to-tickets. Документация правится в PR этапа
(правило AGENTS.md). (правило AGENTS.md).
## 10. Границы: чего не делаем ## 10. Границы: чего не делаем
@@ -486,8 +509,8 @@ smoke-проверки, а не «дашборд зелёный». Докуме
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд. - Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
- Полный словарь торговых событий Метрики (detail, remove, impressions), - Полный словарь торговых событий Метрики (detail, remove, impressions),
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`. пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
- `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток визитов - Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
Метрики Про). визитов Метрики Про); сама колонка `Sign` уже в схеме, статикой (см. 1.1).
- Событийный лог заказов, CDC/Debezium, HTTP-сервис заказов, шапка+строки, - Событийный лог заказов, CDC/Debezium, HTTP-сервис заказов, шапка+строки,
отдельный поток возвратов — отклонены в #15/#18. отдельный поток возвратов — отклонены в #15/#18.
- Файловые дропы как источник — зона следующего стенда (Lakehouse), у нас - Файловые дропы как источник — зона следующего стенда (Lakehouse), у нас
@@ -532,7 +555,8 @@ smoke-проверки, а не «дашборд зелёный». Докуме
Logs API — это скачанный TSV»); trade-off «инкремент экономнее, слепок Logs API — это скачанный TSV»); trade-off «инкремент экономнее, слепок
надёжнее» + сноска про compacted topic; identity stitching перестаёт быть надёжнее» + сноска про compacted topic; identity stitching перестаёт быть
чистой теорией; «прямое чтение прод-базы — анти-приём, в бою — реплика или чистой теорией; «прямое чтение прод-базы — анти-приём, в бою — реплика или
выгрузка»; полный словарь торговых событий — теория. выгрузка»; колонка `Sign` без механики — честное ограничение стенда
(в бою 1/+1 и `sum(Sign)`); полный словарь торговых событий — теория.
В v1 при заморозке — указатель на v2 в README (форму решить при рождении В v1 при заморозке — указатель на v2 в README (форму решить при рождении
v2, этап 0). v2, этап 0).
@@ -549,6 +573,11 @@ v2, этап 0).
- сцена «разные consumer groups → дубли»; - сцена «разные consumer groups → дубли»;
- словарь регионов из CSV той же машинерией, что каталог товаров (оживляет - словарь регионов из CSV той же машинерией, что каталог товаров (оживляет
`RegionCityID`); `RegionCityID`);
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
синтетическая постановка — осознанный приём;
- лекция «`Sign` и CollapsingMergeTree»: почему на стенде `sum(Sign)` =
`count()`, а в бою — нет; частый вопрос на собеседованиях;
- лекция про идентичность «как в бою»: `setUserID` и first-party id, - лекция про идентичность «как в бою»: `setUserID` и first-party id,
детерминированная против вероятностной склейки, identity graph, детерминированная против вероятностной склейки, identity graph,
кросс-девайс, CDP — с рамкой «мы склеили через транзакции, потому что трекер кросс-девайс, CDP — с рамкой «мы склеили через транзакции, потому что трекер