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

GET /api/v1/dev-stats/duration/histogram — Гистограмма длительностей просмотра

Назначение

Распределение просмотров объектов застройщика по длительности — «как долго» смотрят, в виде бинов (0–10 с / 10–30 с / 30–60 с / 1–3 мин / 3–5 мин / 5–10 мин / 10 мин+) с числом и долей просмотров в каждом, плюс средняя и медианная длительность. Показывает глубину интереса (беглый скан vs вдумчивое изучение). В отличие от сводок, гистограмме нужно распределение сырых длительностей, которого нет в rollup-мерах (там только сумма/среднее) — поэтому метод читает сырьё property_object_view_facts, склеивая open/closed-строки одного просмотра по PropertyObjectViewId (argMax по IngestionTime).

Metadata

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

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

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

HTTP-контракт

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

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

Общий вход — DevStatsFilterModel. DeveloperId — из identity-claims актора. Границы бинов каноничны (фиксированы), опционально переопределяются возрастающим массивом секунд.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иначе 403)Tenant isolation, fail-closed
DateFromquery dateFromDateOnlyДаISO yyyy-MM-dd
DateToquery dateToDateOnlyДаISO; DateFrom <= DateTo; окно ≤ 366 дней (сырьё — cost-guard)
ProjectIdPropertyObjectIdqueryGuid?Нетуровни иерархии (сверху вниз)
ViewerKindquery viewerKindViewerKind?Нетwire guest/agent/investor; unknown → ValidationError
ViewSourcequery viewSourceViewSource?Нетwire значения ViewSource
BinEdgesSecondsquery binEdgesIReadOnlyList<int>?Нетстрого возрастающие, ≥ 0, ≤ 12 границ; default [0,10,30,60,180,300,600]

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho актораЗастройщик (= scope).
dateFrom / dateTostring (ISO date)ДаechoПрименённый период.
totalViewslongДаcount() схлопнутых PropertyObjectViewIdВсего просмотров в выборке (дедуп по PropertyObjectViewId).
avgDurationSecondsintДаsum(dur) / count() (guard 0)Средняя длительность, сек.
medianDurationSecondsintДаquantileExact(0.5)(dur)Медианная длительность, сек.
binsIReadOnlyList<DurationBinModel>ДаGROUP BY bucketагрегатБины по возрастанию границы.

DurationBinModel

ПолеТипИсточникОписание
fromSecondsintнижняя граница бина (roundDown)Включительно.
toSecondsint?следующая границаИсключительно; null — последний открытый бин (fromSeconds+).
labelstringвычисленоЧеловекочитаемая метка (напр. 1–3 мин, 10 мин+).
viewslongcount() в бинеЧисло просмотров.
sharedecimalviews / totalViews (guard 0)Доля бина, 0..1 (2 знака).

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

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md).
  • Метод читает сырьё property_object_view_facts (в rollup распределения длительностей нет), поэтому окно ограничено 366 днями (cost-guard).
  • Длительность просмотра разрешается dedup-устойчиво: closed-строка (property-object-view-ended, реальная длительность) побеждает open-строку (dur=0) по IngestionTime; порядок доставки не важен, at-least-once дубли схлопываются по PropertyObjectViewId (INV-STA-3). Просмотр с одним лишь open-фактом даёт dur=0 и попадает в первый бин.
  • Окно свежести событие→факт ≤ 5 мин (INV-STA-5); операция read-only (состояние не меняется, событий нет).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал DeveloperId ≠ актор → отказ DEV_STATS_FORBIDDEN (403).
  2. Проверка входа. Период задан и корректен, окно ≤ 366 дней (чтение сырья дороже rollup); BinEdgesSeconds строго возрастает, ≥ 0, ≤ 12 значений; enum ViewerKind/ViewSource — из допустимого множества (неизвестное → ValidationError); иерархия «сверху вниз».
  3. Что читаем. Гистограмме нужно распределение сырых длительностей, которого нет в rollup-мерах (там только сумма/среднее), поэтому читаем факты property_object_view_facts в два уровня, dedup-tolerant без FINAL:
    • схлопывание просмотра: GROUP BY PropertyObjectViewIdargMax(DurationSeconds, IngestionTime) — closed-строка (позже по IngestionTime, реальная длительность) побеждает open-строку (0); один просмотр = одна длительность;
    • бинирование: roundDown(dur, [binEdges]), GROUP BY binLow ORDER BY binLow, count(); параллельно count(), sum(dur), quantileExact(0.5)(dur) для total/avg/median. Заданные уровни иерархии — префикс-скан по ключу (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, ViewedAt, FactId); диапазон дат режет партиции toYYYYMM(ViewedAt).
  4. Расчёты и маппинг. Для каждой границы binEdges[i] формируется DurationBinModel: fromSeconds = binEdges[i], toSeconds = binEdges[i+1] (или null для последнего), label человекочитаемая (сек/мин), views из группы (0, если бин пуст), share = views / totalViews. avgDurationSeconds = sum(dur) / count (guard 0), medianDurationSeconds = quantileExact(0.5). Пустая выборка → totalViews = 0, бины с нулями (стабильная форма ответа).
  5. Результат. Возвращается DurationHistogramResult (200). SLO чтения сырья — p95 ≤ 600 мс. Недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нет/некорректный период, окно > 366 дней, невалидные binEdges, неизвестный enum, иерархия «снизу вверх»Ошибка валидации запросаno retry
UNAUTHORIZED401Нет JWTТребуется авторизацияno retry
DEV_STATS_FORBIDDEN403Нет dev-stats.view или DeveloperId ≠ акторДоступ к статистике застройщика запрещёнno retry
DEV_STATS_UNAVAILABLE503ClickHouse недоступен / timeoutСтатистика временно недоступнаretry с backoff

Пустая выборка — не ошибка: 200 с totalViews = 0 и нулевыми бинами. SLO чтения сырья — p95 ≤ 600 мс (строже окно, чем у rollup-методов).

Acceptance Criteria

  • Распределение по дефолтным бинам. Дано авторизованный застройщик и корректный период. Когда GET /duration/histogram без binEdges. Тогда 200 с 7 бинами [0,10,30,60,180,300,600] (последний открытый 10 мин+), суммой share ≈ 1, а также avgDurationSeconds/medianDurationSeconds.
  • Схлопывание open/closed. Дано один просмотр имеет open-строку (dur=0) и closed-строку (dur=95). Когда строится гистограмма. Тогда просмотр учтён один раз с dur=95 (INV-STA-3), а не дважды.
  • Только open-факт. Дано просмотр без closed-строки. Когда гистограмма. Тогда dur=0, просмотр попадает в первый бин (0–10 с).
  • Кастомные границы. Дано binEdges=[0,30,120]. Когда запрос. Тогда 3 бина (0–30, 30–120, 120+).
  • Невалидные границы. Дано binEdges=[30,10] (не по возрастанию) либо > 12 значений. Когда запрос. Тогда 400 ValidationError.
  • Пустая выборка. Дано период без просмотров. Когда запрос. Тогда 200, totalViews = 0, бины с нулями (стабильная форма ответа).
  • Guard доли. Дано totalViews = 0. Когда считаются share/avg. Тогда = 0 без ошибки.
  • Слишком длинное окно. Дано окно > 366 дней. Когда запрос. Тогда 400 ValidationError (cost-guard сырья).
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 DEV_STATS_FORBIDDEN (INV-STA-1).
  • Неизвестный enum. Дано viewerKind=vip. Когда запрос. Тогда 400 ValidationError.
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_UNAVAILABLE (retry с backoff).

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

  • Optional-поля — additive; смена дефолтных binEdges — семантически breaking для клиента-визуализатора (согласовывать). Unknown enum → 400.
  • Logs: dev-stats-duration-histogram-read (developerId, dateFrom/dateTo, totalViews, binsCount, elapsed; PII нет). Metrics: dev_stats_duration_histogram_requests_total, latency histogram (SLO p95 ≤ 600 мс — сырьё). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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