العودة إلى المدونة
13 أغسطس 2026Sergei Solod15 دقائق قراءة

كنت أنبّه على كل شيء: كيف حوّلت ضجيج أخطاء المتصفح إلى مراقبة مفيدة في الإنتاج

كان نظام الإبلاغ في الواجهة الأمامية يعامل حجب الإعلانات، وأعطال GTM، وAbortError، وScript error الغامض، وأعطال أجزاء Next.js الحقيقية بالطريقة نفسها. أعدت بناء التنبيهات اعتمادًا على ملكية الخطأ، وتأثيره على المستخدم، وجودة الأدلة، وربط الأحداث، وقدرة التطبيق على الاستعادة.

مراقبة أخطاء المتصفحقابلية رصد الواجهة الأماميةأخطاء JavaScriptمراقبة بيئة الإنتاجNext.jsأداء الويب

أنشأت نظامًا للإبلاغ عن أخطاء جهة العميل لأنني أردت رؤية المشكلات التي تصيب مستخدمين حقيقيين ويصعب عليّ إعادة إنتاجها محليًا. وكان النظام يفعل بالضبط ما طلبته منه: يلتقط الأعطال ويرسلها إليّ.

المشكلة أنه جعل كل شيء تقريبًا يبدو بالدرجة نفسها من الخطورة.

فشل سكربت تحليلات خارجي في التحميل؟ تنبيه أحمر. حُجب سكربت إعلاني؟ تنبيه أحمر. تعذر على زاحف ويب تحميل Google Analytics؟ تنبيه أحمر. استدعى فيديو معاينة play() ثم أُوقف قبل أن تستقر الـ Promise؟ تنبيه أحمر. وصل Script error. غامض بلا مصدر أو مكدس استدعاءات مفيد؟ تنبيه أحمر أيضًا.

وفي التدفق نفسه كانت توجد أعطال تستحق التدخل فعلًا: عنوان URL من تطبيقي أصبح بالخطأ https://example.comhttps://example.com/...، وملف Next.js تابع للتطبيق تحت /_next/static/chunks/... لم يستطع المتصفح تحميله.

طبقة الجمع كانت تعمل. طبقة المراقبة لم تكن تعمل.

هذا الفرق غيّر الطريقة التي أفكر بها في قابلية رصد الواجهة الأمامية. حدث الخطأ دليل على أن شيئًا ما وقع. لكنه ليس بعدُ تشخيصًا، ولا درجة خطورة، ولا حادثًا.

الخطأ الأول كان اعتبار كلمة «خطأ» مرادفًا لكلمة «عاجل»

كان النموذج الأول بسيطًا جدًا:

المتصفح يبلّغ عن خطأ
        ↓
إرسال CLIENT ERROR
        ↓
على المطور التدخل

لكن هذه الصورة تخلط أسئلة مختلفة. هل العطل في كودي أصلًا؟ هل تأثر المسار النشط؟ هل كان إلغاءً متوقعًا؟ هل لدى المتصفح معلومات كافية لتحديد المصدر؟ هل تعافى التطبيق؟ هل عشر رسائل تعني عشرة حوادث، أم عشرة أعراض لحادث واحد؟

قبل الإجابة عن هذه الأسئلة، لا ينبغي أن يتحول كل حدث تلقائيًا إلى تنبيه.

في إحدى المراجعات الأولى كان أمامي نحو ثماني عشرة رسالة. معظمها ضجيج من جهات خارجية أو أحداث طبيعية في دورة الحياة. حالتان كانتا مختلفتين بوضوح: عنوان URL تابع للتطبيق ومشوّه كان خطأ مؤكدًا، وفشل تحميل جزء JavaScript تابع للتطبيق كان قد يحرم الصفحة من كود تحتاجه. ومع ذلك، عرض النظام معظم هذه الرسائل تقريبًا بدرجة الإلحاح نفسها التي عرض بها إعلانًا محجوبًا.

من هنا توقفت عن اعتبار «جمع كل أخطاء المتصفح» و«بناء مراقبة للإنتاج» مهمة واحدة. الجمع يحفظ الأدلة؛ أما المراقبة فتحوّل هذه الأدلة إلى قرارات.

لا يملك المتصفح قناة أخطاء واحدة تحمل المعنى نفسه لكل الأعطال

أنواع الأعطال المختلفة على جهة العميل لا تصل بدلالات متساوية.

يُستخدم حدث error على window للأخطاء المتزامنة في السكربت، ويشارك أيضًا في أخطاء تحميل الموارد. أما رفض Promise غير المعالج فيسلك طريقًا مختلفًا ويطلق المتصفح unhandledrejection. والعناصر التي تحمل سكربتات أو صورًا أو وسائط يمكن أن تطلق أحداث error خاصة بها. وتضيف React وNext.js فوق ذلك إشارات من حدود الأخطاء على مستوى إطار العمل.

window.error
→ ربما خرج خطأ script متزامن إلى الخارج

unhandledrejection
→ Promise مرفوضة لم تكن معالجة في تلك اللحظة

element error
→ تعذر تحميل resource أو استخدامه

framework boundary
→ وصل التنفيذ أو rendering إلى حد خطأ في framework

المعالج العام يرى عرضًا عند حدود النظام؛ ولا يملك بالضرورة السلسلة السببية كاملة.

بعد أن تقبلت هذا الاختلاف، توقفت عن تحويل كل حدث فورًا إلى Error عام ثم منحه درجة الخطورة نفسها.

ملكية المورد هي أول فلتر مفيد

أول تقسيم مفيد فعلًا كان معرفة الجهة التي ينتمي إليها الكود أو المورد الذي فشل.

فشل /_next/static/chunks/app/... ليس كفشل SDK إعلاني على أصل آخر. وعنوان URL خاطئ كوّنه كودي ليس كطلب تحليلات حجبه المتصفح. وأخطاء إضافات المتصفح فئة أخرى تمامًا.

  • التطبيق نفسه: JavaScript وCSS وAPI والوسائط وعناوين URL التي يولدها كودي؛
  • إطار العمل وبيئة التشغيل: Next.js أو React عندما يكونان جزءًا من سلسلة تنفيذ التطبيق؛
  • التكاملات الخارجية: التحليلات والإعلانات والأدوات المصغرة وحزم SDK الخارجية؛
  • البيئة: إضافات المتصفح وزواحف الويب وحالة الشبكة وأدوات الخصوصية والسلوك الخاص ببعض المتصفحات.

هذا لا يعني أن الجهات الخارجية غير مهمة. فقد يكون مزود الدفع أو تسجيل الدخول حرجًا، وقد يؤثر تعطل الإعلانات في الإيراد. لكن سوء صحة التكامل لا يعني تلقائيًا أن التطبيق انهار.

إذا أرسلت كل هذه الأشياء إلى قناة التنبيهات العاجلة نفسها، تفقد القناة معناها.

عطلان في موارد التطبيق علّماني معنى الإشارة القابلة للتصرف

حالة URL الخاطئ كانت واضحة:

https://example.comhttps://example.com/resource

هنا لا حاجة إلى نظريات عن AdBlock أو VPN أو سياسات المتصفح. عنوان URL نفسه غير صالح. في مكان ما كان الكود يضيف الأصل إلى قيمة هي أصلًا عنوان مطلق.

كان هذا الحدث قابلًا للتصرف لأن الدليل محدد، والمورد تابع لي، والمشكلة تشير إلى مسار كود أتحكم فيه.

أما فشل تحميل جزء Next.js فكان مختلفًا:

Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js

هو أيضًا مورد تابع للتطبيق وقد يكسر الصفحة، لكن الحدث وحده لا يثبت السبب. قد يطلب عميل قديم ملفًا من نشر سابق، أو تنتهي مهلة الطلب، أو يتعطل الوكيل العكسي أو CDN، أو تنقطع الشبكة، أو يكون الملف مفقودًا فعلًا.

لذلك لم تكن الخلاصة الصحيحة «عرفت السبب»، بل «هذا النوع عالي الأولوية ويحتاج سياقًا أكبر».

قد تكون درجة الخطورة عالية حتى عندما تكون الثقة في السبب منخفضة.

Script error. إشارة، لا مكدس استدعاءات

Error: Script error.
filename: unknown
line: 0
column: 0

تبدو الرسالة خطيرة لكنها لا تقدم معلومات مفيدة تقريبًا.

تقيّد المتصفحات تفاصيل أخطاء السكربتات عبر الأصول عمدًا. توضح MDN أن window.onerror يحصل على معلومات محدودة ما لم يُحمّل السكربت بإعدادات CORS المناسبة؛ كما أن سلوك crossorigin في <script> يؤثر مباشرة في إمكانية كشف التفاصيل الكاملة.

لذلك لا أفسر Script error. غامضًا تلقائيًا على أنه «انهار تطبيقي». قد يكون المصدر كودي، أو كود طرف ثالث، أو كودًا محقونًا، أو خطأً يمنع المتصفح من كشف تفاصيله بسبب قيود عبر الأصول.

أحتفظ بالحدث وأربطه بالصفحة والبناء والمتصفح والأحداث القريبة، لكن حدثًا واحدًا عند 0:0 لا يكفي لتنبيه عاجل. إذا بدأت البصمة نفسها تتجمع حول إصدار أو مسار بعينه، عندها تتغير الأولوية.

«غير معروف» لا يعني «غير ضار»، لكنه لا يعني «حرج» أيضًا.

قد يكون AbortError خطأً حقيقيًا ومع ذلك جزءًا طبيعيًا من دورة الحياة

أوضح مثال على ذلك جاء من معاينات الفيديو:

AbortError:
The play() request was interrupted by a call to pause()

HTMLMediaElement.play() يعيد Promise، ويمكن أن تُرفض هذه الـ Promise. كما يمكن لعمليات دورة حياة الوسائط أن تلغي تشغيلًا معلقًا عمدًا؛ وتوثق MDN صراحةً أن load() يوقف وعود play() المعلقة بخطأ AbortError.

في شبكة من المعاينات، يمكن أن يحدث ذلك من دون أي عطل ظاهر للمستخدم. يدخل العنصر إلى مجال الرؤية فيستدعي الكود play()، ثم يمرر المستخدم الصفحة فيخرج العنصر، فيوقف الكود الوسيط أو يستبدله قبل اكتمال بدء التشغيل.

رفض الـ Promise حقيقي. أما الحادث الذي يراه المستخدم فقد لا يكون موجودًا أصلًا.

المعالجة الصحيحة تكون عادةً قرب الاستدعاء نفسه: التعامل مع الـ Promise والتمييز بين الإلغاء المتوقع وفشل التشغيل الحقيقي. يجب أن يبقى unhandledrejection شبكة أمان، لا المكان الأول الذي نفهم فيه دورة الحياة الطبيعية للمكوّن.

أعطال الجهات الخارجية تحتاج إلى نموذج صحة مستقل

احتوت سجلاتي المبكرة على أعطال كثيرة من نطاقات التحليلات والإعلانات. بعضها جاء من متصفحات تركّز على الخصوصية وبعضها من زواحف الويب. ومن أكثر الأمثلة عديمة القيمة كتنبيه عاجل زاحف لم يستطع تحميل Google Analytics.

هذا يثبت أن طلب شبكة فشل. لكنه لا يكاد يخبرني شيئًا عن قدرة مستخدم بشري على استخدام التطبيق.

المشكلة لم تكن في حفظ الحدث؛ بل في وضعه في مسار الحوادث نفسه مع جزء JavaScript تابع للتطبيق لم يُحمّل.

  • هل تعطّل التطبيق للمستخدم؟
  • هل التكامل الخارجي يعمل بصورة سليمة؟

يمكن أن يذهب سكربت إعلاني محجوب إلى مقياس تسليم الإعلانات، وأن يذهب فشل التحليلات إلى مقياس تغطية التحليلات. ولا ينبغي أن يوقظني أي منهما تحت عنوان «تعطلت الواجهة» ما لم توجد أدلة على أن وظيفة أساسية تعتمد عليه.

بل إن الفصل يجعل مشاكل الأطراف الخارجية أوضح، لأنني أستطيع تجميعها حسب المزود والمتصفح والمنطقة بدل استقبالها كضجيج أحمر عشوائي.

navigator.onLine سياق، لا دليل على الاتصال

بدأت أيضًا أسجل ما إذا كان المتصفح يعتبر نفسه متصلًا. هذا مفيد، لكن فقط كإشارة مساعدة.

كانت لدي أحداث صنفتها المنظومة كأعطال شبكة بينما كان السجل يقول في الوقت نفسه:

Online: true

لا يوجد تناقض. تحذر MDN صراحةً من أن navigator.onLine يعتمد على استدلالات المتصفح ونظام التشغيل. قد يكون الجهاز متصلًا بشبكة محلية ومع ذلك لا يصل إلى أصلي. ويزيد VPN والجدار الناري وDNS والانقطاعات الجزئية للشبكة الصورة تعقيدًا.

online === false
→ دليل قوي على احتمال وجود مشكلة في البيئة

online === true
→ لا يثبت أن origin أو resource كانا قابلين للوصول

هذا الفرق الصغير يمنع نظام المراقبة من تحويل تلميح إلى تشخيص واثق لكنه خاطئ.

عطل جذري واحد قد يولد عدة أحداث في المتصفح

بعد تحسين الجمع ظهر نوع آخر من الضجيج: حادث واحد قد يولد عدة رسائل.

قد يطلق جزء JavaScript أولًا resource.error، ثم يرمي محمل الوحدات ChunkLoadError، ثم تعبر React أو Next.js حدًا للأخطاء، وبعدها تحدد منطقية الاستعادة إعادة تحميل. إذا أرسل كل مستوى تنبيهًا مستقلًا، بدا فعل واحد من المستخدم كأنه عدة أعطال منفصلة في الإنتاج.

خمس رسائل تبدو نفسيًا كأنها خمسة مستخدمين متضررين، مع أنها قد تأتي من جلسة واحدة ومورد واحد.

إزالة التكرار بمقارنة نص الرسالة وحده لا تكفي. المطلوب هو ربط الأحداث في حادث واحد:

session
+ نافذة زمنية قصيرة
+ error class موحدة
+ first-party resource
+ client build
+ route

أحتفظ بالأحداث الخام، لكن ما يصل إلى الإنسان يجب أن يكون تمثيل الحادث الأعلى إشارة. فإذا كان لدي بالفعل حد أخطاء يحتوي مكدسًا من كودي وعنوان URL الدقيق للجزء، فلا حاجة إلى تنبيه عاجل ثانٍ من خطأ مورد عام سبقه.

ننبّه على الحوادث، ونحتفظ بالأحداث.

السياق المحيط بالخطأ أصبح أهم من نص الخطأ نفسه

أصبحت القياسات التشغيلية لاحقًا أكثر تنظيمًا:

clientBuildId
resource URL
resourceResponseStatus
resourceTransferSize
resourceDurationMs
serviceWorkerVersion
serviceWorkerController
serviceWorkerState
chunkRecoveryScheduled
online
stack / component stack

وبالنسبة إلى الوسائط، كنت أسجل أيضًا رمز خطأ الوسيط، وحالة HTTP عندما يمكن رصدها بصورة مستقلة، وContent-Type الفعلي، وما إذا كان الفشل أقرب إلى خطأ HTTP أم إلى مشكلة في التسليم عبر الشبكة.

أتاحت هذه الحقول أسئلة لا تجيب عنها سلسلة استثناء واحدة: هل بدأت الأعطال مع بناء معين؟ هل استلم المتصفح استجابة HTTP؟ هل كان Service Worker يتحكم في الصفحة؟ هل بدأت منطقية الاستعادة؟ هل تشير عدة أحداث إلى المورد نفسه؟ هل تعطل المسار الحالي فعلًا؟

يمكن لـ Resource Timing API توفير مدة المورد ومعلومات النقل، وحالة الاستجابة حيثما كانت مدعومة ومسموحًا بها. لكن لهذه الحقول قيودًا: توقيت الموارد عبر الأصول مقيّد، وقد تعطي الموارد المخزنة في الذاكرة المؤقتة transferSize: 0، وresponseStatus غير متاح في كل المتصفحات. لذلك يجب أن يبقى null و0 حالتين لهما معنى، لا أن يتحولا إلى يقين مصطنع.

ChunkLoadError عرض، لا كاشف 404

غيّر حادث لاحق الطريقة التي أفسر بها أعطال تحميل الأجزاء.

أبلغ المتصفح عن ChunkLoadError في جزء layout من Next.js، وكانت القياسات المحسنة تحتوي أيضًا على:

