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

DevStats Service (Pin.DevStats)

Назначение

dev-stats владеет аналитикой внимания к недвижимости для застройщика (ADR-0057): кто, сколько, как долго и как часто смотрит его проекты / здания / группы планировок / планировки / объекты недвижимости — с временными рядами, разбивками и воронкой конверсии. Это read-only аналитический контекст поверх ClickHouse: собственные ClickHouseFact-таблицы наполняются batch-консюмерами из событий user-activity (просмотры/сессии/действия) и обогащаются иерархией из реплик-измерений chessboard; API отдаёт типизированные ряды/точки/сводки застройщику строго по его собственному DeveloperId. Операционная модель просмотров/сессий остаётся в user-activity (PostgreSQL, ADR-0054/0056) — сюда уходит только 3-летний массив фактов и агрегатов.

Metadata

ПолеЗначение
Статусdraft
Owning teamPIN Analytics (dev-stats)
DomainАналитика внимания застройщика (developer view analytics)
Data classificationInternal/Sensitive (факты несут UserId/AnonymousId — Sensitive на уровне строк; наружу — только агрегаты: типы зрителей и уникальные счётчики, без ФИО/контактов)
Runtime criticalitylow-medium (ЛК-аналитика; недоступность не блокирует каталог/сделки/просмотры)

Ownership

ArtifactOwner/Rule
ClickHouse databasepin_dev_stats (владеет своими таблицами; не делит схему с invest-analytics/pin_analytics)
PostgreSQL schemaне используется (ClickHouse-only, ADR-0057); ingest-offset — Kafka; чекпоинт backfill — служебная таблица job-а
API areadevelopers (12 read-only метрик; scope жёстко = свой DeveloperId); public/admin — нет
Owned factsproperty_object_view_facts (facts/property-object-view-facts.md), session_facts (facts/session-facts.md)
Owned rollups (MV)property_object_view_daily (facts/property-object-view-daily-rollup.md), funnel_daily (facts/funnel-daily-rollup.md)
Owned dimensions (реплики)dim_developer, dim_project, dim_building, dim_layout_group, dim_layout, dim_property_object (dimensions/)
Produced eventsнет (leaf-аналитика: потребители метрик читают API) — events/README.md
Consumed eventspin.user-activity.{property-object-view-ended, session-ended, action-tracked}.v1; pin.chessboard.{developer, project, building, layout-group, layout, property-object}-upserted.v1; pin.favorites.favorite-added.v1; pin.selections.selection-item-added.v1; pin.crm.{lead-created, booking-created, booking-paid, deal-created, deal-completed}.v1
Search indexesнет

Ответственность И Границы

  • Делает: batch-ingestion просмотров/сессий/действий в append-only факты ClickHouse (batch-вставка, ≤ 1000 строк / 5 с, offset после подтверждённой вставки); обогащение просмотра иерархией (DeveloperId→…→PropertyObjectId) из реплик-измерений dim_* перед вставкой факта; классификацию зрителя (ViewerKind) и источника (ViewSource); дневные rollup-ы (AggregatingMergeTree/SummingMergeTree) для рядов за месяцы/годы без сканов сырых фактов; воронку views→favorites→selections→leads→bookings→ deals; read-only API метрик/графиков строго в scope застройщика.
  • НЕ делает: не владеет доменом chessboard (иерархия объектов) и user-activity (просмотры/сессии) — держит лишь реплики-измерения и факты, не source of truth; не пишет синхронно из API-хендлеров (хендлеры read-only; запись — только Background batch-ingest); не раскрывает персональные данные зрителей; не показывает историю цен (это invest-analytics); не отдаёт данные чужого застройщика.
  • Source of truth: property_object_view_facts, session_facts и rollup-ы — источник аналитики просмотров (не операционных данных). Иерархия объектов — chessboard; открытые сессии/просмотры — user-activity.
  • Реплики/проекции: dim_* — реплики справочников chessboard (ReplacingMergeTree, версия по UpsertedAt) для JOIN-free фильтрации и обогащения фактов.

Зависимости

