Перейти к контенту
БОЛЬШИЕ·ПРОДАЖИ Пульт →
Все документы

24 · Funnel & Tracking Runbook — As-Built

Status: ✅ Production, last updated 2026-05-27 after CAPI ship Audience: Developer / agent picking up the funnel for the first time Replaces: “future plan” sections of #13 tracking-events, #14 meta-ads-attribution, #17 data-flow-diagram, #18 gtm-tag-setup

This doc is the single source of truth for what’s actually shipped. The other tracking docs describe the journey. This one describes the destination.


0 · TL;DR — the conversion signal stack

Three independent layers, all firing the same conversion events with the same event_id = submission_id so Meta dedupes them as one logical conversion:

                  Browser                              Server (Make.com)
┌──────────────────────────────────┐    ┌────────────────────────────────────┐
│ 1. Inline Meta Pixel              │    │ 3. Make webhook → Graph API CAPI   │
│   - Pixel base (PageView)         │    │   - Lead / Subscribe              │
│   - ViewContent on landings       │    │   - CompleteRegistration           │
│   - InitiateCheckout on modals    │    │   - hashed em + ph + fbc          │
│   - Contact on WhatsApp tap       │    │   - matches Meta user graph @90%+ │
│   - Lead/Sub/CR on /thank-you     │    │   - SURVIVES ad-block / iOS ITP   │
└──────────────────────────────────┘    └────────────────────────────────────┘
              │                                          │
              └────────────────┬─────────────────────────┘

              ┌──────────────────────────────────────┐
              │ 2. Meta-hosted CAPI Gateway          │
              │   - auto-bridges browser events       │
              │   - "Web-only" dataset                │
              │   - low match quality (no PII)        │
              │   - already active, leave alone       │
              └──────────────────────────────────────┘

Why three layers: each one survives a different failure mode. Browser Pixel dies under ad-blockers/DNS sinkholes (~25–40% of paid traffic). CAPI Gateway dies when the browser Pixel never loaded (so it has nothing to bridge). Server-side CAPI from Make fires no matter what — every form submission goes through it.

Industry-typical loss without CAPI = 25–40% of conversions. We’ve closed that gap.


1 · File-by-file map — where every event is fired

LayerFileRoleLines worth knowing
HTML headsrc/layouts/Layout.astroSets window.__KBP__ = {variant, experiment, mechanism} and pushes the same trio to dataLayer before GTM/Pixel init95–112
Pixel basepackages/analytics/src/components/AnalyticsHead.astroLoads fbevents.js inline, fires PageView automatically107
Attributionsrc/conversion/attribution.tsCaptures UTMs/click-IDs into sessionStorage, first-touch merge, derives tier/country from variantIdtierFromVariant 204–210, countryFromVariant 211–214, buildAttribution 217–241
Page-load eventssrc/scripts/page-init.tsCalls fireViewContent on landing pages, fireInitiateCheckout on [data-modal-open] clicks, fireContact on WhatsApp FAB60–70, 115–126, 109–111
Pixel event helperssrc/scripts/pixel-events.tsInline fbq('track', …) wrappers + leadValue(tier, country) mapping + segmentParams() shared payloadleadValue 46–63, segmentParams 71–95, fireSubscribe 121–129
Conversion eventsrc/scripts/conversion-complete.tsFires Lead OR Subscribe + CompleteRegistration on /thank-you, pushes generate_lead to dataLayer54–93
Form payloadsrc/conversion/attribution.ts → buildLeadPayloadWraps form values into the envelope sent to Make webhook265–315
Form submitsrc/conversion/submit.tsPOSTs payload to Make webhook URLfull file
Webhook URLsrc/config/integration.tsSingle source of truth for the Make webhook URLfull file
Analytics configsrc/config/analytics.tsGTM ID + Meta Pixel ID, consumed by <AnalyticsHead>full file
GA4/GTM eventssrc/conversion/tracking.tsform_view/form_start/form_step/form_submit/form_success dataLayer pushes (NOT Pixel)full file
Backfill scriptscripts/capi-backfill.tsOne-off batch sender — pushes historical Sheet leads through Graph APIfull file
Lead-flow testerscripts/test-lead-flow.tsFires representative test payloads at Make webhook for QAfull file

