Consumer: DevStatsFunnelFavoriteAddedConsumer
Наполняет этап Favorites роллапа-воронки funnel_daily: каждое
добавление объекта недвижимости в избранное → uniqState(FavoriteId) по срезу (иерархия × дата). Обогащение иерархией
(DeveloperId → … → LayoutId) — из dim_property_object по PropertyObjectId (событие несёт PropertyObjectId, но
не иерархию). Часть воронки views → favorites → selections → leads → bookings → deals (ADR-0057).
Назначение
Consumer DevStatsFunnelFavoriteAddedConsumer слушает событие favorite-added (топик
pin.favorites.favorite-added.v1), по PropertyObjectId резолвит иерархию объекта недвижимости из
dim_property_object и добавляет частичную строку funnel_daily с заполненным состоянием этапа
Favorites; прочие этапы — пустые *State(). Запись — append-only в аналитическое хранилище
pin_dev_stats, не операционный source of truth (данные избранного — в favorites).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | dev-stats (DevStats.Background) |
| Consumer | DevStatsFunnelFavoriteAddedConsumer |
| Обрабатываемое событие | favorite-added |
| Topic | pin.favorites.favorite-added.v1 |
| ContractVersion | 1 (при несовпадении — warn-and-proceed) |
| Consumer group | dev-stats-funnel-favorites |
| Producer | favorites (добавление в избранное / импорт избранного) |
| Эффект (событие → что меняется) | pin_dev_stats.funnel_daily, этап Favorites = uniqState(FavoriteId) |
| Feature flag | dev-stats.funnel.enabled |
Контекст события
- Из метаданных события доступны идентификатор сообщения (
MessageId), время события (CreatedAt) и идентификатор источника (AggregateId = FavoriteId). - Сквозной трейс восстанавливается из метаданных события.
- Ключ идемпотентности —
uniqState(FavoriteId)(бизнес-ключ этапа) + отсев повторногоMessageIdв батче.
Входные данные / payload события
Контракт: event: favorite-added. Consumer читает только:
| Поле payload | Тип | Обяз. | Как используется |
|---|---|---|---|
FavoriteId | Guid | Да | бизнес-ключ этапа → uniqState(FavoriteId) (идемпотентность) |
PropertyObjectId | Guid | Да | lookup dim_property_object → иерархия (DeveloperId … LayoutId) |
OccurredAt | DateTimeOffset | Да | EventDate = toDate(OccurredAt) — ключ дня |
UserId/AnonymousId/Note в воронке не используются (воронка про конверсию объекта недвижимости, не сегмент/PII).
Алгоритм
Консюмер батчевый: шаги 1–4 — на сообщение, шаги 5–6 — на батч (порог ≤ 1000 строк или 5 секунд; freshness SLA ≤ 5 мин).
- Проверить контекст события: сверить
ContractVersion(=1; при несовпадении — warn-and-proceed); идемпотентность поFavoriteId/MessageId— повтор пропускается (no-op). - Проверить payload: заполнены
FavoriteId/PropertyObjectId/OccurredAt;OccurredAt == defaultили> now() + 1 ч— расценивается как битый payload (skip + warning); иначе — пропуск с warning (skip + warning). - Обогащение иерархией: по
PropertyObjectIdвзять изdim_property_objectпоследнюю версию путиDeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId(argMax(…, UpsertedAt), безFINAL); строки нет → late-dimension: отложенное повторное обогащение (park/deferred re-enrich, backoff, ≤ N попыток; при устойчивом промахе → DLQ + алерт «dimension gap», см. funnel-daily-rollup). Просмотр/этап не теряется — восстанавливается после backfill измерения и replay. - Собрать частичную строку
funnel_daily: ключ(DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate),Favorites = uniqState(FavoriteId), прочие этапы — пустые*State(). - Пакетно вставить строку в хранилище
pin_dev_stats.funnel_daily(батч ≤ 1000 строк / 5 с);AggregatingMergeTreeсольёт частичные состояния по ключу. Смещение (offset) фиксируется только после подтверждённой вставки. - Записать метрики/логи; успех → ack, контролируемая ошибка → retry/DLQ.
Диаграмма
Идемпотентность и replay
- Business key:
FavoriteId(uniqState) — повтор события не увеличивает счётчик этапа; дополнительно отсевMessageIdв батче. Replay топика/переигровка с offset безопасны (вставка идемпотентна). - Late-dimension (
dim_property_objectещё не пришёл): стратегия park/deferred re-enrich — как уproperty_object_view_facts.
Retry и DLQ
| Сбой | Retry | DLQ | Ручное действие |
|---|---|---|---|
| Транзиент ClickHouse / пакетной вставки | Да (offset не закоммичен) | Нет | авто-догон по offset |
Нет строки dim_property_object (late-dimension) | deferred re-enrich (backoff, ≤ N попыток) | Да при устойчивом промахе + алерт «dimension gap» | backfill dim_property_object, затем replay из DLQ |
Битый payload (нет PropertyObjectId / невалидный OccurredAt) | Нет | Нет | skip + warning (данные невосстановимы) |
Версия события ≠ ContractVersion | Не сбой: warning-лог | Нет | сверить миграцию контракта favorites |
| Сбой между вставкой и commit offset | Повторная доставка (at-least-once) | Нет | не требуется — uniqState(FavoriteId) идемпотентен |
Наблюдаемость и безопасность
- Logs: consumed / skipped-duplicate / enriched / deferred / failed. Metrics: consume duration, batch size, ingest lag, retries, duplicates. Traces: consume + enrich + ingest spans (корреляция из метаданных события).
- PII: нет (
UserId/AnonymousIdв воронку не пишутся); tenantDeveloperIdклеймится изdim_property_object(fail-closed при отсутствии). Воронка агрегатна — персональные данные не раскрываются.
Кросс-сервисные контракты
| Направление | Контракт |
|---|---|
Ожидает от favorites | Событие favorite-added (pin.favorites.favorite-added.v1, ContractVersion = 1, at-least-once). Гарантии: FavoriteId уникален и стабилен на факт добавления (повтор доставки — тот же id → идемпотентность этапа); PropertyObjectId указывает на существующий (в перспективе) объект недвижимости; OccurredAt — бизнес-время добавления. Удаление из избранного (RemoveFromFavorites) этап воронки не уменьшает (воронка кумулятивна по факту интереса). |
Ожидает от chessboard (через собственный dim-ingest) | Строку dim_property_object с полной иерархией DeveloperId → … → LayoutId и DeveloperId = TenantId. При отставании реплики — late-dimension (park). |
| Предоставляет | Частичную строку funnel_daily: этап Favorites = uniqState(FavoriteId) по срезу (иерархия × EventDate); прочие этапы — пустые state. Потребитель — read API get-conversion-funnel (uniqMerge, без FINAL). Порядок прихода этапов воронки роли не играет — AggregatingMergeTree сливает состояния по ключу. |
| Окно eventual-consistency | ≤ 5 мин (event → видимость в API). В промежутке застройщик видит конверсию favorites/views заниженной по этому объекту — она догоняет после batch-вставки/enrich; при parked-событии — после backfill измерения. |
Предусловия и постусловия
- Предусловия: включён флаг
dev-stats.funnel.enabled; для объекта наполнена строкаdim_property_object(иначе событие паркуется, а не теряется); событие проходит валидацию payload. - Постусловия: в
funnel_dailyпо срезу (иерархия объекта, деньOccurredAt) состояниеFavoritesучитываетFavoriteIdровно один раз (независимо от числа доставок); offset группыdev-stats-funnel-favoritesпродвинут строго после подтверждённой вставки; операционные данныеfavoritesне изменены (запись только в аналитическое хранилище).
Acceptance Criteria
- Happy path: Дано — флаг включён,
dim_property_objectдляPO1есть; Когда — приходитfavorite-added(F1,PO1, деньD); Тогда —uniqMerge(Favorites)по срезуPO1 × Dувеличивается на 1, иерархия соответствуетdim_property_object. - Идемпотентность (повтор доставки): Дано —
F1уже учтён; Когда — то же событие доставлено повторно; Тогда —Favoritesне меняется, offset продвинут (нет двойного счёта). - Late-dimension: Дано — строки
dim_property_objectдляPO1ещё нет; Когда — событие обработано; Тогда — сообщение паркуется и не влияет на счётчик; после появления измерения и replay — этап дозаполняется; при устойчивом промахе (> N попыток) — DLQ + алерт «dimension gap». - Флаг выключен: Дано —
dev-stats.funnel.enabled = off; Когда — приходит событие; Тогда — этап не наполняется, событие ack/skip без записи. - Битый payload: Дано — событие без
PropertyObjectIdили сOccurredAtв будущем; Когда — обработка; Тогда — skip + warning, offset продвинут, агрегаты не искажены. - Отказ хранилища: Дано — ClickHouse недоступен; Когда — batch-вставка падает; Тогда — offset не коммитится, retry с backoff; после восстановления — догон без дублей.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События dev-stats, Event Contract: pin.favorites.favorite-added (FavoriteAddedKafkaEvent)
- Консюмеры (1): Consumer: DevStatsFunnelSelectionItemAddedConsumer
- index.md (1): DevStats Service (Pin.DevStats)
- facts (1): funnel_daily (ClickHouse rollup / воронка)
Связанные документы
- Роллап-цель: funnel-daily-rollup.md; измерение: dim_property_object.
- Контракт события: favorites/events/favorite-added.md.
- Соседи-воронки: selection-item-added.md, lead-created.md.
- ADR-0057, ADR-0056.