Отчет/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 фактов) |
Доступ
| Actor | Permission | Tenant/Brand scope | Audit |
|---|---|---|---|
застройщик (developer) | dev-stats.export | DeveloperId = актор (из claims, не из UI); fail-closed | ExportSensitive — обязательная запись аудита |
dev-stats.export — отдельный permission поверх dev-stats.view (каталог —
identity/domain/permission.md). Каждая выгрузка пишет audit-запись
(actor + фильтр + время + формат + число строк) — требование безопасности README §Безопасность. Scope
жёстко = свой DeveloperId; выгрузка чужого застройщика невозможна.
Исходные Source Data
| Источник | Владелец | Тип | Freshness | Join key |
|---|---|---|---|---|
property_object_view_daily | dev-stats | ClickHouse rollup (AggregatingMergeTree) | ≤ 5 мин | DeveloperId + иерархия + EventDate |
funnel_daily | dev-stats | ClickHouse rollup (воронка) | ≤ 5 мин | DeveloperId + иерархия + EventDate |
property_object_view_facts | dev-stats | ClickHouse fact (для разреза ViewSource) | ≤ 5 мин | DeveloperId + PropertyObjectId + ViewedAt |
session_facts | dev-stats | ClickHouse fact | ≤ 5 мин | SessionId ∈ property_object_view_facts |
dim_project/building/layout_group/layout/property_object | dev-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) |
DateFrom | Date | Да | — | Начало периода; режет партиции |
DateTo | Date | Да | — | ≥ DateFrom; ≤ max date range (см. Лимиты) |
ProjectId | UUID? | Нет | * | Только проекты своего DeveloperId |
BuildingId / LayoutGroupId / LayoutId / PropertyObjectId | UUID? | Нет | * | Уровни иерархии (префикс order key) |
ViewerKind | enum? | Нет | * | Guest/Agent/Investor |
ViewSource | enum? | Нет | * | Catalog/Search/Selection/Favorites/News/DirectLink/Unknown |
granularity | enum | Нет | period | period/day/week/month |
groupBy | enum | Нет | project | Уровень иерархии строк отчёта |
Строка Отчета DevStatsReportRowModel
Одна строка = узел иерархии (уровень groupBy) × период/интервал (granularity). Типизированная модель,
без object/json/dynamic.
| Поле | Тип | Обязательность | Расчет | Masking |
|---|---|---|---|---|
HierarchyPath | string | Да | путь названий из dim_* (Проект / Здание / …) | — |
NodeId | Guid | Да | id узла уровня groupBy | — |
PeriodStart | Date | Да | начало интервала (granularity) | — |
Views | long | Да | uniqMerge(Views) | агрегат |
UniqViewers | long | Да | uniqMerge(UniqViewers) | агрегат |
RepeatViews | long | Да | uniqMerge(Views) − uniqMerge(UniqViewers) | агрегат |
AvgDurationSeconds | int | Да | sumMerge(DurationSum) / uniqMerge(Views) | агрегат |
DurationSumSeconds | long | Да | sumMerge(DurationSum) | агрегат |
Favorites | long | Нет | uniqMerge(Favorites) | воронка (флаг) |
Leads | long | Нет | uniqMerge(Leads) | воронка |
Bookings | long | Нет | uniqMerge(Bookings) | воронка |
Deals | long | Нет | uniqMerge(Deals) | воронка |
ViewToLeadConversion | decimal | Нет | Leads / Views (scale 4) | производная |
Разрез ViewerKind/ViewSource — при заданном фильтре добавляет столбцы-доли или отдельные строки
сегмента (по groupBy). Строки собираются постранично при потоковой генерации.
Модель Totals DevStatsReportTotalsModel
Итоги по всей выборке фильтра (не сумма строк по уникальным зрителям — пересчёт из состояний, чтобы
UniqViewers не задваивался между узлами).
| Поле | Тип | Обязательность | Расчет | Precision |
|---|---|---|---|---|
TotalViews | long | Да | uniqMerge(Views) по всей выборке | целое |
TotalUniqViewers | long | Да | uniqMerge(UniqViewers) по всей выборке (не Σ строк) | целое |
TotalRepeatViews | long | Да | TotalViews − TotalUniqViewers | целое |
TotalDurationSeconds | long | Да | sumMerge(DurationSum) | целое |
AvgDurationSeconds | int | Да | TotalDurationSeconds / TotalViews | целое (сек) |
FunnelTotals | DevStatsFunnelTotals | Нет | uniqMerge этапов + конверсии | scale 4 (конверсии) |
Формат Export-а
Лист XLSX attention (+ лист funnel при активной воронке); PDF — те же таблицы с шапкой фильтра и
блоком тоталов. Заголовки — русские; числа — целые, конверсии — проценты.
| Column | Заголовок | Тип | Формат | Nullable | Источник |
|---|---|---|---|---|---|
HierarchyPath | Иерархия | string | текст | Нет | DevStatsReportRowModel.HierarchyPath |
PeriodStart | Период | Date | dd.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 | Конверсия в лид | decimal | 0.00% | Да | .ViewToLeadConversion |
Строка тоталов (DevStatsReportTotalsModel) — под таблицей, выделена; шапка листа содержит период,
DeveloperId (маскированный), примененные фильтры и время генерации (UTC).
Процесс Генерации
- Проверить permission
dev-stats.export, tenant scope (DeveloperId = актор, fail-closed) и лимиты выгрузок актора/tenant. - Провалидировать
DevStatsFilterModel(обязательныеDateFrom/DateTo,DateTo ≥ DateFrom, диапазон ≤ лимита, id-фильтры принадлежат своемуDeveloperId). - Оценить размер выборки: ≤
Max rows sync→ синхронная генерация в ответе; иначе — поставить асинхронную задачу выгрузки и вернуть202+exportJobId. - Прочитать данные из
pin_dev_stats—*Merge/argMaxбезFINAL, префикс-скан по order key + партиции поDateFrom/DateTo; названия узлов — изdim_*. - Сформировать строки отчёта (
DevStatsReportRowModel, потоково) и итоги (DevStatsReportTotalsModel, пересчётuniqMergeпо всей выборке). - Сериализовать XLSX или PDF; положить в объектное хранилище с временной ссылкой (retention ≤ 7 дней).
- Записать audit-запись (
ExportSensitive: actor,DeveloperId, фильтр, формат, число строк, время); для async — статус задачи и безопасную ссылку на файл.
Жизненный цикл асинхронной выгрузки
Крупная выгрузка (> Max rows sync) исполняется фоновой задачей; клиент получает 202 + exportJobId и
опрашивает её статус. Аудируется и постановка задачи, и её финал.
| Статус | Смысл | Что видит застройщик |
|---|---|---|
Queued | задача принята, ждёт исполнителя | «выгрузка готовится» |
Running | идёт чтение витрины и сериализация | прогресс/спиннер |
Ready | файл в объектном хранилище, ссылка активна | кнопка «скачать» (ссылка живёт ≤ 7 дней) |
Downloaded | файл скачан ≥ 1 раз | повторное скачивание до истечения ссылки |
Expired | retention истёк (≤ 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 запрашивается экспорт, then403до чтения, в аудит пишется запись отказа; файл не создаётся. - 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 range | 3 года (совпадает с TTL фактов) | ExportDateRangeTooWide |
| Concurrent exports | 3 на застройщика | ExportConcurrencyLimit |
| Форматы | XLSX, PDF | ExportFormatNotSupported |
Наблюдаемость
| Тип | Правило |
|---|---|
| Metrics | dev_stats_export_duration_ms, dev_stats_export_rows, dev_stats_export_file_bytes, dev_stats_export_errors_total{reason} |
| Logs | started / generated / downloaded / expired / failed; всегда с actor + фильтр (без raw id зрителей) |
| Traces | correlationId, tenantId (= DeveloperId), exportJobId |
| Audit | ExportSensitive — обязательная запись на каждую выгрузку (актор + фильтр + время) |
Операционные alert-ы экспорта (рост ошибок/латентности) — на ops-борде
../dashboards/dev-stats-ingest-overview.md.
Проверка
| Проверка | Ожидаемый Результат |
|---|---|
| Tenant isolation | Выгрузка содержит только DeveloperId = актор; чужой id → 403, audit-запись отказа. |
| Обязательные фильтры | Без DateFrom/DateTo — 400, файл не создаётся. |
| Sync/async порог | > 10 000 строк переводит выгрузку в async-джоб (202 + exportJobId). |
| Тоталы | TotalUniqViewers ≤ Σ UniqViewers строк (пересчёт из состояний, не сумма). |
| Дедуп | Повтор факта не задваивает Views/этапы воронки в строках и тоталах. |
| Audit | Каждая успешная и отклонённая выгрузка оставляет ExportSensitive-запись. |
| PII | В файле нет ФИО/контактов/UserId/AnonymousId/SessionId — только агрегаты. |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Модели ответа dev-stats (типизированные ряды / точки / сводки)
- Use Cases (1): UC-STA-005. Экспорт отчёта статистики (Excel / PDF) с аудитом
- Дашборды (2): Панель Метрик Застройщика: dev-stats-overview (аналитика внимания), Ops-борд: dev-stats-ingest-overview (здоровье ingestion)
- index.md (1): DevStats Service (Pin.DevStats)
Связанные Документы
- Процесс генерации — § «Процесс Генерации» выше; панель —
../dashboards/dev-stats-overview.md. - Фильтр/модели ответа:
../domain/dev-stats-filter.md,../domain/response-models.md,../domain/viewer-classification.md. - Источники:
property-object-view-daily-rollup,funnel-daily-rollup,property-object-view-facts,session-facts, измерения../dimensions/. - Сервис: README (Безопасность —
dev-stats.export, аудит; Каталог метрик). - ADR: ADR-0057 (dev-stats), ADR-0056 (границы CH), ADR-0052 (tenancy).