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

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 = актор; список пагинируется постранично.

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature groupattention-analytics
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-time-to-first-view.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 фильтрует и dim_property_object, и property_object_view_facts (fail-closed)
Auditnone (read-only агрегаты)

HTTP-контракт

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

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

Общий фильтр — DevStatsFilterModel (../../domain/dev-stats-filter.md) + PagingQuery. DateFrom/DateTo здесь — окно публикации объекта недвижимости.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
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?Нетсужение (один объект)
Unitquery unitTtfvUnit? (enum)Нетhours (default) / days; единица ttfv; unknown → ValidationError
Pagingquery page/pageSizePagingQueryНетdefault page=1, pageSize=50 (≤ 200); порядок — ttfv asc (быстрее — выше), объекты недвижимости без просмотра — в конце

ViewerKind/ViewSource в этой метрике не применяются: первый просмотр учитывается независимо от типа зрителя/источника.

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho акторЗастройщик.
from / tostring (ISO date)ДаechoОкно публикации.
unitstring (enum wire)Даecho (после дефолта)Единица ttfv (hours/days).
overallTtfvOverallModelДаагрегацияИтог по опубликованным объектам недвижимости окна.
propertyObjectsпостранично (PropertyObjectTtfvModel)Дапагинированная выборкаОбъекты недвижимости по возрастанию ttfv (без просмотра — в конце).

TtfvOverallModel

ПолеТипОписание
propertyObjectsPublishedlongОбъектов недвижимости опубликовано в окне (из dim_property_object).
propertyObjectsViewedlongИз них получили ≥ 1 просмотр.
propertyObjectsNotYetViewedlongОпубликованы, но ни разу не просмотрены.
viewedSharedecimalpropertyObjectsViewed / propertyObjectsPublished (0..1, 3 знака).
avgTtfvdecimal?Средний TTFV просмотренных объектов недвижимости (в unit), null если propertyObjectsViewed = 0.
medianTtfvdecimal?Медиана TTFV (quantile(0.5)), null при пустоте.
p90Ttfvdecimal?90-й перцентиль TTFV, null при пустоте.

PropertyObjectTtfvModel

ПолеТипОписание
propertyObjectIdGuidОбъект.
projectId / buildingIdGuidИерархия (из dim_property_object).
publishedAtstring (ISO datetime)Момент публикации.
firstViewedAtstring? (ISO datetime)Первый просмотр; null — ещё не просмотрен.
ttfvdecimal?Время до первого просмотра в unit; null — ещё не просмотрен.
viewedboolБыл ли хотя бы один просмотр.

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

  • Пользователь застройщика авторизован; есть 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 (событий нет).

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора; если клиент прислал DeveloperId ≠ актор → отказ FORBIDDEN (403).
  2. Проверка входа. Обязательные DateFrom/DateTo, from <= to, диапазон ≤ 366 дней; enum Unit — из допустимого множества (неизвестное → ValidationError); PageSize <= 200.
  3. Что читаем. 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) режут диапазон.
  4. Расчёты и маппинг (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.
  5. Результат. Возвращается TimeToFirstViewResult (200): overall + постраничный список объектов по возрастанию ttfv (без просмотра — в конце). Нет опубликованных объектов в окне — не ошибка: нулевой overall + пустая страница. Недоступность ClickHouse → DEV_STATS_QUERY_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Нет dateFrom/dateTo, from > to, диапазон > 366 дней, unknown unitОшибка валидации запросаno retry
UNAUTHORIZED401Нет/невалиден JWTТребуется авторизацияno retry
FORBIDDEN403Нет dev-stats.view или DeveloperId != акторДоступ запрещёнno retry
DEV_STATS_QUERY_UNAVAILABLE503ClickHouse недоступен / 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.

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

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

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