Acquisition Funnels — privacy-safe варианты атрибуции
v0.1 (2026-05-31) — Проектный документ. Варианты воронок привлечения (landing → install → activation → revenue), выведенные из реальной consent/privacy-модели Pulse и состязательно проверенные на нарушение приватности. Вводные: мобильные приложения раздаются через App Store / Google Play; настольные — прямой загрузкой с лендинга (возможна быстрая генерация per-download сборок). Каждый вариант приведён к shippable-форме (фиксы аудита уже вшиты). Статус: scope зафиксирован — вариант A (2026-06-01): V1+V4 спина, V2/V3 как платные дополнения (см. §8). Далее — план реализации.
Связанные: specification.md (§9.1 consent, §17 marketing), marketing-patterns.md, contracts/consent-purposes.yaml, contracts/telemetry/v0.1.0.yaml · код: kontinuum-pulse/crates/{pulse-consent,pulse-core,pulse-contracts,identity-mapping}.
1. Ключевая поправка: анонимного ingest в Pulse нет
Любая «лендинговая аналитика» сталкивается с архитектурным инвариантом: каждое событие требует per-subject consent_token с sub ровно 32 байта; nullable/анонимного пути ingest не существует.
verify.rs:338отвергаетsub != 32 байт;events.subject_id BYTEA NOT NULL(contracts/ddl/03_pulse_runtime.sql:17-32); INSERT привязываетverified.claims.sub(ingest.rs:231-248).- Purpose криптографически прибит: header 33 ==
claims.pur==event_type.purpose, иначе reject (verify.rs:175-187,ingest.rs:156-203). - В
consent-purposes.yamlнет цели с basis «site-level / audience-measurement» — толькоconsent/legitimate_interest/contract, все привязаны к субъекту. - TOFU (
X-Client-Key) — bootstrap ключа устройства под уже существующийsub, а не анонимного субъекта (ingest.rs:12-21,309-360).
Следствия для архитектуры воронки:
- Лендинг НЕ порождает Pulse-событий. Он только (а) держит UTM транзиентно (URL / in-memory, без записи на устройство) и (б) переносит его через границу установки first-party-средствами.
- Первое Pulse-событие рождается in-app на первом запуске, когда минтится
subject_id, и несёт UTM. Conversion-time stitch приземляется внутри приложения, не на сайте. - Любая «аналитика лендинга» (pageviews, источники) — отдельная off-Pulse агрегатная поверхность (без субъекта) либо распределение UTM по install-событиям. В Pulse её класть нельзя.
Это делает модель строже umami: umami хеширует IP+UA на сервере для сессий, а здесь сайт вообще не создаёт субъектных событий.
2. Жёсткие privacy-ограничения (источник истины)
Любой вариант обязан их соблюдать (все — из кода/контрактов, не из прозы):
- Per-subject токен:
sub32 байта; нет анонимного ingest (verify.rs:338). - Purpose-limitation крипто-прибит:
header33 == pur == event_type.purpose(verify.rs:175-187). Нельзя подмешать новую цель в чужой токен. - Короткий TTL:
exp-iat ≤ 3600s, ±60s skew (lib.rs:49-53,verify.rs:189-216). - Граница identity-mapping: только изолированный сервис (:15433, отдельный at-rest ключ, подписанный+ACL+аудируемый API) видит реальные идентификаторы и
(subject_id ↔ email); pulse-core получает email только через подписанныйresolve_marketing_contact, никогда изbilling.*напрямую. - Нет PII / нет fingerprinting в телеметрии: каждое поле контракта
pii:false; потенциально-PII хешируется (link_id,crash_signature,hashed_subject_id); нельзя вводить поле без классификации. - Нет device-fingerprint (canvas/fonts/WebGL), нет cross-site пикселей, нет IP-join между сайтом и приложением; атрибуция — только self-reported / observable first-party сигналы.
- iOS: device-signal fingerprinting запрещён Apple DPLA 3.3.9 (Privacy Manifest / Required Reason с 2024) и нашим инвариантом — двойной запрет.
- Нет client-side conversion-пикселя, нет re-targeting/lookalike из email/phone-хешей; paid-ad атрибуция — UTM + server-side privacy-preserving conversion, только с согласием. Meta/Facebook-pixel — off-limits.
- IDFA/GAID/AAID и raw IP — персональные данные: не использовать как ключ джойна, не персистить raw IP, не вшивать PII (email/phone) в install/download-ссылку.
- Marketing email/push: GDPR lawful basis И ePrivacy opt-in; re-validation на момент send; EU — double-opt-in, opt-out по умолчанию, unsubscribe + sender в письме.
- Engagement (open/click) tracking — отдельный opt-in
marketing.email_engagement_tracking; нельзя вывести из email-согласия. billing_transactional=legal_basis=contract(GDPR 6(1)(b)), не opt-out-able; никогда не репурпозить под marketing/attribution.- Новая воронка → новая цель в
consent-purposes.yaml, на которую ссылаетсяevent_type.purpose(purpose события ⊆ реестра). Изменение реестра/контракта/шаблона — обязательный human-in-the-loop, не авто. - Право на забвение: всё субъектное (events + features + decisions + outbound) удаляется ≤24ч; в append-only Merkle-audit остаётся только неразрешимый dangling-псевдоним.
- Агрегатный экспорт: K-анонимность И DP-шум для когорт < K. Органического iOS install-referrer не существует → per-user органический iOS нельзя обещать без отдельного first-party сигнала (введённый код / явная вставка с согласием).
- Приватность — структурный/compile-time инвариант, не per-region runtime-условие.
3. Переиспользуемые примитивы
app.install_completedevent_type — естественный носитель first-touchutm_*(после ратификации полей); уже пишетfeature_store.installed_at.pulse_consent::{sign_token, verify_token, extract_kid}— COSE_Sign1/Ed25519, purpose-binding, TTL ≤1ч,kid = BLAKE3(pubkey).- ingest pipeline (
ingest.rs) + TOFU client-key bootstrap. feature_derive::derive— единственный писательfeature_store; место для arm-проекции UTM.feature_store+billing.payment_received → paid_at— точка джойна campaign→subject→revenue.billing.payment_received.amount_centsуже есть (telemetry/v0.1.0.yaml:835-865) — для ROI нужен только arm, не новое поле.- Цели:
analytics.usage(legitimate_interest),marketing.attribution(consent,hashed_subject_id, 90д),marketing.email(+consent_grants,preflight::check). - identity-mapping:
resolve_marketing(subject→marketing_subject_id),resolve_marketing_contact(broker→email),pseudonymize,erase_subject,rotate_salt. - Merkle-audit + 24ч-erasure (наследуют все варианты).
- Google Play Install Referrer API — детерминированный, GAID-free, fingerprint-free носитель publisher-set UTM-строки.
- Паттерн opaque first-party campaign-label (Play referrer / desktop-штамп / waitlist-код) — атрибуция как данные, которые сами закодировали в first-party канал, не считанные с устройства.
- K-анонимность / DP-шум на агрегатном экспорте.
4. Варианты воронок (все — compliant_with_fix)
| Механизм | Платформы | Privacy-basis | Баннер | |
|---|---|---|---|---|
| V1 Conversion-time UTM-stitch | in-app событие на first-run несёт UTM | Desktop ✅ / Android ✅ / iOS-органика ⚠️ | новая цель analytics.acquisition (legitimate_interest) | нет |
| V2 Server-side conversion | postback в рекламную сеть без пикселя | все (для согласившихся) | marketing.attribution (consent, hashed_subject_id) | да |
| V3 iOS paid aggregate | SKAN4 / AdServices, вне Pulse | iOS (paid) | нет токена — сигнал не входит в Pulse | нет |
| V4 Waitlist/email-мост | код активации / universal-link + email-канал | iOS-органика 🎯 / прочие fallback | V1-цель (stitch) + marketing.email (письма) | conditional |
V1 — Conversion-time UTM-stitch (in-app, выделенная цель)
Самый дешёвый и почти готовый путь. Лендинг держит UTM транзиентно (URL/память, без записи). CTA переносит UTM через границу установки first-party-средствами; на first-run приложение эмитит событие с utm_*, привязанными к свежему subject_id, и проецирует их в feature_store. Revenue-джойн — через billing.payment_received.
- Desktop: штамповать только грубый 1:N campaign-ярлык (одно значение на всю кампанию), никогда уникальный per-download nonce, без логирования IP/UA против него — иначе это скрытый click-ID (нарушение §6/§9). Code-signing не позволяет стампить внутри подписи → ярлык в имени файла / пути загрузки (тот же 1:N).
- Android: Google Play Install Referrer (publisher-set строка, без GAID/fingerprint).
- iOS-органика: код вводит пользователь явно, либо явная вставка из буфера с Privacy-Manifest Required-Reason; никакого молчаливого чтения pasteboard. Нет кода →
utm=null, честная «органика».
Вшитые фиксы аудита:
- UTM нельзя везти на
ops(это purpose-repurposing:ops= только crashes/errors/perf,consent-purposes.yaml:28-35). Завестиanalytics.acquisition(legitimate_interest, явно scoped «first-party acquisition measurement») или новый event_typeacquisition.attributed; поляutm_source/medium/campaignсpii:false, через HITL-публикацию контракта. - ⚠️ Валидатор молча пропускает необъявленные поля (
telemetry.rs:206-210) и пишетevents.fieldsдословно (ingest.rs:244) — «протащить» UTM без объявления технически можно, но это нарушение §5/§13; делать нельзя. - Revenue: arm в
feature_derive, проецирующийamount_cents(поле уже в контракте).
Что строить: цель analytics.acquisition в реестре (HITL); utm_* поля (HITL); arm в feature_derive (UTM + amount_cents); desktop 1:N штамп + чтение на first-run; Play Install Referrer reader; iOS code-entry / explicit-paste UI.
V2 — Server-side privacy-preserving conversion (consent-gated)
Для отчёта конверсий обратно в рекламные сети (напр. Google Enhanced Conversions) без клиентского пикселя. После согласия marketing.attribution Pulse резолвит субъект в hashed_subject_id и эмитит server-side conversion; postback без пикселя.
Вшитые фиксы: на проводе — только attribution-only id (gclid/wbraid/UTM-click-id, снятый first-party на клике), не хеш email; запретить resolve_marketing_contact в allowed-callers этого action (lint, что адаптер не читает email/billing.*); добавить regional_overrides EU (explicit_consent_required:true, default_state:opt_out) на саму цель (marketing.attribution), не полагаясь на наследуемый region-preset.
Что строить: conversion-report action + preflight wiring; адаптер провайдера; UI согласия; K-анон+DP-guard на любой агрегат.
V3 — iOS paid aggregate (SKAN4 / AdServices, вне Pulse)
Честный ответ для платного iOS. SKAdNetwork 4 / AdAttributionKit и Apple Ads AdServices-токен атрибутируют платные iOS-инсталлы без IDFA, без ATT, без fingerprint — но только агрегатно (SKAN) либо Apple-only (AdServices). Эти сигналы никогда не входят в Pulse: нет subject_id, нет токена.
Вшитые фиксы: PrivacyInfo.xcprivacy + App-Store privacy-label (Required-Reason обязателен, даже без in-app баннера — «zero disclosure» это overclaim); low-entropy схема conversion-value (без таймстемпов/точной выручки, HITL-ревью); при импорте в дашборд — K-анон И DP-шум (dp_budget.rs), не только K-анон; хранить в таблице без колонки subject_id (невозможность джойна на уровне схемы).
V4 — First-party waitlist / email-мост (спасение органического iOS)
Пользователь добровольно вводит email на лендинге (это и есть first-party consent-событие); waitlist хранит UTM (server-side, транзиентно) под кодом активации; на first-run пользователь вводит код (или universal-link continuity), детерминированно связывая campaign→subject даже на органическом iOS. Email живёт только за границей identity-mapping/billing; UTM-stitch приземляется через V1.
Вшитые фиксы: сбор email и marketing-opt-in разбить (opt-out по умолчанию, EU double-opt-in; письмо с кодом — транзакционное, не marketing); карта code→UTM живёт внутри identity-mapping с коротким TTL (≤30д) + 24ч-erasure, без raw-IP-джойна, строка удаляется на redemption; open/click-трекинг выключен без отдельного marketing.email_engagement_tracking.
5. Privacy-вердикты (состязательный аудит)
Все четыре — compliant_with_fix: чистая privacy-модель, ни одного high-severity, который не снимается хирургическим фиксом без потери growth-ценности.
| Вариант | Вердикт | Что было бы нарушением «как написано» |
|---|---|---|
| V1 | compliant_with_fix | (a) UTM на ops = purpose-repurposing; (b) уникальный per-download токен = скрытый click-ID; (c) молчаливое чтение pasteboard на iOS |
| V2 | compliant_with_fix | дрейф к загрузке хеша email в сеть; EU opt-out не закреплён структурно на цели |
| V3 | compliant_with_fix | overclaim «zero disclosure» (нужен Privacy Manifest); high-entropy conversion-value; K-анон без DP |
| V4 | compliant_with_fix | опора на несуществующее поле install_completed.utm_*; ops-repurposing; bundling email-сбора с marketing-согласием |
Фиксы из §4 закрывают все пункты. Подтверждено по коду: per-subject 32-байтный инвариант, no-PII-in-telemetry, граница identity-mapping и right-to-erasure держатся во всех вариантах (feature_derive.rs:104-119 — джойн subject_id↔subject_id↔paid_at, broker и billing.* не трогает).
6. Диаграмма
7. Рекомендация и последовательность
- Спина: V1 (с целью
analytics.acquisition) + V4 как first-party спасение органического iOS и свой email-канал. Desktop+Android — детерминированное ядро. - V2 — когда нужно кормить рекламные сети (consent-gated, без пикселя).
- V3 — только для платного iOS, агрегатно, вне Pulse.
- Честный разрыв (сознательная privacy-цена, не баг): органический iOS-пользователь, не введший код, остаётся «органикой». Чинить fingerprint'ом нельзя (DPLA 3.3.9 + §7).
8. Зафиксированный scope (2026-06-01)
Выбран вариант A — вся рекомендованная композиция:
- Спина: V1 (conversion-time UTM-stitch, цель
analytics.acquisition) + V4 (waitlist/email-мост для органического iOS и свой email-канал). - Платные дополнения: V2 (server-side conversion в рекламные сети) и V3 (iOS SKAN4/AdServices, вне Pulse) — подключаются по потребности paid-acquisition.
Порядок реализации (детальный план — отдельным документом):
- HITL-публикация контракта ✅ (2026-06-01, Plan 1): цель
analytics.acquisitionвconsent-purposes.yaml; event_typeapp.acquisition_attributed(utm_*pii:false) в telemetry v0.1.0;feature_derivearm (first-touch UTM + накоплениеgross_revenue_cents_total)- seam campaign→subject→revenue sqlx-verified.
- V1 ядро: arm в
feature_derive(UTM +amount_cents); desktop 1:N штамп + чтение на first-run; Play Install Referrer reader; iOS code-entry / explicit-paste UI. - V4: waitlist + код активации внутри identity-mapping (TTL + 24ч-erasure); universal-link continuity; разделённые consent-grants (email-сбор vs
marketing.email). - V2/V3 — по сигналу paid-acquisition (consent-gated / off-Pulse соответственно).