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 = актор.
| Поле | Значение |
|---|
| Сервис/контекст | dev-stats |
| API area | developers |
| Feature group | attention-analytics |
| Статус | draft |
| Документ | docs/06-services/dev-stats/api/developers/get-activity-heatmap.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (застройщик — роль developer, Bearer JWT) |
| Auth policy / Permissions | permission dev-stats.view (identity/domain/permission.md) |
| Scope (RBAC) | организация застройщика; scope строго = свой DeveloperId |
| Record-level | не применяется |
| Tenant isolation | DeveloperId = актор из identity-claims фильтрует все запросы (fail-closed) |
| Audit | none (read-only агрегаты) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/dev-stats/activity-heatmap |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md).
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
DeveloperId | identity claims | Guid | Да | != Guid.Empty; = актор (иной → 403) | Tenant isolation |
DateFrom | query dateFrom | DateOnly | Да | ISO; DateFrom <= DateTo; диапазон ≤ 366 дней | — |
DateTo | query dateTo | DateOnly | Да | ISO; ≤ сегодня (UTC) | — |
ProjectId | query projectId | Guid? | Нет | сужение (префикс ключа) | — |
BuildingId | query buildingId | Guid? | Нет | сужение | — |
LayoutGroupId | query layoutGroupId | Guid? | Нет | сужение | — |
LayoutId | query layoutId | Guid? | Нет | сужение | — |
PropertyObjectId | query propertyObjectId | Guid? | Нет | сужение | — |
ViewerKind | query viewerKind | ViewerKind? | Нет | Guest/Agent/Investor; unknown → ValidationError | — |
ViewSource | query viewSource | ViewSource? | Нет | Catalog/Search/…; unknown → ValidationError | — |
TimeZone | query timeZone | string? (IANA) | Нет | default Europe/Moscow; должна быть в списке IANA (toHour(ViewedAt, tz)); unknown → ValidationError | — |
Metric | query metric | HeatmapMetric? (enum) | Нет | views (default) / uniqViewers; определяет maxCellValue/пик | — |
TimeZone — IANA-строка; timezone-сущности в справочнике pin.handbook нет
(ADR-0063).
Модель ответа ActivityHeatmapResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
developerId | Guid | Да | echo актор | — | Застройщик. |
from / to | string (ISO date) | Да | echo | — | Границы диапазона. |
timeZone | string | Да | echo (после дефолта) | — | Применённая IANA-таймзона раскладки. |
metric | string (enum wire) | Да | echo | — | Ведущая мера (views/uniqViewers). |
totalViews | long | Да | sum(views) | — | Всего просмотров за период. |
maxCellValue | long | Да | max по ведущей мере | — | Максимум ячейки (для шкалы цвета). |
peakDayOfWeek | int? | Нет | ячейка-argMax | — | День недели пика (1–7), null при пустоте. |
peakHour | int? | Нет | ячейка-argMax | — | Час пика (0–23), null при пустоте. |
cells | IReadOnlyList<HeatmapCellModel> | Да (может быть пуст) | агрегация | — | Непустые ячейки; отсутствующая ячейка = 0. |
HeatmapCellModel
| Поле | Тип | Описание |
|---|
dayOfWeek | int | День недели 1=Пн … 7=Вс (toDayOfWeek). |
hour | int | Час 0–23 (toHour в TimeZone). |
views | long | Просмотры в ячейке (uniq(PropertyObjectViewId)). |
uniqViewers | long | Уникальные зрители в ячейке. |
Возвращаются только непустые ячейки (клиент дорисовывает нули до 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 (событий нет).
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом
dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал
DeveloperId ≠ актор → отказ FORBIDDEN (403).
- Проверка входа. Обязательные
DateFrom/DateTo, from <= to, диапазон ≤ 366 дней; TimeZone ∈
списке IANA (default Europe/Moscow); enum ViewerKind/ViewSource/Metric — из допустимого множества
(неизвестное → ValidationError). Существование иерархии не проверяется.
- Что читаем. Rollup
property_object_view_daily дневной и теряет час, поэтому heatmap считаем по
сырью property_object_view_facts за период по scope: DeveloperId — головной компонент ключа →
префикс-скан; опциональная иерархия удлиняет префикс; партиции toYYYYMM(ViewedAt) режут диапазон дат.
- Расчёты и маппинг (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.
- Результат. Возвращаются только непустые ячейки
ActivityHeatmapResult (200; нули клиент
дорисовывает). Пустой период — cells = [], maxCellValue = 0, пик null. Недоступность ClickHouse →
DEV_STATS_QUERY_UNAVAILABLE (503).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Нет dateFrom/dateTo, from > to, диапазон > 366 дней, unknown enum/TimeZone | Ошибка валидации запроса | no retry |
UNAUTHORIZED | 401 | Нет/невалиден JWT | Требуется авторизация | no retry |
FORBIDDEN | 403 | Нет dev-stats.view или DeveloperId != актор | Доступ запрещён | no retry |
DEV_STATS_QUERY_UNAVAILABLE | 503 | ClickHouse недоступен / 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы