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

Отчет/Export: dev-stats-export (выгрузка аналитики застройщика)

Аудируемая выгрузка метрик внимания застройщика (ADR-0057) в XLSX (основной) и PDF (презентационный) по тем же DevStatsFilterModel, что и панель ../dashboards/dev-stats-overview.md. Строки отчёта — метрики по выбранному уровню иерархии (проект/здание/группа/планировка/объект недвижимости), плюс итоги. Read-only над ClickHouse pin_dev_stats (dedup-tolerant *Merge/argMax, без FINAL); крупные выгрузки уходят в асинхронную задачу. Контракт и процесс генерации описаны ниже (§ «Процесс Генерации»).

Назначение

Даёт застройщику выгружаемый отчёт внимания к его недвижимости за период: по каждому узлу выбранной иерархии — просмотры, уникальные зрители, повторность, длительность, воронку конверсии; плюс тоталы по всей выборке. Decision value — офлайн-анализ спроса, отчётность внутри застройщика, сверка с продажами. Аудитория — застройщик (роль developer). Owning service — dev-stats (DevStats.Api — генерация, DevStats.Background — async-джоб для крупных выгрузок). Персональные данные зрителей не выгружаются — только агрегаты и типы (ADR-0057 §6).

Классификация

ПолеЗначение
Тип отчетаXLSX (основной) / PDF (презентационный)
Аудиториязастройщик (роль developer)
ЧувствительностьInternal/Sensitive — источники несут UserId/AnonymousId, но в выгрузку идут только агрегаты
Freshness SLA≤ 5 мин (freshness фактов; выгрузка отражает текущее состояние pin_dev_stats)
RetentionФайл выгрузки — ≤ 7 дней в объектном хранилище (временная ссылка); данные-источник — 3 года (TTL фактов)

Доступ

ActorPermissionTenant/Brand scopeAudit
застройщик (developer)dev-stats.exportDeveloperId = актор (из claims, не из UI); fail-closedExportSensitive — обязательная запись аудита

dev-stats.export — отдельный permission поверх dev-stats.view (каталог — identity/domain/permission.md). Каждая выгрузка пишет audit-запись (actor + фильтр + время + формат + число строк) — требование безопасности README §Безопасность. Scope жёстко = свой DeveloperId; выгрузка чужого застройщика невозможна.

Исходные Source Data

ИсточникВладелецТипFreshnessJoin key
property_object_view_dailydev-statsClickHouse rollup (AggregatingMergeTree)≤ 5 минDeveloperId + иерархия + EventDate
funnel_dailydev-statsClickHouse rollup (воронка)≤ 5 минDeveloperId + иерархия + EventDate
property_object_view_factsdev-statsClickHouse fact (для разреза ViewSource)≤ 5 минDeveloperId + PropertyObjectId + ViewedAt
session_factsdev-statsClickHouse fact≤ 5 минSessionId ∈ property_object_view_facts
dim_project/building/layout_group/layout/property_objectdev-stats (реплика chessboard)ClickHouse dimension (ReplacingMergeTree)≤ 5 минId → человекочитаемые названия строк

Названия узлов иерархии (ProjectName/BuildingName/…) берутся из dim_* (JOIN-free обогащение строки отчёта), чтобы файл был читаем без справочника id.

Фильтры DevStatsFilterModel

Идентичны переменным панели (дом — ../domain/dev-stats-filter.md). granularity в экспорте задаёт агрегирование по периоду (весь период одной строкой или разбивка по дням/неделям/месяцам); groupBy — уровень детализации строк отчёта.

ПолеТипОбязательностьDefaultОграничения
DeveloperId (TenantId)UUIDДаclaims актораRequired, = актор (не из UI)
DateFromDateДаНачало периода; режет партиции
DateToDateДа≥ DateFrom; ≤ max date range (см. Лимиты)
ProjectIdUUID?Нет*Только проекты своего DeveloperId
BuildingId / LayoutGroupId / LayoutId / PropertyObjectIdUUID?Нет*Уровни иерархии (префикс order key)
ViewerKindenum?Нет*Guest/Agent/Investor
ViewSourceenum?Нет*Catalog/Search/Selection/Favorites/News/DirectLink/Unknown
granularityenumНетperiodperiod/day/week/month
groupByenumНетprojectУровень иерархии строк отчёта

Строка Отчета DevStatsReportRowModel

Одна строка = узел иерархии (уровень groupBy) × период/интервал (granularity). Типизированная модель, без object/json/dynamic.

ПолеТипОбязательностьРасчетMasking
HierarchyPathstringДапуть названий из dim_* (Проект / Здание / …)
NodeIdGuidДаid узла уровня groupBy
PeriodStartDateДаначало интервала (granularity)
ViewslongДаuniqMerge(Views)агрегат
UniqViewerslongДаuniqMerge(UniqViewers)агрегат
RepeatViewslongДаuniqMerge(Views) − uniqMerge(UniqViewers)агрегат
AvgDurationSecondsintДаsumMerge(DurationSum) / uniqMerge(Views)агрегат
DurationSumSecondslongДаsumMerge(DurationSum)агрегат
FavoriteslongНетuniqMerge(Favorites)воронка (флаг)
LeadslongНетuniqMerge(Leads)воронка
BookingslongНетuniqMerge(Bookings)воронка
DealslongНетuniqMerge(Deals)воронка
ViewToLeadConversiondecimalНетLeads / Views (scale 4)производная

Разрез ViewerKind/ViewSource — при заданном фильтре добавляет столбцы-доли или отдельные строки сегмента (по groupBy). Строки собираются постранично при потоковой генерации.

Модель Totals DevStatsReportTotalsModel

Итоги по всей выборке фильтра (не сумма строк по уникальным зрителям — пересчёт из состояний, чтобы UniqViewers не задваивался между узлами).

ПолеТипОбязательностьРасчетPrecision
TotalViewslongДаuniqMerge(Views) по всей выборкецелое
TotalUniqViewerslongДаuniqMerge(UniqViewers) по всей выборке (не Σ строк)целое
TotalRepeatViewslongДаTotalViews − TotalUniqViewersцелое
TotalDurationSecondslongДаsumMerge(DurationSum)целое
AvgDurationSecondsintДаTotalDurationSeconds / TotalViewsцелое (сек)
FunnelTotalsDevStatsFunnelTotalsНетuniqMerge этапов + конверсииscale 4 (конверсии)

Формат Export-а

Лист XLSX attention (+ лист funnel при активной воронке); PDF — те же таблицы с шапкой фильтра и блоком тоталов. Заголовки — русские; числа — целые, конверсии — проценты.

ColumnЗаголовокТипФорматNullableИсточник
HierarchyPathИерархияstringтекстНетDevStatsReportRowModel.HierarchyPath
PeriodStartПериодDatedd.MM.yyyyНет.PeriodStart
ViewsПросмотрыlong# ##0Нет.Views
UniqViewersУникальныеlong# ##0Нет.UniqViewers
RepeatViewsПовторныеlong# ##0Нет.RepeatViews
AvgDurationSecondsСр. длительность, сint# ##0Нет.AvgDurationSeconds
FavoritesВ избранноеlong# ##0Да.Favorites
LeadsЛидыlong# ##0Да.Leads
DealsСделкиlong# ##0Да.Deals
ViewToLeadConversionКонверсия в лидdecimal0.00%Да.ViewToLeadConversion

Строка тоталов (DevStatsReportTotalsModel) — под таблицей, выделена; шапка листа содержит период, DeveloperId (маскированный), примененные фильтры и время генерации (UTC).

Процесс Генерации

  1. Проверить permission dev-stats.export, tenant scope (DeveloperId = актор, fail-closed) и лимиты выгрузок актора/tenant.
  2. Провалидировать DevStatsFilterModel (обязательные DateFrom/DateTo, DateTo ≥ DateFrom, диапазон ≤ лимита, id-фильтры принадлежат своему DeveloperId).
  3. Оценить размер выборки: ≤ Max rows syncсинхронная генерация в ответе; иначе — поставить асинхронную задачу выгрузки и вернуть 202 + exportJobId.
  4. Прочитать данные из pin_dev_stats*Merge/argMax без FINAL, префикс-скан по order key + партиции по DateFrom/DateTo; названия узлов — из dim_*.
  5. Сформировать строки отчёта (DevStatsReportRowModel, потоково) и итоги (DevStatsReportTotalsModel, пересчёт uniqMerge по всей выборке).
  6. Сериализовать XLSX или PDF; положить в объектное хранилище с временной ссылкой (retention ≤ 7 дней).
  7. Записать audit-запись (ExportSensitive: actor, DeveloperId, фильтр, формат, число строк, время); для async — статус задачи и безопасную ссылку на файл.

Жизненный цикл асинхронной выгрузки

Крупная выгрузка (> Max rows sync) исполняется фоновой задачей; клиент получает 202 + exportJobId и опрашивает её статус. Аудируется и постановка задачи, и её финал.

СтатусСмыслЧто видит застройщик
Queuedзадача принята, ждёт исполнителя«выгрузка готовится»
Runningидёт чтение витрины и сериализацияпрогресс/спиннер
Readyфайл в объектном хранилище, ссылка активнакнопка «скачать» (ссылка живёт ≤ 7 дней)
Downloadedфайл скачан ≥ 1 разповторное скачивание до истечения ссылки
Expiredretention истёк (≤ 7 дней), файл удалён«ссылка устарела, сформируйте заново»
Failedгенерация не удаласьсообщение об ошибке + предложение повторить

Переходы: Queued → Running → Ready → Downloaded → Expired; из Queued/Running возможен Failed. Промежуточное состояние read-only не влияет на доменные данные — метрики остаются доступны в API независимо от статуса выгрузки.

Идемпотентность, конкурентность и восстановление

  • Идемпотентность запроса. Повторный запрос с тем же (DeveloperId, фильтр, формат), пока активная задача не завершилась, не создаёт вторую задачу — возвращается тот же exportJobId (защита от двойного клика). После завершения повторный запрос формирует новую задачу (данные могли измениться — freshness).
  • Лимит конкурентности. При превышении Concurrent exports (3 на застройщика) новый запрос отклоняется ExportConcurrencyLimit — «проигрывает» позже пришедший; уже идущие задачи не прерываются. Клиент повторяет после завершения одной из активных.
  • Частичный отказ / восстановление. Отказ чтения витрины или сериализации переводит задачу в Failed без публикации частичного файла (пользователь никогда не получает неполный отчёт). Повтор — идемпотентен на уровне данных (чтение dedup-tolerant): та же выборка даст тот же результат. Файлы чистятся по retention (≤ 7 дней) независимо от исхода скачивания.
  • Согласованность снапшота. Каждая выгрузка отражает состояние pin_dev_stats на момент чтения; идущая параллельно ingestion может добавить строки — они попадут в следующую выгрузку, текущий файл остаётся консистентным снапшотом (без частичных дописей).

Acceptance Criteria (Given/When/Then)

  • Given застройщик с dev-stats.export и выборкой ≤ 10 000 строк, when запрашивает XLSX, then получает файл синхронно; пишется ExportSensitive-аудит.
  • Given выборка > 10 000 строк, when запрашивает выгрузку, then получает 202 + exportJobId, задача проходит Queued → Running → Ready, файл доступен по временной ссылке ≤ 7 дней.
  • Given клиент дважды кликнул «выгрузить» с тем же фильтром, when первая задача ещё Running, then второй запрос возвращает тот же exportJobId (без дубля задачи).
  • Given уже 3 активные выгрузки застройщика, when приходит четвёртый запрос, then отказ ExportConcurrencyLimit; активные не прерываются.
  • Given сбой витрины во время генерации, when задача падает, then статус Failed, частичный файл не публикуется, аудит фиксирует исход.
  • Given переданный DeveloperId ≠ актор, when запрашивается экспорт, then 403 до чтения, в аудит пишется запись отказа; файл не создаётся.
  • Given повтор факта в источнике, when формируется отчёт, then Views/этапы воронки в строках и тоталах не задваиваются (dedup-tolerant).

Мутации и события

Экспорт не меняет доменное состояние и не публикует события (dev-stats — leaf-аналитика). Единственная запись — audit-лог доступа и (для async) строка состояния асинхронной задачи выгрузки в её служебной таблице.

Лимиты

ЛимитЗначениеОшибка
Max rows (sync)10 000 строкExportTooLargeUseAsync
Max rows (async job)200 000 строкExportRowLimitExceeded
Max date range3 года (совпадает с TTL фактов)ExportDateRangeTooWide
Concurrent exports3 на застройщикаExportConcurrencyLimit
ФорматыXLSX, PDFExportFormatNotSupported

Наблюдаемость

ТипПравило
Metricsdev_stats_export_duration_ms, dev_stats_export_rows, dev_stats_export_file_bytes, dev_stats_export_errors_total{reason}
Logsstarted / generated / downloaded / expired / failed; всегда с actor + фильтр (без raw id зрителей)
TracescorrelationId, tenantId (= DeveloperId), exportJobId
AuditExportSensitive — обязательная запись на каждую выгрузку (актор + фильтр + время)

Операционные alert-ы экспорта (рост ошибок/латентности) — на ops-борде ../dashboards/dev-stats-ingest-overview.md.

Проверка

ПроверкаОжидаемый Результат
Tenant isolationВыгрузка содержит только DeveloperId = актор; чужой id → 403, audit-запись отказа.
Обязательные фильтрыБез DateFrom/DateTo400, файл не создаётся.
Sync/async порог> 10 000 строк переводит выгрузку в async-джоб (202 + exportJobId).
ТоталыTotalUniqViewers ≤ Σ UniqViewers строк (пересчёт из состояний, не сумма).
ДедупПовтор факта не задваивает Views/этапы воронки в строках и тоталах.
AuditКаждая успешная и отклонённая выгрузка оставляет ExportSensitive-запись.
PIIВ файле нет ФИО/контактов/UserId/AnonymousId/SessionId — только агрегаты.

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

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

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