قياس إحصاءات الجلسة الكاملة والقياس عن بُعد بشكل موثوق

ميزات الويب: fetchLater AbortController وAbortSignal

لتتبُّع الإحصاءات و"مؤشرات أداء الويب الأساسية" وبيانات القياس عن بُعد التي تغطي زيارة المستخدم لصفحة ويب بالكامل (وليس فقط حتى تحميل الصفحة) بشكل موثوق، استخدِم واجهة برمجة التطبيقات fetchLater().

تُعدّ واجهة برمجة التطبيقات fetchLater() الطريقة الأكثر موثوقية لإرسال البيانات إلى خادم في الحالات التي لا تتطلب استجابة ولا يكون توقيت التسليم عاجلاً، وينطبق ذلك على بيانات مثل إحصاءات المستخدمين والقياس عن بُعد وتتبُّع الأخطاء ومقاييس الأداء مثل Core Web Vitals.

إنّ الأساليب القديمة، مثل إنشاء بكسل <img> في متتبِّع أحداث unload، غير موثوقة (خاصةً على الأجهزة الجوّالة) ويمكن أن تؤثر سلبًا في الأداء (من خلال جعل الصفحات غير مؤهّلة لاستخدام ميزة bfcache).

كيفية الإعداد

  1. جدولة الطلب: فور توفّر البيانات ذات الصلة، يمكنك استدعاء fetchLater() باستخدام حمولة البيانات. يؤدي ذلك إلى وضع البيانات في قائمة انتظار لإرسالها لاحقًا.

  2. تعديل الحمولة حسب الحاجة: إذا أنشأ المستخدم المزيد من البيانات أو تغيّرت الحالة، يمكنك إلغاء الطلب المُجدوَل سابقًا واستدعاء fetchLater() مرة أخرى باستخدام اللقطة المعدَّلة بالكامل.

  3. السماح للمتصفّح بمعالجة بقية الخطوات: عندما يغادر المستخدم الصفحة أو يغلق علامة التبويب، سيرسل المتصفّح الحمولة بشكل موثوق من أحدث طلب 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 لإلغاء عملية الجلب المعلّقة في الحالات التي قد تحتاج فيها البيانات إلى التحديث قبل أن يغادر المستخدم الصفحة
  • يجب تقليل حجم الحمولة لتجنُّب تجاوز الحصة (حوالي 64 كيلوبايت لكل مصدر حاليًا).
  • يجب تضمين طلبات fetchLater() في try/catch لمعالجة الأخطاء المتعلقة بالحصص.
  • يجب رصد توفّر fetchLater() على globalThis وتنفيذ استراتيجية احتياطية للمتصفّحات التي لا تتوافق مع واجهة برمجة التطبيقات.
  • لا تستخدِم عنصر ReadableStream لنص الطلب، لأنّ ذلك سيؤدي إلى حدوث خطأ.

توافق المتصفّح واستراتيجيات الاحتياط

يجب وضع استراتيجية احتياطية إذا لم يحقّق "fetchLater()" هدفك الأساسي. ومع ذلك، نظرًا إلى الموثوقية المحسّنة ومزايا الأداء التي توفّرها واجهة برمجة التطبيقات هذه، يجب استخدام fetchLater() إذا كان المتصفّح متوافقًا معها.

استراتيجية الاحتياط المقترَحة هي استخدام الرمز polyfill أدناه، والذي يتعامل داخليًا مع المتصفّحات غير المتوافقة باستخدام fetch() مع keepalive أو navigator.sendBeacon(). يجب أن يطلب الرمز البرمجي الخاص بك fetchLater() مباشرةً، ولا تطلب أبدًا fetch() أو sendBeacon() أو واجهات برمجة تطبيقات أخرى خاصة بالإشارات بنفسك.

‫fetchLater() polyfill

استخدِم رمز polyfill التالي للسمة fetchLater()، والذي ينفّذ واجهة برمجة التطبيقات بأقرب شكل ممكن في المتصفّحات غير المتوافقة.

يتمثّل الاختلاف الوحيد الملحوظ في السلوك مع هذا الـ 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;
    },
  };
};