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

GET /api/v1/dev-stats/viewers/unique — Уникальные зрители (гость/агент/инвестор)

Назначение

Сколько уникальных зрителей смотрели объекты застройщика за период — общим числом, в разрезе типа зрителя (Guest / Agent / Investor) и в динамике (ряд по дням/неделям/месяцам). Ключевая ценность — корректный дедуп зрителя за весь период: уникальность считается uniqMerge-склейкой HLL-состояний uniqState(coalesce(UserId, AnonymousId)) из rollup property_object_view_daily, поэтому один зритель, заходивший в разные дни, в итоговом числе учтён один раз (простое суммирование дневных счётчиков этого не даёт). Источник блока «аудитория» дашборда ЛК застройщика.

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature groupviewers
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-unique-viewers.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 (агрегат без PII — только счётчики и типы)

HTTP-контракт

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

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

Общий вход — DevStatsFilterModel + грануляция ряда. DeveloperId — из identity-claims актора.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иначе 403)Tenant isolation, fail-closed
DateFromquery dateFromDateOnlyДаISO yyyy-MM-dd
DateToquery dateToDateOnlyДаISO; DateFrom <= DateTo; окно ≤ 3 года
ProjectIdPropertyObjectIdqueryGuid?Нетуровни иерархии (сверху вниз)
ViewerKindquery viewerKindViewerKind?Нетwire guest/agent/investor; unknown → ValidationError
ViewSourcequery viewSourceViewSource?Нетwire значения ViewSource (переводит на срез фактов)
Granularityquery granularitySeriesGranularity (enum)Нетwire day/week/month; default day; unknown → ValidationError

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

ПолеТипОбязательностьИсточникMaskingОписание
developerIdGuidДаecho актораЗастройщик (= scope).
dateFrom / dateTostring (ISO date)ДаechoПрименённый период.
granularitystring (enum wire)ДаechoГрануляция ряда day/week/month.
totalUniqueViewerslongДаuniqMerge(UniqViewers) (без groupBy)агрегатУникальные зрители за весь период (дедуп кросс-день).
byViewerKindIReadOnlyList<ViewerKindCountModel>ДаGROUP BY ViewerKindагрегатРазрез гость/агент/инвестор.
seriesIReadOnlyList<UniqueViewersPointModel>Да (может быть пуст)GROUP BY bucketагрегатДинамика уникальных зрителей по бакетам.

ViewerKindCountModel

ПолеТипИсточникОписание
viewerKindstring (enum wire)ViewerKindguest/agent/investor.
uniqueViewerslonguniqMerge(UniqViewers)Уникальные зрители типа за период.
sharedecimaluniqueViewers / totalUniqueViewers (guard 0)Доля типа, 0..1 (2 знака).

UniqueViewersPointModel

