ดีบาวน์และจัดกลุ่มเหตุการณ์ Analytics หลายรายการ

ฟีเจอร์บนเว็บ: fetchLater AbortController และ AbortSignal

ข้อมูลการวิเคราะห์ การวัดและส่งข้อมูลทางไกล และข้อมูลการวิเคราะห์ Core Web Vitals ส่วนใหญ่มีความสำคัญต่ำกว่าการตอบสนองต่อผู้ใช้ คุณจึงเลื่อนการส่งข้อมูลดังกล่าวออกไปได้อย่างปลอดภัยจนกว่าผู้ใช้จะออกจากหน้าเว็บ ข้อยกเว้นสำหรับเรื่องนี้คือหากคุณต้องการนำเสนอข้อมูลอัปเดตแบบเรียลไทม์ ซึ่งในกรณีนี้ความทันท่วงทีเป็นสิ่งสำคัญ

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

วิธีการนำไปใช้งาน

  1. กำหนดเวลาคำขอ: ทันทีที่ข้อมูลวิเคราะห์ที่เกี่ยวข้องพร้อมใช้งาน ให้เรียกใช้ fetchLater() พร้อมเพย์โหลดข้อมูลและส่งค่า activateAfter ซึ่งจะจัดคิวข้อมูลที่จะส่งหลังจากผ่านระยะเวลาดังกล่าวไปแล้ว หรือหากผู้ใช้ออกจากหน้าเว็บก่อนหน้านั้น

  2. รวมเหตุการณ์หลายรายการเข้าด้วยกัน: หากเหตุการณ์ Analytics ใหม่เกิดขึ้นก่อนที่ระยะหมดเวลา activateAfter จะสิ้นสุดลง ให้ยกเลิกคำขอที่กำหนดเวลาไว้ก่อนหน้านี้และเรียกใช้ fetchLater() อีกครั้ง (โดยใช้ค่า activateAfter เดียวกัน) โดยมีคิวเหตุการณ์ทั้งหมดในเพย์โหลดเดียว

  3. รีเซ็ตคิวเหตุการณ์เมื่อหมดเวลา: หากมีเหตุการณ์ Analytics ใหม่เกิดขึ้นหลังจากส่งบีคอนที่กำหนดเวลาไว้เรียบร้อยแล้ว (นั่นคือ ค่า activated ของผลลัพธ์ fetchLater() คือ true) ให้รีเซ็ตคิวเหตุการณ์

  4. ปล่อยให้เบราว์เซอร์จัดการส่วนที่เหลือ: หากผู้ใช้ออกจากหน้าหรือปิดแท็บก่อนที่ระยะหมดเวลา activateAfter จะสิ้นสุดลง เบราว์เซอร์จะยังคงส่งเพย์โหลดจากการเรียก fetchLater() ล่าสุดได้อย่างน่าเชื่อถือ

โค้ดตัวอย่าง

โค้ดนี้จะติดตามเหตุการณ์ load และ click ทั้งหมดในหน้าเว็บ และจัดกลุ่มเหตุการณ์ทั้งหมดที่เกิดขึ้นภายในระยะหมดเวลา 10 วินาทีเข้าด้วยกัน

// 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 เพื่อยกเลิกการดึงข้อมูลที่รอดำเนินการในกรณีที่เกิดเหตุการณ์ Analytics ใหม่
  • ควรตรวจสอบว่าคุณไม่ได้รวมเหตุการณ์ไว้ด้วยกันมากเกินไปและเกินโควต้า fetchLater() (ปัจจุบันอยู่ที่ประมาณ 64 KB ต่อต้นทาง)
  • ควรรวมการเรียกไปยัง fetchLater() ไว้ใน try/catch เพื่อจัดการข้อผิดพลาดเกี่ยวกับโควต้า
  • ควรใช้ฟีเจอร์ตรวจหาการมีอยู่ของ fetchLater() ใน globalThis และใช้กลยุทธ์สำรองสำหรับเบราว์เซอร์ที่ไม่รองรับ API
  • อย่าใช้ออบเจ็กต์ ReadableStream สำหรับเนื้อหาคำขอ เนื่องจากจะทำให้เกิดข้อผิดพลาด

การรองรับเบราว์เซอร์และกลยุทธ์สำรอง

ดังนั้น โดยทั่วไปจึงต้องมีกลยุทธ์สำรอง

อย่างไรก็ตาม เนื่องจาก API นี้มีประโยชน์ด้านความน่าเชื่อถือและประสิทธิภาพที่ดีขึ้น คุณจึงควรใช้ fetchLater() หากเบราว์เซอร์รองรับ

fetchLater() polyfill

ใช้ Polyfill fetchLater() ขั้นต่ำต่อไปนี้ ซึ่งจะใช้ API อย่างใกล้เคียงที่สุดในเบราว์เซอร์ที่ไม่รองรับ

ความแตกต่างด้านลักษณะการทำงานที่สำคัญเพียงอย่างเดียวของ Polyfill นี้คือ แทนที่จะส่งเพย์โหลดเมื่อผู้ใช้ออกจากหน้าเว็บ ระบบจะส่งเพย์โหลดเมื่อ 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;
    },
  };
};