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

GET /api/v1/dev-stats/views/hierarchy-funnel — Воронка просмотров по иерархии

Назначение

Показывает застройщику, как внимание сужается по иерархии его недвижимости: проект → здание → группа планировок → планировка → объект недвижимости. Для заданного пути фильтра возвращает (а) стадии-этажи — просмотры/ уникальные зрители в scope каждого выбранного уровня (сужающаяся воронка) и (б) разбивку по детям — top-N объектов следующего уровня ниже самого глубокого заданного фильтра с их долей. Источник дерева детализации на дашборде застройщика (../../README.md). Считается из дневного rollup-а property_object_view_daily (AggregatingMergeTree, *Merge, без FINAL); имена узлов джойнятся из реплик-измерений dim_*.

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature grouphierarchy-funnel
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-hierarchy-funnel.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Bearer JWT; роль застройщика — developer)
Auth policy / Permissionspermission dev-stats.viewidentity/domain/permission.md
Scope (RBAC)организация застройщика; DeveloperId строго = актор (fail-closed)
Record-levelне применяется (агрегаты)
Tenant isolationTenantId = DeveloperId обязателен и = актору
Auditnone (read-агрегаты; персональные данные зрителей не выдаются)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/dev-stats/views/hierarchy-funnel
Success status200
Idempotency headerне применяется (safe GET)
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

Общий вход — DevStatsFilterModel (../../domain/dev-stats-filter.md). Заданные иерархические id задают путь воронки; уровень «детей» = первый незаданный уровень ниже.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иначе Forbidden)Tenant isolation
DateFromquery dateFromDateOnlyДаISO; DateFrom <= DateTo
DateToquery dateToDateOnlyДаISO; ≤ сегодня (UTC)
ProjectIdquery projectIdGuid?Нетуровень пути 2
BuildingIdquery buildingIdGuid?Неттребует ProjectId (иерархия сверху вниз без «дыр»)
LayoutGroupIdquery layoutGroupIdGuid?Неттребует BuildingId
LayoutIdquery layoutIdGuid?Неттребует LayoutGroupId
PropertyObjectIdquery propertyObjectIdGuid?Нетлист — детей нет (только стадии)
ViewerKindquery viewerKindViewerKind?НетGuest/Agent/Investor (dimension rollup-а)
ChildrenLimitquery childrenLimitintНет1..100; default 20 (top-N детей по просмотрам)

Правило «без дыр»: задание нижнего id без верхнего → ValidationError (иерархия строится сверху вниз).

Модель ответа HierarchyFunnelResult

ПолеТипОбязательностьИсточникMaskingОписание
dateFromstring (ISO date)ДаechoНачало периода.
dateTostring (ISO date)ДаechoКонец периода.
stagesIReadOnlyList<HierarchyStageModel>Даагрегация по префиксамСтадии сужения: от developer до самого глубокого заданного уровня.
childrenLevelstring? (enum wire)НетвычисленоУровень детей (project/building/layoutGroup/layout/property-object); null, если задан PropertyObjectId.
childrenIReadOnlyList<HierarchyNodeModel>Да (может быть пуст)breakdown next-levelTop-N детей по просмотрам с долей от родителя.

HierarchyStageModel

ПолеТипОбязательностьИсточникОписание
levelstring (enum wire)Даdeveloper/project/building/layoutGroup/layout/property-objectУровень стадии.
idGuidДазаданный id (или DeveloperId)Узел уровня.
namestringДаdim_* по idОтображаемое имя (пусто при отсутствии измерения).
viewslongДаuniqMerge(Views) в scope уровняПросмотры узла.
uniqViewerslongДаuniqMerge(UniqViewers)Уникальные зрители узла.
shareOfParentPercentdecimal?Нетround1(views / parent.views × 100); null для корняДоля от родительской стадии, %.

HierarchyNodeModel

ПолеТипОбязательностьИсточникОписание
idGuidДаGROUP BY <childLevelId>Ребёнок следующего уровня.
namestringДаdim_* по idИмя объекта.
viewslongДаuniqMerge(Views)Просмотры ребёнка.
uniqViewerslongДаuniqMerge(UniqViewers)Уникальные зрители.
durationSumlongДаsumMerge(DurationSum)Суммарная длительность, сек.
sharePercentdecimalДаround1(views / childrenTotal × 100)Доля ребёнка от суммы детей уровня, %.

