تخطّى إلى المحتوى الرئيسي
المراقبة (Observability) في Next.js App Router: ملاحظات ميدانية عن instrumentation.ts وonRequestError والتتبع الذي تسرّب بين الطلبات

المراقبة (Observability) في Next.js App Router: ملاحظات ميدانية عن instrumentation.ts وonRequestError والتتبع الذي تسرّب بين الطلبات

August 20, 2026
هندسة الأنظمة المتكاملة
7 دقيقة قراءة

أهم النقاط

دالة register()، المُصدَّرة من ملف instrumentation.ts، تعمل مرة واحدة فقط عند بدء تشغيل نسخة الخادم — وليس مع كل طلب. وهذا بالضبط المكان الصحيح لتهيئة أداة التتبع (tracing SDK)، وبالضبط سبب عدم صلاحية هذا المكان لتخزين أي بيانات خاصة بطلب معيّن.

حزمة @vercel/otel ودالتها registerOTel() تُغلّف حزمة OpenTelemetry الخاصة بـ Node بإعدادات افتراضية معقولة للمُصدِّرات (exporters) ومعالجات الـ spans، مما يوفر الكثير من الإعداد اليدوي.

onRequestError، المستقرة منذ Next.js 15، تلتقط الأخطاء القادمة من Server Components وRoute Handlers وServer Actions وMiddleware قبل أن يعرض Next.js استجابة الخطأ الخاصة به. كتلة try/catch على مستوى المكوّن لا ترى إلا الشجرة الفرعية الخاصة بها.

تقنية Fluid Compute تعيد استخدام نسخة واحدة من الدالة (function instance) لخدمة عدة طلبات متزامنة، لذا فإن أي متغيّر على مستوى الوحدة (module) يحمل 'التتبع الحالي' يتحول إلى حالة مشتركة قابلة للتعديل بين الطلبات. فقط السياق المبني على AsyncLocalStorage يحافظ على عزل البيانات لكل طلب على حدة.

التتبع (trace) والسجل المُهيكل (structured log) والمقياس (metric) تجيب كل منها عن سؤال مختلف حول نفس الطلب. نحن نُصدّر الثلاثة معًا، لأن الاعتماد على واحد فقط يترك فجوات كانت الأداتان الأخريان ستغطيانها.

ماذا ينفّذ instrumentation.ts فعليًا، ومتى؟

ملف instrumentation.ts هو ملف في جذر المشروع، أو داخل مجلد src/، يقوم Next.js بتحميله قبل أي وحدة أخرى في التطبيق. دالته المُصدَّرة register() تعمل مرة واحدة بالضبط، عند بدء تشغيل نسخة خادم جديدة — وليس مع كل طلب أو مسار أو عملية عرض. منذ الإصدار Next.js 15 أصبح هذا سلوكًا مستقرًا دون الحاجة لأي علم تجريبي (experimental flag)، وهو أمر تعلمناه بالطريقة الصعبة بعد نسخ إعداد كُتب لإصدار أقدم.

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

كيف نُعِدّ OpenTelemetry دون بناء حزمة Node يدويًا؟

حزمة @vercel/otel تُغلّف إعداد حزمة OpenTelemetry القياسية الخاصة بـ Node — اختيار المُصدِّر، وإعداد معالج الـ spans الدفعي (batch span processor)، وسمات المورد مثل اسم الخدمة — خلف استدعاء واحد لدالة registerOTel(). بدونها، يعني الإعداد اليدوي اختيار معالج spans، ومدير سياق (context manager)، ومُصدِّر، وضبط ترتيب التسجيل بشكل صحيح قبل تحميل أي وحدة أخرى.