ПолеТипИсточникОписание
datestring (ISO date)начало бакета (EventDate / toStartOfWeek / toStartOfMonth)Точка ряда.
uniqueViewerslonguniqMerge(UniqViewers) внутри бакетаУникальные зрители бакета.

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

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md).
  • Уникальность считается кросс-день (INV-STA-4): uniqMerge сливает HLL-состояния всех дней периода, зритель из разных дней учтён один раз. Ожидаемое свойство: Σ series[*].uniqueViewers ≥ totalUniqueViewers (зритель в двух бакетах учтён в каждом, а в total — один раз) — это свойство дедупа, а не расхождение.
  • Окно свежести событие→факт ≤ 5 мин (INV-STA-5): свежайшие зрители в окне недосчитаны, число монотонно догоняет.
  • Операция read-only: состояние не меняется, события не публикуются.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view. DeveloperId берётся из identity-claims актора (scope); если клиент прислал DeveloperId ≠ актор → отказ DEV_STATS_FORBIDDEN (403, fail-closed).
  2. Проверка входа. Период задан и корректен (DateFrom <= DateTo, окно ≤ 3 года); enum ViewerKind/ViewSource/Granularity — из допустимого множества (неизвестное → ValidationError, молчаливый дефолт запрещён); иерархия «сверху вниз». Существование иерархии не проверяется: неизвестный id даёт пустой результат.
  3. Что читаем. Дневной rollup property_object_view_daily за период по scope. За один префикс-скан по ключу (DeveloperId, ProjectId, …, PropertyObjectId, EventDate, ViewerKind) считаем:
    • итог и разрез — uniqMerge(UniqViewers) с GROUP BY ViewerKind WITH ROLLUP: grand-total (totalUniqueViewers) и byViewerKind. Дедуп зрителя за период: uniqMerge сливает HLL-состояния всех дней, зритель из разных дней учтён один раз;
    • ряд — uniqMerge(UniqViewers) с GROUP BY bucket (EventDate / toStartOfWeek / toStartOfMonth), ORDER BY bucket. Меры считаются без FINAL; диапазон дат режет партиции toYYYYMM(EventDate). Если задан ViewSource (в rollup не хранится), чтение уходит на сырьё property_object_view_facts с uniq(coalesce(UserId, AnonymousId)).
  4. Расчёты и маппинг. share каждого ViewerKind = uniqueViewers / totalUniqueViewers (guard 0); series[k].date ← начало бакета. Ожидаемое свойство: Σ series[*].uniqueViewers ≥ totalUniqueViewers (зритель в двух бакетах считается в каждом, а в total — один раз) — документируется, не «чинится». Пустой период → нули, пустые byViewerKind/series.
  5. Результат. Возвращается UniqueViewersResult (200). Недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

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

Пустой период — не ошибка: 200 с нулями и пустыми списками.

Acceptance Criteria

  • Итог, разрез и ряд. Дано авторизованный застройщик и корректный период. Когда GET /viewers/unique?granularity=day. Тогда 200 с totalUniqueViewers, byViewerKind (сумма share ≈ 1) и series по возрастанию date.
  • Кросс-день дедуп. Дано один зритель заходил в понедельник и вторник. Когда считается totalUniqueViewers. Тогда он учтён как 1 (INV-STA-4), при этом series за оба дня даёт по 1, то есть Σ series ≥ total.
  • Пустой период. Дано период без просмотров. Когда запрос. Тогда 200, нули и пустые byViewerKind/series (не ошибка).
  • Guard доли. Дано totalUniqueViewers = 0. Когда считается share. Тогда share = 0 без ошибки.
  • Чужой scope. Дано клиент подставил чужой DeveloperId. Когда запрос. Тогда 403 DEV_STATS_FORBIDDEN (INV-STA-1).
  • Неизвестный enum/грануляция. Дано viewerKind=vip или granularity=hour. Когда запрос. Тогда 400 ValidationError (молчаливый дефолт запрещён).
  • Иерархия «снизу вверх» / окно > 3 лет. Дано BuildingId без ProjectId либо окно шире TTL. Когда запрос. Тогда 400 ValidationError (INV-STA-8).
  • Срез по источнику. Дано задан viewSource. Когда запрос. Тогда уникальность считается по сырью (uniq(coalesce(UserId, AnonymousId))), контракт тот же, SLO p95 ≤ 600 мс.
  • Окно свежести. Дано новый зритель минуту назад. Когда запрос. Тогда он ещё не учтён; после ingestion (≤ 5 мин) totalUniqueViewers монотонно растёт (INV-STA-5).
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_UNAVAILABLE (retry с backoff).

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

  • Optional-поля ответа — additive; смена wire-enum ViewerKind/Granularity — breaking. Unknown enum → 400.
  • Logs: dev-stats-unique-viewers-read (developerId, granularity, totalUniqueViewers, elapsed; PII нет). Metrics: dev_stats_unique_viewers_requests_total{granularity}, latency histogram (SLO p95 ≤ 300 мс). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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