Consumer: DevStatsFunnelLeadCreatedConsumer
Наполняет этап Leads воронки funnel_daily: создание лида →
uniqState(LeadId) по срезу (иерархия × дата). Обогащение иерархией — из dim_property_object
по PropertyObjectId. Часть views → favorites → selections → leads → bookings → deals (ADR-0057).
Зависимость по контракту (важно). Текущий
pin.crm.lead-created.v1не несётPropertyObjectId(payload:LeadId,TenantId = AgencyCompanyId,AgentUserId,InvestorUserId,ClientId,Source,ReferralLinkId, даты) — лид это пара «инвестор↔агент», объект недвижимости у него появляется только по источнику (BookingRequested/ConsultationRequested, дом lead.md). ЭтапLeadsворонки требует объект недвижимости для привязки к иерархии застройщика, поэтому нужен additive-onlyOriginPropertyObjectId: Guid?в контракте (без bump версии). Пока поле не выпущено — consumer выключен флагомdev-stats.funnel.enabled, лиды без резолвимого объекта недвижимости не учитываются в объект недвижимости-уровневой воронке (счётчикLeadsпуст, конверсия пропускает этап). Ниже алгоритм описан для контракта сOriginPropertyObjectId.
Назначение
Consumer DevStatsFunnelLeadCreatedConsumer слушает событие lead-created (топик pin.crm.lead-created.v1),
по OriginPropertyObjectId резолвит иерархию из dim_property_object и добавляет частичную строку
funnel_daily с заполненным состоянием этапа Leads; прочие этапы — пустые *State(). Запись —
append-only в аналитическое хранилище pin_dev_stats, не source of truth (лиды — в crm).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | dev-stats (DevStats.Background) |
| Consumer | DevStatsFunnelLeadCreatedConsumer |
| Обрабатываемое событие | lead-created |
| Topic | pin.crm.lead-created.v1 |
| ContractVersion | 1 (warn-and-proceed при несовпадении) |
| Consumer group | dev-stats-funnel-crm-lead |
| Producer | crm (лид-заявка / referral-attach / автосоздание при фиксации) |
| Эффект (событие → что меняется) | pin_dev_stats.funnel_daily, этап Leads = uniqState(LeadId) |
| Feature flag | dev-stats.funnel.enabled (ждёт OriginPropertyObjectId) |
Контекст события
- Из метаданных события доступны идентификатор сообщения (
MessageId), время события (CreatedAt) и идентификатор источника (AggregateId = LeadId). - Сквозной трейс восстанавливается из метаданных события.
- Ключ идемпотентности —
uniqState(LeadId)+ отсев повторногоMessageIdв батче.
Входные данные / payload события
Контракт: event: lead-created. Consumer читает:
| Поле payload | Тип | Обяз. | Как используется |
|---|---|---|---|
LeadId | Guid | Да | бизнес-ключ этапа → uniqState(LeadId) |
OriginPropertyObjectId | Guid? | Условно | lookup dim_property_object → иерархия; null → лид не учитывается в объект недвижимости-воронке (skip) |
CreatedAt | DateTimeOffset | Да | EventDate = toDate(CreatedAt) |
TenantId(=AgencyCompanyId), ФИО/PII — в воронке не используются (агрегат по объекту недвижимости застройщика).
Алгоритм
Консюмер батчевый: шаги 1–4 — на сообщение, шаги 5–6 — на батч (порог ≤ 1000 строк или 5 секунд; freshness SLA ≤ 5 мин).
- Проверить контекст события: сверить
ContractVersion(=1; при несовпадении — warn-and-proceed); идемпотентность поLeadId/MessageId— повтор пропускается (no-op). - Проверить вход:
OriginPropertyObjectIdзаполнен → продолжаем;null(или поле отсутствует в контракте до его выпуска) → пропуск (лид без объекта недвижимости вне объектной воронки, skip + счётчикskip-по-OriginPropertyObjectId). - Обогащение иерархией: по
OriginPropertyObjectIdвзять изdim_property_objectпоследнюю версию пути (argMax(…иерархия…, UpsertedAt), безFINAL); строки нет → отложенное повторное обогащение (deferred re-enrich, backoff, ≤ N попыток; при устойчивом промахе → DLQ + алерт «dimension gap»). - Собрать частичную строку
funnel_daily: ключ(DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate),Leads = uniqState(LeadId), прочие этапы — пустые*State(). - Пакетно вставить строку в хранилище
funnel_daily(батч ≤ 1000 строк / 5 с);AggregatingMergeTreeсольёт состояния по ключу. Смещение (offset) фиксируется только после подтверждённой вставки. - Записать метрики/логи; успех → ack, контролируемая ошибка → retry/DLQ.
Диаграмма
Идемпотентность и replay
- Business key
LeadId(uniqState) — повтор не растит счётчик; +MessageIdв батче. Replay с offset безопасен. Late-dimension → deferred re-enrich (какproperty_object_view_facts).
Retry и DLQ
| Сбой | Retry | DLQ | Ручное действие |
|---|---|---|---|
| Транзиент ClickHouse | Да (offset не закоммичен) | Нет | авто-догон |
Нет строки dim_property_object (late-dimension) | deferred re-enrich (backoff, ≤ N попыток) | Да при устойчивом промахе + алерт «dimension gap» | backfill dim_property_object, затем replay из DLQ |
OriginPropertyObjectId = null / отсутствует в контракте | Нет | Нет | skip + warning (см. зависимость выше) |
Версия события ≠ ContractVersion | Не сбой: warning-лог | Нет | сверить миграцию контракта crm |
| Сбой между вставкой и commit offset | Повторная доставка (at-least-once) | Нет | не требуется — uniqState(LeadId) идемпотентен |
Наблюдаемость и безопасность
- Logs/metrics/traces — consume/enrich/ingest, счётчик skip-по-
OriginPropertyObjectId. PII (ФИО/телефон/ClientId) в воронку не пишется;DeveloperIdклеймится изdim_property_object(fail-closed). Воронка агрегатна.
Кросс-сервисные контракты
| Направление | Контракт |
|---|---|
Ожидает от crm | Событие lead-created (pin.crm.lead-created.v1, ContractVersion = 1, at-least-once). Обязательные: LeadId (уникален и стабилен на лид → идемпотентность этапа), CreatedAt. Требуется additive-only OriginPropertyObjectId: Guid? — без него привязка к иерархии застройщика невозможна (см. блок «Зависимость по контракту»). TenantId = AgencyCompanyId — это агентство лида, не застройщик, и в воронку не переносится; застройщик резолвится из иерархии объекта. ФИО/телефон/ClientId — не переносятся. |
Ожидает от chessboard (через dim-ingest) | Строку dim_property_object с иерархией и DeveloperId для OriginPropertyObjectId; при отставании — late-dimension (park). |
| Предоставляет | Частичную строку funnel_daily: Leads = uniqState(LeadId) по срезу (иерархия × EventDate). Потребитель — read API get-conversion-funnel (uniqMerge, без FINAL). |
| Окно eventual-consistency | ≤ 5 мин. Пока OriginPropertyObjectId не выпущен producer-ом — этап Leads объектной воронки пуст (конверсия пропускает этап; зафиксировано в блоке зависимости и «Известных ограничениях» README). После выпуска и включения флага — этап наполняется, историческая глубина восстанавливается replay/backfill. |
Предусловия и постусловия
- Предусловия: включён флаг
dev-stats.funnel.enabled; контрактlead-createdнесётOriginPropertyObjectId; для объекта наполнена строкаdim_property_object; событие валидно. - Постусловия: для лида с резолвимым объектом — в
funnel_dailyпо срезу (иерархия объекта, деньCreatedAt) состояниеLeadsучитываетLeadIdровно один раз; лид без объекта — не учитывается в объектной воронке (счётчик skip); offset группыdev-stats-funnel-crm-leadпродвинут после подтверждённой вставки; операционные данныеcrmне изменены.
Acceptance Criteria
- Happy path: Дано — флаг включён, контракт с
OriginPropertyObjectId,dim_property_objectдляPO1есть; Когда — приходитlead-created(L1,OriginPropertyObjectId = PO1, деньD); Тогда —uniqMerge(Leads)по срезуPO1 × Dувеличивается на 1. - Идемпотентность (повтор доставки): Дано —
L1учтён; Когда — повторная доставка; Тогда —Leadsне меняется. - Лид без объекта: Дано —
OriginPropertyObjectId = null(или поле ещё не в контракте); Когда — обработка; Тогда — событие skip + инкремент счётчикаskip-по-OriginPropertyObjectId, объектная воронка не изменяется. - Late-dimension: Дано — строки
dim_property_objectнет; Тогда — park → дозаполнение после измерения; при устойчивом промахе — DLQ + алерт. - Отказ хранилища: Дано — ClickHouse недоступен; Тогда — offset не коммитится, retry с backoff, догон без дублей.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (1): События dev-stats
- Консюмеры (2): Consumer: DevStatsFunnelFavoriteAddedConsumer, Consumer: DevStatsFunnelSelectionItemAddedConsumer
- index.md (1): DevStats Service (Pin.DevStats)
- facts (1): funnel_daily (ClickHouse rollup / воронка)
Связанные документы
- Роллап: funnel-daily-rollup.md; измерение: dim_property_object.
- Контракт/дом: crm/events/lead-created.md, crm/domain/lead.md.
- Соседи: booking-created.md, deal-created.md.
- ADR-0057, ADR-0056.