Một hàm dùng chung trong layout gốc của trang web đã biến bộ đếm lượt tiếp cận (exposure counter) của thử nghiệm A/B thành bộ đếm lượt xem trang, làm phình to kích thước mẫu và khiến tỷ lệ chuyển đổi trở nên vô nghĩa. Thử nghiệm này, với tỷ lệ chia 50/50 cho trang chủ, đã báo cáo 178 lượt cho một biến thể và 57 lượt cho biến thể còn lại—khác xa so với tỷ lệ chia đều như mong đợi.

Tại sao lỗi này lại lọt qua được bộ phân phối ngẫu nhiên

Trước tiên, lập trình viên đã kiểm tra bộ phân phối ngẫu nhiên (randomizer) dùng để gán khách truy cập vào một biến thể. Anh ấy đã đọc middleware, kiểm tra logic cookie và chạy một script gọi hàm gán 10.000 lần; kết quả cho ra tỷ lệ chia 50/50 hoàn hảo. Bản thân bộ phân phối ngẫu nhiên hoạt động tốt; vấn đề nằm ở cách thức ghi lại lượt tiếp cận.

Một hàm duy nhất thực hiện hai nhiệm vụ:

  1. Thiết lập biến thể – chạy mỗi khi tải trang để giữ cho trải nghiệm của khách truy cập được nhất quán.
  2. Ghi lại sự kiện tiếp cận (exposure event) – chỉ nên kích hoạt một lần duy nhất cho mỗi khách truy cập, ngay tại thời điểm biến thể xuất hiện lần đầu.

Hàm này nằm trong layout gốc, một component được render trong mỗi lần điều hướng. Vì mã ghi lại lượt tiếp cận được thực thi mỗi khi layout được render, nên mọi lượt xem trang đều được tính là một lượt tiếp cận mới. Hai biến thể của trang chủ sử dụng các cấu trúc layout (layout trees) hơi khác nhau, dẫn đến tỷ lệ xem trang của chúng bị chệch nhau, tạo ra ảo giác rằng bộ phân phối ngẫu nhiên đã bị lỗi.

Tại sao việc đếm sai lại quan trọng

Các con số chuyển đổi—lượt nhấp, lượt đăng ký, lượt mua hàng—đều được ghi lại chính xác. Nhưng mẫu số (số lượng lượt tiếp cận) lại sai. Tỷ lệ chuyển đổi được tính toán trông thấp hơn nhiều so với thực tế, và bất kỳ quyết định nào dựa trên các tỷ lệ đó đều không đáng tin cậy.

Thử nghiệm đã chạy được hai tuần trước khi sự sai lệch này lộ ra, buộc cả đội phải loại bỏ toàn bộ tập dữ liệu.

Cách khắc phục

Giải pháp rất đơn giản: chia tách các trách nhiệm thành các hàm riêng biệt. Mã ghi lại lượt tiếp cận hiện tại sẽ kiểm tra xem khách truy cập đã được tính hay chưa, và chỉ kích hoạt một lần duy nhất cho mỗi người dùng. Mã thiết lập biến thể vẫn giữ nguyên vị trí cũ, tiếp tục chạy trong mỗi lần điều hướng.

Ba bài học rút ra cho bất kỳ ai đang thực hiện thử nghiệm

  • Tách biệt việc thiết lập trạng thái khỏi các sự kiện diễn ra một lần. Một hàm vừa thực hiện gán biến thể vừa ghi lại lượt tiếp cận sẽ gây xung đột vì nhiệm vụ trước lặp lại trong khi nhiệm vụ sau thì không được phép.
  • Tránh đưa logic "chỉ chạy một lần" vào layout gốc. Bất cứ thứ gì được đặt trong một component được render ở mỗi lần tải trang sẽ thực thi lặp đi lặp lại, biến "một lần cho mỗi khách truy cập" thành "một lần cho mỗi lượt xem trang".
  • Khi tỷ lệ quan sát được mâu thuẫn với bộ phân phối ngẫu nhiên, hãy kiểm tra bộ đếm trước. Các lập trình viên thường kiểm tra tính công bằng của bộ phân phối ngẫu nhiên nhưng hiếm khi xác minh xem cơ chế đếm có chính xác hay không.

Những điều cần lưu ý tiếp theo

Bất kỳ thử nghiệm nào dựa vào một bộ đếm duy nhất để ghi nhận lượt tiếp cận đều cần được kiểm tra xem bộ đếm đó nằm ở đâu trong cây component. Nếu bộ đếm nằm trong một layout toàn cục, hãy thêm một bước kiểm tra để liên kết sự kiện với một định danh bền vững—chẳng hạn như cookie hoặc flag trong local-storage. Các đội ngũ cũng nên xây dựng một bước kiểm tra tính hợp lý (sanity check) trong dashboard của họ: nếu phân phối biến thể quan sát được chệch khỏi một biên độ thống kê nhỏ, hãy đánh dấu thử nghiệm đó để kiểm tra lại bộ đếm trước khi kết luận rằng bộ phân phối ngẫu nhiên bị lỗi.

Nói tóm lại, một bộ phân phối ngẫu nhiên hoạt động tốt sẽ trở nên vô dụng nếu không có một bộ đếm lượt tiếp cận đáng tin cậy. Việc chia tách trách nhiệm và đặt các sự kiện diễn ra một lần ra ngoài các component luôn được render sẽ giúp các thử nghiệm A/B đảm bảo tính trung thực và tiết kiệm hàng tuần phân tích lãng phí.