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

GET /api/v1/dev-stats/activity-heatmap — Хитмап активности (час × день недели)

Назначение

Тепловая карта (heatmap) просмотров объектов застройщика по осям час дня (0–23) × день недели (1–7) — когда аудитория смотрит его недвижимость. Помогает застройщику выбирать время публикаций/акций и оценивать ритм спроса. Источник — факты property_object_view_facts; мера — число просмотров (views) и/или уникальные зрители (uniqViewers). Время просмотров хранится в UTC, поэтому раскладка по часам/дням выполняется в таймзоне отображения (TimeZone, default Europe/Moscow). Метод read-only над ClickHouse; общий вход — DevStatsFilterModel, обязателен DeveloperId = актор.

Metadata

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

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

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

HTTP-контракт

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

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

Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md).

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иной → 403)Tenant isolation
DateFromquery dateFromDateOnlyДаISO; DateFrom <= DateTo; диапазон ≤ 366 дней
DateToquery dateToDateOnlyДаISO; ≤ сегодня (UTC)
ProjectIdquery projectIdGuid?Нетсужение (префикс ключа)
BuildingIdquery buildingIdGuid?Нетсужение
LayoutGroupIdquery layoutGroupIdGuid?Нетсужение
LayoutIdquery layoutIdGuid?Нетсужение
PropertyObjectIdquery propertyObjectIdGuid?Нетсужение
ViewerKindquery viewerKindViewerKind?НетGuest/Agent/Investor; unknown → ValidationError
ViewSourcequery viewSourceViewSource?НетCatalog/Search/…; unknown → ValidationError
TimeZonequery timeZonestring? (IANA)Нетdefault Europe/Moscow; должна быть в списке IANA (toHour(ViewedAt, tz)); unknown → ValidationError
Metricquery metricHeatmapMetric? (enum)Нетviews (default) / uniqViewers; определяет maxCellValue/пик

TimeZone — IANA-строка; timezone-сущности в справочнике pin.handbook нет (ADR-0063).

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho акторЗастройщик.
from / tostring (ISO date)ДаechoГраницы диапазона.
timeZonestringДаecho (после дефолта)Применённая IANA-таймзона раскладки.
metricstring (enum wire)ДаechoВедущая мера (views/uniqViewers).
totalViewslongДаsum(views)Всего просмотров за период.
maxCellValuelongДаmax по ведущей мереМаксимум ячейки (для шкалы цвета).
peakDayOfWeekint?Нетячейка-argMaxДень недели пика (1–7), null при пустоте.
peakHourint?Нетячейка-argMaxЧас пика (0–23), null при пустоте.
cellsIReadOnlyList<HeatmapCellModel>Да (может быть пуст)агрегацияНепустые ячейки; отсутствующая ячейка = 0.

HeatmapCellModel

ПолеТипОписание
dayOfWeekintДень недели 1=Пн … 7=Вс (toDayOfWeek).
hourintЧас 023 (toHour в TimeZone).
viewslongПросмотры в ячейке (uniq(PropertyObjectViewId)).
uniqViewerslongУникальные зрители в ячейке.

Возвращаются только непустые ячейки (клиент дорисовывает нули до 7×24).

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

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md).
  • Время просмотров хранится в UTC; раскладка по осям час×день выполняется в таймзоне отображения (TimeZone, default Europe/Moscow) — смена TimeZone двигает просмотры между ячейками, но не меняет totalViews.
  • Метод читает сырьё property_object_view_facts (дневной rollup теряет час), окно ≤ 366 дней (cost-guard); просмотры дедуплены по PropertyObjectViewId (INV-STA-3), гости — по AnonymousId.
  • Окно свежести ≤ 5 мин (INV-STA-5). Операция read-only (событий нет).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал DeveloperId ≠ актор → отказ FORBIDDEN (403).
  2. Проверка входа. Обязательные DateFrom/DateTo, from <= to, диапазон ≤ 366 дней; TimeZone ∈ списке IANA (default Europe/Moscow); enum ViewerKind/ViewSource/Metric — из допустимого множества (неизвестное → ValidationError). Существование иерархии не проверяется.
  3. Что читаем. Rollup property_object_view_daily дневной и теряет час, поэтому heatmap считаем по сырью property_object_view_facts за период по scope: DeveloperId — головной компонент ключа → префикс-скан; опциональная иерархия удлиняет префикс; партиции toYYYYMM(ViewedAt) режут диапазон дат.
  4. Расчёты и маппинг (dedup-tolerant, без FINAL). GROUP BY dayOfWeek = toDayOfWeek(ViewedAt, TimeZone), hour = toHour(ViewedAt, TimeZone), ORDER BY dayOfWeek, hour; меры — views = uniq(PropertyObjectViewId) (схлопывает open/closed и at-least-once дубли, просмотр = один) и uniqViewers = uniq(coalesce(UserId, AnonymousId)) (гости — по AnonymousId). TimeZone — второй аргумент toHour/toDayOfWeek (UTC-хранение → локальная раскладка). maxCellValue/пик — из набора ячеек по ведущей мере Metric.
  5. Результат. Возвращаются только непустые ячейки ActivityHeatmapResult (200; нули клиент дорисовывает). Пустой период — cells = [], maxCellValue = 0, пик null. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нет dateFrom/dateTo, from > to, диапазон > 366 дней, unknown enum/TimeZoneОшибка валидации запросаno retry
UNAUTHORIZED401Нет/невалиден JWTТребуется авторизацияno retry
FORBIDDEN403Нет dev-stats.view или DeveloperId != акторДоступ запрещёнno retry
DEV_STATS_QUERY_UNAVAILABLE503ClickHouse недоступен / timeoutАналитика временно недоступнаretry с backoff

Пустой период — не ошибка: 200 с cells = [].

Acceptance Criteria

  • Хитмап с пиком. Дано авторизованный застройщик и корректный период. Когда GET /activity-heatmap. Тогда 200 с непустыми cells (dayOfWeek 1..7, hour 0..23), maxCellValue и peakDayOfWeek/ peakHour по ведущей мере metric.
  • Раскладка по таймзоне. Дано timeZone=Asia/Yekaterinburg. Когда запрос. Тогда часы просмотров сдвигаются относительно Europe/Moscow, totalViews неизменен; timeZone в ответе эхо-нится.
  • Ведущая мера. Дано metric=uniqViewers. Когда запрос. Тогда maxCellValue/пик берутся по уникальным зрителям, а не по числу просмотров.
  • Только непустые ячейки. Дано активность лишь в части часов. Когда запрос. Тогда возвращаются только непустые ячейки; клиент дорисовывает нули до сетки 7×24.
  • Дедуп просмотра. Дано open+closed одного просмотра. Когда заполняется ячейка. Тогда просмотр учтён один раз (INV-STA-3).
  • Невалидная таймзона / enum. Дано timeZone=Mars/Phobos или viewerKind=vip. Когда запрос. Тогда 400 ValidationError.
  • Окно > 366 дней. Дано слишком широкое окно. Когда запрос. Тогда 400 ValidationError.
  • Пустой период. Дано нет просмотров. Когда запрос. Тогда 200, cells = [], maxCellValue = 0, пик null (не ошибка).
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 FORBIDDEN (INV-STA-1).
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_QUERY_UNAVAILABLE (retry с backoff).

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

  • Новые optional поля — additive; удаление/переименование/смена wire-enum/TimeZone-семантики — breaking.
  • Logs: dev-stats-activity-heatmap (developerId, диапазон, timeZone, cellsCount, elapsed; PII нет). Metrics: dev_stats_query_requests_total{method="activity-heatmap"}, latency histogram (SLO p95 ≤ 600 мс, сырые факты — ../../README.md). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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