2 · Make scenario — KARADA BP (id 4743595)

URL: https://us1.make.com/237825/scenarios/4743595/edit Webhook: https://hook.<your-host>/<webhook-id>

Webhook (id 1)
  └── Router (id 6) — splits payload to 4 parallel branches
        ├── Slack (id 5)          → notification to #karada-leads
        ├── Google Sheet (id 3)   → addRow to Leads tab
        ├── amoCRM HTTP (id 8)    → POST /api/v4/leads/complex
        └── Meta CAPI (3 modules in sequence, each filtered)
              ├── id 10 — Lead         filter: tier ≠ lead_magnet
              ├── id 11 — Subscribe    filter: tier = lead_magnet
              └── id 12 — CompleteReg. filter: funnel_mechanism = registration

CAPI module body (template)

Each of the 3 CAPI HTTP modules uses this body template (event_name + custom_data differ per module):

{
  "data": [
    {
      "event_name": "Lead",
      "event_time": {{formatDate(parseDate(1.submitted_at); "X")}},
      "event_id": "{{1.submission_id}}",
      "action_source": "website",
      "event_source_url": "https://x5.karada.kz{{ifempty(1.attribution.page_path; "/")}}",
      "user_data": {
        "em": ["{{if(1.lead.email; sha256(lower(trim(1.lead.email))); "")}}"],
        "ph": ["{{if(1.lead.phone; sha256(replace(1.lead.phone; "[^0-9]"; "")); "")}}"],
        "client_user_agent": "{{1.meta.user_agent}}"
      },
      "custom_data": {
        "value": {{if(1.tier = "exclusive"; if(1.country = "KG"; 250000; 1000000); if(1.country = "KG"; 51000; 102000))}},
        "currency": "{{if(1.country = "KG"; "KGS"; "KZT")}}",
        "content_name": "big-sales-{{ifempty(1.tier; "unknown")}}",
        "content_category": "{{ifempty(1.country; "unknown")}}"
      }
    }
  ],
  "access_token": "<EAA…>"
}

Module-by-module differences:

Moduleevent_namecustom_dataFilter
10"Lead"with value + currency{{1.tier}} ≠ lead_magnet
11"Subscribe"no value + currency{{1.tier}} = lead_magnet
12"CompleteRegistration"with value + currency{{1.funnel_mechanism}} = registration

Why three separate modules (vs one conditional JSON body):

  • Each module has a fixed payload — no brittle conditional JSON inside IML templates (Make’s null keyword breaks JSON, and triple-escaped quotes break the expression engine, learned the hard way)
  • Filters are explicit and testable in the Make UI
  • Disable one without touching the others when debugging
  • Mirrors the inline-Pixel logic 1:1 → trivial to reason about dedup

Why HTTP module not native facebook-conversions-api app: same predictable pattern as the existing amoCRM HTTP module, full control over payload, no dependency on the app’s evolving connection structure. Native app would be a future cleanup if SHA-256 automation becomes valuable.


3 · Event matrix — what fires per funnel path

Source legend

  • Browser = fbq('track', …) from pixel-events.ts or conversion-complete.ts
  • CAPI = Make HTTP module → Graph API
  • CAPI Gateway = auto-bridged from browser, low match quality, ignore

Per page load (any non-utility landing)

EventSourceFires when
PageViewBrowserEvery page (AnalyticsHead.astro:107)
ViewContentBrowserLanding pages with a baked variantId (page-init.ts:62)

Per CTA interaction

EventSourceTrigger
InitiateCheckoutBrowserAny [data-modal-open] click (page-init.ts:117)
ContactBrowser[data-cta="whatsapp-fab"] tap (page-init.ts:109)

Per form submission — the matrix

Each row = 1 submission. Server CAPI events all dedupe against the browser event by event_id = submission_id.

VariantTierMechanismBrowser firesCAPI fires
homevipregistrationLead + CompleteRegistrationLead + CompleteRegistration
vslvipregistrationLead + CompleteRegistrationLead + CompleteRegistration
kg-homevipregistrationLead + CompleteRegistrationLead + CompleteRegistration
kg-vslvipregistrationLead + CompleteRegistrationLead + CompleteRegistration
exclusiveexclusiveapplicationLeadLead
guidelead_magnetlead-magnetSubscribeSubscribe
Anyanyquick-leadLead (or Subscribe if lead_magnet)Lead (or Subscribe)

Variant → tier → country derivation

// src/conversion/attribution.ts:204
tierFromVariant('home')      = 'vip'         tierFromVariant('exclusive') = 'exclusive'
tierFromVariant('vsl')       = 'vip'         tierFromVariant('guide')     = 'lead_magnet'
tierFromVariant('kg-home')   = 'vip'         tierFromVariant('')          = ''  (falls through to vip in leadValue)

countryFromVariant('kg-*')   = 'KG'          countryFromVariant(anything else) = 'KZ'

leadValue(tier, country) — the canonical value map

This MUST stay in lock-step between pixel-events.ts (browser) and the Make CAPI modules’ IML expression (server). Change one, change the other.

                       KZ              KG
vip / standard / ''   102_000 KZT    51_000 KGS
exclusive             1_000_000 KZT  250_000 KGS
lead_magnet           0  (omitted)   0  (omitted)

Subscribe + lead_magnet events do not include value/currency — Meta rejects value: 0 on monetary events. The website’s segmentParams() helper auto-omits when value=0; the Make Subscribe module simply omits the value key from its template.


4 · Adding a new funnel / landing — checklist

Use this when scaffolding a new variant (e.g. you want to test lp-c or add a Russian-language exclusive funnel ru-exclusive).

Code-side

  • Page file src/pages/<route>.astro with variantId and mechanism consts
  • Variant in tierFromVariant (src/conversion/attribution.ts:204) — return the right tier for the new variant ID
  • Country in countryFromVariant (src/conversion/attribution.ts:211) — KZ vs KG (prefixed with kg- or kz-)
  • leadValue mapping (src/scripts/pixel-events.ts:46) — if introducing a new tier, add a branch
  • Funnel content in src/lib/content.ts and src/conversion/mechanisms.ts
  • PageModal with mechanism, variantId, formId props
  • Test in test-lead-flow.ts — add a scenario entry so QA can fire a representative payload

Make scenario-side (if introducing a new tier or mechanism)

Only needed when adding a new tier that needs different leadValue, or a new mechanism that should fire CompleteRegistration:

  • Update module 10 (Lead) value expression: {{if(1.tier = "<new_tier>"; <new_value_kz>; ...)}}
  • Update module 12 (CompleteRegistration) filter if a new mechanism should trigger it
  • If introducing a new tier that shouldn’t fire Lead (like lead_magnet), add it to the Subscribe module’s filter

Mostly the existing 3 modules don’t need touching for new landings — they already handle vip/exclusive/lead_magnet × KZ/KG × all mechanisms via filters and conditionals.

Meta-side

  • (Optional) Set up a separate Custom Conversion in Events Manager for the new variant if you want isolated reporting (uses content_name=big-sales-<tier> filter)
  • Update AEM event ranking if the new funnel uses a different primary event (e.g. ranking Subscribe above Lead when launching guide-magnet campaigns)
  • Build a Lookalike audience from the new variant’s events once you have ~100 conversions

QA

  • bun run scripts/test-lead-flow.ts <new-variant> — fires through Make webhook end-to-end
  • Events Manager → Test Events → verify both Browser + Server events arrive with matching event_id
  • Events Manager → Diagnostics → match quality 7+/10 within 24h

5 · Verification playbook

Three tools, three different questions answered.

Tool 1 — Meta Pixel Helper (Chrome extension)

Question answered: “Is the browser firing what I think it’s firing?”

  1. Install Meta Pixel Helper
  2. Visit any landing page → click extension icon
  3. See live log: events fired + full params + eventID
  4. If params are wrong here → fix the code
  5. If params are right here but missing in Meta → check next tool

Tool 2 — Events Manager → Test Events tab

Question answered: “Is Meta receiving the events I’m firing?”

  1. Open: https://business.facebook.com/events_manager2/list/pixel → pixel 662311819389495Test Events tab
  2. Copy the Test Event Code at the top (e.g., TEST12345)
  3. Visit landing with ?test_event_code=TEST12345 appended → events appear with timestamps
  4. For server CAPI verification: temporarily add "test_event_code": "<code>" to the CAPI module body, fire test, remove
  5. Dedup verification: same submission should produce one row labeled “Browser + Server · Deduplicated”

Tool 3 — Events Manager → Diagnostics tab

Question answered: “Is Meta’s optimization layer actually using these events?”

  1. Same pixel → Diagnostics tab
  2. Match Quality 0–10 per event:
    • Lead with browser only → ~3–5/10
    • Lead with CAPI + hashed PII → 7–9/10 target
  3. “Issues” count should be 0% within 7 days of any data-quality fix

Tool 4 — Events Manager → Overview

Question answered: “How much volume and from which source?”

  1. Overview tab shows last 7d event count per event type
  2. “Connection method” column shows Browser / Server / Both per event row
  3. After CAPI ship, every Lead should show Both

6 · Backfill procedure (one-off)

When to use: discovered missing data, lost a week of leads to a broken Pixel, doing a one-time migration.

When NOT to use: forward-only events flow through Make automatically, no backfill needed for routine ops.

Steps

# 1. Export the Leads tab as TSV
#    Google Sheets → Leads → File → Download → "Tab-separated values (.tsv)"
#    Save to e.g. ~/Downloads/leads.tsv

# 2. Get the CAPI access token (Events Manager → CAPI → Manage tokens, or
#    rotate fresh per CAPI hygiene). NEVER commit it.

# 3. Dry-run to preview the first event
cd 28_karada-bp
META_CAPI_TOKEN='EAA…' bun run scripts/capi-backfill.ts ~/Downloads/leads.tsv --dry

# 4. Optional: send to Meta's Test Events tab first
META_CAPI_TOKEN='EAA…' bun run scripts/capi-backfill.ts ~/Downloads/leads.tsv --test-code TESTXYZ

# 5. Real send (default --days=7, override with --days=N)
META_CAPI_TOKEN='EAA…' bun run scripts/capi-backfill.ts ~/Downloads/leads.tsv

Idempotent — event_id = original submission_id so re-running doesn’t double-count. Meta dedupes server vs browser vs previous-backfill against the same id.

Meta’s freshness windows (matters for choosing --days):

  • 0–7 days: full optimization weight + attribution (best window)
  • 7–62 days: attribution only (still shows in reports, doesn’t train the algo)
  • >62 days: rejected by Meta

What gets sent per row

Same logic as the live flow (mirrors Make’s 3 modules):

  • tier ≠ lead_magnet → Lead with value + currency
  • tier = lead_magnet → Subscribe without value
  • mechanism = registration AND tier ≠ lead_magnet → also CompleteRegistration

7 · Token rotation — security hygiene

CAPI tokens are bearer credentials. Treat them like passwords.

When to rotate

  • Any time a token appears in chat history, logs, screenshots, or non-secret stores
  • Quarterly as routine hygiene
  • Immediately if a team member with token access offboards

How to rotate

  1. Events Manager → pixel 662311819389495SettingsConversions API
  2. Manage access tokens → identify the token by name (e.g., “Make integration”)
  3. Revoke the old token
  4. Generate a new token → copy once (Meta never re-displays it)
  5. Update Make: scenario 4743595 → modules 10, 11, 12 → replace access_token value in the body template of each
  6. Verify: run bun run scripts/test-lead-flow.ts vsl → confirm execution still succeeds (no 401 / 403 from Graph API)

What lives where

SecretWhere it livesRotation impact
Meta CAPI tokenMake scenario body templates (3 modules)Update all 3 module bodies after rotation
amoCRM bearerMake scenario amoCRM HTTP module headers (1 module)Update 1 module after rotation
Make webhook URLsrc/config/integration.ts (committed)Repo-level secret, rotate via Make scenario UI
Pixel IDsrc/config/analytics.ts (committed)Not a secret, public

8 · Troubleshooting — common failure modes

Symptom: Meta Diagnostics shows “X% affected — value field is missing”

Likely causes:

  1. A legacy GTM Lead tag is still firing with empty {} payload → pause it in GTM
  2. lead_magnet tier is firing Lead with value: 0 → check conversion-complete.ts:54-93 routes lead_magnet to Subscribe, not Lead
  3. A new tier was added without updating leadValue() → defaults to vip values, which IS valid; not a real bug

Diagnostic window: 7-day rolling, warnings decay automatically once the source is fixed.

Symptom: Make scenario auto-deactivated by maxErrors: 3

Cause: 3+ executions failed in a row, usually from malformed JSON in an HTTP body template after a recent edit.

Recovery:

  1. Open scenario in Make UI → see error in red banner
  2. Inspect the failed execution → identify the broken module
  3. Fix the body template (most common: unescaped quote inside an IML expression)
  4. Re-activate the scenario manually (toggle in Make UI)

Symptom: Backfill script reports events_received < sent

Cause: Some events failed Meta validation (usually missing required field, malformed fbc, expired event_time).

Diagnostic: Meta’s response body in stderr includes per-event error messages. Inspect with fbtrace_id if opening a Meta support ticket.

Symptom: Server-side CAPI events not appearing in Diagnostics

Likely causes:

  1. event_id mismatch with browser Pixel → dedup absorbs server event silently (this is actually fine — it means Meta is dedup’ing as designed; the event still counts, it just doesn’t add a separate row)
  2. CAPI Gateway active AND our server CAPI both firing → check that event_id is consistent so Meta dedupes both into one
  3. Token invalid → Graph API returns 401, Make execution would error (not silent fail)

Symptom: Inline Pixel never loaded (CSP / ad-blocker)

By design: server CAPI still fires from Make regardless. Match quality bumps because hashed em/ph carry the signal even without the cookie. iOS Safari ITP users are also covered this way.


9 · Lessons learned (don’t relearn the hard way)

Make IML / blueprint

  • null keyword in IML outputs nothing, breaks JSON. Use "" or omit the key.
  • Use single-escape \" inside jsonStringBodyContent, not \\\" (triple-escape breaks the expression engine).
  • maxErrors: 3 auto-deactivates a scenario silently — Make sends an email but it’s easy to miss.
  • Filters on modules within a route are AND-applied — modules whose filter doesn’t pass are skipped without counting an operation.

Meta CAPI

  • Standard event names (Lead / Subscribe / CompleteRegistration) are case-sensitive and must match Meta’s spec exactly.
  • value MUST be > 0 on monetary events (Lead / ViewContent / InitiateCheckout / Purchase). Omit instead of sending 0.
  • event_id dedup is per-event-name. Lead + CompleteRegistration with the same id are 2 separate conversions, both counted.
  • fbc format is fb.1.<ms_timestamp>.<fbclid> — subdomain_index always 1 for non-fb domains.
  • Hashing: email → sha256(lower(trim(email))), phone → sha256(digits_only(phone)) (no +, no spaces).

Architecture

  • Inline Pixel survives the 25–40% of paid traffic that ad-blocks googletagmanager.com. Always keep critical events inline.
  • CAPI Gateway is Meta’s auto-bridge — leave it alone, it complements our server CAPI, dedup handles overlap.
  • Don’t replay leads through the Make webhook to backfill — would re-trigger Slack/Sheet/amoCRM as duplicates. Use the standalone script.

10 · Quick reference card

Pixel ID            662311819389495
Make scenario ID    4743595
Make webhook        https://hook.<your-host>/<webhook-id>
amoCRM endpoint     https://yusup.amocrm.ru/api/v4/leads/complex
amoCRM pipeline     9111838 (Источник = "Таргет", enum_id 436603)
amoCRM city field   706141 (Населенный пункт)
Sheet ID            1vvnVz1o5gsrvbGaiiaNzQclmwHBBt-6DTHDHQ7OkCOg
Graph API version   v22.0
Production URL      https://x5.karada.kz
Staging URL         https://karadabp.flowleads.dev

Standard event optimization priority

1. Lead                ← primary, most data, most flexibility
2. CompleteRegistration ← committed users
3. InitiateCheckout    ← mid-funnel intent
4. Subscribe           ← lead-magnet (future)
5. ViewContent         ← top-funnel
6. PageView            ← traffic baseline
7. Contact             ← WhatsApp tap
8. (slot reserved)

Set this ranking in Events Manager → Overview → Configure Web Events.


Last verified end-to-end: 2026-05-27 — Make execution 1e510d0376c0401aa32ff858426d55b1 succeeded with status 1, CAPI backfill fbtrace_id=AQcj-R8dj4hoMsfhzD6LfXa received all 32 events.