惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
The Blog of Author Tim Ferriss
Microsoft Azure Blog
Microsoft Azure Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
人人都是产品经理
人人都是产品经理
Vercel News
Vercel News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
月光博客
月光博客
博客园 - 聂微东
GbyAI
GbyAI
C
Check Point Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
T
The Exploit Database - CXSecurity.com
Google DeepMind News
Google DeepMind News
Simon Willison's Weblog
Simon Willison's Weblog
G
GRAHAM CLULEY
雷峰网
雷峰网
I
Intezer
E
Exploit-DB.com RSS Feed
W
WeLiveSecurity
Engineering at Meta
Engineering at Meta
S
Securelist
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 司徒正美
N
News and Events Feed by Topic
Webroot Blog
Webroot Blog
Recent Announcements
Recent Announcements
SecWiki News
SecWiki News
T
Tor Project blog
Security Latest
Security Latest
博客园 - Franky
S
Schneier on Security
A
Arctic Wolf
Y
Y Combinator Blog
L
LINUX DO - 最新话题
The GitHub Blog
The GitHub Blog
The Register - Security
The Register - Security
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Сквозная аналитика B2C на коленке: Google Sheets, Python и Claude Code за две недели
Дмитрий Волошин · 2026-06-15 · via Все публикации подряд на Хабре

Средний

13 мин

2

TL;DR

  • Сквозная аналитика для B2C - это связать «клик в рекламе → заявку → оплату → выручку → ROMI». Звучит просто, ломается на стыках.

  • Ядро MVP - три источника (реклама + веб-аналитика + биллинг), Google Sheets как витрина и пара экранов в BI - реально собирается за ~две недели. Claude Code тут дает ×5 к скорости: разведка незнакомых API, ETL, парсинг, тесты, бойлерплейт.

  • Атрибуция, метрики и грабли конкретных API - это то, где LLM даже вредит. Его скорость работы усложняет задачу анализа и осмысления.

  • Технически интересного по дороге набралось много: Google Sheets в роли БД, фаззи-резолвинг namespace, идемпотентный upsert, классификация ошибок для ретраев, fail-closed гейт свежести, read-only как защита от собственного кода и от LLM-агента.

Минимальная сквозная: три источника, которые замыкают «клик → деньги»

Прежде чем разбирать, что в этом сюжете интересного, покажу скучную часть - то, что собирается за две недели и работает. Чтобы получить именно сквозную воронку, а не «дашборд по расходам», нужно минимум три источника, и они должны смыкаться в деньги:

  1. Рекламный кабинет (у меня - Яндекс.Директ): расход, клики, показы, CTR, CPC по кампаниям. Это «сколько потратили».

  2. Веб-аналитика (Метрика): источники трафика и динамика. С важной оговоркой - только как leading-сигнал тренда, не для абсолютов (про это ниже).

  3. Биллинг (своя админка и БД): оплаты с UTM-привязкой и выручка. Это «сколько вернулось».

Связка «реклама ↔ биллинг» по UTM - и есть та самая сквозная нить. Остальные источники (CRM, Telegram Ads, VK Ads, вебинарная платформа) - это уже наращивание охвата, их добавляешь по одному позже. Но три перечисленных дают замкнутый контур «потратили → вернулось» - минимально достаточный для ROMI.

Поток данных в MVP простой:

[Директ API]  ─┐
[Метрика API] ─┼──>  Python ETL  ──>  Google Sheets (витрина)  ──>  BI-дашборд
[Биллинг БД]  ─┘      (pandas)         один лист = одна сущность

Никаких Kafka, Airflow и DWH. На старте они только тормозят. Оркестратор - cron, хранилище - Google Sheets, «процессор» - Python с pandas. Это и есть «на коленке», и для MVP этого достаточно.

С чего все началось: где трех источников недостаточно

Звучит так, будто описанная выше схема и закрывает задачу. Если бы это было так, статья закончилась бы абзацем выше, а у меня ушли бы те самые две недели вместо уже месяцев. Картина, знакомая любому, кто заходил в маркетинг B2C: данные есть, но они в пяти разных кабинетах, и ни один не отвечает на главный вопрос - окупается ли реклама.

  • Рекламный кабинет знает расход, клики, показы - но «конверсии» там это его внутренние цели (часто - регистрации на вебинар), а не реальные заявки и тем более не оплаты.

  • Веб-аналитика знает визиты и источники - но с погрешностью и без денег.

  • Биллинг знает оплаты и выручку - но не знает, из какой рекламы пришел человек.

  • CRM знает сделки - но матчится с остальным по принципу «как повезет».

«Сквозная аналитика» - это попытка склеить все это в одну воронку клик → заявка → оплата → выручка и посчитать поверх нее ROMI. Маркетологу нужен один экран; мне как разработчику нужно, чтобы цифры на этом экране не были искаженными.

Дальше я покажу вам две вещи:

  1. MVP-ядро - что собирается за пару недель (этот раздел короткий, потому что тут как раз все просто).

  2. Грабли - почему «дособрать до честной сквозной» заняло потом еще месяцы (этот раздел длинный, потому что тут все интересное).

Google Sheets как база данных (да, серьезно)

Самое спорное решение. Идея: каждый «билдер» (отдельный Python-модуль) собирает один Google-лист. build_channel_spend.py → лист «Расход по каналам», build_leads.py → лист «Лиды», и т.д. Лист - это, по сути, материализованная вьюха. Джойнов в Sheets нет - все джойны делаются в pandas в памяти, а в лист пишется уже готовая tidy-таблица.

Почему Sheets, а не сразу Postgres:

  • Ноль инфраструктуры. Сервис-аккаунт, gspread, и через 10 минут ты пишешь данные.

  • Бесплатный UI «из коробки». Маркетолог видит, правит, комментирует прямо в таблице. Для BI поверх - тоже готовый источник.

  • Прозрачность. Любой косяк виден глазами, без SELECT.

Где больно:

  • Лимиты API. Google Sheets отдает 429/503 на пиках. Лечится ретраями (см. ниже) и батч-операциями (batchGet/одна запись на лист вместо построчной).

  • Нет транзакций и типов. Числа читаются строками («1 234,56»), даты - как попало. Каждый загрузчик обязан сам приводить типы.

  • Нет джойнов и индексов. Все в pandas. На сотнях тысяч строк это ок, на десятках миллионов - уже нет.

Ключевой паттерн, который спасает от дублей при инкрементальном сборе, - идемпотентный upsert по стабильному ключу. В Sheets его пишешь руками:

def upsert_rows(existing: list[dict], fresh: list[dict], key: str) -> list[dict]:
    """Слить свежие строки в существующие по стабильному ключу (а не по позиции).
    Свежая строка с тем же key перезаписывает старую; остальные сохраняются."""
    by_key = {r[key]: r for r in existing}
    for r in fresh:
        by_key[r[key]] = r           # перезапись или добавление
    return list(by_key.values())

Сказка заканчивается, когда выберешь «ключ» неудачно. У меня было такое: ключом для рекламного объявления я взял слаг + заголовок + дата. Оказалось, у части креативов совпадали заголовок и минута создания - и upsert схлопывал разные объявления в одно. Лечится переходом на стабильный id из самой системы. Вывод из этого опыта вечен: ключ должен быть идентификатором, а не «вроде бы уникальной» комбинацией полей.

Когда Sheets перестает тянуть (у меня это случилось на этапе BI-дашборда с прямым коннектором), витрину переносишь в Postgres - но к этому моменту схема уже отлажена на Sheets, и перенос механический.

Атрибуция - это не «прикрутить UTM»

Вот здесь LLM ускоряет написание кода, но архитектуру атрибуции придумываешь сам. Это самая недооцененная часть сквозной аналитики.

Namespace-ад

Выяснилось, что «продукт» в моей системе живет в трех несводимых пространствах:

  • Слаг лендинга - кусок URL посадочной (/lessons/python-basic/);

  • utm_campaign - метка в ссылке объявления (python, py-2026, cplus);

  • Слаг в биллинге - идентификатор продукта в оплатах.

Эти три не равны друг другу 1:1. Маркетолог в utm пишет как привык; лендинг живет своей жизнью; биллинг - третьей. Чтобы посчитать «расход на продукт X», нужно сшить пространства. Решение - приоритетный резолвер: официальная карта utm → продукт → fallback на слаг лендинга → fallback на имя кампании:

def resolve_product(utm_campaign, landing_slug, campaign_name, utm_map, name_index):
    # 1) официальная карта маркетолога (приоритет)
    if (slug := utm_map.get(utm_campaign.lower())):
        return slug, "utm"
    # 2) slug посадочной (часто = course_slug)
    if landing_slug:
        return landing_slug, "slug"
    # 3) фаззи-матч по имени кампании (для архивных РК без меток)
    if (slug := fuzzy_match(campaign_name, name_index)):
        return slug, "name"
    return None, "unmatched"

Третий шаг - фаззи-матч по имени - спасает там, где меток нет вообще. Например, в одном рекламном источнике до 70% кампаний оказались лид-формами без ссылки на сайт (заявка заполняется внутри площадки) - у них нет ни utm, ни landing-slug, только название кампании. Только по имени их и привяжешь к продукту.

Важная деталь нормализации имен: нельзя схлопывать различающие символы. Я споткнулся на том, что универсальная нормализация убивала ++ и #, и C++ склеивался с C#. Пришлось делать отдельную нормализацию, которая их сохраняет:

def norm_product(s: str) -> str:
    # сохраняем ++ и #, иначе C++ == C# (реальный баг на 200+ объявлениях)
    s = s.lower().replace("++", "plusplus").replace("#", "sharp")
    return re.sub(r"[^0-9a-zа-яе]+", "", s)

Дедуп лида

Что вообще считать «лидом»? Один человек оставил заявку, заказал обратный звонок и записался на вебинар - это один лид или три? Договорились: человек × продукт = один лид, категория по приоритету. Без дедупа все воронки завышены кратно. Дедуп - обычный groupby, но определение сущности - продуктовое решение, которое ни одна LLM за тебя не примет.

First-touch / last-touch и почему ROMI считается когортно

Дальше - атрибуция касаний. Лид мог прийти по рекламе, а оплатить через месяц по прямому заходу. Если считать «выручка этого месяца / расход этого месяца», получишь мусор: лаг оплаты в B2C - 1-3 месяца. Поэтому ROMI считается по когортам: берем пользователей, пришедших из рекламы в месяце T, и смотрим их оплаты в окне T…T+3, без двойного счета.

Тут я поймал себя на ошибке: первая версия форвард-когорты давала артефактный «+200%» ROMI, потому что при стабильном расходе выручка одного месяца попадала в несколько перекрывающихся окон и считалась многократно. Переписал на помесячный срез + годовой итог. Мораль: в когортной атрибуции двойной счет - это дефолтное состояние, пока ты явно от него не защитился.

Грабли внешних API (где LLM точно не спас)

Все примеры - из подключения реальных рекламных API, и ни один из этих нюансов не написан в их документации внятно. Ну либо я их не нашел. И Claude тоже.

Грабля №1: summary-эндпоинт, который игнорирует даты

Подключаю статистику одного рекламного API. Есть два эндпоинта: summary (итоги) и day (по дням). Беру summary, передаю date_from/date_to - получаю числа. Беру другой период - получаю… те же числа до рубля.

summary молча игнорирует диапазон дат и отдает lifetime-итог за все время. Если бы я не сверил два непересекающихся окна вручную, насчитал бы расход «за месяц», равный расходу за три года. Проверка, которая это вскрыла, заняла пять строк:

old = api.stats(level="plans", date_from="2024-06-01", date_to="2025-01-01", kind="summary")
new = api.stats(level="plans", date_from="2025-05-01", date_to="2026-06-14", kind="summary")
assert sum_spent(old) != sum_spent(new), "summary игнорирует даты - это lifetime!"
# assert падает. Вывод: для оконного среза только day.

Claude Code сгенерировал мне клиент к этому API за минуты. Но «эй, а ты заметил, что summary не фильтрует по датам?» он не сказал. Это ловится только эмпирикой и паранойей. Последнее для работы с LLM – must.

Грабля №2: два разных лимита у дневной статистики и кламп из текста ошибки

Ловлю две разные ошибки, которые маскируют друг друга:

  • дневная статистика доступна только за последние ~14 месяцев (плавающий floor);

  • интервал одного запроса ограничен ~732 днями.

Хитрость: когда я пробовал floor на полном диапазоне, API возвращал ошибку про интервал (732 дня), а не про floor, и я чуть не сделал неверный вывод. Решение - динамически вытаскивать допустимую дату из текста ошибки и клампить:

FLOOR_RE = re.compile(r"minimal allowed date is '(\d{4})-(\d{1,2})-(\d{1,2})'")

def clamp_from(api, date_from, date_to, probe_id):
    # пробуем 1-дневное окно (не упремся в лимит интервала), ловим floor
    try:
        api.stats(ids=[probe_id], date_from=date_from, date_to=date_from, kind="day")
        eff = date_from
    except ApiError as e:
        if (m := FLOOR_RE.search(str(e))):
            eff = f"{int(m[1]):04d}-{int(m[2]):02d}-{int(m[3]):02d}"
        else:
            raise
    # и отдельно не даем интервалу превысить ~731 день
    span_floor = (date.fromisoformat(date_to) - timedelta(days=731)).isoformat()
    return max(eff, span_floor)

Парсить дату из текста ошибки - некрасиво, но floor плавающий (зависит от «сегодня»), а хардкодить его - значит сломаться через месяц. Лучше некрасиво и правильно.

Грабля №3: предел атрибуции - лид-формы

Уже упоминал, но повторю как принцип: у части площадок (лид-формы соцсетей) заявка живет внутри площадки, ссылки на сайт нет, UTM до биллинга не доезжает. Это значит: расход и охват по такому каналу ты видишь, а вот связать конкретное объявление с оплатой - нельзя в принципе, не из-за кривого кода, а из-за модели данных. Вывод в дашборде: для таких каналов показываем расход и охват, а не покурсовый ROMI, и прямо пишем об этом баннером. Лучше явный пробел, чем фейковая точность.

Грабля №4: API не отдает то, что показывает в кабинете

Еще пример из основного рекламного API: у адаптивных (responsive) объявлений в кабинете UTM-ссылка есть, а через API поле с этой ссылкой не отдается. То есть глазами видно, программно - нет. Лечится тем же fallback на имя кампании. Мораль: то, что есть в UI, не равно тому, что есть в API. Всегда проверяй на реальных данных, а не по докам.

Read-only как мантра (особенно когда с LLM)

Отдельный архитектурный принцип, который стал еще важнее в эпоху LLM-агентов: код никогда не пишет в рекламные кабинеты. Только GET и отчеты.

Никаких POST/PUT/DELETE ставок, бюджетов, статусов кампаний. Любая «правка» - это текстовая рекомендация маркетологу, который меняет руками. Причина простая: цена ошибки. Баг в чтении - кривой график. Баг в записи - слитый рекламный бюджет или остановленные кампании.

С LLM-агентом это перестает быть гигиеной и становится защитой контура. Когда часть кода генерит модель, ты не можешь на 100% гарантировать, что в какой-то ветке не прилетит мутирующий запрос. Поэтому ограничение - на уровне архитектуры и доступов: токены и обвязка позволяют только чтение. Агент физически не может навредить кабинету, даже если очень захочет.

Второй слой защиты от «агент насорил» - тесты чистых функций как контракт. Я жестко делю код на:

  • чистые функции (агрегация, парсинг, резолв, нормализация) - без сети и I/O, детерминированные;

  • I/O-слой (API, Sheets, БД) - тонкий, инъектируемый.

Вся бизнес-логика - в чистых функциях, и они покрыты тестами под завязку (у меня их больше тысячи). Это дает сгенерированному коду верифицируемость: LLM пишет функцию, тест ее ловит. Пример - то самое разделение, которое делает namespace-резолвер тестируемым без единого сетевого вызова:

# чистая функция - тестируется без сети
def aggregate_by_product(rows, utm_map, name_index):
    acc = {}
    for r in rows:
        slug, src = resolve_course(r["utm"], r["slug"], r["name"], utm_map, name_index)
        bucket = acc.setdefault(slug, {"spent": 0.0, "clicks": 0})
        bucket["spent"]  += to_float(r["spent"])   # API отдает строкой
        bucket["clicks"] += int(r["clicks"])
    return acc

# тест - детерминированный, без единого запроса
def test_cpp_csharp_not_merged():
    rows = [{"utm": "cplus", "slug": "", "name": "C++ разработчик", "spent": "100", "clicks": "2"}]
    out = aggregate_by_product(rows, utm_map={}, name_index=NAME_INDEX)
    assert "cpp" in out and "csharp" not in out

Сетевой слой при этом инъектируется (session или fake-клиент), так что и ретраи, и пагинацию я тестирую без реальных запросов. Чем больше кода пишет LLM, тем важнее, чтобы за ним стоял детерминированный тест. Это не «good practice ради галочки», это способ доверять машинно-сгенерированному коду.

Как пайплайн не падает по ночам

Несколько приемов, которые превратили набор скриптов в систему, которая будит меня только когда реально сломалось. Шутка. Никто меня не будит.

Fail-closed гейт свежести. Перед сборкой дневных отчетов - проверка, что входные данные биллинга реально обновились сегодня (по updated_at, а не по «файл существует»). Если данные устарели - пайплайн останавливается с ошибкой и не обновляет витрину. Это сознательный отказ от graceful degradation: лучше показать вчерашние данные с пометкой, чем сегодняшние наполовину.

Классификация ошибок для ретраев. Не все подряд ретраить. 429, 5xx, сетевые таймауты - транзиентные, ретраим с экспоненциальным бэкоффом. 401/403/404 - фатальные, ретраить бессмысленно, надо чинить:

def is_transient(exc) -> bool:
    if isinstance(exc, (ConnectionError, Timeout)):
        return True
    if isinstance(exc, HttpError) and exc.status in (429, 500, 502, 503, 504):
        return True
    return False   # 401/403/404 и прочее - наружу, это не «повтори попозже»

@retry(retry=retry_if_exception(is_transient),
       stop=stop_after_attempt(4), wait=wait_exponential(min=2, max=20))
def call(...): ...

Decoupling: внешний API не в критическом пути сборки. Грабля, на которой я обжегся: дашборд собирался одним скриптом, который последовательно строил три витрины, и одна из них дергала живой внешний API. Если этот API падал - не записывалась ни одна из трех витрин, дашборд устаревал целиком из-за одного источника. Переделал: «тяжелый» источник пишет свой результат в промежуточный лист-шину заранее (отдельным шагом), а сборка дашборда читает готовую шину и переживает ее отсутствие (graceful → канал просто пустой). Принцип: сбой некритичного источника не должен ронять весь дашборд.

Idempotent + lock-файлы. Каждый шаг идемпотентен (повторный запуск не плодит дубли), а на дневные и недельные задачи - lock-файл от двойного запуска. Плюс watchdog: если к такому-то времени нет флага успешного прогона - письмо мне. Сама система мониторит, что сама отработала.

Грабли по метрикам

Самое ценное, что дала сквозная аналитика, - это опыт интерпретации метрик и баги с цифрами. Три примера.

1. Конверсии рекламного кабинета ≠ лиды. В кабинете «конверсий» было в 5-10 раз больше, чем реальных заявок в биллинге. Потому что туда настроены цели «регистрация на вебинар» с нескольких счетчиков. Выдавать это за лиды - обманывать себя. Реальный CPL считается только по событиям биллинга, а кабинетные конверсии помечаются «(вебинары)» и не смешиваются.

2. ROMI по прямой атрибуции в окне 1-3 мес - отрицательный. И это не приговор рекламе. Прямая атрибуция (первое касание → оплата в окне) не ловит assist/halo-эффект, оплаты по другому каналу позже, прямые заходы вне меток. Три независимых метода (помесячный, lead-когорты, last-touch) дали отрицательный ROMI - а значит, истинный вклад канала виден только в multi-touch на длинном горизонте (6+ мес). Правильный вывод: «в окне 1-3 мес по прямому касанию не окупается» ≠ «реклама не работает». Разница между этими формулировками - это вся зрелость аналитики.

3. Валютный баг на ~миллиард. В выручке вдруг возник порядок, которого быть не может. Корень: у части рублевых оплат стояла метка валюты USD, и выгрузка послушно домножала их на курс - рублевая сумма превращалась в «долларовую» и раздувалась в ~70 раз. Лечится защитной функцией: если валюта не RUB, берем исходную сумму, а не конвертированную; плюс детектор аномалий как бэкстоп. Мораль: любое поле, пришедшее извне, считай враждебным, пока не доказано обратное - особенно если на нем строится выручка.

Эти три пункта - главная причина, почему я считаю, что сквозную аналитику вообще стоит строить, даже если «и так примерно понятно». Начинаешь разбираться в нюансах.

Дашборд: DataLens поверх Postgres-витрины

Финальный слой - единый экран для руководителя. С BI-инструментом определились сразу: Яндекс DataLens. Удобный, читается прямо из Postgres через прямой коннектор, не требует промежуточных выгрузок и устраивает по скорости публикации виджетов. Технически дашборд читает витрину напрямую из Postgres (отдельный шаг пайплайна переписывает несколько таблиц в БД), BI подхватывает свежие данные сам, без человека.

Пара нюансов, которые сэкономят ваше время:

  • Коннектор DataLens удваивает % в именах колонок в генерируемом SQL - колонку «CR, %» он прочитать не может. Лечится переименованием %pct на уровне витрины.

  • Egress к части сервисов из не-РФ-инфры приходится гнать через SOCKS-прокси на РФ-релее - отдельная маленькая инженерная история про autossh и proxies в requests, но это уже про инфраструктуру, не про аналитику.

Экранов на MVP достаточно двух: «обзор» (расход/выручка/ROMI/тренд) и «воронка по каналам». Остальное наращивается.

Что реально дал Claude Code (и где думал я)

Теперь обещанный в начале статьи баланс, ради которого статья и затевалась.

Где Claude Code дал ×5 к скорости:

  • Разведка незнакомых API. Накидать клиент, перебрать эндпоинты, понять формат ответа - то, на что руками уходит день, делается за час диалога.

  • ETL-бойлерплейт. Пагинация, ретраи, парсинг, приведение типов, запись в Sheets - скучный код, который LLM пишет ровно и быстро.

  • Тесты. Сгенерировать таблицу кейсов для чистой функции - почти бесплатно.

  • Документация и рефакторинг. Привести модуль к единому стилю, дописать docstrings.

Где LLM не помог и не мог и не надо вообще на него надеяться:

  • Заметить, что summary врет про даты. Это эмпирика и паранойя, а не генерация.

  • Придумать модель атрибуции. Namespace, дедуп, когорты с лагом, защита от двойного счета - это архитектурные решения под конкретные данные.

  • Сказать «эти конверсии - фейк». Понимание, что кабинетные конверсии ≠ лиды, доменное, а не алгоритмическое.

  • Поймать валютный баг. Аномалию надо было заподозрить по «не может такого быть».

Если свести в одну фразу: LLM снимает с тебя «как написать», но не снимает «что правильно». В сквозной аналитике 80% сложности - во втором.

И про «две недели». За две недели собирается именно ядро: три источника → Sheets → связка до оплаты → пара экранов. Это работающий MVP, который уже отвечает на «окупается ли в первом приближении». Все, что описано в разделах про грабли, атрибуцию и метрики, - это следующие месяцы итераций. И вот тут LLM экономит уже не проценты, а просто делает реалистичным то, что команда разработки в принципе доводит до прода: держать в голове и Директ, и биллинг, и CRM, и инфру, и тесты. Только Claude помогает это делать без команды.

Итого: чек-лист, если хотите повторить

  1. Начните с трех источников, замыкающих деньги (реклама + веб + биллинг). Не тащите все сразу.

  2. Google Sheets - нормальная витрина для MVP. Один билдер = один лист, джойны в pandas, идемпотентный upsert по стабильному id.

  3. Атрибуцию проектируйте руками. Резолвер с приоритетами (карта → slug → имя), дедуп по продуктовому определению лида, когортный ROMI с защитой от двойного счета.

  4. Считайте внешние данные враждебными. Проверяйте API на реальных данных (даты, валюты, отсутствующие поля), а не по докам.

  5. Read-only по умолчанию. Код только читает рекламные кабинеты. С LLM-агентом это уже не гигиена, а защита бюджета.

  6. Чистые функции + тесты = контракт с LLM. Бизнес-логика без I/O, покрыта тестами; тогда сгенерированному коду можно доверять.

  7. Fail-closed на свежести. Лучше громко не обновить, чем тихо показать неверное.

  8. Точность важнее красоты. Если канал не атрибутируется - напишите это прямо. Отрицательный ROMI по прямой атрибуции - повод разобраться, а не спрятать.

Сквозную аналитику для B2C реально поднять малыми силами и быстро - LLM сделал это посильным для одного человека. Но «быстро» относится к коду и рутине; думать про то, что цифры значат на самом деле, по-прежнему приходится самому. И, прямо скажем, это хорошая новость.

Дмитрий Волошин, сооснователь и генеральный директор OTUS, основатель Клуба менторов, основатель Школы бизнеса Ninox. Заметки про управление, найм и фаундерский опыт: t.me/coffee_notes