ضبط متغيّر البيئة OTEL_EXPORTER_OTLP_ENDPOINT يوجّه الـ spans إلى مُجمِّع (collector) — سواء كان Honeycomb أو Axiom أو Datadog أو OpenTelemetry Collector مُستضاف ذاتيًا، فكلها تقبل بروتوكول OTLP. على Vercel، بمجرد تسجيل @vercel/otel، تلتقط لوحة Observability الخاصة بالمنصة نفس بيانات التتبع تلقائيًا، وهو ما يغطي جزءًا كبيرًا من تصحيح الأخطاء اليومي دون إنشاء خلفية منفصلة.

حزمة OpenTelemetry الكاملة لـ Node تحتاج بيئة تشغيل Node.js. هذا نادرًا ما يمثّل قيدًا حقيقيًا لنا الآن — فتقنية Fluid Compute تجعل من Node.js الخيار الافتراضي العملي على Vercel، وبيئة edge القديمة لم تكن أبدًا مناسبة لأداة ذات حالة مثل هذه.

لماذا تسرّبت الـ spans بين طلبات لا علاقة بينها؟

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

السبب هو السلوك الأساسي لتقنية Fluid Compute: فهي تعيد استخدام نسخة دالة واحدة نشطة لخدمة العديد من الطلبات المتزامنة بدلاً من إنشاء نسخة جديدة لكل طلب. هذا جيد لتقليل زمن البدء البارد (cold start)، وهو بالضبط سبب خطورة أي متغيّر بسيط على مستوى الوحدة عليها — فالمتغيّر يصبح حالة مشتركة قابلة للتعديل بين كل الطلبات الجارية على تلك النسخة، وأي طلبين متداخلين سيكتب أحدهما فوق قيمة الآخر.

الحل هو AsyncLocalStorage، وهي أداة أساسية في Node.js تحصر قيمة ما ضمن سلسلة الاستدعاء غير المتزامن (async call chain) الحالية بدلاً من حصرها في الوحدة (module). وهي نفس الآلية التي يُبنى عليها مدير السياق الخاص بـ OpenTelemetry، ونفس السبب الذي يجعل دالتي headers() وcookies() في Next.js نفسه قابلتين لأن تكونا محصورتين بالطلب دون تمرير كائن الطلب في كل مكان.

ماذا تلتقط onRequestError مما لا تستطيع كتلة try/catch على مستوى المكوّن التقاطه؟

onRequestError هي دالة اختيارية مُصدَّرة من instrumentation.ts، مستقرة منذ Next.js 15. يستدعيها Next.js في كل مرة يلتقط فيها خطأ غير معالَج أثناء عرض Server Component، أو تنفيذ Route Handler، أو تشغيل Server Action، أو داخل Middleware — قبل أن يعرض حدود الخطأ الخاصة به أو يُعيد استجابة 500 للعميل.

تستقبل الدالة الخطأ نفسه، وكائن طلب يحتوي على المسار والطريقة (method) والترويسات (headers)، وكائن سياق يصف نوع الموجّه (router kind)، ومسار المسار (route path)، ونوع المسار — render أو route أو action أو middleware. هذا الجزء الأخير هو ما لا تمنحنا إياه مجموعة متفرقة من كتل try/catch مجانًا: حدود الخطأ داخل مكوّن واحد لا تعرف إلا شجرتها الفرعية، ولا تعرف إلى أي مسار تنتمي أو ما إذا كان الخطأ قد حدث أثناء عملية عرض أو أثناء Server Action.

نقلنا كل عملية تسجيل خطأ عشوائية كانت مبعثرة عبر معالِجات المسارات إلى هذه الدالة الواحدة. لا شيء آخر في الكود يقرر كيفية الإبلاغ عن الأخطاء الآن.

كيف نربط التتبع بسطر السجل الذي يشرح ما حدث؟

معرّف الـ span وحده لا يفيد من يقرأ سجلًا أثناء حادثة ما — يجب أن يحمل السجل نفس معرّف التتبع (trace ID) الخاص بالطلب الذي أنتجه. سجلات Vercel التشغيلية (Runtime Logs) تلتقط مخرجات console تلقائيًا، لكنها لا تربط سطر السجل بالتتبع إلا إذا أُرفقت المعرّفات صراحةً.

نقرأ الـ span المرتبط بسياق OpenTelemetry النشط حاليًا ونُرفق معرّف التتبع ومعرّف الـ span الخاصين به بكل سطر سجل يُصدره الطلب، باستخدام غلاف بسيط للسجل مبني على pino. ولأن هذا السياق هو نفسه الذي تنشره AsyncLocalStorage، يظل هذا صحيحًا تحت الطلبات المتزامنة لنفس السبب الذي جعل الحل السابق يعمل.

التتبع مقابل السجلات المهيكلة مقابل المقاييس — ماذا يجب أن نُصدّر فعليًا؟

كل إشارة تجيب عن سؤال مختلف، ولا تُغني أي منها عن الأخرى. التتبع يُظهر أين ذهب الوقت داخل طلب واحد — تسلسل شلالي عبر استدعاء قاعدة بيانات، وواجهة برمجية خارجية، وعملية عرض — وهو ما نلجأ إليه لتحديد الـ span البطيء الوحيد. السجل المهيكل يُظهر ما حدث، وبأي بيانات، في طلب واحد، وهو ما نبحث فيه بمعرّف الطلب أثناء حادثة ما. المقياس يُظهر عدد مرات حدوث شيء ما عبر كل الطلبات، وهو ما يغذي لوحاتنا وحدود التنبيه لدينا.

نُصدّر الثلاثة الآن، ومعرّف التتبع هو ما يربط أي ارتفاع مفاجئ في لوحة مقاييس بسطر السجل المحدد والـ span المحدد الذي يشرحه.

الأسئلة الشائعة

**س:** هل نحتاج إلى العلم experimental.instrumentationHook لملف instrumentation.ts في Next.js 16؟

**ج:** لا. ملف instrumentation.ts ودالته المُصدَّرة register() مستقران منذ Next.js 15. العلم التجريبي من Next.js 13 و14 لم يعد موجودًا.

**س:** هل يعمل instrumentation.ts على بيئة Edge؟

**ج:** جزء منه فقط. حزمة OpenTelemetry الكاملة لـ Node تحتاج بيئة Node.js، وهي الخيار الافتراضي العملي على Vercel الآن بعد أن جعلت تقنية Fluid Compute من Node.js الخيار القياسي.

**س:** ما الفرق بين onRequestError وحدود الخطأ error.tsx في React؟

**ج:** حدود error.tsx تلتقط الأخطاء داخل شجرتها الفرعية من المكوّنات وتعرض واجهة بديلة للمستخدم. أما onRequestError فهي دالة على مستوى الإطار (framework) تُستدعى مع أخطاء عبر Server Components وRoute Handlers وServer Actions وMiddleware بغض النظر عن أي حدود خطأ معينة — وظيفتها الإبلاغ، لا العرض.

**س:** هل يمكننا استخدام متغيّر على مستوى الوحدة بدلاً من AsyncLocalStorage لتمرير سياق الطلب؟

**ج:** فقط إذا كانت النسخة التي تشغّل الكود لا تخدم أكثر من طلب واحد في كل مرة. تقنية Fluid Compute تعيد استخدام النُسخ عبر طلبات متزامنة، لذا يتحول المتغيّر على مستوى الوحدة إلى حالة مشتركة قابلة للتعديل، وسيتسرب بين طلبات لا علاقة بينها.

**س:** هل ما زلنا بحاجة إلى مزوّد مراقبة منفصل إذا كنا على Vercel؟

**ج:** تعرض لوحة Observability في Vercel التتبعات تلقائيًا بمجرد تسجيل @vercel/otel، وهذا يغطي الكثير من تصحيح الأخطاء الروتيني. الاحتفاظ الأطول بالبيانات، أو الربط بين الخدمات المختلفة، أو التنبيهات المخصصة عادة ما تتطلب مع ذلك التصدير عبر OTLP إلى خلفية مخصصة.