GET /api/v1/dev-stats/views/summary — Сводка просмотров (кто/сколько/как долго/как часто)
Назначение
Одна агрегированная сводка внимания к недвижимости застройщика за период по выбранному уровню иерархии
(проект / здание / группа планировок / планировка / объект недвижимости — задаётся набором фильтров
DevStatsFilterModel): сколько просмотров, кто смотрел (уникальные
зрители и их типы), как долго (средняя длительность) и как часто (повторные просмотры). Отдаёт
headline-числа totalViews / uniqueViewers / avgDurationSeconds / repeatRate плюс разбивку по ViewerKind
(Guest / Agent / Investor). Источник карточек-KPI дашборда ЛК застройщика
(панель dev-stats-overview собирает несколько таких метрик). Читает дневной rollup
property_object_view_daily без сканирования сырых фактов.
| Поле | Значение |
|---|
| Сервис/контекст | dev-stats |
| API area | developers (read-only ЛК-аналитика) |
| Feature group | views |
| Статус | draft |
| Документ | docs/06-services/dev-stats/api/developers/get-views-summary.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (Bearer JWT: пользователь застройщика — менеджер developer-компании) |
| Auth policy / Permissions | permission dev-stats.view (каталог — identity/domain/permission.md) |
| Scope (RBAC) | организация застройщика: scope строго = свой DeveloperId |
| Record-level | не применяется (агрегаты по объектам компании, не по записям пользователя) |
| Tenant isolation | TenantId = DeveloperId; фильтр DeveloperId обязателен и = актор (fail-closed, injection из identity, не из клиента) |
| Audit | none (read-агрегат без PII; выгрузка — отдельный dev-stats.export) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/dev-stats/views/summary |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Общий вход — DevStatsFilterModel. DeveloperId берётся из identity-claims
актора (scope), не принимается от клиента.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
DeveloperId | identity claims | Guid | Да | != Guid.Empty; = актор (клиентское значение отвергается → 403) | Tenant isolation, fail-closed |
DateFrom | query dateFrom | DateOnly | Да | ISO yyyy-MM-dd | — |
DateTo | query dateTo | DateOnly | Да | ISO; DateFrom <= DateTo; окно ≤ 3 года (= TTL фактов) | — |
ProjectId | query projectId | Guid? | Нет | уровень иерархии (сужает scope) | — |
BuildingId | query buildingId | Guid? | Нет | требует заданного ProjectId (иерархия сверху вниз) | — |
LayoutGroupId | query layoutGroupId | Guid? | Нет | — | — |
LayoutId | query layoutId | Guid? | Нет | — | — |
PropertyObjectId | query propertyObjectId | Guid? | Нет | самый глубокий уровень | — |
ViewerKind | query viewerKind | ViewerKind? (enum) | Нет | wire guest/agent/investor; неизвестное → ValidationError (молчаливый дефолт запрещён) | — |
ViewSource | query viewSource | ViewSource? (enum) | Нет | wire catalog/search/selection/favorites/news/direct-link/unknown | — |
Модель ответа ViewsSummaryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
developerId | Guid | Да | echo актора | — | Застройщик (= scope). |
dateFrom / dateTo | string (ISO date) | Да | echo | — | Применённый период. |
level | string (enum wire) | Да | вычислено | — | Самый глубокий заданный уровень иерархии: developer/project/building/layout-group/layout/property-object. |
totalViews | long | Да | uniqMerge(Views) | — | Всего просмотров (dedup по PropertyObjectViewId). |
uniqueViewers | long | Да | uniqMerge(UniqViewers) | агрегат | Уникальные зрители (coalesce(UserId, AnonymousId)), дедуп за весь период. |
totalDurationSeconds | long | Да | sumMerge(DurationSum) | — | Суммарная длительность просмотров, сек. |
avgDurationSeconds | int | Да | sumMerge(DurationSum) / uniqMerge(Views) (guard 0) | — | Средняя длительность просмотра, сек. |
repeatViews | long | Да | uniqMerge(Views) − uniqMerge(UniqViewers) | — | Просмотры сверх первого на каждого уникального зрителя. |
repeatRate | decimal | Да | repeatViews / totalViews (guard 0) | — | Доля повторных просмотров, 0..1 (2 знака). |
byViewerKind | IReadOnlyList<ViewerKindSummaryModel> | Да | GROUP BY ViewerKind | агрегат | Разбивка по типу зрителя. |
ViewerKindSummaryModel
| Поле | Тип | Источник | Описание |
|---|
viewerKind | string (enum wire) | ViewerKind | guest / agent / investor. |
views | long | uniqMerge(Views) | Просмотры этого типа зрителя. |
uniqueViewers | long | uniqMerge(UniqViewers) | Уникальные зрители типа. |
avgDurationSeconds | int | sumMerge/uniqMerge | Средняя длительность, сек. |
share | decimal | views / totalViews (guard 0) | Доля в общем числе просмотров, 0..1. |
Предусловия и согласованность данных
- Пользователь застройщика авторизован; есть permission
dev-stats.view; scope DeveloperId разрешён и
равен актору (INV-STA-1, fail-closed — ../../README.md).
- Сводка наполняется batch-консюмером из
pin.user-activity.property-object-view-ended.v1; окно свежести
событие→факт ≤ 5 мин (INV-STA-5). В окне headline-числа недооценивают самые свежие просмотры —
деградация, не ошибка; после подтверждённой вставки счётчики монотонно догоняют.
uniqueViewers дедуплен за весь период кросс-день (INV-STA-4): зритель, заходивший в разные дни, учтён
один раз, поэтому repeatViews = totalViews − uniqueViewers ≥ 0.
- Операция read-only: состояние сервиса не меняется, события не публикуются.
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом
dev-stats.view. DeveloperId берётся из identity-claims актора (scope) и не принимается от клиента;
если клиент прислал DeveloperId ≠ актор → отказ DEV_STATS_FORBIDDEN (403, fail-closed).
- Проверка входа.
DateFrom/DateTo заданы и DateFrom <= DateTo, окно ≤ 3 года (= TTL фактов);
enum ViewerKind/ViewSource — из допустимого множества (неизвестное значение → ValidationError,
молчаливый дефолт запрещён); иерархия задаётся «сверху вниз» (нельзя BuildingId без ProjectId).
Существование иерархии и статусы не проверяются: неизвестный id даёт пустую сводку, а не ошибку.
- Что читаем. Дневной rollup
property_object_view_daily за период по заданному уровню иерархии.
Один проход с GROUP BY ViewerKind WITH ROLLUP даёт и разбивку по типу зрителя, и общий итог. Меры
считаются dedup-tolerant склейкой состояний — uniqMerge(Views), uniqMerge(UniqViewers),
sumMerge(DurationSum) — без FINAL. Диапазон EventDate режет партиции по месяцам
(toYYYYMM); заданные уровни иерархии образуют префикс-скан по ключу rollup-а (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate, ViewerKind). Если задан ViewSource
(в rollup не хранится), чтение переключается на сырьё
property_object_view_facts (медленнее, SLO p95 ≤ 600 мс).
- Расчёты и маппинг.
avgDurationSeconds = totalDurationSeconds / totalViews (guard 0);
repeatViews = totalViews − uniqueViewers; repeatRate = repeatViews / totalViews (guard 0, 2 знака);
share каждого ViewerKind = его views / totalViews; level = самый глубокий заданный уровень
иерархии. Пустой результат (нет фактов) → нули и пустой byViewerKind (не ошибка).
- Результат. Возвращается
ViewsSummaryResult (200): headline-числа и разбивка byViewerKind.
Недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Нет dateFrom/dateTo, DateFrom > DateTo, окно > 3 лет, неизвестный enum, BuildingId без ProjectId | Ошибка валидации запроса | no retry |
UNAUTHORIZED | 401 | Нет JWT | Требуется авторизация | no retry |
DEV_STATS_FORBIDDEN | 403 | Нет dev-stats.view или DeveloperId ≠ актор | Доступ к статистике застройщика запрещён | no retry |
DEV_STATS_UNAVAILABLE | 503 | ClickHouse недоступен / timeout | Статистика временно недоступна | retry с backoff |
Пустой период (нет фактов) — не ошибка: 200 с нулями и пустым byViewerKind. Фильтр ViewSource
переводит запрос на срез по property_object_view_facts (в rollup ViewSource не хранится) — SLO p95 ≤ 600 мс вместо
≤ 300 мс.
Acceptance Criteria
- Сводка по уровню. Дано авторизованный застройщик, корректный период и
ProjectId своего проекта.
Когда GET /views/summary. Тогда 200 с totalViews/uniqueViewers/avgDurationSeconds/repeatRate,
разбивкой byViewerKind (сумма share ≈ 1) и level = project.
- Пустой период. Дано период без фактов. Когда запрос. Тогда
200, все headline-числа = 0,
byViewerKind = [] (не ошибка).
- Кросс-день дедуп. Дано один зритель заходил в 3 разных дня периода. Когда считается
uniqueViewers.
Тогда он учтён как 1 (INV-STA-4), а repeatViews = totalViews − uniqueViewers.
- Guard деления на ноль. Дано
totalViews = 0. Когда считаются производные. Тогда
avgDurationSeconds = repeatRate = share = 0 без ошибки.
- Чужой scope. Дано клиент подставил
DeveloperId другого застройщика. Когда запрос. Тогда
403 DEV_STATS_FORBIDDEN; чужие агрегаты не возвращаются (INV-STA-1, fail-closed).
- Неизвестный enum. Дано
viewerKind=vip. Когда запрос. Тогда 400 ValidationError (молчаливый дефолт
запрещён).
- Иерархия «снизу вверх». Дано
BuildingId без ProjectId. Когда запрос. Тогда 400 ValidationError.
- Окно шире TTL. Дано
dateTo − dateFrom > 3 года. Когда запрос. Тогда 400 ValidationError
(INV-STA-8).
- Срез по источнику. Дано задан
viewSource=search. Когда запрос. Тогда 200 с тем же контрактом, но
чтение идёт по сырью (property_object_view_facts), SLO p95 ≤ 600 мс вместо ≤ 300 мс.
- Окно свежести. Дано просмотр случился минуту назад и ещё не сведён в факт. Когда запрос. Тогда он не
учтён; после ingestion (≤ 5 мин)
totalViews монотонно возрастает (INV-STA-5), запрос не падает.
- Недоступность витрины. Дано ClickHouse недоступен/timeout. Когда запрос. Тогда
503 DEV_STATS_UNAVAILABLE с подсказкой retry.
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; удаление/переименование/смена wire-enum
ViewerKind/ViewSource
— breaking (версия метода). Unknown enum на входе → ValidationError (не дефолт).
- Logs:
dev-stats-views-summary-read (developerId, level, dateFrom/dateTo, totalViews, elapsed; PII нет).
Metrics: dev_stats_summary_requests_total{level}, latency histogram (SLO p95 ≤ 300 мс). Traces:
span приёма запроса → span обработки → span чтения ClickHouse; correlation — сквозной trace
traceparent. Dashboard/alerts —
борд ingest (лаг влияет на свежесть, ../../README.md).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы