Перейти к основному содержимому

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)
KindClickHouse 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

ПолеЗначение
Databasepin_dev_stats
Tablesession_facts
EngineReplacingMergeTree (dedup по ключу сортировки)
Owner servicedev-stats (DevStats.Background пишет, DevStats.Api читает)
Source eventspin.user-activity.session-ended.v1
Transform version1
Freshness SLA≤ 5 минут (batch-вставка, ≤ 1000 строк / 5 секунд)

Columns

ColumnTypeNullableSourcePartition/OrderPII
FactIdStringнет= SourceEventIdorder (последний компонент)нет
SourceEventIdStringнетидентификатор события (EventId)noneнет
TenantIdStringнетплатформенный sentinel (сессия кросс-застройщик)noneнет
BrandIdNullable(String)дане используется, NULLnoneнет
EventTimeDateTimeнет= StartedAt (начало сессии — для диапазона дат)PARTITION BY toYYYYMM(EventTime), order (3-й компонент)нет
IngestionTimeDateTimeнетnow() при вставке; версия dedup при чтенииnoneнет
SessionIdUUIDнетpayload; ключ связи с property_object_view_factsnoneнет
UserIdNullable(UUID)даpayload; NULL для гостяnoneкосвенно
AnonymousIdUUIDнетpayload; идентификатор гостяnoneнет
ViewerKindLowCardinality(String)нетконсюмер из наличия UserId: Guest / Userorder (2-й компонент)нет
DeveloperIdNullable(UUID)дане заполняется в v1 (NULL); зарезервировано под будущую денормализацию single-developer-сессийorder (1-й компонент)нет
DurationSecondsInt32нетpayload; суммарная длительность сессииnoneнет
StartedAtDateTimeнетpayload; = EventTimenoneнет
LastActivityAtDateTimeнетpayload; момент закрытия сессии (polling-delta)noneнет

Инварианты

КодИнвариантСмысл
INV-SF-1 AppendOnlyСтроки только добавляются; update/delete/upsert запрещеныкорректировки — новой строкой + dedup при чтении
INV-SF-2 DedupByFactIdFactId = SourceEventId — последний компонент ключа; чтение argMax(DurationSeconds, IngestionTime) по SessionIdat-least-once не задваивает сессию
INV-SF-3 DeveloperAttributionViaViewsСессия не привязана к застройщику напрямую (DeveloperId = NULL в v1); привязка — через SessionId ∈ property_object_view_facts WHERE DeveloperId = акторзастройщик видит только сессии, касавшиеся его объектов
INV-SF-4 ViewerKindCoarseViewerKind сессии ∈ {Guest, User} — без деления Agent/Investorогрубление на уровне сессии (роль не разрешается)
INV-SF-5 CrossDeveloperTenantTenantId = платформенный sentinel (00000000-…-000000000000) — сессия кросс-застройщикtenant-scope сессии задаётся связью с просмотрами, не самой строкой
INV-SF-6 Ttl3YearsTTL и глубина от 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.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы