Debounce and batch multiple analytics events

ویژگی‌های وب: fetchLater AbortController و AbortSignal

اکثر داده‌های تجزیه‌وتحلیل، دورسنجی، و تجزیه‌وتحلیل Core Web Vitals اولویت کمتری نسبت‌به پاسخ دادن به کاربر دارند، بنابراین می‌توانید ارسال آن را تا زمانی که کاربر صفحه را ترک کند به‌تعویق بیندازید. استثنای این مورد زمانی است که نیاز به ارائه به‌روزرسانی‌های هم‌زمان دارید، که در این صورت به‌موقع بودن اهمیت دارد.

بهترین راه برای ارائه به‌روزرسانی‌های تحلیلی هم‌زمان، درحالی‌که هنوز چراغ‌های راهنما را به حداقل می‌رساند، استفاده از fetchLater() با گزینه پیکربندی activateAfter است. این به شما امکان می‌دهد به‌طور مؤثر همه رویدادهای تجزیه‌وتحلیل را که در یک پنجره زمانی مشخص رخ می‌دهند، لرزش‌گیری و دسته‌بندی کنید و آن‌ها را در یک فانک واحد قرار دهید که حتی اگر کاربر قبل‌از انقضای مهلت زمانی صفحه را ترک کند، به‌طور مطمئن ارسال می‌شود.

نحوه پیاده‌سازی

  1. زمان‌بندی درخواست: به‌محض دردسترس قرار گرفتن داده‌های تجزیه‌وتحلیل مربوطه، fetchLater() را با بار داده‌هایتان فراخوانی کنید و مقدار activateAfter را ارسال کنید. این کار داده‌ها را در صف قرار می‌دهد تا پس‌از گذشت آن مدت زمان یا اگر کاربر زودتر صفحه را ترک کند، ارسال شوند.

  2. چند رویداد را باهم دسته‌بندی کنید: اگر رویداد تجزیه‌وتحلیل جدیدی قبل‌از منقضی شدن زمان استراحت activateAfter رخ دهد، درخواست زمان‌بندی‌شده قبلی را لغو کنید و دوباره fetchLater() را (با همان مقدار activateAfter) با صف کامل رویداد در یک بار داده واحد فراخوانی کنید.

  3. بازنشانی صف رویداد پس‌از منقضی شدن مهلت: اگر رویداد تجزیه‌وتحلیل جدیدی پس‌از ارسال موفقیت‌آمیز فانوس زمان‌بندی‌شده رخ دهد (یعنی مقدار activated نتیجه fetchLater() برابر با true باشد)، صف رویداد را بازنشانی کنید.

  4. اجازه دهید مرورگر بقیه کارها را انجام دهد: اگر کاربر قبل‌از منقضی شدن مهلت activateAfter از صفحه خارج شود یا برگه را ببندد، مرورگر همچنان به‌طور مطمئن بار مفید را از آخرین تماس fetchLater() شما ارسال خواهد کرد.

کد نمونه

این کد همه رویدادهای load و click را در صفحه ردیابی می‌کند و همه رویدادهایی را که در یک مهلت ۱۰ ثانیه‌ای رخ می‌دهند باهم گروه‌بندی می‌کند.

// Replace with your analytics endpoint.
const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';

// Replace with a time window of your choice. All analytics events that
// occur within this time window will be batched together.
const BATCH_WINDOW = 10 * 1000;

// The maximum number of events to batch. Pick a number that is unlikely
// to overflow the fetchLater() quota for the page.
const MAX_QUEUE_SIZE = 100;

const eventQueue = [];
let fetchLaterResult;
let fetchLaterController;

function trackEvent(eventData) {
  // If the previously queued beacon has already been sent, or if the
  // max queue size has been met, reset the queue.
  if (fetchLaterResult?.activated || eventQueue.length > MAX_QUEUE_SIZE) {
    fetchLaterController = null;
    fetchLaterResult = null;
    eventQueue.length = 0;
  }

  eventQueue.push(eventData);

  // Abort any pending beacons before creating a new one.
  if (fetchLaterController) {
    fetchLaterController.abort();
  }
  fetchLaterController = new AbortController();

  // Schedule a fetch for the events to be sent when the batch window expires.
  // IMPORTANT: wrap the call in a try/catch to handle quota errors.
  try {
    fetchLaterResult = fetchLater(ANALYTICS_ENDPOINT, {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify(eventQueue),
      signal: fetchLaterController.signal,
      activateAfter: BATCH_WINDOW,
    });
  } catch (error) {
    // Handle errors as needed.
  }
}

