From e46b55d81d7875a347b0d132f793331227da7864 Mon Sep 17 00:00:00 2001 From: Bilal Mansouri <124762008+bighadj22@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:18:14 +0100 Subject: [PATCH] feat(tracking): Meta Pixel + Conversions API compliance overhaul Browser pixel + server CAPI rebuilt to Meta's current spec, verified live end-to-end (browser events, server Lead via durable Workflow, Meta audit rows). Client (cod-server/src/lib/capi.ts): - Graph API v18.0 (expired) -> v26.0 - Network/5xx throw -> Cloudflare Workflow retries actually engage; 4xx returns success:false (Meta rejects whole batch - fail fast) - PII per docs: phone normalized (+00213 forms fixed), city/names diacritic-folded, fn/ln/external_id/event_source_url/content_ids added - fbc/fbp/IP/UA unhashed, as Meta requires Durability - the runtime cancels un-awaited promises after response, which silently dropped every workflow trigger: - All 3 Purchase trigger sites (dashboard status, Yalidine + ZR webhooks) now run via executionCtx.waitUntil - Lead moved from inline one-shot fetch to the same durable Workflow path (capi-{orderId}-Lead), with event_source_url Merchant control (migration 0018): - conversion_event: Lead or Purchase - explicitly chosen by the merchant, never defaulted; gates every send - test_mode toggle routes CAPI to Meta's test stream - ad_account_name reference label; access token now write-only (masked reads, keep-on-empty save - same rule as dzverify/Sendili) Checkout validation: - Algerian mobile only (05/06/07 + 8 digits), server-enforced and normalized to canonical local form; storefront blur-validation with ar/fr/en messages - InitiateCheckout fires on first checkout interaction, not form visibility Browser pixel: - Custom deferred loader (dropped events when window.load stalled) replaced with Meta's canonical base code Audit + docs: capi_event_log rows for sent/failed/skipped incl. Lead; docs/META-TRACKING.md end-user guide (trigger chain, webhook relation, test mode, troubleshooting); README Growth Engine claims re-verified. --- README.md | 11 +- cod-astro/theme01/src/core/actions/index.ts | 16 +- cod-astro/theme01/src/core/api/client.ts | 5 +- cod-astro/theme01/src/pages/thank-you.astro | 5 +- .../theme/components/layout/BaseHead.astro | 43 +-- .../product/ProductDetailContent.astro | 1 + cod-astro/theme01/src/theme/content/ar.ts | 1 + cod-astro/theme01/src/theme/content/en.ts | 1 + cod-astro/theme01/src/theme/content/fr.ts | 1 + cod-astro/theme01/src/theme/content/types.ts | 1 + .../theme01/src/theme/scripts/product.ts | 74 +++-- .../src/theme/scripts/track-abandonment.ts | 8 +- cod-client-astro/locales/ar/settings.json | 17 +- cod-client-astro/locales/en/settings.json | 17 +- cod-client-astro/locales/fr/settings.json | 17 +- .../src/features/settings/api.test.ts | 6 +- .../settings/components/TrackingSettings.tsx | 124 +++++++- .../src/features/settings/types.ts | 8 +- .../0018_pixel_conversion_settings.sql | 10 + .../abandoned-orders/store-routes.ts | 4 +- .../src/endpoints/orders/orders.test.ts | 1 + .../src/endpoints/orders/routes.test.ts | 1 + .../endpoints/orders/status-transitions.ts | 26 +- cod-server/src/endpoints/store-otp/phone.ts | 20 +- cod-server/src/endpoints/store/CONTEXT.md | 2 +- .../endpoints/store/capi-lead-trigger.test.ts | 147 +++++++++ cod-server/src/endpoints/store/handlers.ts | 57 ++-- .../src/endpoints/store/otp-gate.test.ts | 20 +- cod-server/src/endpoints/store/routes.test.ts | 17 +- .../src/endpoints/store/validation.test.ts | 63 ++++ cod-server/src/endpoints/store/validation.ts | 13 +- cod-server/src/endpoints/stores/CONTEXT.md | 23 +- cod-server/src/endpoints/stores/handlers.ts | 26 +- .../src/endpoints/stores/routes.test.ts | 75 ++++- cod-server/src/endpoints/stores/routes.ts | 10 +- cod-server/src/endpoints/webhooks/handlers.ts | 30 +- .../src/endpoints/webhooks/webhooks.test.ts | 1 + cod-server/src/lib/capi-log.ts | 32 ++ cod-server/src/lib/capi.test.ts | 234 +++++++++++++++ cod-server/src/lib/capi.ts | 85 ++++-- cod-server/src/openapi/schemas/store.ts | 17 +- cod-server/src/workflows/capi-helpers.test.ts | 80 +++++ cod-server/src/workflows/capi-helpers.ts | 47 +++ cod-server/src/workflows/capi.ts | 169 +++++++---- cod-shared/db/schema.ts | 6 + cod-shared/queries/pixel-config.test.ts | 164 ++++++++++ cod-shared/queries/pixel-config.ts | 29 +- cod-shared/queries/store.ts | 9 +- docs/META-TRACKING.md | 284 ++++++++++++++++++ 49 files changed, 1811 insertions(+), 247 deletions(-) create mode 100644 cod-server/src/db/migrations/0018_pixel_conversion_settings.sql create mode 100644 cod-server/src/endpoints/store/capi-lead-trigger.test.ts create mode 100644 cod-server/src/endpoints/store/validation.test.ts create mode 100644 cod-server/src/lib/capi-log.ts create mode 100644 cod-server/src/lib/capi.test.ts create mode 100644 cod-server/src/workflows/capi-helpers.test.ts create mode 100644 cod-shared/queries/pixel-config.test.ts create mode 100644 docs/META-TRACKING.md diff --git a/README.md b/README.md index 674448f..ea93646 100644 --- a/README.md +++ b/README.md @@ -109,11 +109,14 @@ CodFlow v1.1.0 — here's what works today: - ✅ Partial returns with automatic inventory restock ### Growth Engine -- ✅ Meta Conversions API (CAPI) fires `Purchase` only at confirmed delivery -- ✅ 7-day attribution window compliance -- ✅ Advanced data hashing (phone, city, IP, User-Agent) +- ✅ Meta Pixel (browser) + Conversions API (server) dual setup with event deduplication +- ✅ Merchant-chosen conversion event: `Lead` at order placement or `Purchase` at confirmed delivery +- ✅ Test Mode toggle routes CAPI events to Meta's test stream (`test_event_code`) +- ✅ Graph API v26.0 with 7-day attribution window compliance +- ✅ PII hashed per Meta spec (phone, names, city, zip, country, external_id); IP, User-Agent, `fbp`, `fbc` sent unhashed as required - ✅ `fbp` and `fbc` attribution preservation -- ✅ Durable retry with Cloudflare Workflows +- ✅ Durable retry with Cloudflare Workflows (network + Meta 5xx, exponential backoff) +- ✅ CAPI event audit log (`capi_event_log`) for every send attempt ### AI & Agentic (MCP) - ✅ RFC 9728 OAuth Protected Resource Discovery with dynamic client registration diff --git a/cod-astro/theme01/src/core/actions/index.ts b/cod-astro/theme01/src/core/actions/index.ts index ec0f3d4..9a2cae3 100644 --- a/cod-astro/theme01/src/core/actions/index.ts +++ b/cod-astro/theme01/src/core/actions/index.ts @@ -66,7 +66,19 @@ export const server = { z.string().min(10).max(1024).optional() ), }), - handler: async (input) => { + handler: async (input, context) => { + // Forward the shopper's attribution headers so cod-server records the + // visitor, not this worker — same mechanism as core/endpoints/abandoned.ts. + const forwardedHeaders: Record = {}; + const userAgent = context.request.headers.get("User-Agent"); + if (userAgent) forwardedHeaders["User-Agent"] = userAgent; + const clientIp = + context.request.headers.get("CF-Connecting-IP") ?? + context.request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim(); + if (clientIp) forwardedHeaders["X-Forwarded-For"] = clientIp; + const referer = context.request.headers.get("Referer"); + if (referer) forwardedHeaders["Referer"] = referer; + const result = await placeOrder({ customerName: input.customerName, phone: input.phone, @@ -86,7 +98,7 @@ export const server = { fbc: input.fbc, fbp: input.fbp, otpToken: input.otpToken, - }); + }, forwardedHeaders); if (!result.success) { throw new Error(result.error); diff --git a/cod-astro/theme01/src/core/api/client.ts b/cod-astro/theme01/src/core/api/client.ts index ca53eb5..c00a726 100644 --- a/cod-astro/theme01/src/core/api/client.ts +++ b/cod-astro/theme01/src/core/api/client.ts @@ -141,12 +141,13 @@ export async function submitReview( } export async function placeOrder( - body: Record + body: Record, + forwardedHeaders?: Record ): Promise<{ success: true; data: { orderNumber: string; orderId: string; total: number } } | { success: false; error: string }> { try { const res = await fetch(`${COD_SERVER_URL}/store/orders`, { method: "POST", - headers: storeHeaders(), + headers: { ...storeHeaders(), ...forwardedHeaders }, body: JSON.stringify(body), }); const json = (await res.json()) as any; diff --git a/cod-astro/theme01/src/pages/thank-you.astro b/cod-astro/theme01/src/pages/thank-you.astro index 5b58fb1..577ed63 100644 --- a/cod-astro/theme01/src/pages/thank-you.astro +++ b/cod-astro/theme01/src/pages/thank-you.astro @@ -112,7 +112,10 @@ const steps = [