Bài 6/10Kiến thức

Bài 6, Enhanced Ecommerce: cài purchase tracking đúng cho doanh thu báo cáo khớp với sổ kế toán

Schema dataLayer của GA4 cho e-commerce, 6 event chuẩn (view_item → purchase), cách push từ Shopify / WooCommerce / Next.js custom, và 3 sai sót khiến doanh thu GA4 luôn lệch với MISA.

The Data Way14 phút đọc
Series Google Analytics, The Data Way

Nếu doanh nghiệp bạn bán hàng online, đây là bài quan trọng nhất series.

Khi purchase event được track đúng, mọi báo cáo doanh thu trong GA4 sẽ có nghĩa: doanh thu theo kênh, theo chiến dịch ads, theo từng sản phẩm. Mọi tích hợp với Google Ads, Facebook Ads, TikTok Ads cũng dựa vào event này.

Nhưng nếu track sai, và rất nhiều website ở Việt Nam đang track sai mà không biết, thì mỗi báo cáo GA4 đều chứa số liệu sai lệch. Marketing dựa vào đó để quyết ngân sách → lãng phí ngân sách vào kênh không hiệu quả.


dataLayer là gì, giải thích đơn giản

dataLayer là một JavaScript array trên trang web, đóng vai trò “người đưa tin” giữa website và GTM.

Khi website có sự kiện quan trọng (user xem sản phẩm, thêm giỏ, thanh toán), code của website push một object vào dataLayer. GTM “nghe” mọi push lên dataLayer, match với trigger đã cài, fire tag tương ứng → gửi event đến GA4.

Cấu trúc cơ bản của một push:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: "view_item",
  ecommerce: {
    currency: "VND",
    value: 450000,
    items: [{
      item_id: "SKU-12345",
      item_name: "Áo thun cotton trắng size M",
      price: 450000,
      quantity: 1,
      item_category: "Áo thun",
    }],
  },
});

3 phần quan trọng:

  • event: tên event GTM sẽ nghe (custom event trigger)
  • ecommerce: object chứa dữ liệu chuẩn của GA4 ecommerce
  • items: mảng các sản phẩm liên quan (xem, thêm giỏ, mua)

Mistake phổ biến, push thiếu reset

GA4 có một quirk: nếu bạn push 2 lần liên tiếp với cùng nested object ecommerce, lần thứ 2 có thể bị bỏ qua vì giá trị giống nhau.

Best practice: push null để reset trước khi push event mới:

window.dataLayer.push({ ecommerce: null }); // reset
window.dataLayer.push({
  event: "purchase",
  ecommerce: { ... },
});

Bỏ dòng reset → purchase event sau đó trong cùng session có thể không gửi value → revenue trong GA4 thấp hơn thực tế.


6 ecommerce event chuẩn của GA4

Sử dụng đúng tên chuẩn này → GA4 tự kích hoạt báo cáo Monetization → Ecommerce purchases, Reports → Monetization. Đặt sai tên → mất hết báo cáo này.

EventKhi nào pushPhổ biến ở đâu
view_item_listUser xem trang danh sách sản phẩm (category, search results)Trang category, search
view_itemUser mở chi tiết 1 sản phẩmTrang chi tiết sản phẩm
add_to_cartUser thêm vào giỏClick nút Add to Cart
begin_checkoutUser bắt đầu thanh toánClick nút Checkout
purchaseThanh toán thành côngTrang Thank You / sau khi server xác nhận đơn
refundHoàn hàngKhi admin xử lý refund

Còn 4 event khác ít dùng hơn: select_item, add_payment_info, add_shipping_info, view_cart. Cài sau khi có data từ 6 cái trên.

Schema items, phần hay sai nhất

items là array. Mỗi phần tử là một sản phẩm. Field bắt buộc/khuyến nghị:

{
  item_id: "SKU-12345",        // BẮT BUỘC — mã sản phẩm
  item_name: "Áo thun trắng",  // BẮT BUỘC — tên sản phẩm
  price: 450000,                // KHUYẾN NGHỊ — giá đơn vị (số, không có "VND")
  quantity: 1,                  // KHUYẾN NGHỊ — số lượng
  currency: "VND",              // có thể đặt ở level trên hoặc trong từng item
  item_brand: "ACME",           // tuỳ chọn
  item_category: "Áo thun",     // tuỳ chọn — phân loại
  item_variant: "Size M",       // tuỳ chọn — biến thể (size, màu)
  discount: 50000,              // tuỳ chọn — số tiền giảm
  affiliation: "Shopee",        // tuỳ chọn — nguồn bán
  index: 0,                     // tuỳ chọn — vị trí trong list (cho view_item_list)
}