Предусловия и согласованность данных

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md).
  • Счётчики читаются из rollup property_object_view_daily (окно свежести ≤ 5 мин, INV-STA-5), а имена узлов стадий/детей — из реплик-измерений dim_*. При отставании chessboard-консюмера имя может быть пустым/устаревшим, но views/uniqViewers при этом корректны (INV-STA-6): пользователь видит верные числа при, возможно, ещё не подгруженном названии.
  • Путь фильтра строится «сверху вниз без дыр» (задать BuildingId можно только вместе с ProjectId и т.д.).
  • Операция read-only: состояние не меняется, события не публикуются.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал иной id → отказ Forbidden (403).
  2. Проверка входа. DateFrom <= DateTo; правило «без дыр» по иерархии (задание нижнего id без верхнего → ValidationError); ChildrenLimit ∈ 1..100. Существование узлов не проверяется: неизвестный id даёт пустой результат.
  3. Что читаем. Дневной rollup property_object_view_daily за период по пути фильтра. Путь фильтра — префикс-скан по ключу (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, …); диапазон дат режет партиции toYYYYMM(EventDate). Стадии — набор агрегатов по всё более длинным префиксам того же условия (по одному uniqMerge на уровень пути). Дети — то же условие + GROUP BY <childLevelId> (первый незаданный уровень), ORDER BY uniqMerge(Views) DESC LIMIT ChildrenLimit.
  4. Расчёты, маппинг и имена. Меры dedup-tolerant — uniqMerge(Views), uniqMerge(UniqViewers), sumMerge(DurationSum)без FINAL. Имена стадий/детей берутся из реплик-измерений dim_project/dim_building/dim_layout_group/dim_layout/dim_property_object по id (реплики chessboard в той же БД). shareOfParentPercent = views / parent.views; sharePercent детей = views / Σ children (guard знаменателя > 0).
  5. Результат. Возвращается HierarchyFunnelResult (200): stages и children. Freshness ≤ 5 мин; недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400«Дыра» в иерархии / невалидный диапазон / ChildrenLimit вне 1..100Ошибка валидации запросаno retry
UNAUTHORIZED401Нет JWTТребуется авторизацияno retry
Forbidden403DeveloperId != актор или нет dev-stats.viewДоступ запрещёнno retry
DEV_STATS_UNAVAILABLE503ClickHouse недоступен / timeoutАналитика временно недоступнаretry с backoff

Пустая иерархия (нет просмотров) — не ошибка: 200, stages с нулями, children = [].

Acceptance Criteria

  • Стадии + дети. Дано авторизованный застройщик, projectId своего проекта и buildingId. Когда GET /views/hierarchy-funnel. Тогда 200 со stages от developer до building (сужающиеся views), childrenLevel = layoutGroup и top-N children с sharePercent (сумма ≈ 100).
  • Лист без детей. Дано задан propertyObjectId (самый глубокий уровень). Когда запрос. Тогда childrenLevel = null, children = [], только stages.
  • Доли сужения. Дано непустые стадии. Когда считаются доли. Тогда shareOfParentPercent корня — null, у остальных — views / parent.views × 100 (guard знаменателя > 0).
  • Имя из реплики отстаёт. Дано узел ещё не реплицирован в dim_*. Когда возвращается стадия/ребёнок. Тогда name пустой, но счётчики верны (INV-STA-6).
  • «Дыра» в иерархии. Дано buildingId без projectId. Когда запрос. Тогда 400 ValidationError.
  • Границы childrenLimit. Дано childrenLimit=0 или > 100. Когда запрос. Тогда 400 ValidationError; при валидном лимите возвращается не более N детей по убыванию просмотров.
  • Пустая иерархия. Дано неизвестный или бес-просмотровый путь. Когда запрос. Тогда 200, stages с нулями, children = [] (не ошибка).
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 Forbidden (INV-STA-1).
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_UNAVAILABLE (retry с backoff).

Совместимость и наблюдаемость

  • Новые optional поля — additive; смена набора/wire-значений уровней (level/childrenLevel) — breaking.
  • Logs: hierarchy-funnel-read (developerId, deepestLevel, childrenLevel, childrenCount, elapsed; PII нет). Metrics: dev_stats_hierarchy_funnel_requests_total{level}, latency histogram (SLO p95 ≤ 300 мс). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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