// Track page loads.
window.addEventListener('load', () => {
  trackEvent({type: 'page_load'});
});

// Track click events.
window.addEventListener('click', (event) => {
  trackEvent({type: 'click', target: serializeElement(event.target)});
});

روال‌های مطلوب

  • باید از fetchLater() با گزینه activateAfter برای دسته‌بندی چندین رویداد Analytics که در نزدیکی یکدیگر رخ می‌دهند استفاده کنید.
  • باید از AbortController برای لغو واکشی معلقه در مواردی که رویداد تجزیه‌وتحلیل جدیدی رخ می‌دهد استفاده کنید.
  • باید بررسی کنید تا مطمئن شوید رویدادهای زیادی را باهم دسته‌بندی نکنید و از سهمیه fetchLater() (درحال‌حاضر تقریباً ۶۴ کیلوبایت برای هر مبدأ) فراتر نروید.
  • باید فراخوانی‌های fetchLater() را در try/catch قرار دهید تا خطاهای سهمیه را مدیریت کنید.
  • باید وجود fetchLater() را در globalThis شناسایی کنید و استراتژی جایگزینی برای مرورگرهایی که از این میانای برنامه‌سازی کاربردی پشتیبانی نمی‌کنند پیاده‌سازی کنید.
  • از شیء ReadableStream برای بدنه درخواست استفاده نکنید، زیرا این کار باعث ایجاد خطا می‌شود.

پشتیبانی مرورگر و راهبردهای جایگزین

بنابراین، معمولاً به یک استراتژی جایگزین نیاز است.

بااین‌حال، با توجه به مزایای بهبود قابلیت اطمینان و عملکرد این میانای برنامه‌سازی کاربردی، اگر مرورگر از fetchLater() پشتیبانی می‌کند، باید از آن استفاده شود.

‫fetchLater() شبیه‌ساز مرورگر

از شبیه‌ساز مرورگر fetchLater() حداقلی زیر استفاده کنید که میانای برنامه‌سازی کاربردی را تا حد امکان در مرورگرهای پشتیبانی‌نشده پیاده‌سازی می‌کند.

تنها تفاوت رفتاری قابل‌توجه با این شبیه‌ساز مرورگر این است که به‌جای ارسال بار مفید هنگام خروج کاربر از صفحه، هر زمان که visibilityState صفحه به «پنهان» تغییر کند، آن را ارسال می‌کند، زیرا این قابل‌اعتمادترین سیگنال پایان جلسه است که امروزه به‌طور گسترده دردسترس است.

globalThis.fetchLater ??= function fetchLater(url, init = {}) {
  let timeoutHandle;
  let activated = false;

  function sendNow() {
    if (!(init.signal && init.signal.aborted)) {
      // Use fetch keepalive if the browser supports it or if custom fetch
      // parameters are specified (e.g. custom headers or methods).
      // Otherwise fall back to `navigator.sendBeacon()`.
      if (
        'keepalive' in Request.prototype ||
        init.method !== 'POST' ||
        init.headers
      ) {
        fetch(url, Object.assign({}, init, {keepalive: true}));
        activated = true;
      } else {
        activated = navigator.sendBeacon(url, init.body);
      }
    }
    destroy();
  }

  function destroy() {
    document.removeEventListener('visibilitychange', sendNow);
    clearTimeout(timeoutHandle);
  }

  if (document.visibilityState === 'hidden') {
    // If the beacon was created while the page is already hidden, send data
    // ASAP but wait until the next microtask to allow all sync code to run.
    queueMicrotask(sendNow);
  } else {
    document.addEventListener('visibilitychange', sendNow);

    if (typeof init.activateAfter === 'number' && init.activateAfter >= 0) {
      timeoutHandle = setTimeout(sendNow, init.activateAfter);
    }
  }

  if (init.signal) {
    init.signal.addEventListener('abort', destroy);
  }

  return {
    get activated() {
      return activated;
    },
  };
};