Documentation

Meta Pixel + Conversions API — Dokumentasi & Alur

Dokumen ini menjelaskan bagaimana tracking di semogabisa.com bekerja: Meta Pixel di browser + Conversions API (CAPI) di server, dengan deduplikasi via event_id.


1. Latar belakang & tujuan

Kenapa butuh dua jalur (Pixel + CAPI)?

Browser-only Pixel sudah lama menjadi standar untuk tracking ke Meta. Tapi sejak iOS 14.5 (ATT) dan tumbuhnya ad blocker, 30–50% event dari browser hilang sebelum sampai ke Meta.

Conversions API mengirim event yang sama dari server kita langsung ke Meta — server-to-server, tidak bisa di-block. Ini cara Meta meng-handle privacy era pasca-cookies.

Hasil: dua jalur paralel (browser + server) untuk event yang sama, dengan ID unik supaya Meta tidak hitung dua kali.

Goal yang dicapai

GoalSolusi di project ini
Track registration & deposit accuratelyEvent CompleteRegistration + Purchase fired setelah backend confirm
Reduce lost tracking from iOS/ad blockersServer CAPI sebagai fallback untuk Pixel yang ke-block
Avoid duplicate eventsSame event_id di browser + server → Meta dedup dalam ~3 hari

2. Arsitektur

Sistem tracking kita berjalan di 3 tempat:

Tempat 1 — Browser user

Saat halaman load, ada dua hal yang aktif di browser:

  • Meta Pixel (fbevents.js) — script dari Meta yang di-inject di <head>. Tugas: set cookie _fbp, terima fbq("track", ...) calls, kirim ke facebook.net/tr.
  • Kode aplikasi kita (React + TrackingProvider) — yang memanggil fbq() dan mengirim ke server kita sendiri.

Saat event dipicu (misal user klik daftar), kode kita melakukan dua hal sekaligus:

  1. Panggil fbq("track", "Purchase", data, { eventID }) — Pixel kirim ke Meta dari browser
  2. POST ke /api/capi dengan body { event_id, email, phone, ... } — endpoint kita sendiri

Tempat 2 — Server Next.js kita (/api/capi)

Server kita menerima request dari step 2 di atas, lalu:

  1. SHA-256 hash semua PII (email, phone, username)
  2. Baca IP user dari header x-forwarded-for
  3. Baca user agent dari header user-agent
  4. Build payload final sesuai spec Meta CAPI
  5. POST ke graph.facebook.com/v23.0/{PIXEL_ID}/events dengan access_token

Tempat 3 — Meta Graph API

Meta menerima dua event untuk action yang sama: satu dari browser Pixel, satu dari server kita. Karena keduanya pakai event_id yang persis sama, Meta:

  1. Dedup — hitung sebagai 1 event (bukan 2)
  2. Match user — cocokkan event ke user Facebook via fbp, fbc, hashed email/phone, IP, user agent
  3. Attribute — kalau user ini sebelumnya klik ad, event di-attribute ke campaign tersebut

Kenapa pakai 2 jalur?

Browser Pixel bisa di-block oleh:

  • iOS 14.5+ App Tracking Transparency
  • Ad blocker (uBlock, Brave shields, dll)
  • Privacy mode browser
  • Network firewall

Server-to-server (CAPI) tidak bisa di-block karena tidak lewat browser user. Jadi kalau Pixel di-block, server CAPI tetap kirim. Kalau Pixel tidak di-block, dua jalur kirim → Meta dedup → tetap 1 event.

Komponen utama

LayerFileTugas
Browser Pixel initapp/layout.tsxInject fbevents.js + fbq('init', PIXEL_ID)
Client trackingcontexts/tracking.tsxGenerate event_id, fire fbq(), POST /api/capi
Server CAPIapp/api/capi/route.tsHash PII, append IP/UA, forward ke Meta
Debug consolecomponents/tracking-console.tsxVisual debugger FAB di kiri-bawah
PageView trackercomponents/page-view-tracker.tsxAuto-fire PageView setiap route change

3. Asal usul setiap field — dari mana data datang?

Setiap field di payload yang dikirim ke Meta berasal dari source berbeda. Beberapa hardcoded, beberapa dari state user, beberapa dari cookie, beberapa dari HTTP header. Section ini mentrace setiap field.

3.1 Top-level event fields

Kolom Required? mengikuti spec resmi Meta CAPI. "Required" = Meta tolak event tanpa field ini. "Recommended" = Meta terima tapi match quality / attribution rusak. "Optional" = aman dikosongkan.

FieldRequired?SourceTipeLokasi di code
event_nameRequiredHardcoded string di call siteLiteralSetiap pemanggilan track(), contoh "Purchase"
event_timeRequiredUnix timestamp saat event fire (client time, dalam detik)Numbertracking.tsx:67Math.floor(Date.now() / 1000). Server fallback Date.now()/1000 di route.ts:62 kalau client tidak kirim
action_sourceRequiredHardcoded "website"Literalroute.ts:64
event_idRecommended (Required untuk dedup)Generated random saat track() dipanggilUUID-like (evt_xxx)tracking.tsx:66uuid(). Server reject 400 kalau missing di route.ts:45-49 karena tanpa ini sistem dedup kita pecah
event_source_urlRequired (karena action_source: website)window.location.href saat fire, fallback header referer di serverURL stringtracking.tsx:95-97, fallback route.ts:65-66
user_dataRequired (object harus ada, dengan minimal 1 identifier)Lihat section 3.2Objectroute.ts:67-77
custom_dataOptional di top-level, Required value+currency untuk event PurchaseLihat section 3.3Objectroute.ts:78
test_event_codeOptional (hanya saat testing)Env META_TEST_EVENT_CODEStringroute.ts:81

End-to-end trace untuk Purchase:

// ─── Step 1: call site (app/(app)/deposit/page.tsx) ───
const eventId = track({
  event_name: "Purchase",            // ← hardcoded literal
  custom_data: { value: amount, ... },
  user_data: { email, phone, external_id },
});

// ─── Step 2: di contexts/tracking.tsx ───
const event_id = uuid();             // ← "evt_a3f5b9e2..." generated di sini
const event_time = Math.floor(Date.now() / 1000);  // ← 1716033600
window.fbq("track", "Purchase", custom_data, { eventID: event_id });  // browser
fetch("/api/capi", {
  body: JSON.stringify({ event_name, event_id, event_time, ... })
});

// ─── Step 3: di app/api/capi/route.ts ───
{
  data: [{
    event_name,                       // ← "Purchase" passthrough
    event_time,                       // ← 1716033600 (client time, atau server fallback)
    event_id,                         // ← "evt_a3f5b9e2..." passthrough
    action_source: "website",         // ← hardcoded di server
    event_source_url,                 // ← "http://localhost:3000/deposit"
    ...
  }]
}

3.2 user_data fields — 3 kategori source

Aturan dasar dari Meta: object user_data itu sendiri Required, dan minimal harus ada 1 identifier di dalamnya. Identifier yang valid termasuk salah satu dari: em, ph, external_id, client_ip_address + client_user_agent (pair), fbp, fbc. Event ditolak kalau tidak ada satupun. Karena itu setiap field di bawah technically "Optional" sendiri-sendiri, tapi secara kolektif minimal 1 wajib ada.

A. Diisi dari state user — di-hash SHA-256 di server

Field di MetaRequired?Source di stateDiteruskan sebagaiServer hashContoh transformasi
emOptional (recommended — match quality tertinggi)user.email di contexts/user.tsxuser_data.email (plaintext) di POST bodyroute.ts:68[email protected]["a3f5b9..."]
phOptional (recommended)user.phoneuser_data.phoneroute.ts:69+62 812 3456 7890["7b8c9d..."]
external_idOptional (recommended)user.usernameuser_data.external_idroute.ts:70-72andidewi["1f2e3d..."]

State user email/phone/username diisi pada:

TriggerLokasiBagaimana
Form register submitregister/page.tsx:60-65setUser({ username, email, phone, balance }) dengan input dari form
Login (saat ini mock)user.tsx:23-28Default user andidewi digunakan
Edit di Profile pageprofile/page.tsxSaat user save (belum wired ke setUser, hanya local state)

B. Diambil dari cookie atau URL — tidak di-hash

FieldRequired?SourceCara kerja
fbpOptional (recommended — perlu untuk attribution organik)Cookie _fbp yang di-set oleh fbevents.js saat Pixel inittracking.tsx:44-46readCookie("_fbp"). Kalau belum ada (Pixel belum sempat init), pakai placeholder fb.1.{ts}.{random}
fbcOptional (Required hanya kalau user datang dari ad click)Cookie _fbc yang di-set Pixel dari ?fbclid= URL param, atau di-construct manualtracking.tsx:47-54 — baca cookie real dulu, kalau tidak ada cek ?fbclid= di URL dan construct

Format fbp dan fbc:

_fbp = fb.1.1716033600123.1234567890
       │  │  │             └─ random ID (10 digit)
       │  │  └─ Unix timestamp dalam millisecond
       │  └─ subdomain index (selalu 1)
       └─ version (selalu fb)

_fbc = fb.1.1716033600123.IwAR1abc...
                          └─ value dari ?fbclid= di URL

Kapan fbc ada? Hanya kalau user datang dari ad click (URL mengandung ?fbclid=...). Visitor organik = fbc kosong, normal.

C. Diambil dari HTTP request header — tidak di-hash

FieldRequired?Source headerFallbackLokasi di code
client_ip_addressOptional sendiri, tapi Required ketika tidak ada em/ph/external_id (pair dengan client_user_agent)x-forwarded-for (first IP dalam comma-list)x-real-iproute.ts:52-55
client_user_agentOptional sendiri, tapi Required ketika tidak ada em/ph/external_id (pair dengan client_ip_address)user-agentroute.ts:56

Di production behind Vercel/Cloudflare, kedua header ini di-set otomatis oleh edge proxy. Di localhost biasanya IP = ::1 (IPv6 loopback) — itu normal, event tetap delivered tapi tidak bisa match siapa pun.

3.3 custom_data fields — per-event, dari call site

Tidak ada hashing, tidak ada transformasi server. Langsung passthrough dari call site ke Meta. Required/Optional di kolom Required? mengacu ke Meta Pixel standard events reference.

Eventcustom_data fieldRequired?Source value
PageViewcontent_nameOptional (Meta PageView tidak punya required custom_data)pathname dari usePathname() di page-view-tracker.tsx:21
ViewContentcontent_idsOptional (Required untuk Advantage+ catalog ads)[game.id] — dari object game yang diklik
content_nameOptionalgame.name"Lucky Sevens"
content_categoryOptionalgame.category"Slot"
content_typeOptionalHardcoded "game"
CompleteRegistrationcontent_nameOptionalHardcoded "signup"
statusOptionalHardcoded "completed"
currencyOptionalHardcoded "IDR"
valueOptionalHardcoded 0 (registrasi gratis)
PurchasevalueRequiredamount — angka dari input form deposit
currencyRequiredHardcoded "IDR"
content_nameOptionalHardcoded "deposit"
content_typeOptionalHardcoded "deposit"
order_idOptional (recommended — untuk reporting & dedup di sisi reporting)txId generated saat deposit confirmed: "TX" + Date.now().toString(36).toUpperCase()

Catatan untuk Purchase: Meta akan menolak event Purchase yang tidak punya value dan currency. Karena di code deposit/page.tsx keduanya selalu di-set dari amount input + hardcoded "IDR", requirement ini selalu terpenuhi. Jangan hapus salah satunya tanpa update logic-nya juga.

3.4 Bagaimana semua data digabung jadi satu payload

Data datang dari 3 sumber, lalu di-gabungkan jadi payload final. Berikut alurnya step-by-step saat user trigger event (misal klik "Bayar Sekarang"):

Step 1 — User klik tombol di browser. Handler di page component (misal deposit/page.tsx) memanggil track({ event_name, custom_data, user_data }). Call site sudah punya:

  • event_name (hardcoded di kode, contoh "Purchase")
  • custom_data.value, currency, order_id (sebagian hardcoded, value dari input form)
  • user_data.email, phone, external_id (dari state user di context — diisi saat register)

Step 2 — Function track() menambahkan field runtime. Di contexts/tracking.tsx, saat track() dipanggil, dia menambahkan:

  • event_id = uuid() — UUID baru per call (ini yang dipakai untuk dedup)
  • event_time = Math.floor(Date.now() / 1000) — timestamp sekarang
  • event_source_url = window.location.href — URL halaman saat event fire
  • fbp — baca dari cookie _fbp (di-set oleh Meta Pixel)
  • fbc — baca dari cookie _fbc atau dari ?fbclid= query param

Step 3 — Dua jalur paralel.

Jalur browser:

  • fbq("track", event_name, custom_data, { eventID: event_id }) — Meta Pixel kirim langsung ke facebook.net/tr. Tidak ada PII di sini.

Jalur server:

  • fetch("/api/capi", { body: JSON.stringify({ event_name, event_id, event_time, custom_data, user_data, fbp, fbc, ... }) })
  • PII di body ini masih plaintext — akan di-hash di server next step.

Step 4 — Server /api/capi melengkapi payload. Di route.ts, server menambahkan field yang hanya bisa dilakukan di server:

  • SHA-256 hash emailem, phoneph, usernameexternal_id
  • Baca x-forwarded-for header → client_ip_address
  • Baca user-agent header → client_user_agent
  • Hardcode action_source: "website"
  • Append test_event_code kalau env META_TEST_EVENT_CODE ada

Step 5 — POST ke Meta. Server kirim ke https://graph.facebook.com/v23.0/{PIXEL_ID}/events?access_token=... dengan payload lengkap.

3.5 Ringkasan: setiap layer menyumbang apa

LayerContribute apa
Call site (page components)event_name, custom_data, state user (email/phone/username), kondisi business (amount, txId, pathname, game info)
track() function (tracking.tsx)event_id (uuid baru), event_time (sekarang), event_source_url (dari location.href), fbp/fbc (dari cookie)
Server route handler (route.ts)SHA-256 hashing PII, baca IP/UA dari request header, hardcode action_source, append test_event_code kalau ada, default event_time kalau missing
Meta sideMatch user via fbp/fbc/em/ph/external_id, dedup via event_id, attribute ke ad campaign

4. Alur lengkap satu event (contoh: Purchase)

Skenario: user mau deposit Rp 100.000 via QRIS. Berikut yang terjadi dari klik tombol sampai event terdaftar di Meta.

Step 1 — User klik "Bayar Sekarang"

Di halaman deposit, user pilih nominal Rp 100.000, pilih metode QRIS, klik tombol bayar. Handler confirm() di deposit/page.tsx ke-trigger.

State berubah: setProcessing(true) → tombol berubah jadi "Memproses…".

Step 2 — Tunggu backend confirm payment

Handler await ke payment processor (saat ini di-mock dengan setTimeout 1400ms — di production diganti fetch ke real payment gateway atau menunggu webhook).

Penting: event Meta belum di-fire di step ini. Kalau payment gagal, tidak ada event yang terkirim.

Step 3 — Backend balas sukses, generate txId

Setelah await selesai (artinya payment confirmed), handler generate transaction ID:

const txId = "TX" + Date.now().toString(36).toUpperCase()
// Contoh: "TX1MWXYZ4K"

Step 4 — Panggil track() untuk fire event

const eventId = track({
  event_name: "Purchase",
  custom_data: {
    value: 100000,
    currency: "IDR",
    content_name: "deposit",
    order_id: "TX1MWXYZ4K",
  },
  user_data: {
    email: "[email protected]",
    phone: "+62 812 3456 7890",
    external_id: "andidewi",
  },
});

Step 5 — track() generate event_id dan kirim dua jalur

Di dalam track() (tracking.tsx:62-117):

  1. event_id = uuid() → contohnya "evt_a3f5b9e2...". ID ini dipakai untuk kedua jalur supaya Meta bisa dedup.

  2. Jalur browser: panggil fbq("track", "Purchase", { value, currency }, { eventID: event_id }). Meta Pixel di browser langsung kirim request ke https://www.facebook.com/tr dengan parameter event.

  3. Jalur server: fetch("/api/capi", { body }) dengan body berisi semua data termasuk PII plaintext (email/phone/username).

Kedua jalur berangkat hampir bersamaan dari browser.

Step 6 — Server /api/capi proses request

Di route.ts:

  1. Parse JSON body dari request
  2. Hash PII: sha256("[email protected]")"a3f5b9..." (64 char hex). Sama untuk phone dan username.
  3. Baca IP user dari header x-forwarded-for
  4. Baca user-agent dari header user-agent
  5. Build payload Meta CAPI dengan action_source: "website", event_time, dan semua field tadi
  6. Kalau env META_TEST_EVENT_CODE di-set, append ke payload sebagai test_event_code

Step 7 — Server POST ke Meta Graph API

POST https://graph.facebook.com/v23.0/1302640427964663/events
     ?access_token=EAAxxxxx
Content-Type: application/json

{ "data": [{ "event_name": "Purchase", "event_id": "evt_a3f5b9e2...", ... }] }

Meta balas dalam ~200ms:

{ "events_received": 1, "fbtrace_id": "AbCdEf..." }

Step 8 — Server balas client, client log ke debug console

Server return ke browser:

{ "status": "sent", "meta": { "events_received": 1, ... }, "payload": {...} }

Debug console FAB di kiri-bawah aplikasi log entry "server" untuk event ini, di samping entry "browser" yang sudah di-log step 5.

Step 9 — Meta dedup

Di sisi Meta, ada 2 event yang masuk:

  • Path browser (lewat facebook.com/tr) dengan event_id = evt_a3f5b9e2...
  • Path server (lewat graph.facebook.com/.../events) dengan event_id = evt_a3f5b9e2... (sama)

Karena ID identik dan masuk dalam window ~3 hari, Meta hitung sebagai 1 Purchase event, bukan 2. Di Test Events tab, ini di-label "Deduplicated".

Step 10 — Redirect ke success page

Di browser, handler confirm() selesai. State update: setSuccessData(...), setProcessing(false), lalu router.push("/success") — user lihat halaman sukses dengan info txId dan eventId.

Catatan penting

  • Kalau Pixel di-block (ad blocker / iOS ATT), Path browser gagal tapi Path server tetap kirim. Meta tetap dapat 1 event. Tidak ada dedup karena hanya 1 source, tapi event tetap recorded.
  • Kalau server /api/capi error (token salah, network issue), Path server gagal tapi Path browser tetap kirim. Sebaliknya, Meta tetap dapat 1 event.
  • Kedua jalur fail di waktu bersamaan sangat jarang — itu yang bikin sistem ini robust.

5. Empat event yang di-track

5.1 PageView — setiap route change

PropertyValue
Fired olehcomponents/page-view-tracker.tsx:16-25
TriggerSetiap usePathname() berubah
CaraAuto, tidak ada user action diperlukan
user_data saat anonymousfbp, fbc, IP, UA
user_data saat authenticated+ em, ph, external_id (hashed)
custom_data{ content_name: pathname }
// page-view-tracker.tsx
track({
  event_name: "PageView",
  custom_data: { content_name: pathname },
  user_data: authed ? { email, phone, external_id: username } : {},
});

5.2 ViewContent — buka detail game

PropertyValue
Fired olehapp/(app)/lobby/page.tsx:27-44
TriggerKlik game tile di lobby
custom_data{ content_ids, content_name, content_category, content_type: "game" }
// lobby/page.tsx
track({
  event_name: "ViewContent",
  custom_data: {
    content_ids: [game.id],
    content_name: game.name,
    content_category: game.category,
    content_type: "game",
  },
  user_data: { email, phone, external_id: username },
});

5.3 CompleteRegistration — setelah registrasi sukses

PropertyValue
Fired olehapp/register/page.tsx:42-58
TriggerForm submit + backend confirmed (saat ini mock dengan setTimeout)
PentingFire setelah await selesai — bukan saat klik tombol
custom_data{ content_name: "signup", status: "completed", currency: "IDR", value: 0 }
// register/page.tsx
setSubmitting(true);
await new Promise((r) => setTimeout(r, 900));  // ← future: real API call to /api/register
//                                                    Saat itu sudah server-confirmed
const eventId = track({
  event_name: "CompleteRegistration",
  custom_data: { content_name: "signup", status: "completed", currency: "IDR", value: 0 },
  user_data: { email: form.email, phone: form.phone, external_id: form.username },
});

5.4 Purchase — setelah deposit confirmed

PropertyValue
Fired olehapp/(app)/deposit/page.tsx:23-46
TriggerPayment processor confirmed (saat ini mock)
PentingFire setelah await selesai — bukan saat klik bayar
custom_data{ value, currency: "IDR", content_name: "deposit", order_id }
// deposit/page.tsx
setProcessing(true);
await new Promise((r) => setTimeout(r, 1400));  // ← future: real call to payment processor webhook
const txId = "TX" + Date.now().toString(36).toUpperCase();

const eventId = track({
  event_name: "Purchase",
  custom_data: {
    value: amount,
    currency: "IDR",
    content_name: "deposit",
    content_type: "deposit",
    order_id: txId,
  },
  user_data: { email: user.email, phone: user.phone, external_id: user.username },
});

6. Mekanisme deduplikasi

Aturan dasar

Same event_id between browser & server within ~3 days → Meta counts it ONCE.

Implementasi di code

// contexts/tracking.tsx:66
const event_id = uuid();  // Satu UUID untuk dua jalur

// Path A — Browser Pixel
window.fbq("track", event_name, custom_data, { eventID: event_id });

// Path B — Server CAPI  
fetch("/api/capi", {
  method: "POST",
  body: JSON.stringify({ event_name, event_id, ... }),
});

Server kemudian forward event_id itu ke Meta sebagai bagian payload (route.ts:65).

Verifikasi dedup berjalan

Di Meta Events Manager → Test Events, klik event yang masuk:

✓ Purchase     Browser    12:34:56    Received
✓ Purchase     Server     12:34:57    Received · Deduplicated  ← label ini = dedup OK

Kalau tidak ada label "Deduplicated" padahal event masuk dari kedua source, berarti event_id-nya tidak sama. Debug di local FAB console — bandingkan event_id dari entry browser vs server.


7. Handling PII (Personally Identifiable Information)

Field yang di-hash (SHA-256, mandatory)

Field di MetaSourceLokasi hashing
emuser.emailroute.ts:68
phuser.phoneroute.ts:69
external_iduser.usernameroute.ts:70-72

Field yang NOT di-hash

FieldSource
fbpCookie _fbp yang di-set Meta Pixel
fbcCookie _fbc (dari ?fbclid= query param) atau constructed
client_ip_addressHeader x-forwarded-for atau x-real-ip di route.ts:54-55
client_user_agentHeader user-agent di route.ts:56

Aturan hashing yang benar

// route.ts:20-21
const sha = (s: string) =>
  crypto.createHash("sha256").update(s.trim().toLowerCase()).digest("hex");
  • .trim() — hapus whitespace leading/trailing
  • .toLowerCase() — normalize case ([email protected] dan [email protected] jadi hash yang sama)
  • .digest("hex") — output 64-char hex string

Penting: hashing dilakukan di server

Kita tidak hash di browser. Client kirim plaintext email/phone via HTTPS ke /api/capi, server yang hash. Alasan:

  1. Hash di browser bisa di-tampered (user bisa modify fbq runtime)
  2. Server-side hashing memastikan konsistensi normalization
  3. HTTPS sudah secure dari client ke server kita sendiri

Match quality score

Semakin banyak parameter user_data yang lengkap, semakin tinggi match quality score (max 10/10):

User stateParameter yang availableExpected score
Anonymous visitorfbp, IP, UA, optional fbc3-5
Authenticated user+ em, ph, external_id8-10

8. Environment variables

# .env.local

# Public — exposed ke browser, dipakai di app/layout.tsx untuk init Pixel
NEXT_PUBLIC_META_PIXEL_ID=1302640427964663

# Server-only — dipakai di app/api/capi/route.ts
META_PIXEL_ID=1302640427964663
META_ACCESS_TOKEN=EAAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Optional — saat di-set, event di-tag dan hanya muncul di Test Events tab
META_TEST_EVENT_CODE=TEST12345

Behavior tanpa env

Kalau META_PIXEL_ID atau META_ACCESS_TOKEN kosong, route handler return mocked response (route.ts:83-89):

{
  "status": "mocked",
  "reason": "META_PIXEL_ID or META_ACCESS_TOKEN not configured",
  "payload": { /* what would have been sent */ }
}

Berguna untuk development tanpa kredensial real.


9. Testing workflow

A. Setup test environment

  1. Set semua env di .env.local termasuk META_TEST_EVENT_CODE
  2. Restart dev server: bun run dev
  3. Buka https://business.facebook.com/events_manager2/list/pixel/1302640427964663/test_events di tab terpisah (= dashboard Test Events Meta)

B. Trigger semua 4 event

Buka http://localhost:3000?fbclid=ABC123TEST (fbclid bikin fbc cookie supaya match quality kelihatan):

ActionEvent yang di-fire
Load landingPageView (anonymous)
Klik "Daftar" → submit formPageView × 1 + CompleteRegistration
Klik "Mulai Main" di successPageView (authenticated)
Klik game di lobbyViewContent
Klik "+ Deposit"PageView
Submit deposit formPurchase

C. Verifikasi di 3 tempat

1. Local debug console (FAB "Meta Pixel" kiri-bawah app)

  • Browser entries muncul instant
  • Server entries muncul ~200-500ms kemudian
  • Kedua punya event_id yang identik

2. DevTools Network tab

  • Filter facebook.net/tr — request dari browser Pixel
  • Filter /api/capi — request ke server kita
  • Response /api/capi harusnya: { status: "sent", meta: { events_received: 1, ... } }

3. Meta Test Events dashboard

  • Event muncul dalam 30-60 detik
  • Setiap event punya 2 baris: Browser dan Server, dengan label "Deduplicated" di server entry
  • Klik event row → expand "Customer Information Parameters" → harusnya 7/7 detected untuk authenticated user

D. Cleanup setelah testing

Hapus META_TEST_EVENT_CODE dari .env.local sebelum deploy production — kalau tidak, event production akan tetap masuk ke Test tab dan tidak muncul di analytics asli.


10. Troubleshooting

Browser Pixel events tidak fire

Cek di Network tab — ada request ke facebook.net/tr?

  • Tidak ada request sama sekali → Pixel script gagal load. Cek NEXT_PUBLIC_META_PIXEL_ID di .env.local, restart dev. Atau ad blocker memblokir fbevents.js.
  • Request ada tapi fbq is not defined → race condition, PageViewTracker fire sebelum script loaded. Browser-side PageView pertama akan miss, tapi server-side via CAPI tetap masuk. Tidak ada action required.

Server CAPI events return error

Cek response /api/capi di Network tab.

ResponsePenyebabSolusi
status: "mocked"Env belum di-setIsi META_PIXEL_ID + META_ACCESS_TOKEN, restart dev
status: "error", meta: { error: "Invalid OAuth access token" }Token salah/expiredGenerate token baru di Events Manager → Settings → Conversions API
status: "error", meta: { error: "Unsupported request" }Pixel ID salah atau token tidak punya akses ke Pixel iniVerifikasi META_PIXEL_ID sesuai dengan Pixel yang di-assign ke system user
Status 502, network errorConnectivity issueCek apakah server bisa reach graph.facebook.com

Dedup tidak nyambung (events muncul tapi tidak ber-label "Deduplicated")

Cek di FAB console — event_id browser dan server harus persis sama untuk event yang sama. Kalau beda:

  • Cek tracking.tsx:66const event_id = uuid() di-generate sekali per call
  • Cek bahwa event_id di-pass ke fbq sebagai eventID (case-sensitive) dan ke /api/capi body sebagai event_id

fbp / fbc empty di Test Events

  • fbp empty → Pixel belum sempat set cookie. Refresh page, retry.
  • fbc empty → User tidak punya ?fbclid= di URL. Normal untuk visitor organik. Pastikan ad campaign URL pakai {{fbclid}} macro.

Match quality rendah (<6 untuk authenticated user)

Cek parameter yang missing di Test Events → expand event:

Parameter missingPenyebab
em / phUser tidak punya email/phone di state, atau form belum di-submit dengan data lengkap
external_idUsername kosong
client_ip_addressBehind proxy yang tidak set x-forwarded-for — common di local dev (IP = ::1), normal
fbpPixel belum init saat event fire

11. Production checklist

Sebelum deploy live:

  • HTTPS wajib — fbq() tidak fire di http:// non-localhost. Pastikan deploy ke hosting auto-HTTPS (Vercel, Netlify, Cloudflare).
  • Hapus META_TEST_EVENT_CODE dari env production. Set hanya di staging/dev.
  • Domain Verification di Business Settings → Brand Safety → Domains → add domain production. Wajib untuk attribution iOS 14.5+.
  • Aggregated Event Measurement (AEM) di Events Manager → configure prioritas 8 event untuk domain. Untuk casino/game site, biasanya: Purchase > CompleteRegistration > ViewContent > PageView.
  • Replace mock backend calls dengan real API:
    • Register flow → real /api/auth/register endpoint
    • Deposit flow → real payment processor webhook
    • track() tetap fire setelah real API confirm success (pattern sudah benar)
  • Rate limiting di /api/capi route (opsional) — proteksi dari abuse jika endpoint exposed
  • Logging & monitoring — log CAPI errors ke observability tool (Sentry, Datadog, dll)
  • Token rotation — Meta token never expires, tapi best practice rotate setiap 90 hari. Set reminder di calendar.
  • Privacy policy — pastikan halaman privacy mention Meta Pixel dan CAPI sebagai data processor

12. Referensi


Last updated: 2026-06-04