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 team | PIN Analytics (dev-stats) |
| Domain | Аналитика внимания застройщика (developer view analytics) |
| Data classification | Internal/Sensitive (факты несут UserId/AnonymousId — Sensitive на уровне строк; наружу — только агрегаты: типы зрителей и уникальные счётчики, без ФИО/контактов) |
| Runtime criticality | low-medium (ЛК-аналитика; недоступность не блокирует каталог/сделки/просмотры) |
Ownership
| Artifact | Owner/Rule |
|---|---|
| ClickHouse database | pin_dev_stats (владеет своими таблицами; не делит схему с invest-analytics/pin_analytics) |
| PostgreSQL schema | не используется (ClickHouse-only, ADR-0057); ingest-offset — Kafka; чекпоинт backfill — служебная таблица job-а |
| API area | developers (12 read-only метрик; scope жёстко = свой DeveloperId); public/admin — нет |
| Owned facts | property_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 events | pin.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-ов/измерений и чтение метрик | Полный отказ сервиса (метрики недоступны); операционные контексты не затронуты |
identity | service | Аутентификация застройщика, 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
Область developers — 12 read-only метрик (permission dev-stats.view; общий вход DevStatsFilterModel,
DeveloperId = актор; чтение метрик dedup-tolerant argMax/*Merge без FINAL):
- get-views-summary.md — сводка (views/uniqViewers/avgDuration/repeatRate) + разбивка
ViewerKind; - get-views-trend.md — динамика day/week/month + сравнение периодов;
- get-unique-viewers.md — уникальные зрители (
uniqMerge) + разрезViewerKind+ ряд; - get-duration-histogram.md — распределение длительностей + avg/median;
- get-repeat-frequency.md — частота повторных визитов (пара «зритель × объект»);
- get-hierarchy-funnel.md — сужение внимания по иерархии + top-N детей;
- get-top-objects.md — топ объектов уровня по просмотрам/времени (
PagingQuery); - get-conversion-funnel.md — воронка
views→favorites→…→deals; - get-session-depth.md — глубина сессии (объектов застройщика за сессию);
- get-activity-heatmap.md — хитмап активности час × день недели;
- get-bounce-signal.md — доля коротких просмотров по объекту (сигнал отказа);
- 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.v1 | closed-факт просмотра → property_object_view_facts | consumers/property-object-view-ended.md |
pin.user-activity.action-tracked.v1 | open-факт (ViewPropertyObject) + вход воронки каталога | consumers/action-tracked.md |
pin.user-activity.session-ended.v1 | сессия → session_facts | consumers/session-ended.md |
pin.chessboard.property-object-upserted.v1 | реплика dim_property_object | consumers/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_daily | consumers/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/SLA | freshness фактов < 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 flags | dev-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_daily | get-views-summary |
| Динамика | Ряд day/week/month + сравнение периодов | property_object_view_daily | get-views-trend |
| Уникальные зрители | uniqMerge(UniqViewers) + разрез/ряд | property_object_view_daily | get-unique-viewers |
| Длительность | Распределение по бинам + avg/median | property_object_view_facts | get-duration-histogram |
| Повторность | Частота визитов на пару «зритель × объект» | property_object_view_facts | get-repeat-frequency |
| Иерархия | Сужение внимания проект→…→объект + top-N детей | property_object_view_daily | get-hierarchy-funnel |
| Топ объектов | Рейтинг по просмотрам/времени | property_object_view_daily | get-top-objects |
| Воронка | views→favorites→…→deals + конверсии | funnel_daily | get-conversion-funnel |
| Глубина сессии | Объектов застройщика за сессию + распределение | property_object_view_facts | get-session-depth |
| Хитмап | Просмотры час × день недели (таймзона) | property_object_view_facts | get-activity-heatmap |
| Сигнал отказа | Доля коротких просмотров (<N с) по объекту | property_object_view_facts | get-bounce-signal |
| Время до 1-го просмотра | Публикация объекта → первый просмотр (TTFV) | dim_property_object + property_object_view_facts | get-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 (кросс-день уникальность). Уникальные зрители =
uniqMergeHLL-состояний по дням, не сумма дневных счётчиков: зритель, заходивший в разные дни, за период учтён один раз. Следствие, документируемое в методах-рядах:Σ по бакетам ряда ≥ итог за период(это не расхождение, а свойство дедупа). - 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пишется общим (DeveloperIdnullable) — простой вариант (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; ClickHousepin_dev_stats— аналитическое хранилище, не operational source of truth.
Связанные Документы
- Факты/rollup-ы/измерения: facts/, dimensions/ (по
clickhouse-fact-template); эталон —
invest-analytics/domain/price-history-fact.md. - Соседи:
user-activity/(источник просмотров/сессий/действий),chessboard/(источник измерений),favorites/,selections/,crm/(воронка),invest-analytics/(второй ClickHouse-контекст,pin_analytics). - ADR: ADR-0058 (переименование Лот→Объект), ADR-0057 (dev-stats/ClickHouse), ADR-0056 (границы CH, simplicity), ADR-0054 (user-activity), ADR-0052 (tenancy).
- Обзор: technical-assignment.md, service-boundaries.md, glossary.md.