resourceResponseStatus: 200
resourceDurationMs: 170523
serviceWorkerState: activated
chunkRecoveryScheduled: true

المدة المسجلة تقارب 170 ثانية. أياً كان السبب الجذري الدقيق لذلك الحادث، كانت هذه البيانات كافية لرفض قاعدة مبسطة أكثر من اللازم:

ChunkLoadError === الخادم أعاد 404

في أعطال أجزاء أخرى لم تكن هناك حالة استجابة قابلة للرصد أصلًا. ظهر بعضها كمهلة منتهية، وبعضها تضمن معلومات نقل. فئة الخطأ واحدة؛ لكن الأدلة المحيطة مختلفة.

هذا مهم خصوصًا مع Next.js لأن الملفات تحت /_next/static/ تحمل عادةً تجزئات للمحتوى ومصممة لتعامل كموارد غير قابلة للتغيير. وتوضح وثائق Next.js الحالية للاستضافة الذاتية ترويسات تخزين مؤقت طويلة لهذه الأصول. لذلك قد يشمل ChunkLoadError اختلافًا بين الإصدارات عند النشر، أو عميلًا قديمًا، أو الشبكة، أو وكيلًا عكسيًا، أو CDN، أو ذاكرة مؤقتة، أو Service Worker، أو ملف بناء مفقودًا فعلًا.

لا أريد من طبقة التنبيه أن تخترع السبب. أريدها أن تحفظ الأدلة التي أحتاجها للتحقيق.

أخطاء الوسائط علمتني الدرس نفسه من طبقة أخرى

في بعض الأحداث أبلغ عنصر الوسائط عن:

MEDIA_ELEMENT_ERROR: Format error

لو أخذت الرسالة وحدها لبدت كأنها مشكلة توافق في الترميز.

لكن التحقق الإضافي من التسليم في بعض تلك الأحداث أظهر:

HTTP status: 410
Content-Type: text/html; charset=utf-8
failure kind: http

طلب المتصفح فيديو، لكنه تلقى استجابة خطأ HTTP تحتوي HTML. لم يستطع عنصر الوسائط فك HTML كفيديو، فظهر العرض الخارجي على هيئة “Format error”. التشخيص المفيد كان في طبقة التسليم.

الطبقة التي تلاحظ العطل أولًا ليست بالضرورة الطبقة التي تسببت فيه.

«عطل الترميز» و«انقطاع الشبكة» و«خلل الذاكرة المؤقتة» و«جزء مفقود» كلها استنتاجات. يجب أن تبدأ القياسات التشغيلية بوصف ما لوحظ.

أقيّم أعطال المتصفح عبر خمسة أبعاد

1. الملكية

هل المصدر من التطبيق نفسه، أم من إطار العمل/بيئة التشغيل، أم من طرف ثالث، أم من البيئة؟

2. تأثير المستخدم

هل تعطل المسار النشط أو العرض أو تسجيل الدخول أو الدردشة أو الدفع أو تدفق أساسي آخر؟ أم فشل إعلان اختياري أو تحليلات أو تحميل مسبق أو معاينة بينما بقيت الصفحة قابلة للاستخدام؟

3. جودة الأدلة

هل لدي مكدس استدعاءات من كودي، وعنوان URL للمورد، وحالة HTTP، ومعرّف البناء، ومكدس المكونات؟ أم ليس لدي سوى Script error. عند 0:0؟

4. التكرار والانتشار

هل هو حدث من جلسة واحدة، أم تظهر البصمة نفسها لدى مستخدمين ومسارات ومتصفحات مختلفة بعد الإصدار نفسه؟

5. الاستعادة

هل تعافى التطبيق تلقائيًا؟ هل حُددت إعادة تحميل للجزء؟ هل نجح البديل؟ هل ما زال المستخدم عالقًا؟

first-party واضح
+ تأثير مستخدم كبير
+ أدلة قوية
+ عدة sessions
+ لا recovery
= incident عاجل

third-party
+ وظيفة اختيارية
+ أدلة ضعيفة
+ حالة منفردة
+ المستخدم غير متأثر
= metric أو أولوية منخفضة

يجب أن يكون إسقاط الأحداث محافظًا ومدروسًا

عندما يصبح الضجيج مؤلمًا، يكون من المغري كتابة عشرات التعبيرات النمطية وإسقاط كل ما يزعجك. هذا خطر.

إذا أسكت كل AbortError فقد أخفي طلبات API حقيقية أُلغيت. وإذا حذفت كل Script error. فقد أفقد عنقودًا خاصًا بمتصفح لا يصبح مهمًا إلا بعد التجميع. وإذا تجاهلت كل أعطال الجهات الخارجية فقد أفوّت مزود دفع أو مصادقة أو موافقة معطلًا.

ALERT
→ incident قوي يحتاج تدخلًا

RETAIN / AGGREGATE
→ احتفظ وعدّ؛ نبّه عند ظهور cluster

METRIC / SAMPLE
→ ضجيج متوقع أو ضعيف الأثر؛ احتفظ بالاتجاه والأمثلة

بهذه الطريقة يصبح النظام أهدأ من دون أن يصبح أعمى.

المصنّف الجيد هو في جوهره سياسة مكتوبة في الكود

الكود التالي ليس من مشروعي الإنتاجي. إنه مثال مختصر للسياسة التي أتمنى لو بدأت بها:

function classifyClientEvent(event) {
  const owner = classifyOwner(event);

  if (isExpectedMediaCancellation(event)) {
    return { severity: "metric", reason: "expected-cancellation" };
  }

  if (owner === "first-party" && breaksActiveRoute(event)) {
    return { severity: "alert", reason: "first-party-user-impact" };
  }

  if (isActiveFirstPartyChunkFailure(event)) {
    return { severity: "alert", reason: "application-chunk" };
  }

  if (owner === "third-party") {
    return { severity: "aggregate", reason: "integration-health" };
  }

  if (isOpaqueScriptError(event)) {
    return { severity: "aggregate", reason: "insufficient-evidence" };
  }

  return { severity: "aggregate", reason: "needs-correlation" };
}

العمل الصعب مخفي داخل دوال مثل breaksActiveRoute(). لا يمكن حل ذلك من اسم الاستثناء وحده؛ فهو يحتاج سياق المسار، وملكية المورد، وبيانات حد الأخطاء، وأحيانًا معرفة خاصة بالمنتج.

يجب أن تتبع البصمة الحادث، لا نص الرسالة

مقارنة الرسائل حرفيًا طريقة سيئة لإزالة التكرار. تتغير إزاحات المكدس المصغر بين الأبنية، وتتغير تجزئات الأجزاء، وتحتوي عناوين URL معرفات ديناميكية، وتختلف صياغة المتصفحات.

{
  errorClass,
  normalizedFirstPartyResource,
  routeFamily,
  clientBuild,
  sessionId,
  shortTimeBucket
}

في انهيار عام للتطبيق قد يكون أعلى إطار من كودي في مكدس الاستدعاءات هو الأنسب. وفي فشل جزء يكون المورد المطبّع أهم. وفي حادث تسليم وسائط قد تكون فئة فشل HTTP ومسار الوسائط أهم من رسالة الخطأ الخارجية التي يعرضها المتصفح.

resource.error
→ ChunkLoadError
→ framework boundary
→ recovery scheduled

بهذا يمكن لسلسلة أحداث مرتبطة أن تصبح حادثًا واحدًا مع أربع ملاحظات مرفقة، بدل أن تبدو كأربع أعطال عاجلة.

يجب أن تكون مهمة قناة التنبيهات العاجلة أضيق بكثير

أحتفظ بالتنبيه الفوري لحالات مثل: خطأ تشغيل تابع للتطبيق مع مكدس مفيد يكسر المسار النشط؛ حد أخطاء React/Next.js يؤثر فعلًا في التفاعل؛ فشل JavaScript أو CSS تابع للتطبيق ومطلوب حاليًا؛ ChunkLoadError متكرر عبر جلسات أو أبنية؛ فشل API أو بيانات أساسية يترك المستخدم عالقًا؛ أو خرق واضح لثوابت كودي مثل توليد URL مشوّه.

أما Script error. واحد وغامض، أو فشل مورد تابع للتطبيق مع استعادة ناجحة، أو خطأ وسائط لم تُعرف طبقته الجذرية بعد، أو شذوذ خاص بمتصفح يحتاج إلى تجميع، فأحتفظ به أولًا ولا أطلق تنبيهًا عاجلًا.

