یک تابع مشترک در root layout سایت، شمارنده مواجهه (exposure counter) تست A/B را به شمارنده بازدید صفحه (page-view counter) تبدیل کرد که باعث بزرگ‌نمایی اندازه نمونه و بی‌معنی شدن نرخ‌های تبدیل شد. این تست که یک تقسیم ۵۰/۵۰ از صفحه اصلی بود، ۱۷۸ مورد برای یک واریانت و ۵۷ مورد برای دیگری گزارش کرد که با تقسیم مساوی مورد انتظار فاصله زیادی داشت.

چگونه این خطا از رندومایزر عبور کرد

توسعه‌دهنده ابتدا رندومایزری را که بازدیدکنندگان را به یک واریانت اختصاص می‌داد، بررسی کرد. او middleware را خواند، منطق کوکی را بازبینی کرد و اسکریپتی را اجرا کرد که تابع تخصیص را ۱۰,۰۰۰ بار فراخوانی می‌کرد؛ نتیجه یک تقسیم ۵۰/۵۰ کامل بود. خودِ رندومایزر درست کار می‌کرد؛ مشکل در نحوه ثبت مواجهه (exposure) بود.

یک تابع واحد دو وظیفه را انجام می‌داد:

  1. تعیین واریانت – در هر بار بارگذاری صفحه اجرا می‌شود تا تجربه بازدیدکننده ثابت بماند.
  2. ثبت رویداد مواجهه – باید فقط یک بار برای هر بازدیدکننده، در لحظه‌ای که واریانت برای اولین بار ظاهر می‌شود، اجرا شود.

این تابع در root layout قرار داشت، یعنی کامپوننتی که در هر بار ناوبری (navigation) رندر می‌شود. از آنجایی که کد ثبت مواجهه هر بار که لایوت رندر می‌شد اجرا می‌شد، هر بازدید از صفحه به عنوان یک مواجهه جدید محاسبه می‌شد. دو واریانت صفحه اصلی از درخت‌های لایوت (layout trees) کمی متفاوتی استفاده می‌کردند، بنابراین نرخ بازدید صفحات آن‌ها با هم متفاوت شد و توهمِ خراب بودن رندومایزر را ایجاد کرد.

چرا این خطای شمارش اهمیت داشت

اعداد تبدیل — کلیک‌ها، ثبت‌نام‌ها، خریدها — به درستی ثبت شده بودند. اما مخرج کسر (تعداد مواجهات) اشتباه بود. نرخ‌های تبدیل محاسبه‌شده بسیار کمتر از واقعیت به نظر می‌رسیدند و هرگونه تصمیمی که بر اساس آن نرخ‌ها گرفته می‌شد، غیرقابل اعتماد بود.

تست به مدت دو هفته اجرا شد تا اینکه این اختلاف آشکار شد و تیم را مجبور کرد کل مجموعه داده‌ها را کنار بگذارد.

راه حل

راه حل ساده بود: تقسیم مسئولیت‌ها به توابع مجزا. کد ثبت مواجهه اکنون بررسی می‌کند که آیا بازدیدکننده قبلاً شمارش شده است یا خیر، و فقط یک بار برای هر کاربر اجرا می‌شود. کد تعیین واریانت در جای خود باقی می‌ماند و به اجرای خود در هر ناوبری ادامه می‌دهد.

سه نکته برای هر کسی که آزمایش انجام می‌دهد

  • تعیین وضعیت (state-setting) را از رویدادهای یک‌باره جدا کنید. تابعی که هم واریانت را اختصاص می‌دهد و هم یک مواجهه را ثبت می‌کند، دچار تداخل خواهد شد؛ زیرا اولی تکرار می‌شود در حالی که دومی نباید تکرار شود.
  • از منطق‌های یک‌باره (one-shot) در root layout خودداری کنید. هر چیزی که در کامپوننتی قرار گیرد که در هر بار بارگذاری صفحه رندر می‌شود، به طور مکرر اجرا خواهد شد و «یک بار برای هر بازدیدکننده» را به «یک بار برای هر بازدید از صفحه» تبدیل می‌کند.
  • وقتی تقسیم مشاهده‌شده با رندومایزر در تضاد است، ابتدا شمارنده را بازبینی کنید. توسعه‌دهندگان اغلب عدالت رندومایزر را آزمایش می‌کنند، اما به ندرت صحت مکانیسم شمارش را تأیید می‌کنند.

آنچه باید در آینده مراقب آن باشید

هر آزمایشی که برای مواجهه به یک شمارنده واحد متکی است، باید از نظر محل قرارگیری آن شمارنده در درخت کامپوننت (component tree) بازبینی شود. اگر شمارنده در یک لایوت سراسری (global layout) قرار دارد، بررسی‌ای اضافه کنید که رویداد را به یک شناسه پایدار (persistent identifier) — مانند یک کوکی یا پرچم local-storage — متصل کند. تیم‌ها همچنین باید یک بررسی صحت (sanity check) در داشبوردهای خود ایجاد کنند: اگر توزیع واریانت‌های مشاهده‌شده فراتر از یک حاشیه آماری کوچک تغییر کرد، قبل از اینکه فرض کنید رندومایزر خراب است، آزمایش را برای بازبینی شمارنده علامت‌گذاری کنید.

به طور خلاصه، یک رندومایزر با عملکرد خوب، بدون یک شمارش مواجهه قابل اعتماد، بی‌فایده است. تقسیم مسئولیت‌ها و قرار دادن رویدادهای یک‌باره در خارج از کامپوننت‌هایی که همیشه رندر می‌شوند، باعث می‌شود تست‌های A/B دقیق باقی بمانند و هفته‌ها از تحلیل‌های هدر رفته جلوگیری شود.