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

GET /api/v1/dev-stats/session-depth — Глубина сессии (объектов за сессию)

Назначение

Показывает застройщику глубину сессии — сколько разных объектов (объектов недвижимости) застройщика в среднем просматривают за одну пользовательскую сессию, плюс медиану/перцентиль/максимум и распределение по корзинам глубины. Высокая глубина = аудитория «ходит» по проекту; глубина = 1 = единичные точечные заходы. Источник данных — факты просмотров property_object_view_facts: сессия связывается с застройщиком через свои просмотры его объектов недвижимости (SessionId ∈ property_object_view_facts WHERE DeveloperId = актор, см. session-facts.md), поэтому отдельная привязка сессии к застройщику не нужна. Общий вход — DevStatsFilterModel, обязателен DeveloperId = актор.

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature groupattention-analytics
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-session-depth.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 при подстановке чужого id)
Auditnone (read-only агрегаты; экспорт — отдельный dev-stats.export)

HTTP-контракт

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

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иной id → 403)Tenant isolation (fail-closed)
DateFromquery dateFromDateOnlyДаISO yyyy-MM-dd; DateFrom <= DateTo; диапазон ≤ 366 дней (guard скана)
DateToquery dateToDateOnlyДаISO yyyy-MM-dd; ≤ сегодня (UTC)
ProjectIdquery projectIdGuid?Нетсужение по иерархии (префикс ключа)
BuildingIdquery buildingIdGuid?Нетсужение
LayoutGroupIdquery layoutGroupIdGuid?Нетсужение
LayoutIdquery layoutIdGuid?Нетсужение
PropertyObjectIdquery propertyObjectIdGuid?Нетсужение
ViewerKindquery viewerKindViewerKind? (enum)НетGuest/Agent/Investor; unknown → ValidationError
ViewSourcequery viewSourceViewSource? (enum)НетCatalog/Search/…; unknown → ValidationError
GroupByquery groupBySessionDepthGroupBy? (enum)Нетnone (default) / viewerKind; unknown → ValidationError

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho акторЗастройщик.
from / tostring (ISO date)ДаechoГраницы диапазона.
sessionsCountlongДаcount() над per-sessionЧисло сессий, коснувшихся объектов недвижимости застройщика.
avgPropertyObjectsPerSessiondecimalДаavg(objects)Средняя глубина сессии (2 знака).
medianPropertyObjectsPerSessiondecimalДаquantile(0.5)(objects)Медианная глубина.
p90PropertyObjectsPerSessiondecimalДаquantile(0.9)(objects)90-й перцентиль глубины.
maxPropertyObjectsPerSessionintДаmax(objects)Максимум объектов за сессию.
distributionIReadOnlyList<SessionDepthBucketModel>Да (может быть пуст)гистограммаРаспределение сессий по корзинам глубины.
byViewerKindIReadOnlyList<SessionDepthByViewerKindModel>?Нетпри GroupBy=viewerKindРазбивка глубины по типу зрителя.

SessionDepthBucketModel

ПолеТипОписание
bucketstringКорзина: 1 / 2 / 3-5 / 6-10 / 11+.
sessionslongЧисло сессий в корзине.
sharedecimalДоля сессий (0..1, 3 знака).

SessionDepthByViewerKindModel

ПолеТипОписание
viewerKindstring (enum wire)Guest/Agent/Investor.
sessionsCountlongСессии данного типа.
avgPropertyObjectsPerSessiondecimalСредняя глубина по типу.
medianPropertyObjectsPerSessiondecimalМедиана по типу.

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

  • Пользователь застройщика авторизован; есть 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 (событий нет).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора (не из query); если клиент прислал DeveloperId ≠ актор → отказ FORBIDDEN (403).
  2. Проверка входа. Обязательные DateFrom/DateTo, DateFrom <= DateTo, диапазон ≤ 366 дней; enum ViewerKind/ViewSource/GroupBy — из допустимого множества (неизвестное → ValidationError, молчаливый дефолт запрещён). Существование иерархии в chessboard не проверяется: неизвестный ProjectId → пустой результат.
  3. Что читаем. Rollup property_object_view_daily непригоден (агрегирует SessionId прочь), поэтому читаем сырьё property_object_view_facts за период по scope: DeveloperId — головной компонент ключа (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, ViewedAt, FactId) → префикс-скан; опциональные уровни иерархии удлиняют префикс; партиции toYYYYMM(ViewedAt) режут диапазон дат.
  4. Расчёты и маппинг (dedup-tolerant, без FINAL). Вложенная агрегация по сессии: GROUP BY SessionIdobjects = 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.
  5. Результат. Возвращается SessionDepthResult (200). Пустой диапазон (нет сессий) — не ошибка: нули + пустой distribution. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

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

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

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

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