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

GET /api/v1/dev-stats/bounce-signal — Сигнал отказа (короткие просмотры по объекту)

Назначение

Доля коротких просмотров (< N секунд) в разрезе объектов застройщика — «сигнал отказа» (bounce): высокая доля отказов при заметном трафике = карточка/цена объекта недвижимости отталкивает (открыли и сразу ушли). Даёт застройщику ранжированный список объектов-кандидатов на ревизию медиа/описания/цены. Источник — факты property_object_view_facts; длительность каждого просмотра разрешается dedup-устойчиво (argMax(DurationSeconds, IngestionTime) по PropertyObjectViewId — closed-строка property-object-view-ended побеждает open). Метод read-only над ClickHouse; общий вход — DevStatsFilterModel, обязателен DeveloperId = актор; список объектов пагинируется постранично.

Metadata

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

HTTP-контракт

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

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

Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md) + PagingQuery для списка объектов.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иной → 403)Tenant isolation
DateFromquery dateFromDateOnlyДаISO; from <= to; диапазон ≤ 366 дней
DateToquery dateToDateOnlyДаISO; ≤ сегодня (UTC)
ProjectIdquery projectIdGuid?Нетсужение (префикс ключа)
BuildingIdquery buildingIdGuid?Нетсужение
LayoutGroupIdquery layoutGroupIdGuid?Нетсужение
LayoutIdquery layoutIdGuid?Нетсужение
PropertyObjectIdquery propertyObjectIdGuid?Нетсужение (детализация по одному объекту)
ViewerKindquery viewerKindViewerKind?НетGuest/Agent/Investor; unknown → ValidationError
ViewSourcequery viewSourceViewSource?НетCatalog/Search/…; unknown → ValidationError
ThresholdSecondsquery thresholdSecondsint?Нетdefault 10; диапазон 1..120 (порог «короткого» просмотра)
MinViewsquery minViewsint?Нетdefault 20; ≥ 1; минимальная выборка для попадания объекта недвижимости в ранжирование (сглаживание шума)
ExcludeUnmeasuredquery excludeUnmeasuredbool?Нетdefault false; при true исключает просмотры без измеренной длительности (только open-факт, dur = 0)
Pagingquery page/pageSizePagingQueryНетdefault page=1, pageSize=50 (≤ 200); порядок — bounceRate desc, затем totalViews desc

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho акторЗастройщик.
from / tostring (ISO date)ДаechoГраницы диапазона.
thresholdSecondsintДаecho (после дефолта)Порог «короткого» просмотра.
minViewsintДаechoМинимальная выборка для ранжирования.
overallBounceOverallModelДаагрегацияИтог по всем объектам фильтра.
propertyObjectsпостранично (PropertyObjectBounceModel)Дапагинированная выборкаОбъекты по убыванию bounceRate (сильнейший сигнал сверху).

BounceOverallModel

ПолеТипОписание
totalViewslongВсего просмотров (по PropertyObjectViewId).
bounceViewslongПросмотры короче thresholdSeconds.
bounceRatedecimalbounceViews / totalViews (0..1, 3 знака); 0, если totalViews = 0.
avgDurationSecondsdecimalСредняя разрешённая длительность просмотра.

PropertyObjectBounceModel

ПолеТипОписание
propertyObjectIdGuidОбъект.
projectId / buildingIdGuidИерархия (из факта).
totalViewslongПросмотры объекта недвижимости (≥ minViews).
bounceViewslongКороткие просмотры объекта недвижимости.
bounceRatedecimalДоля отказов объекта недвижимости (0..1).
avgDurationSecondsdecimalСредняя длительность просмотра объекта недвижимости.

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

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md).
  • Метод читает сырьё property_object_view_facts (rollup хранит avg/sum, а не распределение), окно ≤ 366 дней (cost-guard). Длительность на просмотр разрешается dedup-устойчиво: closed побеждает open по IngestionTime (INV-STA-3); просмотр только с open-фактом даёт dur=0 и считается коротким (bounce), если не задан excludeUnmeasured=true.
  • minViews сглаживает шум: при крошечной выборке bounceRate статистически неустойчив, поэтому объекты с totalViews < minViews в ранжирование не попадают.
  • Окно свежести ≤ 5 мин (INV-STA-5): свежий короткий просмотр может ещё не быть учтён. Операция read-only (событий нет), персональные ключи наружу не выходят (INV-STA-7).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал DeveloperId ≠ актор → отказ FORBIDDEN (403).
  2. Проверка входа. Обязательные DateFrom/DateTo, from <= to, диапазон ≤ 366 дней; ThresholdSeconds ∈ 1..120, MinViews >= 1, PageSize <= 200; enum ViewerKind/ViewSource — из допустимого множества (неизвестное → ValidationError).
  3. Что читаем. Rollup property_object_view_daily непригоден (хранит avg/sum, а не распределение длительностей), поэтому читаем сырьё property_object_view_facts за период по scope: DeveloperId — головной компонент ключа → префикс-скан; опциональная иерархия удлиняет префикс; партиции toYYYYMM(ViewedAt) режут диапазон дат.
  4. Расчёты и маппинг (dedup-tolerant, без FINAL). Внутренний слой — разрешение длительности на просмотр: GROUP BY PropertyObjectViewIddur = argMax(DurationSeconds, IngestionTime) (closed-строка property-object-view-ended побеждает open с dur = 0; at-least-once дубли схлопываются), плюс argMax по PropertyObjectId/ProjectId/BuildingId. ExcludeUnmeasured=trueHAVING dur > 0 (просмотры только с open-фактом отбрасываются — см. Assumptions). Внешний слой по объекту: totalViews = count(), bounceViews = countIf(dur < ThresholdSeconds), bounceRate = bounceViews / totalViews, avgDurationSeconds = avg(dur), GROUP BY PropertyObjectId HAVING totalViews >= MinViews ORDER BY bounceRate DESC, totalViews DESC LIMIT PageSize OFFSET Offset. overall — те же агрегаты без GROUP BY PropertyObjectId; общее число объектов после HAVING — отдельный count-запрос.
  5. Результат. Возвращается BounceSignalResult (200): overall + постраничный список объектов по убыванию bounceRate. Нет просмотров / ни один объект не набрал MinViews — не ошибка: нулевой overall + пустая страница. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

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

Отсутствие данных / объектов недвижимости выше MinViewsне ошибка: 200 с нулевым overall и пустой страницей.

Acceptance Criteria

  • Ранжирование по сигналу. Дано авторизованный застройщик, период и объекты с трафиком. Когда GET /bounce-signal. Тогда 200 с overall и постраничным propertyObjects по убыванию bounceRate (сильнейший сигнал сверху), тай-брейк — totalViews desc.
  • Порог короткого просмотра. Дано thresholdSeconds=30. Когда запрос. Тогда bounceViews считает просмотры короче 30 с; bounceRate = bounceViews / totalViews (3 знака).
  • Сглаживание шумных объектов. Дано объект с totalViews=5 при minViews=20. Когда запрос. Тогда объект в ранжирование не попадает (устойчивость bounceRate).
  • Only-open bounce. Дано просмотр только с open-фактом (dur=0). Когда excludeUnmeasured=false (default). Тогда он считается коротким; при excludeUnmeasured=true — исключается (HAVING dur > 0).
  • Дедуп длительности. Дано open(0)+closed(8) одного просмотра. Когда разрешается длительность. Тогда dur=8 (INV-STA-3), просмотр учтён один раз.
  • Порог вне диапазона. Дано thresholdSeconds=0 или > 120, либо pageSize > 200. Когда запрос. Тогда 400 ValidationError.
  • Пустой сигнал. Дано нет просмотров или ни один объект не набрал minViews. Когда запрос. Тогда 200 с нулевым overall и пустой страницей (не ошибка).
  • Ключи зрителей не раскрываются. Дано любой ответ/лог. Когда клиент читает. Тогда только счётчики/доли, без UserId/AnonymousId (INV-STA-7).
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 FORBIDDEN (INV-STA-1).
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_QUERY_UNAVAILABLE (retry с backoff).

Assumptions

  • Просмотр только с open-фактом (action-tracked ViewPropertyObject, closed так и не пришёл — прямой заход/брошенная вкладка) разрешается в dur = 0 и считается коротким (bounce). Для «чистого» сигнала измеренных просмотров — ExcludeUnmeasured=true (proxy «измерено» = dur > 0). Оба варианта задокументированы, чтобы не путать нуль-смещение длительности.
  • MinViews отсекает шум на объектах с крошечной выборкой — при малом трафике bounceRate статистически неустойчив.

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

  • Новые optional поля — additive; удаление/переименование/смена дефолтов threshold/minViews-семантики — breaking (версия метода).
  • Logs: dev-stats-bounce-signal (developerId, диапазон, threshold, propertyObjectsReturned, elapsed; PII нет). Metrics: dev_stats_query_requests_total{method="bounce-signal"}, latency histogram (SLO p95 ≤ 600 мс, сырые факты — ../../README.md). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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