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 = актор;
список объектов пагинируется постранично.
| Поле | Значение |
|---|
| Сервис/контекст | dev-stats |
| API area | developers |
| Feature group | attention-analytics |
| Статус | draft |
| Документ | docs/06-services/dev-stats/api/developers/get-bounce-signal.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) |
| Audit | none (read-only агрегаты; персональные данные зрителей не раскрываются) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/dev-stats/bounce-signal |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md) +
PagingQuery для списка объектов.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
DeveloperId | identity claims | Guid | Да | != Guid.Empty; = актор (иной → 403) | Tenant isolation |
DateFrom | query dateFrom | DateOnly | Да | ISO; from <= to; диапазон ≤ 366 дней | — |
DateTo | query dateTo | DateOnly | Да | ISO; ≤ сегодня (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? | Нет | Guest/Agent/Investor; unknown → ValidationError | — |
ViewSource | query viewSource | ViewSource? | Нет | Catalog/Search/…; unknown → ValidationError | — |
ThresholdSeconds | query thresholdSeconds | int? | Нет | default 10; диапазон 1..120 (порог «короткого» просмотра) | — |
MinViews | query minViews | int? | Нет | default 20; ≥ 1; минимальная выборка для попадания объекта недвижимости в ранжирование (сглаживание шума) | — |
ExcludeUnmeasured | query excludeUnmeasured | bool? | Нет | default false; при true исключает просмотры без измеренной длительности (только open-факт, dur = 0) | — |
Paging | query page/pageSize | PagingQuery | Нет | default page=1, pageSize=50 (≤ 200); порядок — bounceRate desc, затем totalViews desc | — |
Модель ответа BounceSignalResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
developerId | Guid | Да | echo актор | — | Застройщик. |
from / to | string (ISO date) | Да | echo | — | Границы диапазона. |
thresholdSeconds | int | Да | echo (после дефолта) | — | Порог «короткого» просмотра. |
minViews | int | Да | echo | — | Минимальная выборка для ранжирования. |
overall | BounceOverallModel | Да | агрегация | — | Итог по всем объектам фильтра. |
propertyObjects | постранично (PropertyObjectBounceModel) | Да | пагинированная выборка | — | Объекты по убыванию bounceRate (сильнейший сигнал сверху). |
BounceOverallModel
| Поле | Тип | Описание |
|---|
totalViews | long | Всего просмотров (по PropertyObjectViewId). |
bounceViews | long | Просмотры короче thresholdSeconds. |
bounceRate | decimal | bounceViews / totalViews (0..1, 3 знака); 0, если totalViews = 0. |
avgDurationSeconds | decimal | Средняя разрешённая длительность просмотра. |
PropertyObjectBounceModel
| Поле | Тип | Описание |
|---|
propertyObjectId | Guid | Объект. |
projectId / buildingId | Guid | Иерархия (из факта). |
totalViews | long | Просмотры объекта недвижимости (≥ minViews). |
bounceViews | long | Короткие просмотры объекта недвижимости. |
bounceRate | decimal | Доля отказов объекта недвижимости (0..1). |
avgDurationSeconds | decimal | Средняя длительность просмотра объекта недвижимости. |
Предусловия и согласованность данных
- Пользователь застройщика авторизован; есть 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).
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом
dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал
DeveloperId ≠ актор → отказ FORBIDDEN (403).
- Проверка входа. Обязательные
DateFrom/DateTo, from <= to, диапазон ≤ 366 дней;
ThresholdSeconds ∈ 1..120, MinViews >= 1, PageSize <= 200; enum ViewerKind/ViewSource — из
допустимого множества (неизвестное → ValidationError).
- Что читаем. Rollup
property_object_view_daily непригоден (хранит avg/sum, а не распределение
длительностей), поэтому читаем сырьё property_object_view_facts за период по scope: DeveloperId —
головной компонент ключа → префикс-скан; опциональная иерархия удлиняет префикс; партиции
toYYYYMM(ViewedAt) режут диапазон дат.
- Расчёты и маппинг (dedup-tolerant, без
FINAL). Внутренний слой — разрешение длительности на
просмотр: GROUP BY PropertyObjectViewId → dur = argMax(DurationSeconds, IngestionTime) (closed-строка
property-object-view-ended побеждает open с dur = 0; at-least-once дубли схлопываются), плюс argMax
по PropertyObjectId/ProjectId/BuildingId. ExcludeUnmeasured=true → HAVING 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-запрос.
- Результат. Возвращается
BounceSignalResult (200): overall + постраничный список объектов по
убыванию bounceRate. Нет просмотров / ни один объект не набрал MinViews — не ошибка: нулевой
overall + пустая страница. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Нет dateFrom/dateTo, from > to, диапазон > 366 дней, threshold вне 1..120, 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 |
Отсутствие данных / объектов недвижимости выше 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы