GET /api/v1/dev-stats/time-to-first-view — Время до первого просмотра (TTFV)
Назначение
Время от публикации объекта недвижимости до его первого просмотра (time-to-first-view, TTFV) — насколько быстро
новая недвижимость застройщика привлекает внимание. Низкий TTFV = «горячий» объект; высокий/отсутствие
просмотра = «холодный» инвентарь, которому нужно продвижение. Публикация берётся из измерения
dim_property_object (PublishedAt = PropertyObjectEntity.CreatedAt), первый просмотр — min(ViewedAt)
из фактов property_object_view_facts. Метод отбирает объекты недвижимости застройщика, опубликованные
в окне [DateFrom, DateTo], и считает по ним агрегаты TTFV + пагинированный список; объекты недвижимости без единого
просмотра учитываются отдельно (propertyObjectsNotYetViewed). Метод read-only над ClickHouse; общий вход —
DevStatsFilterModel, обязателен DeveloperId = актор; список пагинируется постранично.
| Поле | Значение |
|---|
| Сервис/контекст | dev-stats |
| API area | developers |
| Feature group | attention-analytics |
| Статус | draft |
| Документ | docs/06-services/dev-stats/api/developers/get-time-to-first-view.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 фильтрует и dim_property_object, и property_object_view_facts (fail-closed) |
| Audit | none (read-only агрегаты) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/dev-stats/time-to-first-view |
| Success status | 200 |
| Idempotency header | не применяется (safe GET) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md) +
PagingQuery. DateFrom/DateTo здесь — окно публикации объекта недвижимости.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
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? | Нет | сужение (один объект) | — |
Unit | query unit | TtfvUnit? (enum) | Нет | hours (default) / days; единица ttfv; unknown → ValidationError | — |
Paging | query page/pageSize | PagingQuery | Нет | default page=1, pageSize=50 (≤ 200); порядок — ttfv asc (быстрее — выше), объекты недвижимости без просмотра — в конце | — |
ViewerKind/ViewSource в этой метрике не применяются: первый просмотр учитывается независимо от типа
зрителя/источника.
Модель ответа TimeToFirstViewResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
developerId | Guid | Да | echo актор | — | Застройщик. |
from / to | string (ISO date) | Да | echo | — | Окно публикации. |
unit | string (enum wire) | Да | echo (после дефолта) | — | Единица ttfv (hours/days). |
overall | TtfvOverallModel | Да | агрегация | — | Итог по опубликованным объектам недвижимости окна. |
propertyObjects | постранично (PropertyObjectTtfvModel) | Да | пагинированная выборка | — | Объекты недвижимости по возрастанию ttfv (без просмотра — в конце). |
TtfvOverallModel
| Поле | Тип | Описание |
|---|
propertyObjectsPublished | long | Объектов недвижимости опубликовано в окне (из dim_property_object). |
propertyObjectsViewed | long | Из них получили ≥ 1 просмотр. |
propertyObjectsNotYetViewed | long | Опубликованы, но ни разу не просмотрены. |
viewedShare | decimal | propertyObjectsViewed / propertyObjectsPublished (0..1, 3 знака). |
avgTtfv | decimal? | Средний TTFV просмотренных объектов недвижимости (в unit), null если propertyObjectsViewed = 0. |
medianTtfv | decimal? | Медиана TTFV (quantile(0.5)), null при пустоте. |
p90Ttfv | decimal? | 90-й перцентиль TTFV, null при пустоте. |
PropertyObjectTtfvModel
| Поле | Тип | Описание |
|---|
propertyObjectId | Guid | Объект. |
projectId / buildingId | Guid | Иерархия (из dim_property_object). |
publishedAt | string (ISO datetime) | Момент публикации. |
firstViewedAt | string? (ISO datetime) | Первый просмотр; null — ещё не просмотрен. |
ttfv | decimal? | Время до первого просмотра в unit; null — ещё не просмотрен. |
viewed | bool | Был ли хотя бы один просмотр. |
Предусловия и согласованность данных
- Пользователь застройщика авторизован; есть permission
dev-stats.view; scope DeveloperId = актор
(INV-STA-1, fail-closed — ../../README.md).
- Кандидаты окна — объекты недвижимости, чей
PublishedAt (= PropertyObjectEntity.CreatedAt)
реплицирован в dim_property_object из pin.chessboard.property-object-upserted.v1. Объект, у которого
PublishedAt ещё не долетел (гонка измерения, INV-STA-6), в окно не попадает и появится после
upsert/backfill измерения — пользователь в промежуточном состоянии видит меньше опубликованных объектов,
чем есть, это eventual consistency, не ошибка.
- Первый просмотр —
min(ViewedAt) из фактов; устойчив к дублям open/closed (несут ~одинаковый
ViewedAt, INV-STA-3). Окно свежести факта ≤ 5 мин (INV-STA-5): свежий первый просмотр может ещё не
учитываться, тогда объект временно числится непросмотренным.
ViewerKind/ViewSource не применяются: первый просмотр учитывается независимо от типа зрителя и
источника. Окно чтения ≤ 366 дней (cost-guard). Операция read-only (событий нет).
Алгоритм
- Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом
dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал
DeveloperId ≠ актор → отказ FORBIDDEN (403).
- Проверка входа. Обязательные
DateFrom/DateTo, from <= to, диапазон ≤ 366 дней; enum Unit — из
допустимого множества (неизвестное → ValidationError); PageSize <= 200.
- Что читаем. Rollup-ы непригодны (нужны публикация объекта и точный
min времени первого просмотра),
поэтому соединяем по PropertyObjectId два источника:
dim_property_object (кандидаты, опубликованные в окне): argMax(PublishedAt, UpsertedAt),
argMax(ProjectId, UpsertedAt), argMax(BuildingId, UpsertedAt) по scope,
GROUP BY PropertyObjectId HAVING PublishedAt ∈ [DateFrom, DateTo]. Измерение — ReplacingMergeTree,
ORDER BY (PropertyObjectId), без партиций (сотни тысяч строк) → недорогой полный скан
(argMax(*, UpsertedAt) — последняя версия, без FINAL);
property_object_view_facts (первый просмотр): min(ViewedAt) по scope, GROUP BY PropertyObjectId;
DeveloperId — головной компонент ключа → префикс-скан; партиции toYYYYMM(ViewedAt) режут диапазон.
- Расчёты и маппинг (dedup-tolerant, без
FINAL). LEFT JOIN кандидатов к первому просмотру по
PropertyObjectId; min(ViewedAt) устойчив к дублям (open/closed одного просмотра несут ~одинаковый
ViewedAt). ttfvSeconds = dateDiff('second', PublishedAt, firstViewedAt) при
firstViewedAt IS NOT NULL AND firstViewedAt >= PublishedAt; ttfv = ttfvSeconds / (Unit=hours ? 3600 : 86400). overall: propertyObjectsPublished = count() кандидатов,
propertyObjectsViewed = countIf(firstViewedAt IS NOT NULL), propertyObjectsNotYetViewed — разность,
avg/quantile(0.5)/quantile(0.9) по ttfvSeconds просмотренных. Список —
ORDER BY viewed DESC, ttfvSeconds ASC LIMIT PageSize OFFSET Offset, total = propertyObjectsPublished.
- Результат. Возвращается
TimeToFirstViewResult (200): overall + постраничный список объектов по
возрастанию ttfv (без просмотра — в конце). Нет опубликованных объектов в окне — не ошибка: нулевой
overall + пустая страница. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Нет dateFrom/dateTo, from > to, диапазон > 366 дней, unknown unit | Ошибка валидации запроса | no retry |
UNAUTHORIZED | 401 | Нет/невалиден JWT | Требуется авторизация | no retry |
FORBIDDEN | 403 | Нет dev-stats.view или DeveloperId != актор | Доступ запрещён | no retry |
DEV_STATS_QUERY_UNAVAILABLE | 503 | ClickHouse недоступен / timeout | Аналитика временно недоступна | retry с backoff |
Нет опубликованных объектов недвижимости / просмотров — не ошибка: 200 с нулевым overall и пустой страницей.
Acceptance Criteria
- TTFV по опубликованным. Дано авторизованный застройщик и окно публикации с объектами. Когда
GET /time-to-first-view. Тогда 200 с overall (propertyObjectsPublished/Viewed/NotYetViewed,
avg/median/p90Ttfv) и постраничным списком по возрастанию ttfv (быстрее — выше).
- Непросмотренный объект. Дано объект опубликован в окне, но не просмотрен. Когда запрос. Тогда
viewed=false, firstViewedAt=null, ttfv=null, объект — в конце списка и в propertyObjectsNotYetViewed.
- Единица измерения. Дано
unit=days. Когда запрос. Тогда ttfv/avgTtfv/medianTtfv — в днях
(unit=hours по умолчанию — в часах).
- Просмотр раньше публикации. Дано объект смотрели до
PublishedAt (перепубликация). Когда считается
TTFV. Тогда ttfv=null (guard firstViewedAt >= PublishedAt), при этом объект числится просмотренным.
- Публикация ещё не реплицирована. Дано
PublishedAt объекта не долетел в dim_property_object. Когда
запрос. Тогда объект не попадает в окно (INV-STA-6); появится после upsert/backfill измерения.
- Guard пустого множества. Дано
propertyObjectsViewed = 0. Когда считаются перцентили. Тогда
avg/median/p90Ttfv = null без ошибки.
- Пустое окно. Дано нет объектов, опубликованных в окне. Когда запрос. Тогда
200, нулевой overall,
пустая страница (не ошибка).
- Границы окна/пагинации. Дано окно > 366 дней,
dateTo > сегодня, неизвестный unit или
pageSize > 200. Когда запрос. Тогда 400 ValidationError.
- Чужой scope / нет права. Дано чужой
DeveloperId или нет dev-stats.view. Когда запрос. Тогда
403 FORBIDDEN (INV-STA-1).
- Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда
503 DEV_STATS_QUERY_UNAVAILABLE (retry с backoff).
Assumptions
- Публикация =
PropertyObjectEntity.CreatedAt, реплицированная в dim_property_object.PublishedAt из
pin.chessboard.property-object-upserted.v1. Объекты недвижимости, у которых PublishedAt
ещё не долетел (NULL — гонка измерения), в окно не попадают (появятся после backfill/upsert измерения).
- Первый просмотр ищется от
DateFrom включительно; если объект смотрели ещё до окна публикации (пере-
публикация с сохранением просмотров), firstViewedAt может быть раньше PublishedAt — такие пары
ttfv = null (guard firstViewedAt >= PublishedAt) и объект недвижимости считается просмотренным без валидного TTFV.
Совместимость и наблюдаемость
- Новые optional поля — additive; удаление/переименование/смена wire-
unit-семантики — breaking.
- Logs:
dev-stats-time-to-first-view (developerId, окно, unit, propertyObjectsPublished, elapsed; PII нет). Metrics:
dev_stats_query_requests_total{method="time-to-first-view"}, latency histogram (SLO p95 ≤ 600 мс, сырые
факты — ../../README.md). Traces: span приёма → span обработки → span чтения ClickHouse;
correlation — сквозной trace traceparent.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы