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

GET /api/v1/dev-stats/top-objects — Топ объектов по просмотрам и времени

Назначение

Рейтинг объектов застройщика выбранного уровня (property-object/layout/layoutGroup/building/project) — топ по числу просмотров и по суммарному времени внимания. Отвечает на вопрос «что смотрят больше и дольше всего». Источник виджетов «Топ объектов недвижимости / планировок / проектов» на дашборде застройщика (../../README.md, метрики «По объектам/планировкам/проектам»). Считается из дневного rollup-а property_object_view_daily (AggregatingMergeTree, *Merge, без FINAL), имена — из реплик dim_*; рейтинг пагинируется постранично.

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature grouptop-objects
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-top-objects.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/top-objects
Success status200
Idempotency headerне применяется (safe GET)
Correlationсквозной trace через traceparent

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

Общий вход — DevStatsFilterModel (../../domain/dev-stats-filter.md) + уровень рейтинга, сортировка и пагинация PagingQuery.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иначе Forbidden)Tenant isolation
DateFromquery dateFromDateOnlyДаISO; DateFrom <= DateTo
DateToquery dateToDateOnlyДаISO; ≤ сегодня (UTC)
ObjectTypequery objectTypeTopObjectType (enum)Даproperty-object/layout/layoutGroup/building/project; неизвестное → ValidationError
SortByquery sortByTopObjectSort (enum)Нетviews (default) / durationSum; определяет ключ ORDER BY … DESC
ProjectIdquery projectIdGuid?Нетограничивает рейтинг под-деревом (существование не проверяется)
BuildingIdquery buildingIdGuid?Неттребует ProjectId
LayoutGroupIdquery layoutGroupIdGuid?Нет
LayoutIdquery layoutIdGuid?Нет
ViewerKindquery viewerKindViewerKind? (enum)НетGuest/Agent/Investor (dimension rollup-а)
Pagingquery page/pageSizePagingQueryНетpageSize 1..100 (default 20); page ≥ 1

Enum TopObjectType: PropertyObject/Layout/LayoutGroup/Building/Project; TopObjectSort: Views/ DurationSum (wire — нижний регистр; хранение — .HasConversion<string>()).

Модель ответа — постраничный рейтинг

Постраничная выдача: items, totalCount, page, pageSize, hasMore.

TopObjectRow

ПолеТипОбязательностьИсточникMaskingОписание
rankintДапозиция в отсортированном ряду (с учётом страницы)Место в рейтинге, 1-based.
objectTypestring (enum wire)Даecho ObjectTypeУровень объекта.
idGuidДаGROUP BY <levelId>Идентификатор объекта.
namestringДаdim_* по idОтображаемое имя (пусто при отсутствии измерения).
parentPathstring?Нетdim_* вверх (проект / проект→здание …)Контекст в иерархии (для объекта недвижимости/планировки).
viewslongДаuniqMerge(Views)Число просмотров.
uniqViewerslongДаuniqMerge(UniqViewers)Уникальные зрители.
durationSumlongДаsumMerge(DurationSum)Суммарное время внимания, сек.
avgDurationintДаdurationSum / views (0 при views=0)Средняя длительность на просмотр, сек.

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

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

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал иной id → отказ Forbidden (403).
  2. Проверка входа. ObjectType/SortBy ∈ enum (неизвестное → ValidationError); DateFrom <= DateTo; pageSize ∈ 1..100. Существование иерархических id не проверяется.
  3. Что читаем. Дневной rollup property_object_view_daily за период, GROUP BY <levelId> (колонка уровня ObjectType: PropertyObjectId/LayoutId/LayoutGroupId/BuildingId/ProjectId). Scope-фильтр — префикс-скан по ключу (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, …); диапазон дат режет партиции toYYYYMM(EventDate). Сортировка по SortBy: uniqMerge(Views) DESC или sumMerge(DurationSum) DESC; страница — LIMIT pageSize OFFSET (page−1)×pageSize; totalCountuniqExact(<levelId>) тем же условием (число объектов с просмотрами).
  4. Расчёты, маппинг и имена. Меры dedup-tolerant — uniqMerge(Views), uniqMerge(UniqViewers), sumMerge(DurationSum)без FINAL; avgDuration = durationSum / views (guard views>0). Имена и parentPath — из реплик-измерений dim_property_object/dim_layout/dim_layout_group/dim_building/ dim_project по id (реплики chessboard в той же БД). rank = глобальная позиция с учётом OFFSET.
  5. Результат. Возвращается постраничный рейтинг TopObjectRow (200): items + totalCount/page/pageSize/hasMore. Freshness ≤ 5 мин; недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

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

Пустой рейтинг (нет просмотров) — не ошибка: 200 с items = [], totalCount = 0.

Acceptance Criteria

  • Рейтинг по просмотрам. Дано авторизованный застройщик, objectType=property-object и период. Когда GET /top-objects без sortBy. Тогда 200, строки по убыванию views, rank 1-based сквозной с учётом страницы, totalCount = число объектов уровня с просмотрами.
  • Сортировка по времени. Дано sortBy=durationSum. Когда запрос. Тогда порядок — по убыванию суммарного времени внимания, avgDuration = durationSum / views (guard views>0).
  • Пагинация. Дано page=2&pageSize=20. Когда запрос. Тогда items — вторая страница, rank продолжает нумерацию (21…), hasMore отражает наличие следующих.
  • Имя из реплики отстаёт. Дано объект ещё не в dim_*. Когда строка возвращается. Тогда name/ parentPath пустые, но views/durationSum и место в рейтинге верны (INV-STA-6).
  • Обязательный уровень. Дано objectType не задан или неизвестен. Когда запрос. Тогда 400 ValidationError.
  • Границы пагинации. Дано pageSize=0 или > 100. Когда запрос. Тогда 400 ValidationError.
  • Пустой рейтинг. Дано период без просмотров. Когда запрос. Тогда 200, items = [], totalCount = 0 (не ошибка).
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 Forbidden (INV-STA-1).
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_UNAVAILABLE (retry с backoff).

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

  • Новые optional поля строки — additive; удаление/переименование/смена wire-значений objectType/ sortBy — breaking (версия метода). Unknown enum → ValidationError.
  • Logs: top-objects-read (developerId, objectType, sortBy, page, pageSize, elapsed; PII нет). Metrics: dev_stats_top_objects_requests_total{objectType,sortBy}, latency histogram (SLO p95 ≤ 300 мс). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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