GET /api/v1/dev-stats/views/hierarchy-funnel — Воронка просмотров по иерархии
Назначение
Показывает застройщику, как внимание сужается по иерархии его недвижимости: проект → здание → группа планировок → планировка → объект недвижимости. Для заданного пути фильтра возвращает (а) стадии-этажи — просмотры/
уникальные зрители в scope каждого выбранного уровня (сужающаяся воронка) и (б) разбивку по детям —
top-N объектов следующего уровня ниже самого глубокого заданного фильтра с их долей. Источник дерева
детализации на дашборде застройщика (../../README.md). Считается из дневного rollup-а
property_object_view_daily (AggregatingMergeTree, *Merge, без FINAL);
имена узлов джойнятся из реплик-измерений dim_*.
| Поле | Значение |
|---|
| Сервис/контекст | dev-stats |
| API area | developers |
| Feature group | hierarchy-funnel |
| Статус | draft |
| Документ | docs/06-services/dev-stats/api/developers/get-hierarchy-funnel.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT; роль застройщика — developer) |
| Auth policy / Permissions | permission dev-stats.view — identity/domain/permission.md |
| Scope (RBAC) | организация застройщика; DeveloperId строго = актор (fail-closed) |
| Record-level | не применяется (агрегаты) |
| Tenant isolation | TenantId = DeveloperId обязателен и = актору |
| Audit | none (read-агрегаты; персональные данные зрителей не выдаются) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/dev-stats/views/hierarchy-funnel |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Общий вход — DevStatsFilterModel (../../domain/dev-stats-filter.md).
Заданные иерархические id задают путь воронки; уровень «детей» = первый незаданный уровень ниже.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
DeveloperId | identity claims | Guid | Да | != Guid.Empty; = актор (иначе Forbidden) | Tenant isolation |
DateFrom | query dateFrom | DateOnly | Да | ISO; DateFrom <= DateTo | — |
DateTo | query dateTo | DateOnly | Да | ISO; ≤ сегодня (UTC) | — |
ProjectId | query projectId | Guid? | Нет | уровень пути 2 | — |
BuildingId | query buildingId | Guid? | Нет | требует ProjectId (иерархия сверху вниз без «дыр») | — |
LayoutGroupId | query layoutGroupId | Guid? | Нет | требует BuildingId | — |
LayoutId | query layoutId | Guid? | Нет | требует LayoutGroupId | — |
PropertyObjectId | query propertyObjectId | Guid? | Нет | лист — детей нет (только стадии) | — |
ViewerKind | query viewerKind | ViewerKind? | Нет | Guest/Agent/Investor (dimension rollup-а) | — |
ChildrenLimit | query childrenLimit | int | Нет | 1..100; default 20 (top-N детей по просмотрам) | — |
Правило «без дыр»: задание нижнего id без верхнего → ValidationError (иерархия строится сверху вниз).
Модель ответа HierarchyFunnelResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
dateFrom | string (ISO date) | Да | echo | — | Начало периода. |
dateTo | string (ISO date) | Да | echo | — | Конец периода. |
stages | IReadOnlyList<HierarchyStageModel> | Да | агрегация по префиксам | — | Стадии сужения: от developer до самого глубокого заданного уровня. |
childrenLevel | string? (enum wire) | Нет | вычислено | — | Уровень детей (project/building/layoutGroup/layout/property-object); null, если задан PropertyObjectId. |
children | IReadOnlyList<HierarchyNodeModel> | Да (может быть пуст) | breakdown next-level | — | Top-N детей по просмотрам с долей от родителя. |
HierarchyStageModel
| Поле | Тип | Обязательность | Источник | Описание |
|---|
level | string (enum wire) | Да | developer/project/building/layoutGroup/layout/property-object | Уровень стадии. |
id | Guid | Да | заданный id (или DeveloperId) | Узел уровня. |
name | string | Да | dim_* по id | Отображаемое имя (пусто при отсутствии измерения). |
views | long | Да | uniqMerge(Views) в scope уровня | Просмотры узла. |
uniqViewers | long | Да | uniqMerge(UniqViewers) | Уникальные зрители узла. |
shareOfParentPercent | decimal? | Нет | round1(views / parent.views × 100); null для корня | Доля от родительской стадии, %. |
HierarchyNodeModel
| Поле | Тип | Обязательность | Источник | Описание |
|---|
id | Guid | Да | GROUP BY <childLevelId> | Ребёнок следующего уровня. |
name | string | Да | dim_* по id | Имя объекта. |
views | long | Да | uniqMerge(Views) | Просмотры ребёнка. |
uniqViewers | long | Да | uniqMerge(UniqViewers) | Уникальные зрители. |
durationSum | long | Да | sumMerge(DurationSum) | Суммарная длительность, сек. |
sharePercent | decimal | Да | 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: состояние не меняется, события не публикуются.
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом
dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал иной id → отказ
Forbidden (403).
- Проверка входа.
DateFrom <= DateTo; правило «без дыр» по иерархии (задание нижнего id без верхнего
→ ValidationError); ChildrenLimit ∈ 1..100. Существование узлов не проверяется: неизвестный id даёт
пустой результат.
- Что читаем. Дневной rollup
property_object_view_daily за период по пути фильтра. Путь фильтра —
префикс-скан по ключу (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, …); диапазон дат режет партиции toYYYYMM(EventDate). Стадии — набор агрегатов по всё более длинным
префиксам того же условия (по одному uniqMerge на уровень пути). Дети — то же условие +
GROUP BY <childLevelId> (первый незаданный уровень), ORDER BY uniqMerge(Views) DESC LIMIT ChildrenLimit.
- Расчёты, маппинг и имена. Меры 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).
- Результат. Возвращается
HierarchyFunnelResult (200): stages и children. Freshness ≤ 5 мин;
недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | «Дыра» в иерархии / невалидный диапазон / ChildrenLimit вне 1..100 | Ошибка валидации запроса | no retry |
UNAUTHORIZED | 401 | Нет JWT | Требуется авторизация | no retry |
Forbidden | 403 | DeveloperId != актор или нет dev-stats.view | Доступ запрещён | no retry |
DEV_STATS_UNAVAILABLE | 503 | ClickHouse недоступен / 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы