Skip to content

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).

Следствия для архитектуры воронки:

  1. Лендинг НЕ порождает Pulse-событий. Он только (а) держит UTM транзиентно (URL / in-memory, без записи на устройство) и (б) переносит его через границу установки first-party-средствами.
  2. Первое Pulse-событие рождается in-app на первом запуске, когда минтится subject_id, и несёт UTM. Conversion-time stitch приземляется внутри приложения, не на сайте.
  3. Любая «аналитика лендинга» (pageviews, источники) — отдельная off-Pulse агрегатная поверхность (без субъекта) либо распределение UTM по install-событиям. В Pulse её класть нельзя.

Это делает модель строже umami: umami хеширует IP+UA на сервере для сессий, а здесь сайт вообще не создаёт субъектных событий.


2. Жёсткие privacy-ограничения (источник истины)

Любой вариант обязан их соблюдать (все — из кода/контрактов, не из прозы):

  1. Per-subject токен: sub 32 байта; нет анонимного ingest (verify.rs:338).
  2. Purpose-limitation крипто-прибит: header33 == pur == event_type.purpose (verify.rs:175-187). Нельзя подмешать новую цель в чужой токен.
  3. Короткий TTL: exp-iat ≤ 3600s, ±60s skew (lib.rs:49-53, verify.rs:189-216).
  4. Граница identity-mapping: только изолированный сервис (:15433, отдельный at-rest ключ, подписанный+ACL+аудируемый API) видит реальные идентификаторы и (subject_id ↔ email); pulse-core получает email только через подписанный resolve_marketing_contact, никогда из billing.* напрямую.
  5. Нет PII / нет fingerprinting в телеметрии: каждое поле контракта pii:false; потенциально-PII хешируется (link_id, crash_signature, hashed_subject_id); нельзя вводить поле без классификации.
  6. Нет device-fingerprint (canvas/fonts/WebGL), нет cross-site пикселей, нет IP-join между сайтом и приложением; атрибуция — только self-reported / observable first-party сигналы.
  7. iOS: device-signal fingerprinting запрещён Apple DPLA 3.3.9 (Privacy Manifest / Required Reason с 2024) и нашим инвариантом — двойной запрет.
  8. Нет client-side conversion-пикселя, нет re-targeting/lookalike из email/phone-хешей; paid-ad атрибуция — UTM + server-side privacy-preserving conversion, только с согласием. Meta/Facebook-pixel — off-limits.
  9. IDFA/GAID/AAID и raw IP — персональные данные: не использовать как ключ джойна, не персистить raw IP, не вшивать PII (email/phone) в install/download-ссылку.
  10. Marketing email/push: GDPR lawful basis И ePrivacy opt-in; re-validation на момент send; EU — double-opt-in, opt-out по умолчанию, unsubscribe + sender в письме.
  11. Engagement (open/click) tracking — отдельный opt-in marketing.email_engagement_tracking; нельзя вывести из email-согласия.
  12. billing_transactional = legal_basis=contract (GDPR 6(1)(b)), не opt-out-able; никогда не репурпозить под marketing/attribution.
  13. Новая воронка → новая цель в consent-purposes.yaml, на которую ссылается event_type.purpose (purpose события ⊆ реестра). Изменение реестра/контракта/шаблона — обязательный human-in-the-loop, не авто.
  14. Право на забвение: всё субъектное (events + features + decisions + outbound) удаляется ≤24ч; в append-only Merkle-audit остаётся только неразрешимый dangling-псевдоним.
  15. Агрегатный экспорт: K-анонимность И DP-шум для когорт < K. Органического iOS install-referrer не существует → per-user органический iOS нельзя обещать без отдельного first-party сигнала (введённый код / явная вставка с согласием).
  16. Приватность — структурный/compile-time инвариант, не per-region runtime-условие.

3. Переиспользуемые примитивы

  • app.install_completed event_type — естественный носитель first-touch utm_* (после ратификации полей); уже пишет 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-stitchin-app событие на first-run несёт UTMDesktop ✅ / Android ✅ / iOS-органика ⚠️новая цель analytics.acquisition (legitimate_interest)нет
V2 Server-side conversionpostback в рекламную сеть без пикселявсе (для согласившихся)marketing.attribution (consent, hashed_subject_id)да
V3 iOS paid aggregateSKAN4 / AdServices, вне PulseiOS (paid)нет токена — сигнал не входит в Pulseнет
V4 Waitlist/email-мосткод активации / universal-link + email-каналiOS-органика 🎯 / прочие fallbackV1-цель (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_type acquisition.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.

Для отчёта конверсий обратно в рекламные сети (напр. 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-ценности.

ВариантВердиктЧто было бы нарушением «как написано»
V1compliant_with_fix(a) UTM на ops = purpose-repurposing; (b) уникальный per-download токен = скрытый click-ID; (c) молчаливое чтение pasteboard на iOS
V2compliant_with_fixдрейф к загрузке хеша email в сеть; EU opt-out не закреплён структурно на цели
V3compliant_with_fixoverclaim «zero disclosure» (нужен Privacy Manifest); high-entropy conversion-value; K-анон без DP
V4compliant_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. Диаграмма

plantuml Diagram

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.

Порядок реализации (детальный план — отдельным документом):

  1. HITL-публикация контракта ✅ (2026-06-01, Plan 1): цель analytics.acquisition в consent-purposes.yaml; event_type app.acquisition_attributed (utm_* pii:false) в telemetry v0.1.0; feature_derive arm (first-touch UTM + накопление gross_revenue_cents_total)
    • seam campaign→subject→revenue sqlx-verified.
  2. V1 ядро: arm в feature_derive (UTM + amount_cents); desktop 1:N штамп + чтение на first-run; Play Install Referrer reader; iOS code-entry / explicit-paste UI.
  3. V4: waitlist + код активации внутри identity-mapping (TTL + 24ч-erasure); universal-link continuity; разделённые consent-grants (email-сбор vs marketing.email).
  4. V2/V3 — по сигналу paid-acquisition (consent-gated / off-Pulse соответственно).