وأرسل عادةً إلى المقاييس أو التشخيصات المأخوذة كعينات: أعطال موارد الإعلانات أو التحليلات المعروفة؛ إلغاءات الوسائط المتوقعة من نوع AbortError؛ أعطال الجهات الخارجية الخاصة بالزواحف؛ الأعطال المقترنة بإشارة قوية إلى انقطاع الاتصال؛ والموارد الاستباقية الاختيارية التي لا تؤثر في المسار النشط.

يجب أن يعني التنبيه العاجل أثرًا على المستخدم يمكن التصرف حياله، لا مجرد حجم شكاوى المتصفح الخام.

أفضل قياس الحوادث على عدّ كل «الأخطاء» معًا

  • حوادث التطبيق لكل 1,000 جلسة؛
  • الجلسات المتأثرة حسب معرّف البناء؛
  • حوادث حدود الأخطاء حسب المسار؛
  • أعطال الأجزاء حسب المورد والنشر؛
  • معدل فشل التكاملات الخارجية حسب المزود؛
  • حجم الإلغاءات المتوقعة، حتى يبقى الارتفاع المفاجئ مرئيًا؛
  • معدل نجاح الاستعادة؛
  • عدد الحوادث الفريدة منفصلًا عن عدد الأحداث الخام.

«حدثت رسالة واحدة» نادرًا ما يكون حدًا مفيدًا للتنبيه في الإنتاج. أما «الحادث نفسه التابع للتطبيق يؤثر الآن في عدة جلسات مستقلة على البناء الجديد والاستعادة تفشل» فهو أقرب إلى إشارة تستحق التدخل.

المراقبة على جهة العميل لا تثبت السبب الجذري بمفردها

للقياسات التشغيلية في المتصفح حدود صريحة.

غياب حالة HTTP قد يعني أن الـ API لم يوفّرها، أو أن المتصفح لا يدعم الحقل، أو أن قيدًا عبر الأصول منعها، أو أن الطلب أُلغي، أو أن هناك فجوة رصد أخرى. وكون Service Worker نشطًا لا يثبت أنه أعاد موردًا قديمًا. وظهور ChunkLoadError بعد نشر لا يثبت اختلاف الإصدارات. كما أن online: true لا يثبت أن الأصل كان قابلًا للوصول.

دور مراقبة العميل هو تضييق الفرضيات. قد تظل سجلات الخادم والوكيل العكسي وبيانات النشر وحالة الذاكرة المؤقتة وإعادة الإنتاج الفعلية ضرورية.

ولا أريد أن تتحول قابلية الرصد إلى جمع بلا حدود لبيانات المستخدم. يجب أن يوجد كل حقل لأنه يساعد فعلًا على التمييز بين أنماط الأعطال.

القياسات الأفضل ليست قياسات أكثر، بل قياسات تميّز بين الأعطال بصورة أفضل.

قاعدتي الآن: أجمع الأحداث، أحقق في الحوادث، وأنبّه على الأثر

في البداية أردت من نظام الإبلاغ أن يجيب عن سؤال بسيط: «هل تعطل شيء؟». في الإنتاج يصبح السؤال واسعًا إلى حد أنه لا يفيد. دائمًا يوجد زاحف لا يصل إلى التحليلات، أو أداة خصوصية تحجب إعلانًا، أو Promise للوسائط تُلغى عمدًا، أو مستخدم يفقد الاتصال، أو SDK خارجي يتصرف بصورة سيئة.

هل هو من عندنا؟
هل فقد المستخدم وظيفة؟
ما قوة الأدلة؟
هل يتكرر؟
هل تعافى التطبيق؟
هل هذه عدة events أم incident واحد؟

عندما بنيت المراقبة حول الأسئلة الأضيق، توقف سيل الرسائل الحمراء عن كونه ضجيجًا وأصبح أداة هندسية.

خطأ المتصفح ملاحظة. الحادث تفسير مترابط للأثر على المستخدم. والتنبيه قرار بأن الوقت حان لتدخل إنسان.

لا أريد أن أتعامل مع هذه الأشياء الثلاثة كأنها شيء واحد مرة أخرى.