ЗависимостьТипЗачем НужнаFailure mode
Kafka (pin.user-activity.*)queueПоток просмотров/сессий/действий → фактыЛаг ingestion: метрики отстают, консюмер догоняет по offset (вставка идемпотентна)
Kafka (pin.chessboard.*-upserted)queueОбновление реплик-измерений dim_*Устаревшая/отсутствующая версия → факт обогащается по последней известной строке dim; backfill при вводе
Kafka (pin.favorites/pin.selections/pin.crm.*)queueЭтапы воронки funnel_dailyНижние этапы отстают; верхние (просмотры) считаются независимо
ClickHouse (pin_dev_stats)dbХранение фактов/rollup-ов/измерений и чтение метрикПолный отказ сервиса (метрики недоступны); операционные контексты не затронуты
identityserviceАутентификация застройщика, permission dev-stats.view, разрешение scope DeveloperIdОтказ авторизации → 401/403

Redis/PostgreSQL/Elasticsearch не используются (ADR-0056/0057). Синхронных вызовов chessboard/user-activity нет — только Kafka.

Проекты

src/services/dev-stats/
Pin.DevStats.Api/ # read-only Query-эндпоинты метрик (area developers)
Pin.DevStats.Application/ # обработка запросов метрик, построение CH-запросов, DevStatsFilterModel
Pin.DevStats.Domain/ # ClickHouseFact-модели, dim-модели, enums (ViewerKind/ViewSource), типизированные модели ответа
Pin.DevStats.Infrastructure/ # ClickHouse-хранилище (linq2db): контекст БД, репозиторий фактов/измерений, batch-вставка
Pin.DevStats.Background/ # ingest-консюмеры (facts/dimensions/funnel) + backfill-джобы
Pin.DevStats.Migrator/ # DDL таблиц (facts/rollups/dimensions) + materialized views
Pin.DevStats.Contracts/ # DTO рядов/точек/сводок метрик (типизированные)

ER-обзор

API

