GET /api/v1/dev-stats/session-depth — Глубина сессии (объектов за сессию)
Назначение
Показывает застройщику глубину сессии — сколько разных объектов (объектов недвижимости) застройщика в среднем
просматривают за одну пользовательскую сессию, плюс медиану/перцентиль/максимум и распределение по
корзинам глубины. Высокая глубина = аудитория «ходит» по проекту; глубина = 1 = единичные точечные заходы.
Источник данных — факты просмотров property_object_view_facts: сессия связывается с
застройщиком через свои просмотры его объектов недвижимости (SessionId ∈ property_object_view_facts WHERE DeveloperId = актор, см.
session-facts.md), поэтому отдельная привязка сессии к застройщику не нужна.
Общий вход — DevStatsFilterModel, обязателен DeveloperId = актор.
| Поле | Значение |
|---|
| Сервис/контекст | dev-stats |
| API area | developers |
| Feature group | attention-analytics |
| Статус | draft |
| Документ | docs/06-services/dev-stats/api/developers/get-session-depth.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 при подстановке чужого id) |
| Audit | none (read-only агрегаты; экспорт — отдельный dev-stats.export) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/dev-stats/session-depth |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md).
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
DeveloperId | identity claims | Guid | Да | != Guid.Empty; = актор (иной id → 403) | Tenant isolation (fail-closed) |
DateFrom | query dateFrom | DateOnly | Да | ISO yyyy-MM-dd; DateFrom <= DateTo; диапазон ≤ 366 дней (guard скана) | — |
DateTo | query dateTo | DateOnly | Да | ISO yyyy-MM-dd; ≤ сегодня (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? (enum) | Нет | Guest/Agent/Investor; unknown → ValidationError | — |
ViewSource | query viewSource | ViewSource? (enum) | Нет | Catalog/Search/…; unknown → ValidationError | — |
GroupBy | query groupBy | SessionDepthGroupBy? (enum) | Нет | none (default) / viewerKind; unknown → ValidationError | — |
Модель ответа SessionDepthResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
developerId | Guid | Да | echo актор | — | Застройщик. |
from / to | string (ISO date) | Да | echo | — | Границы диапазона. |
sessionsCount | long | Да | count() над per-session | — | Число сессий, коснувшихся объектов недвижимости застройщика. |
avgPropertyObjectsPerSession | decimal | Да | avg(objects) | — | Средняя глубина сессии (2 знака). |
medianPropertyObjectsPerSession | decimal | Да | quantile(0.5)(objects) | — | Медианная глубина. |
p90PropertyObjectsPerSession | decimal | Да | quantile(0.9)(objects) | — | 90-й перцентиль глубины. |
maxPropertyObjectsPerSession | int | Да | max(objects) | — | Максимум объектов за сессию. |
distribution | IReadOnlyList<SessionDepthBucketModel> | Да (может быть пуст) | гистограмма | — | Распределение сессий по корзинам глубины. |
byViewerKind | IReadOnlyList<SessionDepthByViewerKindModel>? | Нет | при GroupBy=viewerKind | — | Разбивка глубины по типу зрителя. |
SessionDepthBucketModel
| Поле | Тип | Описание |
|---|
bucket | string | Корзина: 1 / 2 / 3-5 / 6-10 / 11+. |
sessions | long | Число сессий в корзине. |
share | decimal | Доля сессий (0..1, 3 знака). |
SessionDepthByViewerKindModel
| Поле | Тип | Описание |
|---|
viewerKind | string (enum wire) | Guest/Agent/Investor. |
sessionsCount | long | Сессии данного типа. |
avgPropertyObjectsPerSession | decimal | Средняя глубина по типу. |
medianPropertyObjectsPerSession | decimal | Медиана по типу. |
Предусловия и согласованность данных
- Пользователь застройщика авторизован; есть permission
dev-stats.view; scope DeveloperId = актор
(INV-STA-1, fail-closed — ../../README.md).
- Сессия связывается с застройщиком через свои просмотры его объектов (
SessionId ∈ property_object_view_facts WHERE DeveloperId = актор), отдельной привязки сессии к застройщику нет.
Глубина учитывает только объекты недвижимости этого застройщика; объекты других застройщиков в той же
сессии в счёт не идут.
- Метод читает сырьё
property_object_view_facts (rollup агрегирует SessionId прочь), окно ≤ 366 дней
(cost-guard). Глубина на сессию — uniq(PropertyObjectId) (INV-STA-3): open/closed одного просмотра и
at-least-once дубли по одному PropertyObjectId считаются один раз.
- Окно свежести ≤ 5 мин (INV-STA-5): незавершённые/только что закрытые сессии могут дообогащаться, число
монотонно догоняет. Операция read-only (событий нет).
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом
dev-stats.view. DeveloperId берётся из identity-claims актора (не из query); если клиент прислал
DeveloperId ≠ актор → отказ FORBIDDEN (403).
- Проверка входа. Обязательные
DateFrom/DateTo, DateFrom <= DateTo, диапазон ≤ 366 дней; enum
ViewerKind/ViewSource/GroupBy — из допустимого множества (неизвестное → ValidationError,
молчаливый дефолт запрещён). Существование иерархии в chessboard не проверяется: неизвестный
ProjectId → пустой результат.
- Что читаем. Rollup
property_object_view_daily непригоден (агрегирует SessionId прочь), поэтому
читаем сырьё property_object_view_facts за период по scope: DeveloperId — головной компонент ключа
(DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, ViewedAt, FactId) →
префикс-скан; опциональные уровни иерархии удлиняют префикс; партиции toYYYYMM(ViewedAt) режут диапазон
дат.
- Расчёты и маппинг (dedup-tolerant, без
FINAL). Вложенная агрегация по сессии: GROUP BY SessionId
→ objects = uniq(PropertyObjectId) (схлопывает open/closed-строки одного просмотра по общему
PropertyObjectId и at-least-once дубли — каждый объект в сессии считается один раз),
viewerKind = anyLast(ViewerKind). Внешний слой: avg(objects), quantile(0.5), quantile(0.9),
max(objects), count() (= число сессий). Гистограмма — второй проход:
bucket = multiIf(objects=1,'1', objects=2,'2', objects<=5,'3-5', objects<=10,'6-10','11+'),
count() GROUP BY bucket, share = sessions / Σ sessions. При GroupBy=viewerKind внешние агрегаты
берутся GROUP BY viewerKind.
- Результат. Возвращается
SessionDepthResult (200). Пустой диапазон (нет сессий) — не ошибка: нули +
пустой distribution. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Нет dateFrom/dateTo, from > to, диапазон > 366 дней, unknown enum | Ошибка валидации запроса | 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 с нулевыми агрегатами.
Acceptance Criteria
- Глубина + распределение. Дано авторизованный застройщик и корректный период. Когда
GET /session-depth. Тогда 200 с avg/median/p90/maxPropertyObjectsPerSession, sessionsCount и
distribution по корзинам 1/2/3-5/6-10/11+ (сумма share ≈ 1).
- Дедуп внутри сессии. Дано в одной сессии один объект открывался 3 раза (3 просмотра). Когда считается
глубина. Тогда объект учтён как
1 (uniq(PropertyObjectId), INV-STA-3), а не 3.
- Разбивка по типу зрителя. Дано
groupBy=viewerKind. Когда запрос. Тогда byViewerKind заполнен
(глубина по Guest/Agent/Investor); при groupBy=none — byViewerKind = null.
- Только чужие объекты в сессии. Дано в сессии смотрели лишь объекты другого застройщика. Когда запрос.
Тогда сессия не попадает в
sessionsCount этого застройщика (scope-фильтр, INV-STA-1).
- Пустой период. Дано нет сессий. Когда запрос. Тогда
200, нулевые агрегаты, пустой distribution
(не ошибка).
- Чужой scope / нет права. Дано чужой
DeveloperId или нет dev-stats.view. Когда запрос. Тогда
403 FORBIDDEN (INV-STA-1).
- Неизвестный enum / окно > 366 дней. Дано
viewerKind=vip/groupBy=xxx либо окно шире лимита. Когда
запрос. Тогда 400 ValidationError.
- Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда
503 DEV_STATS_QUERY_UNAVAILABLE (retry с backoff).
Совместимость и наблюдаемость
- Новые optional поля ответа — additive; удаление/переименование/смена wire-enum — breaking (версия метода).
- Logs:
dev-stats-session-depth (developerId, диапазон, sessionsCount, elapsed; PII нет). Metrics:
dev_stats_query_requests_total{method="session-depth"}, latency histogram (SLO p95 ≤ 600 мс — сырые
факты, ../../README.md). Traces: span приёма → span обработки → span чтения ClickHouse;
correlation — сквозной trace traceparent.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы