Files
clickstream-ch-kafka-supers…/generator/KNOWN_ISSUES.md
T
ddadmin e2d06841de fix(generator): сохранена фактура визита при восстановлении
- Зачем:
  - визит после восстановления не должен менять браузер и источники перехода внутри одного click_id.
- Что:
  - добавлен base_click_id в state v3 для восстановления донора фактуры.
  - исправлено восстановление timestamp offset без потери микросекунд.
  - расширены тесты и стыковая проверка browser/source и device/os/geo.
- Проверка:
  - uv run --with-requirements generator/requirements.txt pytest generator/tests -q.
  - bash -n scripts/check_generated_analytics.sh.
  - git diff --cached --check.
2026-07-04 21:46:23 +03:00

9.7 KiB
Raw Blame History

Генератор: известные проблемы и контекст для доработки

Статус (2026-06-11): исторический дефект старой плоской генерации закрыт для режима steady-stream. Генератор строит визиты с общим click_id, монотонным временем событий, путём по страницам воронки, популяцией возвращающихся пользователей и состоянием для активных визитов.

Эта заметка больше не является предупреждением «генератор концептуально сломан». Она оставлена как учебный разбор старого дефекта и как место для небольших остаточных ограничений.

Заметка написана при дизайне Superset-дашборда (ветка docs/advanced-clickstream-course): разбирались, почему на дашборде Unique Users == Unique Sessions, и по ходу вскрылось, что старая реализация генератора на тот момент семантику сессии не чинила, а ломала сильнее. Разбор оставлен, чтобы не переоткрывать этот дефект заново и показать, почему новая модель устроена иначе.

TL;DR

  • Модель интенсивности потока (сколько событий и когда) — нормальная. Poisson по тикам + дневной коэффициент + jitter сохранены.
  • Генеративная модель сущностей переписана. Поток больше не штампует свежий click_id на каждое событие: визит живёт несколько событий, пользователь может вернуться в новом визите после кулдауна.
  • Вывод: старый дефект Sessions == Events не считается актуальным блокером. Дальше генератор можно улучшать уже как работающую учебную модель, а не как концептуально сломанный источник.

Текущие ограничения

  • Тип события остаётся pageview для всех событий. Это осознанное ограничение: текущий дашборд и уроки строят воронку по page_url_path, а не по event_type.
  • Device/geo-профиль пользователя стабилен между визитами. Смену устройства генератор пока не моделирует.
  • При долгом простое больше 30 минут активный визит закрывается без досылки остатка. Популяция пользователей при этом сохраняется.

Доменная модель (как задумано в DDL)

В этой модели сущности связаны иерархически:

user_domain_id (постоянный пользователь, cookie)
  └── click_id (визит/сессия — envelope из 1..N событий с общим device/geo)
        └── event_id (отдельное событие: pageview, click, purchase ...)
  • click_id — это визит, а не одиночный клик: в статическом сиде один click_id честно группирует 1..27 событий, медиана 10 (dds.click = «контекст сессии пользователя», см. sql/ddl/dds/30_dds.sql; точные цифры — профиль сид-датасета в CONTEXT.md, ранняя оценка «1..7» была замером по срезу файла).
  • user_domain_id живёт в device_events, привязан к click_id.

Что генератор делает правильно

Файл generator/src/clickstream_generator/intensity.py, calculate_events_count():

  • Poisson-процесс прихода событий: λ на минуту → λ на тик → розыгрыш Пуассона.
  • Дневной коэффициент _hour_factor(): день (918) ×1.2, ночь (05) ×0.7.
  • Jitter ±GEN_JITTER_PCT% и границы min/max_events_per_tick.

Это адекватная модель интенсивности во времени. Претензий к ней нет.

Исторический корневой дефект: старая модель сущностей

До переработки 2026-06-11 прежняя реализация в монолитном generator/generator.py на каждое событие в батче делала примерно следующее:

base_browser = self.rng.choice(self.dictionary.browser_events)  # случайная сид-строка
...
new_event_id  = self._new_uuid()
new_click_id  = self._new_uuid()                                 # СВЕЖИЙ click_id на КАЖДОЕ событие
new_timestamp = self._current_timestamp()                        # ~now() для всех событий батча
...
device_event = {**base_device, "click_id": new_click_id}         # user_domain_id наследуется из сида

Три уровня иерархии (пользователь → сессия → событие) схлопываются в плоскую выборку:

  1. click_id свежий на каждое событие → один визит = одно событие. Визит как группа из нескольких событий никогда не формируется.
  2. user_domain_id переиспользуется из сид-пула (~99 значений), но без привязки к сессиям — события одного пользователя разлетаются отдельными click_id. Иерархию «пользователь → его сессии → события сессии» из потока не восстановить.
  3. Время у всех событий батча ≈ now() — даже временно́й структуры визита (последовательность событий внутри сессии) нет.

Что это даёт в данных

Статический сид (data/*.jsonl) Поток из генератора
click_id визит-envelope (1..27 событий, медиана 10) свежий на событие → click_id ≈ событие
user_domain_id 1:1 с click_id переиспользуется (потолок ~99)
Семантика Sessions == Users (вырождено по пользователю) Sessions == Events (сессия = одно событие)

Парадокс был таким: статический сид как учебная основа был лучше, потому что на нём click_id нёс осмысленную семантику визита, а старый генератор её ломал.

Что сделано в новой модели

Чинили именно генеративную модель сущностей, не переписывая математику интенсивности:

  1. Иерархическая генерация вместо плоской выборки:
    • есть ограниченная популяция пользователей с постоянным user_domain_id;
    • пользователь со временем открывает 1..N визитов;
    • визит порождает последовательность событий с одним click_id, общим device/geo-контекстом и путём по страницам.
  2. Тиковый слой:
    • событийный бюджет тика превращается в рождения визитов через среднюю длину визита;
    • активные визиты живут между тиками;
    • выпускаются только события, у которых наступила запланированная метка времени.
  3. Время событий внутри визита строго растёт и не прилипает к одному now() для всего батча.
  4. Состояние сохраняет популяцию, активные визиты, накопленный бюджет рождения визитов, номер тика и состояние ГПСЧ.

После этой переделки поток на длинном окне и при штатных параметрах даёт здоровую пирамиду users < sessions < events, и дашборд может показывать разницу «пользователь vs сессия» честными числами.

Ссылки

  • Код: generator/src/clickstream_generator/intensity.pycalculate_events_count(); generator/src/clickstream_generator/generation.pyEventGenerator.generate_batch().
  • Доменная модель: sql/ddl/dds/30_dds.sql, sql/ddl/dm/40_dm.sql.
  • Контекст обсуждения: ветка docs/advanced-clickstream-course, дизайн Superset-дашборда (урок 6 курса).