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

GET /api/v1/dev-stats/repeat-frequency — Повторные просмотры и частота возврата на объект

Назначение

Насколько часто зрители возвращаются к объектам застройщика — глубина повторного интереса. Считает по парам «зритель × объект» (coalesce(UserId, AnonymousId) × PropertyObjectId) число заходов (uniq(PropertyObjectViewId)) и строит распределение по бакетам частоты (1 / 2 / 3 / 4+ визита), плюс headline: доля возвращавшихся пар (repeatRate) и среднее число визитов на пару. Дополняет сводный repeatViews из get-views-summary детальным распределением, которого агрегатные меры rollup не дают, поэтому читает сырьё property_object_view_facts (дедуп заходов по PropertyObjectViewId).

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature grouprepeat
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-repeat-frequency.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 (агрегат-распределение; UserId/AnonymousId — только внутренний ключ группировки, наружу не выдаётся)

HTTP-контракт

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

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
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
VisitBucketEdgesquery visitBucketsIReadOnlyList<int>?Нетстрого возрастающие, ≥ 1, ≤ 8 границ; default [1,2,3,4] (4 → бакет 4+)

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho актораЗастройщик (= scope).
dateFrom / dateTostring (ISO date)ДаechoПрименённый период.
viewerObjectPairslongДаcount() пар (зритель × объект)агрегатВсего пар с ≥ 1 заходом.
repeatPairslongДаcountIf(visits > 1)агрегатПары с возвратом (визитов > 1).
repeatRatedecimalДаrepeatPairs / viewerObjectPairs (guard 0)Доля возвращавшихся пар, 0..1 (2 знака).
avgVisitsPerPairdecimalДаavg(visits)Среднее число заходов на пару (2 знака).
maxVisitsPerPairintДаmax(visits)Максимум заходов на один объект одним зрителем.
bucketsIReadOnlyList<RepeatBucketModel>ДаGROUP BY bucketагрегатРаспределение по числу визитов.

RepeatBucketModel

ПолеТипИсточникОписание
visitsFromintнижняя границаВключительно (напр. 1, 4).
visitsToint?следующая граница − 1null — последний открытый бакет (4+).
labelstringвычислено1 / 2 / 3 / 4+.
pairslongcount() в бакетеЧисло пар «зритель × объект».
sharedecimalpairs / viewerObjectPairs (guard 0)Доля бакета, 0..1 (2 знака).

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

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md).
  • Метод читает сырьё property_object_view_facts (агрегатные меры rollup не дают per-pair распределения), окно ≤ 366 дней (cost-guard).
  • Число заходов на пару «зритель × объект» дедуплено по PropertyObjectViewId (INV-STA-3): open+closed одного просмотра и at-least-once дубли считаются одним заходом.
  • Ключ зрителя (coalesce(UserId, AnonymousId)) используется только для группировки и наружу не выходит (INV-STA-7). Окно свежести ≤ 5 мин (INV-STA-5); операция read-only (событий нет).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал DeveloperId ≠ актор → отказ DEV_STATS_FORBIDDEN (403).
  2. Проверка входа. Период задан и корректен, окно ≤ 366 дней (чтение сырья); VisitBucketEdges строго возрастает, ≥ 1, ≤ 8; enum ViewerKind/ViewSource — из допустимого множества (неизвестное → ValidationError); иерархия «сверху вниз».
  3. Что читаем. Детальное распределение частоты, которого агрегатные меры rollup не дают — читаем факты property_object_view_facts в два уровня, dedup-tolerant без FINAL:
    • заходы на пару: GROUP BY viewerKey = coalesce(UserId, AnonymousId), PropertyObjectIduniq(PropertyObjectViewId) — заходы дедуплены по PropertyObjectViewId (open + closed одного просмотра = один заход; at-least-once-дубли не растят счётчик);
    • распределение над per-pair: bucket = roundDown(visits, [visitBucketEdges]), GROUP BY bucket ORDER BY bucket, count(); параллельно count(), countIf(visits > 1), avg(visits), max(visits) для headline. Заданные уровни иерархии — префикс-скан по ключу (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, ViewedAt, FactId) (группировка по PropertyObjectId попадает на компонент ключа); диапазон дат режет партиции toYYYYMM(ViewedAt).
  4. Расчёты и маппинг. Для каждой границы visitBucketEdges[i]RepeatBucketModel: visitsFrom = edges[i], visitsTo = edges[i+1] − 1 (или null для последнего открытого), label (4+ для открытого), pairs (0, если пусто), share = pairs / viewerObjectPairs. repeatRate = repeatPairs / viewerObjectPairs (guard 0, 2 знака); avgVisitsPerPair = avg(visits) (2 знака). Пустая выборка → viewerObjectPairs = 0, бакеты с нулями. Персональные ключи (viewerKey) используются только для группировки и в ответ не попадают (наружу — счётчики/доли).
  5. Результат. Возвращается RepeatFrequencyResult (200). SLO чтения сырья — p95 ≤ 600 мс. Недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

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

Пустая выборка — не ошибка: 200 с viewerObjectPairs = 0 и нулевыми бакетами. SLO чтения сырья — p95 ≤ 600 мс.

Acceptance Criteria

  • Распределение по частоте. Дано авторизованный застройщик и корректный период. Когда GET /repeat-frequency без visitBuckets. Тогда 200 с бакетами 1/2/3/4+, суммой share ≈ 1, repeatRate и avgVisitsPerPair.
  • Дедуп заходов. Дано зритель открыл объект один раз, но пришли open+closed и повторная доставка. Когда считаются визиты. Тогда пара учтена как 1 заход (INV-STA-3), а не 2–3.
  • Повторный визит. Дано зритель заходил на объект в 3 разных просмотра. Когда считается распределение. Тогда пара попадает в бакет 3, вносит вклад в repeatPairs и repeatRate.
  • Пустая выборка. Дано период без просмотров. Когда запрос. Тогда 200, viewerObjectPairs = 0, бакеты с нулями (не ошибка).
  • Guard доли. Дано viewerObjectPairs = 0. Когда считаются share/repeatRate. Тогда = 0 без ошибки.
  • Невалидные бакеты. Дано visitBuckets=[2,1] (не по возрастанию), < 1 или > 8 значений. Когда запрос. Тогда 400 ValidationError.
  • Ключи зрителей не раскрываются. Дано любой ответ. Когда клиент читает результат. Тогда в нём только счётчики/доли; UserId/AnonymousId отсутствуют (INV-STA-7), в логах их тоже нет.
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 DEV_STATS_FORBIDDEN (INV-STA-1).
  • Неизвестный enum / окно > 366 дней. Дано viewerKind=vip либо окно шире лимита. Когда запрос. Тогда 400 ValidationError.
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_UNAVAILABLE (retry с backoff).

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

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

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

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

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