funnel_daily (ClickHouse rollup / воронка)
Дневная воронка конверсии по иерархии застройщика × дата: views → favorites → selections → leads → bookings → deals. Показывает застройщику, как внимание к его недвижимости превращается в лиды и сделки.
ADR-0057. Онлайн-чтение — через *Merge, без FINAL.
Назначение
funnel_daily — AggregatingMergeTree-таблица, где на каждый уровень иерархии и дату хранятся уникальные
счётчики этапов воронки. Наполняется из разных источников:
- этап views — materialized view
funnel_views_mvнадproperty_object_view_facts(uniqState(PropertyObjectViewId)); - этапы favorites / selections / leads / bookings / bookingsPaid / deals / dealsCompleted —
консюмерами
DevStats.Background, которые слушают доменные событияfavorites/selections/crm, обогащают иерархию изdim_property_object(событие несётPropertyObjectId, но не иерархию) и вставляют частичную строку агрегата.
Каждый этап — uniqState по своему бизнес-ключу (id избранного, лида, брони, сделки), поэтому воронка
идемпотентна к at-least-once: повтор события не увеличивает счётчик. Конверсии между этапами
считаются при чтении отношением uniqMerge соседних этапов.
Классификация
| Поле | Значение |
|---|---|
| Контекст | dev-stats (ADR-0057) |
| Kind | rollup-воронка — AggregatingMergeTree + MV funnel_views_mv + ingest-консюмеры |
| Источники | property_object_view_facts; события favorites/selections/crm (через dim_property_object) |
| Хранилище | ClickHouse, database pin_dev_stats, table funnel_daily |
| Tenant isolation | DeveloperId — головной компонент ключа; фильтр обязателен и = актор |
Table
| Поле | Значение |
|---|---|
| Database | pin_dev_stats |
| Table | funnel_daily (+ MV funnel_views_mv для этапа views) |
| Engine | AggregatingMergeTree (уникальные счётчики этапов; идемпотентность по бизнес-ключам) |
| Owner service | dev-stats (DevStats.Migrator — DDL/MV; DevStats.Background — ingest этапов) |
| Source events | pin.favorites.favorite-added.v1, pin.selections.selection-item-added.v1, pin.crm.lead-created.v1, pin.crm.booking-created.v1, pin.crm.booking-paid.v1, pin.crm.deal-created.v1, pin.crm.deal-completed.v1; этап views — из property_object_view_facts |
| Transform version | 1 |
| Freshness SLA | ≤ 5 минут (batch-вставка / MV) |
Columns
Ключ группировки (иерархия × дата) + агрегатные состояния этапов. EventDate = toDate(<время события>).
| Column | Type | Роль | Partition/Order |
|---|---|---|---|
DeveloperId | UUID | dimension | order (1) |
ProjectId | UUID | dimension | order (2) |
BuildingId | UUID | dimension | order (3) |
LayoutGroupId | UUID | dimension | order (4) |
LayoutId | UUID | dimension | order (5) |
PropertyObjectId | UUID | dimension | order (6) |
EventDate | Date | dimension | PARTITION BY toYYYYMM(EventDate), order (7) |
Views | AggregateFunction(uniq, UUID) | этап 1: uniqState(PropertyObjectViewId) (из property_object_view_facts) | none |
Favorites | AggregateFunction(uniq, UUID) | этап 2: uniqState(FavoriteId) | none |
Selections | AggregateFunction(uniq, UUID) | этап 3: uniqState(SelectionItemId) | none |
Leads | AggregateFunction(uniq, UUID) | этап 4: uniqState(LeadId) | none |
Bookings | AggregateFunction(uniq, UUID) | этап 5: uniqState(BookingId) | none |
BookingsPaid | AggregateFunction(uniq, UUID) | этап 5а: uniqState(BookingId) по booking-paid | none |
Deals | AggregateFunction(uniq, UUID) | этап 6: uniqState(DealId) | none |
DealsCompleted | AggregateFunction(uniq, UUID) | этап 6а: uniqState(DealId) по deal-completed | none |
Каждый источник вставляет частичную строку: заполнено состояние своего этапа, остальные — пустые
*State() (сливаются при merge). Конверсии (favorites/views, leads/favorites, deals/leads и т.д.) —
производные при чтении, отдельными колонками не хранятся.
Инварианты
| Код | Инвариант | Смысл |
|---|---|---|
| INV-FN-1 StageIdempotent | Каждый этап — uniqState по своему бизнес-ключу (PropertyObjectViewId/FavoriteId/SelectionItemId/LeadId/BookingId/DealId) | повтор события (at-least-once) не увеличивает счётчик этапа |
| INV-FN-2 PartialRowsMerge | Источник вставляет частичную строку только со своим этапом; AggregatingMergeTree сливает по ключу | порядок доставки этапов не важен; этапы наполняются независимо |
| INV-FN-3 ConversionsDerived | Конверсии считаются при чтении как stage / prevStage; prevStage = 0 → null | отдельного хранения конверсий нет; нет деления на ноль |
| INV-FN-4 KeyComplete | Ключ (иерархия, EventDate) заполнен обогащением из dim_property_object (событие несёт только PropertyObjectId) | воронка режется по любому уровню иерархии |
| INV-FN-5 NoViewerSplit | Воронка не разбивается по ViewerKind (она про конверсию объектов/иерархии, а не сегмент аудитории) | сегмент зрителя — в property_object_view_daily, не в воронке |
Ожидание монотонности (не жёсткий инвариант): в устоявшемся состоянии верхние этапы ≥ нижних
(views ≥ favorites ≥ … ≥ deals). При лаге источника нижнего этапа это временно нарушается — свойство
freshness, не порча данных (см. ниже).
Согласованность, кросс-сервис и промежуточное состояние
- Что ожидается от продюсеров этапов (
favorites/selections/crm): бизнес-ключ этапа +PropertyObjectIdOccurredAt; доставка at-least-once; иерархию dev-stats добавляет сам изdim_property_object. Контракты входных событий — events/README.md.
- Разные окна свежести этапов. Этап
views(из фактов) обычно свежее нижних;crm-этапы могут догонять свою очередь. Верхние этапы считаются независимо от нижних (частичные состояния сливаются по ключу) — отстающий этап отдаётся текущим (частичным) счётчиком, конверсия к нему помечается «предварительно». - Feature-flag
dev-stats.funnel.enabledгейтит незапущенные этапы (источник ещё не готов к проду): выключенные этапы отдаются с признакомEnabled = false, деградация — кодFUNNEL_DISABLED(см. UC-STA-004). - Late-dimension (нет строки
dim_property_object): этап паркуется (deferred re-enrich), затем ложится в партицию своегоEventDate— идемпотентно (INV-FN-1).
Deduplication
- Идемпотентность: каждый этап —
uniqStateпо бизнес-ключу этапа (FavoriteId,LeadId,BookingId,DealId,PropertyObjectViewId). Повторная доставка события даёт тот же id →uniqне растёт. Дополнительно консюмер отсекает повтор поSourceEventIdв батче. AggregatingMergeTreeсхлопывает частичные состояния по ключу; чтение —uniqMerge+GROUP BYпо срезу иерархии.FINALв online-запросах запрещён (правило аналитического чтения).
Partition И TTL
- Partition key:
toYYYYMM(EventDate). - Order key (подсказка по индексам):
(DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate)— иерархия фильтров застройщика префиксом. Воронка «по проекту за период» = префикс-скан(DeveloperId, ProjectId)+ партиции; детализация до здания/группы/планировки/объекта недвижимости — удлинение префикса.EventDateв хвосте — дневной ряд внутри выбранного уровня. Разбивки по типу зрителя в воронке нет (она про конверсию объектов недвижимости/иерархии, не про сегмент аудитории — для этогоproperty_object_view_daily). - TTL:
EventDate + INTERVAL 3 YEAR.
Consumers И Backfill
| Process | Источник | Этап | Обогащение |
|---|---|---|---|
MV funnel_views_mv | property_object_view_facts (INSERT) | Views | иерархия уже в факте |
favorites-added consumer (../consumers/favorite-added.md) | pin.favorites.favorite-added.v1 | Favorites | dim_property_object по PropertyObjectId |
selection-item-added consumer (../consumers/selection-item-added.md) | pin.selections.selection-item-added.v1 | Selections | dim_property_object |
crm-lead consumer (../consumers/lead-created.md) | pin.crm.lead-created.v1 | Leads | dim_property_object |
crm-booking consumer | pin.crm.booking-created.v1, booking-paid.v1 | Bookings, BookingsPaid | dim_property_object |
crm-deal consumer | pin.crm.deal-created.v1, deal-completed.v1 | Deals, DealsCompleted | dim_property_object |
| Rebuild (будущий) | пересбор из фактов/событий по диапазону | все | — |
Checkpoint каждого консюмера — Kafka offset (commit после подтверждённой batch-вставки). Все консюмеры
пишут в одну таблицу funnel_daily частичными строками; порядок доставки этапов роли не играет —
AggregatingMergeTree сольёт состояния по ключу.
Security
- Доступ — роль застройщика + permission
dev-stats.view; фильтрDeveloperIdобязателен и = актор (fail-closed). Воронка агрегатна — персональные данные лидов/клиентов не раскрываются. - Прямой доступ к ClickHouse отсутствует; чтение — через
DevStats.Api. Не source of truth (операционные данные — вfavorites/selections/crm).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Модели ответа dev-stats (типизированные ряды / точки / сводки)
- API (1): GET /api/v1/dev-stats/conversion-funnel — Воронка конверсии (просмотр→сделка)
- Use Cases (2): UC-STA-004. Воронка конверсии «просмотр → сделка», UC-STA-005. Экспорт отчёта статистики (Excel / PDF) с аудитом
- Консюмеры (9): Consumer: DevStatsActionTrackedConsumer, Consumer: DevStatsFunnelBookingCreatedConsumer, Consumer: DevStatsFunnelBookingPaidConsumer, Consumer: DevStatsFunnelDealCompletedConsumer, Consumer: DevStatsFunnelDealCreatedConsumer, Consumer: DevStatsFunnelFavoriteAddedConsumer, Consumer: DevStatsFunnelLeadCreatedConsumer, Consumer: DevStatsFunnelSelectionItemAddedConsumer, Consumer: DevStatsPropertyObjectViewEndedConsumer
- Отчёты (1): Отчет/Export: dev-stats-export (выгрузка аналитики застройщика)
- Дашборды (1): Панель Метрик Застройщика: dev-stats-overview (аналитика внимания)
- index.md (1): DevStats Service (Pin.DevStats)
- facts (2): property_object_view_daily (ClickHouse rollup / materialized view), PropertyObjectViewFact (ClickHouse)
- registries (1): Реестр сущностей PIN
Связанные документы
- Просмотры: property-object-view-facts.md; дневной rollup просмотров: property-object-view-daily-rollup.md.
- Измерения: ../dimensions/property-object.md.
- События-источники:
docs/06-services/favorites/events/favorite-added.md,docs/06-services/crm/events/{lead-created,booking-created,booking-paid,deal-created,deal-completed}.md. - Шаблон: ../../../99-templates/clickhouse-fact-template.md.
- ADR-0057 (воронка обязательна), ADR-0056 (simplicity-first).