Critical · business-definition risk
Registration denominator включает removed/test/admin пользователей
SQL берет всех пользователей из global_users__ads FINAL и для каждого делает Registration row; фильтра по is_removed/group_name нет. Live: 3,207,201 registration rows; из них 224,349 имеют is_removed=1. В source-users дополнительно 82,868 строк с group_name IN ('Test account','Admin'). В lifecycle нет group_name, значит test/admin нельзя убрать downstream без join-а обратно к users.
Evidence: lifecycle_report__dm.sql:61-77,162-195; global_traffic_quality__dm.sql:41-47 фильтрует is_removed=0; psp_monitoring_hourly__cdm.sql:87-89 исключает Test/Admin.
Critical · silent truncation
Витрина хранит только первые 10 успешных депозитов
Код явно режет successful_deposit_number <= 10 и success_group <= 10. Live сверка: source ads.global_payments__ads содержит 10,756,517 успешных deposit orders; lifecycle показывает 2,105,445 success rows. Missing 8,651,072 orders — это ровно successes beyond the 10th у 112,708 пользователей.
Это может быть intended для lifecycle funnel, но тогда должно быть в названии/описании и явно запрещено использовать как “all successful deposits”. Evidence: lifecycle_report__dm.sql:114,153.
High · aggregation trap
stage коллидирует: “N Attempted” и “N Deposit” имеют один номер
1 Attempted Deposit и 1 Deposit оба лежат на stage=2; и так для N=1..10. Live: 1,662,038 orders представлены одновременно как attempt и success rows с тем же sur_order_id. Если BI агрегирует по stage, attempts и successes смешиваются/дублируются.
Evidence: lifecycle_report__dm.sql:102,141,185; live event distribution checked 2026-06-29.
High · metadata/grain mismatch
unique_key='sur_user_id' не соответствует grain
Declared key/order_by — только sur_user_id, но фактический grain: (sur_user_id, event_name / funnel event). Live: 7.76M rows на 3.21M users; docs сами описывают “одна строка на (sur_user_id, событие воронки)”. На plain ReplicatedMergeTree это не enforcing bug, но вводит в заблуждение тесты, каталог и будущих maintainers.
Evidence: lifecycle_report__dm.sql:3-7,197-203; catalog dm__lifecycle_report__dm.md:42-44.
Medium · field semantics
failed_deposit_number ранжируется по каждому non-SUCCESS статусу отдельно
Поле называется как порядковый номер failed deposit, но window partition = (sur_user_id, status_name), поэтому CANCEL #1 и EXPIRED #1 у одного пользователя оба получают failed_deposit_number=1. Live: 36,955 users имеют duplicate failed-number groups.
Evidence: lifecycle_report__dm.sql:52-56; non-success attempt statuses: CANCEL 481,670 rows, EXPIRED 303,372 rows.
Medium · dictionary dependency
Пустой brand_name из словаря брендов
Live: 94 rows have empty brand_name, concentrated in brand_id=405, 406 on luxury and 55 on main. DAG depends only on users+payments datasets, not on brand dictionary freshness; new brands can land blank.
Evidence: dictGetString('dicts.global_brands_dict', ...) in lifecycle_report__dm.sql:16-19,65-68; DAG schedule dags/lifecycle_report__dm.py:21.
Low · code fragility
Registration branch uses empty join to synthesize payment-shaped NULLs
successful_deposits_empty AS (SELECT * ... WHERE 1=0) is joined via USING (sur_user_id) so Registration rows inherit the union schema. It works today, but is fragile and makes unqualified columns hard to reason about. Safer: explicit NULL casts for payment columns and u.-qualified user fields.
Evidence: lifecycle_report__dm.sql:156-160,162-195. Claude subreview flagged analyzer/ambiguity risk; live build currently succeeds.
Low · monitoring blind spot
Freshness check monitors registration_date, not payment freshness
Monitoring config tracks max registration_date for lifecycle. This proves new registered users arrive, but does not prove new payment attempts/successes are present. For a payment lifecycle mart, add a payment-created freshness/data-volume DQ check too.
Evidence: pipelines/configs/monitoring_config.py:37-45.