מדידה מהימנה של ניתוח נתונים וטלמטריה של סשנים מלאים

תכונות באינטרנט: fetchLater AbortController ו-AbortSignal

כדי לעקוב באופן מהימן אחרי ניתוחים, מדדי Core Web Vitals ונתוני טלמטריה שכוללים את כל הביקור של המשתמש בדף אינטרנט (לא רק עד לטעינת הדף), צריך להשתמש ב-API של fetchLater().

‫fetchLater() API הוא הדרך הכי אמינה לשלוח נתונים לשרת במקרים שבהם לא נדרשת תגובה ומועד המסירה לא דחוף. זה רלוונטי לנתונים כמו ניתוח נתוני משתמשים, טלמטריה, מעקב אחרי שגיאות ומדדי ביצועים כמו Core Web Vitals.

שיטות ישנות יותר, כמו יצירת פיקסל בגודל <img> ברכיב event listener מסוג unload, ידועות כלא אמינות (במיוחד בניידים) ויכולות להשפיע לרעה על הביצועים (כי הן גורמות לכך שהדפים לא עומדים בדרישות ל-bfcache).

איך מטמיעים

  1. תזמון הבקשה: ברגע שהנתונים הרלוונטיים זמינים, קוראים ל-fetchLater() עם המטען הייעודי (payload) של הנתונים. הנתונים יועברו לתור ויישלחו מאוחר יותר.

  2. עדכון המטען הייעודי (payload) לפי הצורך: אם המשתמש יוצר עוד נתונים או שהמצב משתנה, מבטלים את הבקשה שתוזמנה קודם וקוראים שוב ל-fetchLater() עם תמונת המצב המעודכנת במלואה.

  3. הדפדפן מטפל בשאר: כשהמשתמש יוצא מהדף או סוגר את הכרטיסייה, הדפדפן שולח באופן מהימן את המטען הייעודי (payload) מהקריאה האחרונה ל-fetchLater().

קוד לדוגמה

הקוד הזה מודד את משך הסשן של ביקור של משתמש בדף באמצעות fetchLater() כדי להוסיף לתור משואה חדשה כל 10 שניות עם משך הסשן המעודכן.

const ANALYTICS_ENDPOINT = '/path/to/analytics/endpoint';

const sessionData = {
  duration: 0,
  id: crypto.randomUUID(),
};

let fetchLaterController = null;

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

  // Update session duration to the current page time.
  sessionData.duration = performance.now();

  // Schedule a fetch for the data payload to be sent later.
  // IMPORTANT: wrap the call in a try/catch to handle quota errors.
  try {
    fetchLater(ANALYTICS_ENDPOINT, {
      method: 'POST',
      headers: {'content-type': 'application/json'},
      body: JSON.stringify(sessionData),
      signal: fetchLaterController.signal,
    });
  } catch (error) {
    // Handle errors as needed.
  }
}

// Update the session data and queue a new beacon every 10 seconds.
setInterval(queueBeacon, 10000);

שיטות מומלצות

  • מומלץ להשתמש ב-fetchLater() כדי לשלוח נתונים לשרת בכל מצב שבו לא נדרשת תגובה, ולא קריטי שהנתונים יישלחו מיד.
  • מומלץ להשתמש ב-AbortController כדי לבטל את השליפה בהמתנה במקרים שבהם יכול להיות שיהיה צורך לעדכן את הנתונים לפני שהמשתמש עוזב את הדף
  • מומלץ להקטין את גודל המטען הייעודי (payload) כדי לא לחרוג מהמכסה (כרגע בערך 64KB לכל מקור).
  • מומלץ לעטוף קריאות ל-fetchLater() ב-try/catch כדי לטפל בשגיאות שקשורות למכסות.
  • מומלץ להשתמש בזיהוי תכונות כדי לבדוק אם fetchLater() קיים ב-globalThis, ולהטמיע אסטרטגיית חלופות לדפדפנים שלא תומכים ב-API.
  • אל תשתמשו באובייקט ReadableStream בגוף הבקשה, כי זה יגרום לשגיאה.

תמיכה בדפדפנים ושיטות חלופיות

אם fetchLater() לא עומד ביעד הבסיסי, צריך להגדיר שיטה חלופית. עם זאת, לאור היתרונות של ה-API הזה מבחינת אמינות וביצועים, כדאי להשתמש ב-fetchLater() אם הדפדפן תומך בו.

אסטרטגיית הגיבוי המומלצת היא להשתמש ב-polyfill שבהמשך, שמטפל באופן פנימי בדפדפנים שלא תומכים ב-fetch() עם keepalive או navigator.sendBeacon(). הקוד שלכם חייב להפעיל את fetchLater() ישירות – אף פעם אל תפעילו את fetch(), sendBeacon() או ממשקי API אחרים של ביקון בעצמכם.

‫fetchLater() polyfill

משתמשים ב-polyfill המינימלי הבא של fetchLater(), שמטמיע את ה-API בצורה הכי קרובה שאפשר בדפדפנים שלא תומכים בו.

ההבדל היחיד בהתנהגות של ה-polyfill הזה הוא שהוא משתמש ב-visibilitychange כדי לזהות מתי המשתמש עוזב, במקום להסתמך על הטיפול המקורי של הדפדפן בפריקה. זהו פרט הטמעה פנימי – הקוד שלכם לא צריך להאזין לאירוע visibilitychange או לאירועים אחרים במחזור החיים של הדף. פשוט קוראים ל-fetchLater() וה-polyfill מטפל בהצגה.

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;
    },
  };
};