3 quy tắc quan trọng:

  1. pricegiá đơn vị, không phải tổng. Nếu user mua 3 áo giá 450 nghìn → price: 450000, quantity: 3. Tổng value (level trên) phải bằng price * quantity.

  2. item_id phải nhất quán giữa các event. Cùng một sản phẩm → cùng SKU ở view_item, add_to_cart, purchase. Lệch nhau → GA4 không thể link funnel.

  3. currency phải nhất quán và là mã ISO 4217: VND, USD, EUR. Không phải vnd hay đồng.


Cài cho từng nền tảng

Shopify

Tin tốt: app Google & YouTube Channel của Shopify (đã giới thiệu ở bài 3) tự động push tất cả 6 event ecommerce vào dataLayer.

Verify: mở DevTools Console → gõ dataLayer → thêm sản phẩm vào giỏ → kiểm tra dataLayer array có push mới với event add_to_cart không.

Nếu app official cài đúng, bạn chỉ cần làm phần GTM: tạo Custom Event triggers cho mỗi event name (view_item, add_to_cart...) và GA4 Event tag tương ứng.

Nếu cần custom hơn (vd. push thêm parameter affiliation: "Direct" cho mọi đơn hàng) → vào Online Store → Themes → Edit code → mở theme.liquid → thêm script custom dataLayer push.

WooCommerce

Tin tốt: plugin GTM4WP (miễn phí) tự push tất cả ecommerce event vào dataLayer.

Cài plugin → Settings → tab Integration → bật Track classic ecommerce hoặc Track enhanced ecommerce (UA) hoặc Track GA4 ecommerce. Chọn cái phù hợp với phiên bản GA bạn dùng, với GA4 mới: chọn GA4 ecommerce.

Sau khi bật, không cần code gì thêm. WooCommerce events tự push lên dataLayer. Bạn chỉ làm GTM (Custom Event triggers + GA4 Event tags).

Next.js / Custom

Đây là phần phải code. Mỗi lần action xảy ra trên website, gọi function push dataLayer.

Tạo utility:

// src/lib/analytics.ts
type EcommerceItem = {
  item_id: string;
  item_name: string;
  price: number;
  quantity: number;
  currency?: string;
  item_brand?: string;
  item_category?: string;
  item_variant?: string;
};

declare global {
  interface Window {
    dataLayer: Record<string, unknown>[];
  }
}

export function trackEvent(
  eventName: string,
  ecommerce: { currency: string; value: number; items: EcommerceItem[] }
) {
  if (typeof window === "undefined") return;
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({ ecommerce: null }); // reset
  window.dataLayer.push({
    event: eventName,
    ecommerce,
  });
}

Sử dụng trong component:

// trang product detail
useEffect(() => {
  trackEvent("view_item", {
    currency: "VND",
    value: product.price,
    items: [{
      item_id: product.sku,
      item_name: product.name,
      price: product.price,
      quantity: 1,
      item_category: product.category,
    }],
  });
}, [product]);

Tương tự cho add_to_cart, begin_checkout. Purchase event nên gọi từ server action sau khi confirm đơn hàng (chi tiết phần dưới).

Khi nên gọi từ client vs từ server

Client-side (trong useEffect / event handler):

  • view_item, view_item_list, phụ thuộc vào việc user render trang đó
  • add_to_cart, phụ thuộc vào click button của user

Server-side (qua sGTM, hoặc thêm event vào server response để client push):

  • purchase, phải confirm đơn hàng đã ghi vào DB, payment thành công. Nếu push từ client trước khi server xác nhận → có thể track double hoặc track đơn hàng fail
  • refund, luôn là server action

purchase track sai = doanh thu GA4 sai. Đầu tư thời gian làm đúng phần này.


Push purchase đúng cách

Đây là pattern phổ biến cho Next.js + Server Action:

// server action: app/api/checkout/route.ts hoặc trong server action file
"use server";

export async function completePurchase(orderId: string) {
  // 1. Confirm payment với gateway (VNPay, Stripe, MoMo...)
  const payment = await confirmPayment(orderId);
  if (!payment.success) return { success: false };

  // 2. Lưu order vào DB
  const order = await saveOrder(orderId);

  // 3. Trả về response với data để client push lên dataLayer
  return {
    success: true,
    purchaseData: {
      transaction_id: order.id,
      value: order.total,
      currency: "VND",
      tax: order.tax,
      shipping: order.shipping_fee,
      items: order.items.map((it) => ({
        item_id: it.sku,
        item_name: it.name,
        price: it.unit_price,
        quantity: it.qty,
      })),
    },
  };
}

Trong client component:

const handleCheckoutComplete = async () => {
  const result = await completePurchase(orderId);
  if (result.success && result.purchaseData) {
    trackEvent("purchase", result.purchaseData);
    router.push("/thank-you?order=" + result.purchaseData.transaction_id);
  }
};

Pattern này đảm bảo: chỉ push purchase khi payment confirmed và order đã lưu DB. Nếu user reload trang thank-you → không push lại (vì call action đã chạy 1 lần). Nếu payment fail → không push.


3 sai sót khiến doanh thu GA4 ≠ MISA

Sai sót 1, Track double (track ở 2 nơi)

Tình huống: bạn cài track purchase ở client (thank-you page useEffect) + plugin ecommerce (như GTM4WP) cũng tự push event purchase. Một đơn hàng bị track 2 lần → doanh thu nhân đôi.

Cách check: vào GA4 → DebugView → mua thử 1 sản phẩm 100 nghìn → xem có 1 hay 2 event purchase xuất hiện.

Cách fix: chọn một nguồn duy nhất, gỡ cái kia.

Sai sót 2, Không xử lý refund

User mua 1 triệu → bạn refund 100 nghìn vì 1 sản phẩm bị lỗi. Nhưng GA4 vẫn ghi doanh thu là 1 triệu. Báo cáo doanh thu GA4 sẽ luôn cao hơn thực tế.

Cách fix: khi admin xử lý refund trong hệ thống nội bộ, trigger event refund lên dataLayer:

trackEvent("refund", {
  currency: "VND",
  value: 100000, // số tiền refund, KHÔNG phải tổng đơn gốc
  items: [/* item bị refund */],
  transaction_id: "ORDER-123",
});

GA4 sẽ trừ doanh thu refund khỏi báo cáo. Nhưng phải gửi event này, không tự động.

Sai sót 3, Không trừ thuế VAT (hoặc trừ 2 lần)

GA4 có sẵn parameter taxshipping. Quy tắc:

  • value = tổng số tiền user trả thực sự (gross, đã bao gồm VAT và shipping)
  • tax = số tiền VAT trong value đó
  • shipping = phí ship trong value đó

Nếu MISA tính doanh thu không bao gồm VAT (net revenue) → bạn cần báo cáo GA4 ở dạng net cũng → khi lập dashboard, trừ tax ra khỏi value.

Nhiều shop ở Việt Nam push value = giá net (không VAT) → đúng so với MISA, nhưng Google Ads attribution lại nhận con số nhỏ → ROAS báo cáo trong Google Ads thấp hơn thực. Phải nhất quán cách tính giữa MISA và GA4 ngay từ đầu.

Audit doanh thu GA4 vs MISA, quy trình tôi dùng

Mỗi tháng tôi audit 1 lần:

  1. Vào MISA → lấy báo cáo doanh thu online của tháng (gross revenue)
  2. Vào GA4 → Monetization → Ecommerce purchases → cùng tháng → lấy total revenue
  3. So sánh
  4. Lệch dưới 2% → bỏ qua (do timezone, refund chưa report lên GA4...)
  5. Lệch 2-10% → check refund tracking + currency
  6. Lệch >10% → có vấn đề lớn, purchase event sai

Nếu doanh thu GA4 và MISA lệch >10% mà chưa rõ vì sao → mọi báo cáo marketing dựa vào GA4 đều chứa số sai. Sửa ngay.


AI hỗ trợ, phần tiết kiệm nhiều nhất

Sinh dataLayer push code cho framework cụ thể

Prompt:

Tôi đang dùng Next.js 15 App Router + Stripe checkout. Sau khi Stripe
confirm thanh toán (webhook callback), tôi cần push event "purchase" lên
dataLayer client-side để GTM bắt và gửi đến GA4.

Cấu trúc order từ Stripe:
- order.id (string)
- order.amount_total (cent USD, cần convert sang VND)
- order.line_items (array, mỗi item có product.metadata.sku, name, price, quantity)

Yêu cầu:
1. Server-side: receive webhook, lưu order vào DB
2. Client-side: trang /thank-you nhận order_id từ query string, fetch order
   từ DB, push dataLayer purchase event với đầy đủ items[], value, currency
3. Đảm bảo không push double nếu user reload page (dùng sessionStorage hoặc
   gửi flag từ server "already_tracked")
4. Type-safe TypeScript
5. Convert VND không có decimal (Stripe trả về integer cent → chia 100)

Viết code đầy đủ.

Output ra 80-100 dòng code production-ready. So với tự nghĩ + đọc Stripe docs + đọc GA4 ecommerce docs: ~2 giờ thay vì 10 phút.

Sinh dataLayer mẫu để test GTM

Khi bạn build GTM tag cho purchase nhưng chưa có người mua thật để test, bạn có thể tự push event mẫu trong Console:

Sinh cho tôi 3 dataLayer.push() mẫu để test GA4 ecommerce tag trong GTM
Preview mode. Các kịch bản:

1. Mua 1 sản phẩm áo thun 450 nghìn, có VAT 10%, shipping 30 nghìn
2. Mua 3 sản phẩm khác nhau, tổng 1,2 triệu trước VAT
3. Refund toàn bộ đơn 2 (cả 3 sản phẩm)

Mỗi push kèm dataLayer.push({ ecommerce: null }) reset trước.
Items đầy đủ field: item_id, item_name, price, quantity, currency, item_category.

Copy output, paste vào Console của browser, run từng cái, GTM Preview sẽ thấy event tương ứng → kiểm tra tag GA4 bắt đúng.

Audit dataLayer schema hiện tại

Nếu thừa hưởng codebase từ ai đó, không biết dataLayer đang push đúng/sai:

Đây là 10 dataLayer push tôi capture được trên website (trích từ DevTools
Console → dataLayer). Liệt kê:

1. Event nào KHÔNG phải tên chuẩn GA4 → đề xuất tên đúng
2. Field nào thiếu trong items array
3. Vấn đề khác (currency không đúng ISO, value lệch, etc.)
4. Sắp xếp theo mức độ ảnh hưởng cao xuống thấp

[paste dataLayer dump]

AI ra một báo cáo audit nhanh. Tự đọc 10 push để tìm lỗi mất 30 phút. AI ra báo cáo trong 30 giây.


Trước khi sang bài 7

Checklist quan trọng:

  1. ✅ DataLayer đã push 6 ecommerce event chính (view_item → purchase), verify trong Console
  2. ✅ GTM có 6 Custom Event trigger + 6 GA4 Event tag tương ứng
  3. ✅ DebugView GA4 hiển thị 6 event đầy đủ với items[], value, currency
  4. ✅ Mua thử 1 sản phẩm thật → 1 event purchase (không phải 2)
  5. ✅ Doanh thu báo trong GA4 sau 24h khớp với đơn hàng thật ±2%

Khi xong, GA4 của bạn đã có ecommerce tracking hoàn chỉnh. Báo cáo Monetization → Ecommerce purchases có data. Google Ads / Facebook Ads có thể import conversion purchase với value chính xác → bid theo ROAS chính xác.

Bài 7 sẽ là phần kỹ thuật nhất series: Server-side Tagging (sGTM). Đây là bước nâng cao cho doanh nghiệp đã chạy quy mô đủ lớn, gửi event qua server bạn sở hữu thay vì trực tiếp đến Google. Giúp giảm tác động của iOS 14, ad blocker, và cookie banner. Bài đó dành cho ai sẵn sàng tốn ~50-100 USD/tháng cloud + 1 ngày setup.

Nếu bạn dừng ở bài 6

Honestly, nhiều SME Việt không cần sGTM (bài 7). Nếu doanh nghiệp bạn quy mô dưới 5 tỷ doanh thu/tháng online, ecommerce tracking client-side (bài 6) là đủ. Đầu tư thời gian vào bài 8 (debug) và bài 9 (dashboard) tốt hơn.

Bài 7 chỉ cần thiết khi: bạn chạy ads với budget >100 triệu/tháng và conversion attribution là critical, hoặc bạn đã có team dev/devops sẵn sàng vận hành cloud infra.

Đọc tiếp

Đọc xong rồi?

Bạn muốn cài giúp hoặc nhờ rà soát?

30 phút trao đổi miễn phí. Bạn share screen GA4/GTM hiện tại, chúng tôi chỉ ra 3 chỗ đang đo sai và cách sửa. Không sales, không ép.