Область developers12 read-only метрик (permission dev-stats.view; общий вход DevStatsFilterModel, DeveloperId = актор; чтение метрик dedup-tolerant argMax/*Merge без FINAL):

  1. get-views-summary.md — сводка (views/uniqViewers/avgDuration/repeatRate) + разбивка ViewerKind;
  2. get-views-trend.md — динамика day/week/month + сравнение периодов;
  3. get-unique-viewers.md — уникальные зрители (uniqMerge) + разрез ViewerKind + ряд;
  4. get-duration-histogram.md — распределение длительностей + avg/median;
  5. get-repeat-frequency.md — частота повторных визитов (пара «зритель × объект»);
  6. get-hierarchy-funnel.md — сужение внимания по иерархии + top-N детей;
  7. get-top-objects.md — топ объектов уровня по просмотрам/времени (PagingQuery);
  8. get-conversion-funnel.md — воронка views→favorites→…→deals;
  9. get-session-depth.md — глубина сессии (объектов застройщика за сессию);
  10. get-activity-heatmap.md — хитмап активности час × день недели;
  11. get-bounce-signal.md — доля коротких просмотров по объекту (сигнал отказа);
  12. get-time-to-first-view.md — время от публикации до первого просмотра (TTFV).

Дашборд ЛК — клиентская композиция этих методов (dashboards/dev-stats-overview.md), экспорт — аудируемый отчёт (reports/dev-stats-export.md, permission dev-stats.export). Домены-контракты: domain/dev-stats-filter.md (DevStatsFilterModel), domain/viewer-classification.md (ViewerKind/ViewSource), domain/response-models.md (типизированные ряды/точки).

Events И Consumers

Produced-событий нет (events/README.md). Consumed — батч-ingestion (идемпотентность по FactId/SourceEventId, SLA < 5 мин); топики согласованы с продюсерами (ADR-0058):

TopicНазначениеConsumer
pin.user-activity.property-object-view-ended.v1closed-факт просмотра → property_object_view_factsconsumers/property-object-view-ended.md
pin.user-activity.action-tracked.v1open-факт (ViewPropertyObject) + вход воронки каталогаconsumers/action-tracked.md
pin.user-activity.session-ended.v1сессия → session_factsconsumers/session-ended.md
pin.chessboard.property-object-upserted.v1реплика dim_property_objectconsumers/property-object-upserted.md
pin.chessboard.{developer,project,building,layout-group,layout}-upserted.v1реплики dim_*consumers/developer-upserted.md и соседние *-upserted
pin.favorites.favorite-added.v1, pin.selections.selection-item-added.v1, pin.crm.{lead-created,booking-created,booking-paid,deal-created,deal-completed}.v1этапы воронки → funnel_dailyconsumers/favorite-added.md, consumers/selection-item-added.md, consumers/lead-created.md и соседние

Background-джобы (background/, по background-job-template): dimension-backfill.md (первичная загрузка dim_* из chessboard), facts-backfill.md (разовая загрузка фактов из user-activity при вводе).

Runtime И Эксплуатация

ОбластьТребование
Health checks/health/live; /health/ready (ClickHouse, Kafka); /health/startup (миграции DDL + MV)
SLO/SLAfreshness фактов < 5 мин (event → API); API p95 ≤ 300 мс (rollup), ≤ 600 мс (сырые факты); TTL фактов — 3 года (ViewedAt + INTERVAL 3 YEAR)
Dashboardsпродукт — dashboards/dev-stats-overview.md; ops ingest — dashboards/dev-stats-ingest-overview.md
Alertsрост ingest-lag > 15 мин; ошибки batch-вставки; провал обогащения (пустая dim); 5xx метрик
Runbooksпереигрывание консюмера с offset (идемпотентно); повторный backfill измерений/фактов; пересборка MV
Feature flagsdev-stats.funnel.enabled (воронка до готовности источников selections/crm)

Безопасность

  • Actor types: застройщик (роль developer, ЛК-аналитика своих объектов); Менеджер ПИН/админ платформы — отдельные permissions (вне v1).
  • Permission model: dev-stats.view (чтение метрик), dev-stats.export (выгрузка) — каталог в identity/domain/permission.md. Scope строго = свой DeveloperId: каждый запрос жёстко фильтрует DeveloperId = актор (обязательный компонент DevStatsFilterModel).
  • Sensitive data classes: факты несут UserId/AnonymousId/SessionId (Sensitive), но API выдаёт только агрегаты — ФИО/контакты зрителей не раскрываются, только ViewerKind и уникальные счётчики.
  • Audit: экспорт метрик аудируется (actor + фильтр + время) — обязательное требование dev-stats.export.
  • Tenant isolation rule: TenantId = DeveloperId (ADR-0052); фильтр по tenant обязателен и в записи (обогащение), и в чтении; тесты изоляции обязательны.

Каталог метрик

МетрикаЧто показываетФакт/роллапAPI-метод
Сводка просмотровВсего/уникальные/средняя длительность/повторность за периодproperty_object_view_dailyget-views-summary
ДинамикаРяд day/week/month + сравнение периодовproperty_object_view_dailyget-views-trend
Уникальные зрителиuniqMerge(UniqViewers) + разрез/рядproperty_object_view_dailyget-unique-viewers
ДлительностьРаспределение по бинам + avg/medianproperty_object_view_factsget-duration-histogram
ПовторностьЧастота визитов на пару «зритель × объект»property_object_view_factsget-repeat-frequency
ИерархияСужение внимания проект→…→объект + top-N детейproperty_object_view_dailyget-hierarchy-funnel
Топ объектовРейтинг по просмотрам/времениproperty_object_view_dailyget-top-objects
Воронкаviews→favorites→…→deals + конверсииfunnel_dailyget-conversion-funnel
Глубина сессииОбъектов застройщика за сессию + распределениеproperty_object_view_factsget-session-depth
ХитмапПросмотры час × день недели (таймзона)property_object_view_factsget-activity-heatmap
Сигнал отказаДоля коротких просмотров (<N с) по объектуproperty_object_view_factsget-bounce-signal
Время до 1-го просмотраПубликация объекта → первый просмотр (TTFV)dim_property_object + property_object_view_factsget-time-to-first-view

Use-cases (UC-STA): UC-STA-001 статистика проекта, UC-STA-002 детализация, UC-STA-003 сравнение периодов, UC-STA-004 воронка, UC-STA-005 экспорт.

Инварианты аналитики и согласованность (eventual consistency)

Единый набор гарантий, на которые опираются все 12 метрик и консюмеры (коды используются в acceptance- критериях методов):

  • INV-STA-1 (tenant fail-closed). Каждый факт при обогащении и каждый запрос при чтении жёстко фильтруются DeveloperId = актор; клиентский DeveloperId игнорируется. Нарушение scope → 403, не пустой ответ (чужой объём не подтверждается ни числом, ни отсутствием данных).
  • INV-STA-2 (идемпотентный ingest). Факт дедуплен по FactId = SourceEventId; повторная доставка того же события (at-least-once) не меняет счётчики — переигрывание консюмера с offset безопасно.
  • INV-STA-3 (dedup-tolerant чтение без FINAL). Online-чтение — через argMax/*Merge; open- и closed-строки одного просмотра склеиваются по PropertyObjectViewId (реальная длительность из closed побеждает 0 из open по IngestionTime), поэтому недосхлопнутые дубли ReplacingMergeTree/at-least-once не завышают метрику.
  • INV-STA-4 (кросс-день уникальность). Уникальные зрители = uniqMerge HLL-состояний по дням, не сумма дневных счётчиков: зритель, заходивший в разные дни, за период учтён один раз. Следствие, документируемое в методах-рядах: Σ по бакетам ряда ≥ итог за период (это не расхождение, а свойство дедупа).
  • INV-STA-5 (окно свежести ≤ 5 мин). Между событием и появлением факта метрика недосчитывает самые свежие просмотры; после подтверждённой batch-вставки число монотонно догоняет. В окне пользователь видит недооценку, не ошибку (eventual consistency); верхний этап воронки (views) считается независимо от нижних (favorites/leads/deals), поэтому нижние этапы могут отставать сильнее.
  • INV-STA-6 (обогащение last-known). Иерархия и имена берутся из последней известной версии dim_*; при отставании chessboard-консюмера факт всё равно вставляется — имя узла может быть пустым/устаревшим, но счётчики корректны (обогащение по PropertyObjectId → … берёт актуальную на момент вставки строку dim, backfill дозаполняет пропуски).
  • INV-STA-7 (агрегат наружу, без PII). Наружу отдаются только агрегаты — ViewerKind, уникальные счётчики, доли; UserId/AnonymousId/SessionId служат исключительно ключом группировки внутри запроса и в ответ/логи не попадают.
  • INV-STA-8 (3-летнее окно). Факты старше 3 лет отсекаются TTL (ViewedAt + INTERVAL 3 YEAR); запросы с окном шире TTL отклоняются валидацией (окно ≤ 3 года у rollup-методов, ≤ 366 дней у сырьевых — cost-guard).

Кросс-сервисные ожидания (что этот контекст требует от продюсеров):

  • От user-activity: событие несёт дедуп-ключ (SourceEventId), PropertyObjectId, ViewedAt (UTC) и опционально UserId/AnonymousId/SessionId/DurationSeconds/ViewerRole/ViewSource; доставка at-least-once, порядок action-tracked(open) → property-object-view-ended(closed) не гарантирован — длительность разрешается argMax по IngestionTime, а не по порядку доставки.
  • От chessboard: *-upserted несут UpsertedAt (версия для ReplacingMergeTree) и полную иерархию узла; отсутствие строки на момент факта → обогащение по last-known + backfill измерений при вводе.
  • От favorites/selections/crm: события этапов воронки разрешаются к DeveloperId/PropertyObjectId (или иерархии) и суммируются в funnel_daily по уникальному бизнес-ключу этапа (idempotent к повтору); этап без активированного источника отдаётся с enabled=false, а не ломает воронку.

Известные Ограничения И Assumptions

  • Тип зрителя Agent/Investor требует роли зрителя, которой в v1 нет ни в payload, ни в измерениях (dim_user нет, ADR-0056): аутентифицированный зритель пишется как Investor, Agent детектируется по сигналу роли (ViewerRole в событии — additive). Дом — domain/viewer-classification.md.
  • Сессионные метрики считаются через связь сессии с просмотрами застройщика (SessionId в property_object_view_facts); session_facts пишется общим (DeveloperId nullable) — простой вариант (ADR-0056), без прямой привязки сессии к застройщику.
  • Обогащение факта требует строки в dim_* (backfill измерений при вводе); при отставании chessboard-консюмера факт берёт последнюю известную версию измерения.
  • Воронка активируется по мере готовности источников (все этапы views→…→deals подключены); флаг dev-stats.funnel.enabled. Вход воронки каталога (ApplyFilter/Search) — additive-расширение funnel_daily (см. consumers/action-tracked.md).
  • FINAL в online-запросах запрещён — dedup-tolerant чтение через argMax/*Merge; ClickHouse pin_dev_stats — аналитическое хранилище, не operational source of truth.

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