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

GET /api/v1/dev-stats/conversion-funnel — Воронка конверсии (просмотр→сделка)

Назначение

Воронка конверсии недвижимости застройщика: просмотр → избранное → подборка → лид → бронь → сделка (со ступенями bookingsPaid/dealsCompleted) — как внимание к его объектам превращается в лиды и сделки. Источник блока «Воронка» на дашборде застройщика (../../README.md, метрика «Воронка»). Считается из дневного rollup-а funnel_daily (AggregatingMergeTree, uniqMerge по этапам, без FINAL); конверсии между этапами — производные при чтении. За флагом dev-stats.funnel.enabled (нижние этапы активируются по мере готовности источников selections/crm).

Metadata

ПолеЗначение
Сервис/контекстdev-stats
API areadevelopers
Feature groupconversion-funnel
Статусdraft
Документdocs/06-services/dev-stats/api/developers/get-conversion-funnel.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Bearer JWT; роль застройщика — developer)
Auth policy / Permissionspermission dev-stats.viewidentity/domain/permission.md; feature flag dev-stats.funnel.enabled
Scope (RBAC)организация застройщика; DeveloperId строго = актор (fail-closed)
Record-levelне применяется (агрегаты)
Tenant isolationTenantId = DeveloperId обязателен и = актору
Auditnone (read-агрегаты; персональные данные лидов/клиентов не выдаются)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/dev-stats/conversion-funnel
Success status200
Idempotency headerне применяется (safe GET)
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

Общий вход — DevStatsFilterModel (../../domain/dev-stats-filter.md). Воронка не разбивается по типу зрителя (это про конверсию объектов недвижимости, не про сегмент аудитории — см. funnel_daily): ViewerKind/ViewSource игнорируются.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
DeveloperIdidentity claimsGuidДа!= Guid.Empty; = актор (иначе Forbidden)Tenant isolation
DateFromquery dateFromDateOnlyДаISO; DateFrom <= DateTo
DateToquery dateToDateOnlyДаISO; ≤ сегодня (UTC)
ProjectIdquery projectIdGuid?Нетreference dim_project (существование не проверяется)
BuildingIdquery buildingIdGuid?Неттребует ProjectId
LayoutGroupIdquery layoutGroupIdGuid?Нет
LayoutIdquery layoutIdGuid?Нет
PropertyObjectIdquery propertyObjectIdGuid?Нет

Модель ответа ConversionFunnelResult

ПолеТипОбязательностьИсточникMaskingОписание
dateFromstring (ISO date)ДаechoНачало периода.
dateTostring (ISO date)ДаechoКонец периода.
stagesIReadOnlyList<ConversionStageModel>Даагрегация funnel_dailyЭтапы воронки по порядку (views→…→dealsCompleted).
viewToDealPercentdecimal?Нетround1(deals / views × 100); null при views=0Сквозная конверсия просмотр→сделка, %.

ConversionStageModel

ПолеТипОбязательностьИсточникОписание
stagestring (enum wire)Даviews/favorites/selections/leads/bookings/bookingsPaid/deals/dealsCompletedЭтап воронки.
countlongДаuniqMerge(<StageState>)Уникальный счётчик этапа.
conversionFromPrevPercentdecimal?Нетround1(count / prev.count × 100); null для views или prev=0Конверсия от предыдущего этапа, %.
conversionFromViewsPercentdecimal?Нетround1(count / views × 100); null при views=0Конверсия от вершины (просмотров), %.
enabledboolДафлаг источника этапаfalse для этапов, чей источник ещё не активирован (selections — future) — count=0, конверсии null.

Enum этапов FunnelStage: Views/Favorites/Selections/Leads/Bookings/BookingsPaid/Deals/ DealsCompleted (wire — нижний регистр).

Предусловия и согласованность данных

  • Пользователь застройщика авторизован; есть permission dev-stats.view; scope DeveloperId = актор (INV-STA-1, fail-closed — ../../README.md). Флаг dev-stats.funnel.enabled включён, иначе FeatureDisabled (404).
  • Этапы funnel_daily наполняются разными продюсерами и приходят как событийная цепочка (все асинхронно, каждый со своим окном свежести):
    • views — из pin.user-activity.property-object-view-ended.v1 (верхний этап, считается независимо);
    • favorites — из pin.favorites.favorite-added.v1;
    • selections — из pin.selections.selection-item-added.v1;
    • leads/bookings/bookingsPaid/deals/dealsCompleted — из pin.crm.{lead-created,booking-created,booking-paid,deal-created,deal-completed}.v1.
  • Порядок появления этапов между собой не согласован: нижние этапы отстают сильнее верхних, поэтому в промежуточном состоянии воронка может показывать «сужение больше реального» — это eventual consistency (INV-STA-5), а не ошибка. Счётчики этапов идемпотентны к повтору по бизнес-ключу этапа.
  • Этап, чей источник ещё не активирован, отдаётся с enabled=false, count=0 и конверсиями null (не исключается из ответа — стабильная форма).
  • Операция read-only: состояние не меняется, события не публикуются.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь застройщика (роль developer) с правом dev-stats.view; воронка доступна только при включённом флаге dev-stats.funnel.enabled (иначе FeatureDisabled 404). DeveloperId берётся из identity-claims актора; если клиент прислал иной id → отказ Forbidden (403).
  2. Проверка входа. DateFrom <= DateTo. Существование иерархических id не проверяется: неизвестный id даёт нулевую воронку.
  3. Что читаем. Дневной rollup funnel_daily за период по scope. Scope-фильтр — префикс-скан по ключу (DeveloperId, ProjectId, BuildingId, LayoutGroupId, LayoutId, PropertyObjectId, EventDate); диапазон дат режет партиции toYYYYMM(EventDate). Один итог на весь scope (GROUP BY ()) — разбивки по ViewerKind в воронке нет.
  4. Расчёты и маппинг. Меры каждого этапа — uniqMerge(Views), uniqMerge(Favorites), uniqMerge(Selections), uniqMerge(Leads), uniqMerge(Bookings), uniqMerge(BookingsPaid), uniqMerge(Deals), uniqMerge(DealsCompleted) (dedup-tolerant, без FINAL — идемпотентно к at-least-once по бизнес-ключам этапов). Конверсии — производные: conversionFromPrev = count / prevCount, conversionFromViews = count / views, viewToDeal = deals / views (guard знаменателя > 0). enabled этапа = готовность источника (например selectionsfalse до активации события selection-item-added).
  5. Результат. Возвращается ConversionFunnelResult (200): stages и viewToDealPercent. Freshness ≤ 5 мин (верхний этап views считается независимо от нижних); недоступность ClickHouse → DEV_STATS_UNAVAILABLE (503).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Невалидный диапазон/иерархияОшибка валидации запросаno retry
UNAUTHORIZED401Нет JWTТребуется авторизацияno retry
Forbidden403DeveloperId != актор или нет dev-stats.viewДоступ запрещёнno retry
FeatureDisabled404dev-stats.funnel.enabled выключенВоронка недоступнаno retry
DEV_STATS_UNAVAILABLE503ClickHouse недоступен / timeoutАналитика временно недоступнаretry с backoff

Пустая воронка (нет активности) — не ошибка: 200, stages с нулями, конверсии null.

Acceptance Criteria

  • Полная воронка. Дано авторизованный застройщик, включённый флаг и период с активностью. Когда GET /conversion-funnel. Тогда 200, stages в порядке views→…→dealsCompleted, для каждого этапа conversionFromPrevPercent/conversionFromViewsPercent, плюс сквозная viewToDealPercent.
  • Флаг выключен. Дано dev-stats.funnel.enabled=false. Когда запрос. Тогда 404 FeatureDisabled (воронка недоступна).
  • Этап без источника. Дано источник selections ещё не активирован. Когда запрос. Тогда его этап — enabled=false, count=0, конверсии null; остальные этапы считаются нормально.
  • Guard конверсий. Дано views=0 (или prev=0 у этапа). Когда считаются доли. Тогда соответствующие conversionFrom*Percent/viewToDealPercent = null, а не деление на ноль.
  • Верхний этап независим. Дано нижние этапы ещё отстают (лаг crm-консюмера). Когда запрос. Тогда views корректны, нижние счётчики монотонно догоняют по мере ingestion (INV-STA-5) — промежуточное «пере-сужение» не ошибка.
  • Без разбивки по зрителю. Дано заданы viewerKind/viewSource. Когда запрос. Тогда они игнорируются (воронка — про конверсию объектов, не про сегмент аудитории).
  • Пустая воронка. Дано нет активности в scope. Когда запрос. Тогда 200, stages с нулями, конверсии null (не ошибка).
  • Чужой scope / нет права. Дано чужой DeveloperId или нет dev-stats.view. Когда запрос. Тогда 403 Forbidden (INV-STA-1).
  • Недоступность витрины. Дано ClickHouse недоступен. Когда запрос. Тогда 503 DEV_STATS_UNAVAILABLE (retry с backoff).

Совместимость и наблюдаемость

  • Новые optional поля/этапы — additive (при append в хвост); удаление/переименование/смена порядка этапов (stage wire) — breaking (версия метода). Пока источник этапа не активирован — enabled=false, а не breaking-исключение этапа.
  • Logs: conversion-funnel-read (developerId, scope-level, elapsed; PII нет). Metrics: dev_stats_conversion_funnel_requests_total, latency histogram (SLO p95 ≤ 300 мс). Traces: span приёма → span обработки → span чтения ClickHouse; correlation — сквозной trace traceparent.

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

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

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