SessionFact (ClickHouse)
Дом полей append-only факта «завершённая пользовательская сессия». Даёт сессионные метрики контекста
dev-stats: длительность сессий и разбивку гость/пользователь; для метрик застройщика связывается с
просмотрами его объектов недвижимости по SessionId. Онлайн-чтение — дубль-устойчивое (argMax по IngestionTime),
без FINAL.
Назначение
SessionFact хранит по одной строке на закрытую сессию из события pin.user-activity.session-ended.v1
(закрытие — polling-delta, ADR-0056): суммарная длительность и тип зрителя. Сессия не привязана к
застройщику напрямую — она может затрагивать объекты недвижимости разных застройщиков или ни одного. Поэтому выбран
простой вариант (из двух допустимых по канону): session_facts пишется общим (не tenant-scoped),
а сессионные метрики застройщика считаются через связь сессии с просмотрами его объектов недвижимости —
SessionId ∈ property_object_view_facts WHERE DeveloperId = актор; когда нужна полная длительность сессии, API
джойнит property_object_view_facts.SessionId → session_facts.SessionId. Это не требует денормализации застройщика в
сессию и держит session_facts единой узкой таблицей (ADR-0056, simplicity-first).
Стандартные технические колонки факта: FactId, SourceEventId, TenantId, BrandId?, EventTime,
IngestionTime.
Классификация
| Поле | Значение |
|---|---|
| Контекст | dev-stats (ADR-0057) |
| Kind | ClickHouse fact (append-only) |
| Source of truth | операционная сессия — user-activity; аналитическая лента — этот факт |
| Хранилище | ClickHouse, database pin_dev_stats, table session_facts |
| Tenant isolation | сессия кросс-застройщик → TenantId = платформенный sentinel (00000000-0000-0000-0000-000000000000); привязка к застройщику — через SessionId в property_object_view_facts |
Table
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | session_facts |
| Engine | ReplacingMergeTree (dedup по ключу сортировки) |
| Owner service | dev-stats (DevStats.Background пишет, DevStats.Api читает) |
| Source events | pin.user-activity.session-ended.v1 |
| Transform version | 1 |
| Freshness SLA | ≤ 5 минут (batch-вставка, ≤ 1000 строк / 5 секунд) |
Columns
| Column | Type | Nullable | Source | Partition/Order | PII |
|---|---|---|---|---|---|
FactId | String | нет | = SourceEventId | order (последний компонент) | нет |
SourceEventId | String | нет | идентификатор события (EventId) | none | нет |
TenantId | String | нет | платформенный sentinel (сессия кросс-застройщик) | none | нет |
BrandId | Nullable(String) | да | не используется, NULL | none | нет |
EventTime | DateTime | нет | = StartedAt (начало сессии — для диапазона дат) | PARTITION BY toYYYYMM(EventTime), order (3-й компонент) | нет |
IngestionTime | DateTime | нет | now() при вставке; версия dedup при чтении | none | нет |
SessionId | UUID | нет | payload; ключ связи с property_object_view_facts | none | нет |
UserId | Nullable(UUID) | да | payload; NULL для гостя | none | косвенно |
AnonymousId | UUID | нет | payload; идентификатор гостя | none | нет |
ViewerKind | LowCardinality(String) | нет | консюмер из наличия UserId: Guest / User | order (2-й компонент) | нет |
DeveloperId | Nullable(UUID) | да | не заполняется в v1 (NULL); зарезервировано под будущую денормализацию single-developer-сессий | order (1-й компонент) | нет |
DurationSeconds | Int32 | нет | payload; суммарная длительность сессии | none | нет |
StartedAt | DateTime | нет | payload; = EventTime | none | нет |
LastActivityAt | DateTime | нет | payload; момент закрытия сессии (polling-delta) | none | нет |
Инварианты
| Код | Инвариант | Смысл |
|---|---|---|
| INV-SF-1 AppendOnly | Строки только добавляются; update/delete/upsert запрещены | корректировки — новой строкой + dedup при чтении |
| INV-SF-2 DedupByFactId | FactId = SourceEventId — последний компонент ключа; чтение argMax(DurationSeconds, IngestionTime) по SessionId | at-least-once не задваивает сессию |
| INV-SF-3 DeveloperAttributionViaViews | Сессия не привязана к застройщику напрямую (DeveloperId = NULL в v1); привязка — через SessionId ∈ property_object_view_facts WHERE DeveloperId = актор | застройщик видит только сессии, касавшиеся его объектов |
| INV-SF-4 ViewerKindCoarse | ViewerKind сессии ∈ {Guest, User} — без деления Agent/Investor | огрубление на уровне сессии (роль не разрешается) |
| INV-SF-5 CrossDeveloperTenant | TenantId = платформенный sentinel (00000000-…-000000000000) — сессия кросс-застройщик | tenant-scope сессии задаётся связью с просмотрами, не самой строкой |
| INV-SF-6 Ttl3Years | TTL и глубина от EventTime (= StartedAt); 3 года | синхронно с фактами просмотров |
Кросс-сервис, предусловия и видимость
- Что ожидается от
user-activity(session-ended.v1):SessionId,UserId?,AnonymousId,StartedAt,LastActivityAt,DurationSeconds; доставка at-least-once. Контракт события — events/README.md. - Предусловие сессионных метрик застройщика: наполнены и
session_facts, иproperty_object_view_facts(иначе пересечение поSessionIdпусто — застройщик не увидит сессию, даже если она была). - Постусловие/видимость: застройщику отдаются метрики только пересечения
SessionId ∈ property_object_view_facts WHERE DeveloperId = актор. Сессия без просмотров объектов актора для него невидима (не «нулевая», а отсутствующая) — INV-SF-3. - Окно freshness (≤ 5 мин):
session_factsи просмотры наполняются независимыми консюмерами; в норме сессия и её просмотры оказываются доступны в пределах общего окна, но возможен краткий промежуток, когда просмотры уже видны, а закрытие сессии (session-ended) ещё не пришло — тогда полная длительность сессии подтянется позже (склейка поSessionIdидемпотентна).
Deduplication
- Dedup key:
ORDER BYс последним компонентомFactId = SourceEventId; повтор доставки Kafka → идентичный ключ →ReplacingMergeTreeсхлопывает при merge. Консюмер также идемпотентен поSourceEventIdв батче. - Replay behavior: переигрывание топика идемпотентно; чтение дубль-устойчиво (
argMax(DurationSeconds, IngestionTime)поSessionId).FINALв online-запросах запрещён (правило аналитического чтения). - Late event: строка вставляется в партицию своего
StartedAt; TTL — отEventTime.
Partition И TTL
- Partition key:
toYYYYMM(EventTime)— диапазон дат режется партициями. - Order key (подсказка по индексам):
(DeveloperId, ViewerKind, EventTime, FactId).DeveloperIdв v1 —NULL(одинаков для всех строк, селективности не даёт), поэтому фактическая селективность в v1 идёт от партиции (дата) +ViewerKind(Guest/User) — типичный запрос «длительность/количество сессий за период с разбивкой по типу зрителя» — это префикс-скан поViewerKindвнутри партиции.DeveloperIdоставлен головным компонентом ключа как задел: если появится денормализация single-developer-сессий, фильтр по застройщику станет префиксом без пересоздания таблицы.FactIdв хвосте — для dedup. - TTL:
EventTime + INTERVAL 3 YEAR.
Consumers И Backfill
| Process | Назначение | Checkpoint |
|---|---|---|
session-ended consumer (../consumers/session-ended.md) | Приём pin.user-activity.session-ended.v1; ViewerKind из наличия UserId; вставка факта сессии | Kafka offset (commit после batch-вставки) |
| Backfill (будущий) | Разовая перезаливка сессий из истории user-activity при вводе сервиса | Диапазон StartedAt в состоянии job-а (PostgreSQL) |
Связь с застройщиком (пример чтения без FINAL): множество SessionId берётся из property_object_view_facts,
отфильтрованного DeveloperId = актор за период, затем джойнится к session_facts по SessionId для
полной длительности/типа сессии. Так «сессии, в которых смотрели мои объекты недвижимости» и их метрики получаются без
привязки застройщика к самой сессии.
Security
- Доступ — роль застройщика + permission
dev-stats.view; сессионные метрики отдаются только в пересечении с просмотрами объектов недвижимости актора (SessionId ∈ property_object_view_facts WHERE DeveloperId = актор) — застройщик не видит сессии, не касавшиеся его недвижимости. - Персональные данные зрителей не раскрываются (только
ViewerKind, счётчики, длительности). - Прямой доступ к ClickHouse отсутствует; чтение — через
DevStats.Api. Не source of truth.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Классификация зрителя и источника (ViewerKind / ViewSource)
- API (1): GET /api/v1/dev-stats/session-depth — Глубина сессии (объектов за сессию)
- Консюмеры (1): Consumer: DevStatsSessionEndedConsumer
- Фоновые задачи (1): Background Job: FactsBackfillJob
- Отчёты (1): Отчет/Export: dev-stats-export (выгрузка аналитики застройщика)
- Дашборды (1): Панель Метрик Застройщика: dev-stats-overview (аналитика внимания)
- index.md (1): DevStats Service (Pin.DevStats)
- facts (1): PropertyObjectViewFact (ClickHouse)
- registries (1): Реестр сущностей PIN
Связанные документы
- Главный факт: property-object-view-facts.md; rollup: property-object-view-daily-rollup.md.
- Событие-источник:
docs/06-services/user-activity/events/session-ended.md. - Шаблон: ../../../99-templates/clickhouse-fact-template.md.
- ADR-0057 (dev-stats), ADR-0056 (границы ClickHouse, simplicity-first).