ฟังก์ชันที่ใช้ร่วมกันใน root layout ของเว็บไซต์ได้เปลี่ยนตัวนับการเปิดรับการทดสอบ A/B (exposure counter) ให้กลายเป็นตัวนับการเข้าชมหน้าเว็บ (page-view counter) ซึ่งส่งผลให้ขนาดกลุ่มตัวอย่างใหญ่เกินจริงและทำให้อัตรา conversion ไร้ความหมาย การทดสอบนี้เป็นการแบ่งหน้าโฮมเพจแบบ 50/50 แต่ผลลัพธ์กลับรายงานว่ามี 178 hits สำหรับ variant หนึ่ง และเพียง 57 hits สำหรับอีก variant หนึ่ง ซึ่งห่างไกลจากการแบ่งครึ่งที่คาดหวังไว้มาก

ข้อผิดพลาดนี้หลุดรอดการตรวจสอบของตัวสุ่ม (randomizer) ไปได้อย่างไร

ในตอนแรก นักพัฒนาได้ตรวจสอบตัวสุ่มที่ทำหน้าที่กำหนดผู้เข้าชมไปยัง variant ต่างๆ เขาได้อ่าน middleware, ตรวจสอบตรรกะของ cookie และรันสคริปต์ที่เรียกใช้ฟังก์ชันการกำหนดค่า (assignment function) ถึง 10,000 ครั้ง ซึ่งผลลัพธ์ที่ได้คือการแบ่งแบบ 50/50 อย่างสมบูรณ์แบบ ตัว randomizer เองทำงานได้ปกติ แต่ปัญหาอยู่ที่วิธีการบันทึกการเปิดรับ (exposure)

ฟังก์ชันเดียวทำหน้าที่สองอย่าง:

  1. กำหนด variant – ทำงานทุกครั้งที่มีการโหลดหน้าเว็บเพื่อให้ประสบการณ์ของผู้เข้าชมมีความต่อเนื่อง
  2. บันทึกเหตุการณ์การเปิดรับ (exposure event) – ควรทำงานเพียงครั้งเดียวต่อผู้เข้าชมหนึ่งคน ในจังหวะที่ variant ปรากฏขึ้นครั้งแรก

ฟังก์ชันนี้อยู่ใน root layout ซึ่งเป็นคอมโพเนนต์ที่ถูกเรนเดอร์ในทุกการนำทาง (navigation) เนื่องจากโค้ดบันทึกการเปิดรับทำงานทุกครั้งที่ layout ถูกเรนเดอร์ ทุกการเข้าชมหน้าเว็บจึงถูกนับเป็นการเปิดรับครั้งใหม่ เนื่องจาก variant ทั้งสองของหน้าโฮมเพจใช้โครงสร้าง layout tree ที่ต่างกันเล็กน้อย อัตราการเข้าชมหน้าเว็บของทั้งสองจึงแตกต่างกัน จนสร้างภาพลวงตาว่าตัว randomizer ทำงานผิดพลาด

ทำไมการนับผิดพลาดถึงเป็นเรื่องสำคัญ

ตัวเลข conversion ทั้งการคลิก (click-throughs), การลงชื่อเข้าใช้ (sign-ups) และการซื้อ (purchases) ถูกบันทึกไว้อย่างถูกต้อง แต่ตัวหาร (จำนวนการเปิดรับ) นั้นผิดพลาด ทำให้อัตรา conversion ที่คำนวณได้ดูต่ำกว่าความเป็นจริงมาก และการตัดสินใจใดๆ ที่อ้างอิงจากอัตราเหล่านั้นก็ไม่สามารถเชื่อถือได้

การทดสอบดำเนินไปเป็นเวลาสองสัปดาห์ก่อนที่ความคลาดเคลื่อนนี้จะปรากฏขึ้น ทำให้ทีมต้องทิ้งชุดข้อมูลทั้งหมดที่เก็บมา

วิธีแก้ไข

วิธีแก้ไขนั้นตรงไปตรงมา คือการแยกหน้าที่ความรับผิดชอบออกเป็นฟังก์ชันต่างหาก ตอนนี้โค้ดบันทึกการเปิดรับจะตรวจสอบก่อนว่าผู้เข้าชมรายนี้ถูกนับไปแล้วหรือยัง และจะทำงานเพียงครั้งเดียวต่อผู้ใช้เท่านั้น ส่วนโค้ดกำหนด variant ยังคงอยู่ที่เดิมและทำงานต่อไปในทุกการนำทาง

3 บทเรียนสำหรับผู้ที่ทำการทดลอง

  • แยกการกำหนดสถานะ (state-setting) ออกจากเหตุการณ์ที่เกิดขึ้นครั้งเดียว (one-off events) ฟังก์ชันที่ทั้งกำหนด variant และบันทึกการเปิดรับในตัวเดียวกันจะเกิดการขัดแย้งกัน เพราะอย่างแรกต้องทำซ้ำ แต่อย่างหลังต้องไม่ทำซ้ำ
  • หลีกเลี่ยงตรรกะแบบทำงานครั้งเดียว (one-shot logic) ใน root layout อะไรก็ตามที่วางไว้ในคอมโพเนนต์ที่เรนเดอร์ทุกครั้งที่มีการโหลดหน้าเว็บจะทำงานซ้ำๆ เปลี่ยนจาก "หนึ่งครั้งต่อผู้เข้าชม" เป็น "หนึ่งครั้งต่อการเข้าชมหน้าเว็บ"
  • เมื่อการแบ่งกลุ่มที่สังเกตได้ขัดแย้งกับตัว randomizer ให้ตรวจสอบตัวนับก่อน นักพัฒนามักจะทดสอบความยุติธรรมของตัว randomizer แต่ไม่ค่อยตรวจสอบว่ากลไกการนับนั้นแม่นยำหรือไม่

สิ่งที่ควรระวังในอนาคต

การทดลองใดๆ ที่พึ่งพาตัวนับการเปิดรับเพียงตัวเดียว ควรได้รับการตรวจสอบว่าตัวนับนั้นวางอยู่ในตำแหน่งใดของ component tree หากตัวนับอยู่ใน global layout ให้เพิ่มการตรวจสอบที่เชื่อมโยงเหตุการณ์เข้ากับตัวระบุตัวตนที่คงอยู่ (persistent identifier) เช่น cookie หรือ flag ใน local-storage นอกจากนี้ ทีมควรสร้างการตรวจสอบความถูกต้องเบื้องต้น (sanity check) ไว้ใน dashboard ของตนด้วย: หากการกระจายตัวของ variant ที่สังเกตได้เบี่ยงเบนไปเกินขอบเขตทางสถิติเล็กน้อย ให้ทำเครื่องหมายเพื่อตรวจสอบตัวนับก่อนที่จะสรุปว่าตัว randomizer เสีย

กล่าวโดยสรุป ตัว randomizer ที่ทำงานได้ดีจะไร้ประโยชน์หากไม่มีการนับการเปิดรับที่เชื่อถือได้ การแยกหน้าที่ความรับผิดชอบและวางเหตุการณ์ที่เกิดขึ้นครั้งเดียวไว้นอกคอมโพเนนต์ที่ถูกเรนเดอร์ตลอดเวลา จะช่วยให้การทดสอบ A/B มีความแม่นยำและช่วยประหยัดเวลาจากการวิเคราะห์ที่สูญเปล่าไปหลายสัปดาห์