⌨️ Thế Giới Keycap Docs Tracking

thegioikeycap.vn — Tracking Meta Pixel + Conversions API

Plugin tgk-tracking tại wp-content/plugins/tgk-tracking/. Pixel: 1284965829812105

Là plugin riêng, không nằm trong theme — tracking phải sống độc lập với giao diện, đổi/nâng cấp theme không được làm mất dữ liệu chuyển đổi.


1. Event đang bắn

Event Meta Kích hoạt event_id Giá trị
PageView mọi trang frontend pv.{random} —
ViewContent chỉ trang chi tiết sản phẩm vc.{random} giá sản phẩm
AddToCart woocommerce_add_to_cart (cả AJAX và form) atc.{random} giá × số lượng
InitiateCheckout load trang /thanh-toan/ ic.{hash phiên+giỏ} tổng giỏ
CompleteRegistration woocommerce_created_customer, user_register reg.{user_id} —
Purchase trang cảm ơn + đơn sang processing/completed pur.{order_id} tổng đơn

PageView khác ViewContent thế nào

PageView bắn ở mọi trang và không mang thông tin gì — Meta chỉ biết "có người vào site". ViewContent chỉ bắn ở trang sản phẩm và mang content_ids + giá — Meta biết "người này xem sản phẩm X giá Y".

Cần ViewContent vì: (1) retarget được "ai xem sản phẩm X mà chưa mua"; (2) Catalog/Dynamic Ads bắt buộc phải có nó; (3) Meta học chuỗi ViewContent → AddToCart → Purchase để tìm người giống khách đã mua — thiếu mắt xích đầu thì tín hiệu yếu hẳn.


2. Dedup — thiết kế cốt lõi

Meta gộp 2 event thành 1 khi trùng cả event_name và event_id, trong cửa sổ ~48 giờ.

Ba quyết định làm nên việc đó:

a) event_id do SERVER sinh, rồi truyền xuống browser. Nếu để mỗi bên tự sinh thì hai ID không bao giờ trùng và Meta đếm đôi mọi chuyển đổi. Base pixel ở đây cố tình không gọi fbq('track','PageView') như snippet mặc định của Meta — snippet đó không kèm eventID nên không dedup được.

b) Event có định danh nghiệp vụ dùng ID tất định. Purchase → pur.{order_id}. Khách F5 trang cảm ơn 10 lần, webhook SePay về, đơn đổi trạng thái — tất cả đều sinh ra đúng một event_id.

c) UNIQUE KEY trên event_id ở tầng database. INSERT IGNORE nên lần ghi thứ hai bị chặn im lặng. Đây là chốt cuối, không phụ thuộc vào logic PHP có đúng hay không.

Đã kiểm chứng: gọi on_purchase(1707) 6 lần → 1 event, tổng doanh thu ghi nhận đúng 450.000đ (không đội).


3. Vì sao Purchase móc vào 3 chỗ

  • COD: khách thấy trang cảm ơn → bắn tại đó.
  • SePay QR: khách có thể tắt trình duyệt trước khi chuyển khoản xong; đơn chỉ được xác nhận khi webhook về. Không móc vào woocommerce_order_status_processing là mất hẳn chuyển đổi đó.

Bắn nhiều chỗ mà không đội số là nhờ (b) + (c) ở trên.

Bẫy đã xử lý: IP/UA khi bắn từ webhook

Webhook SePay là request của server SePay, không phải của khách. Lấy $_SERVER lúc đó sẽ gửi IP/UA của SePay lên Meta → match sai người.

Xử lý: woocommerce_checkout_create_order chụp _fbp/_fbc/UTM vào meta đơn ngay lúc khách đặt; khi bắn Purchase thì đọc IP/UA từ $order->get_customer_ip_address() / get_customer_user_agent() và fbp/fbc từ meta đơn.


4. Luồng dữ liệu

Hành vi khách
   │
   ├─→ TGK_Events::record()
   │      ├─ ghi bảng wp_tgk_events (NGUỒN SỰ THẬT, lưu cả PII thô + UTM)
   │      ├─ xếp hàng CAPI  ──→ cron mỗi phút ──→ Graph API /events
   │      └─ xếp hàng client ─→ fbq('track', name, params, {eventID})
   │
   └─→ Meta nhận 2 bản cùng event_id → tính 1 chuyển đổi

Vì sao không gửi CAPI đồng bộ trong request của khách: mỗi lần gọi Graph API mất 100–800ms, cộng thẳng vào TTFB — đúng thứ vừa tối ưu được khi bỏ Astro. Nên: ghi DB (nhanh, cục bộ) → cron gửi theo lô.

Ngoại lệ: Purchase được thử gửi ngay (send_now) vì quan trọng nhất và số lượng thấp; lỗi thì cron vẫn thử lại tới 5 lần.

Thêm giỏ bằng AJAX không tải lại trang, nên event client được nhồi vào fragment div.tgk-events của WooCommerce; assets/js/tracking.js đọc ra và bắn ngay khi WooCommerce phát sự kiện added_to_cart.


5. Dữ liệu gửi Meta (match level full)

Mọi PII đều SHA-256 sau khi chuẩn hoá. Chuẩn hoá sai thì hash khác nhau và Meta không match được — đây là nguyên nhân match rate thấp phổ biến nhất.

Field Nguồn Chuẩn hoá
em email đơn/user trim + lowercase
ph điện thoại đơn chỉ số, kèm mã quốc gia: 0979 481 951 → 84979481951
fn ln tên đơn/user lowercase, bỏ ký tự không phải chữ/số
ct st zp country địa chỉ đơn lowercase, bỏ khoảng trắng/dấu câu
external_id user ID hash
fbp cookie _fbp không hash
fbc cookie _fbc, hoặc dựng từ fbclid dạng fb.1.{ts_ms}.{fbclid} không hash
client_ip_address IP thật (bỏ IP nội bộ) không hash
client_user_agent UA không hash

Dữ liệu thô (email/phone chưa hash, UTM, referer) chỉ lưu ở cột raw_request trên server này, không gửi ra ngoài. Đó là ý "lưu toàn bộ dữ liệu về server rồi mới gửi payload enrich".

Đổi mức: option tgk_tracking_settings['match_level'] = full | contact | minimal.


5b. LocalStorage Attribution & Multi-Touch Tracking (WMT Kit)

Bổ sung từ phiên bản 1.1.0 (2026-09-20) theo docs/TRACKING_LOCALSTORAGE_ATTRIBUTION_SPEC.md.

Vấn đề trước đây

Trước đây plugin chỉ đọc $_GET lúc request xử lý. Khi khách bấm link quảng cáo vào landing page rồi chuyển trang xem sản phẩm, vào giỏ, sang thanh toán (/?wc-ajax=checkout), $_GET hoàn toàn rỗng $\rightarrow$ mất attribution (>80% đơn bị gán "Direct") và không có fbc cho CAPI nếu cookie _fbc bị Safari ITP chặn.

Cơ chế hoạt động

  1. Client-side (assets/js/tracking.js): - tgk_anon_id: UUID v4 định danh thiết bị ẩn danh, sinh 1 lần và lưu vĩnh viễn trong localStorage. - tgk_last_touch: Lưu JSON gồm toàn bộ UTM và Click ID (utm_source, utm_medium, utm_campaign, utm_content, utm_term, utm_id, fbclid, gclid, gbraid, wbraid, gad_source, gad_campaignid, ttclid) kèm timestamp ts và TTL 30 ngày. Cập nhật khi có tham số mới. - tgk_first_landing: Lưu URL đầu tiên khách đặt chân vào site (kèm query string). - tgk_first_referrer: Lưu document.referrer ban đầu. - Checkout Bridge: Tự động inject hidden input input[name="tgk_attribution"] vào form.checkout khi tải trang, khi WooCommerce bắn updated_checkout / init_checkout, và trước khi submit đơn.
  2. Server-side (includes/class-events.php): - capture_order_attribution( $order ) đọc tgk_attribution từ $_POST (fallback về self::utm_params() nếu tắt JS). - Tự động dựng _tgk_fbc từ fbclid trong LocalStorage nếu cookie _fbc bị Safari chặn: fb.1.{time_ms}.{fbclid}. - Lưu vào Order Meta: _tgk_utm, _tgk_fbc, _tgk_fbp, _tgk_anon_id, _tgk_first_landing, _tgk_first_referrer.
  3. Quản trị đơn hàng (includes/class-admin.php): - Meta box "Nguồn đơn hàng" trong màn hình đơn hàng WooCommerce hiển thị trọn vẹn: Nguồn, Kênh, Chiến dịch, Quảng cáo, Click ID (Facebook/Google/TikTok), Thiết bị (UUID), Trang đầu, và Tín hiệu khớp Meta (fbp · fbc).

5c. Tối ưu tải máy chủ & Giám sát RUM (2026-09-24)

  • Loại bỏ pingback tự DDoS: Trước đây client bắn fetch(/wp-json/tgk/v1/event) trên mọi PageView và ViewContent chỉ để đánh dấu cột client_fired = 1 trong wp-admin, làm nhân đôi số lượng request vào PHP-FPM worker. Đã gỡ bỏ pingback trên PageView/ViewContent, chỉ giữ lại trên Purchase.
  • Hàng đợi client stub queue (window.tgkTrack): Hứng các lệnh gọi tracking trước khi script tải xong, tương thích tải bất đồng bộ qua GTM mà không mất sự kiện.
  • RUM Core Web Vitals: Tích hợp PerformanceObserver tự động đo TTFB, FCP, LCP, CLS thực tế của khách vào window.TGK_Vitals (chuẩn như apex-tracker.js).

6. Bảo mật

Rủi ro Xử lý
Client tự đặt event_id = pur.{order_id} của đơn thật → Meta coi Purchase thật là trùng và bỏ qua Endpoint REST chỉ nhận event_id khớp ^(atc\|pv\|ic\|vc)\.[a-z0-9]{8,32}$; từ chối pur./reg.
Client bơm giá trị chuyển đổi Giá luôn tính lại từ DB theo product_id, không lấy từ client
Spam endpoint Nonce wp_rest + lọc bot
Bot làm loãng dữ liệu Lọc UA (bot, crawl, ahrefs, facebookexternalhit…) + loại admin/cron/REST/feed. Không lọc thì mỗi lần Googlebot quét là một PageView gửi lên Meta
Token lọt vào backup DB Token ở hằng số TGK_CAPI_TOKEN trong wp-config.php, không lưu DB

7. Quản trị

wp-admin → TGK Tracking: trạng thái cấu hình, thống kê 7 ngày, 50 event gần nhất kèm phản hồi của Meta, nút "Gửi hàng đợi ngay" và "Thử lại event lỗi".

Cột "Có bắn client" thấp hơn "Số lượng" là bình thường — đó chính là phần khách dùng chặn quảng cáo mà chỉ server-side thu được. Đây là thước đo giá trị của việc chạy song song 2 đường.

Cron: tgk_tracking_dispatch mỗi phút, tgk_tracking_prune mỗi ngày (giữ log 180 ngày).


8. Kết quả kiểm chứng

Đã chạy đơn hàng test end-to-end thật (2026-09-04, qua Test Events code TEST8224, không vào dữ liệu thật; đã tắt tạm send-app để không bắn thông báo cho khách/chủ shop).

Toàn bộ funnel đi qua form checkout thật:

Event Số lượng Giá trị
PageView 30 —
ViewContent 1 450.000đ
AddToCart 2 900.000đ
InitiateCheckout 2 900.000đ
Purchase 1 450.000đ
  • Meta trả events_received khớp số gửi, messages: [] — không warning nào
  • Dedup trên đơn thật: 1 lần đặt + 4 lần F5 trang cảm ơn → đúng 1 Purchase. Các ID auto-increment bị nhảy (46, 48, 50) chính là 3 lần trùng bị UNIQUE chặn ở tầng DB.
  • Hash điện thoại đúng: 0979481951 → chuẩn hoá 84979481951 → hash khớp chính xác giá trị gửi lên Meta
  • user_data đủ 8 field hash (em, ph, fn, ln, ct, st, zp, country) + IP + UA thật
  • Đơn cancelled → không ghi event
  • fbc dựng lại từ fbclid đúng format Meta
  • TTFB không đổi: ~0,12s (trước tracking ~0,12–0,18s)

Sau khi test: đơn #1717 đã huỷ để hoàn kho (2 → 1 → 2) rồi xoá hẳn; send-app bật lại; test code tắt; log event dọn sạch.

Khi xoá đơn test, phải huỷ trước rồi mới xoá. Xoá thẳng thì WooCommerce không hoàn kho → tồn kho lệch âm thầm.

Bug đã tìm và sửa trong lúc dựng

  1. should_track() chặn AJAX → mất hẳn AddToCart từ nút icon trên lưới (đường thêm giỏ chính).
  2. Purchase từ webhook gửi IP/UA của server SePay thay vì của khách.
  3. wp_schedule_event() gọi trước khi khai báo chu kỳ tgk_minute → trả false trong im lặng, cron không bao giờ chạy, event nằm mãi ở pending mà không có lỗi nào hiện ra.

9. Kiểm thử lại sau này

Muốn test mà không đụng dữ liệu thật:

  1. Lấy code ở Events Manager → Test Events
  2. Bỏ comment dòng trong wp-config.php: define( 'TGK_TEST_EVENT_CODE', 'TESTxxxx' );
  3. Tắt tạm send-app (hook woocommerce_new_order → bắn thông báo thật cho khách/chủ shop)
  4. Test xong: comment lại test code (để nguyên thì mọi chuyển đổi thật biến mất khỏi báo cáo quảng cáo), bật lại send-app, huỷ-rồi-xoá đơn test

10. Việc còn lại

  • Catalog / content_ids — hiện dùng product ID của WooCommerce. Nếu chạy Advantage+ Catalog Ads thì ID trong feed sản phẩm phải khớp giá trị này, không thì mất khớp sản phẩm và Dynamic Ads không hoạt động.