> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# تتبع الأخطاء Error Tracking للمطورين مع GlitchTip
- URL: https://arabroot.io/articles/تتبع-الاخطاء-error-tracking-مع-glitchtip/
- Published: 2026-10-22T15:15:00.000Z
- Updated: 2026-10-23T08:06:41.000Z
- Description: ماذا يضيف تتبع الأخطاء فوق السجلات، وكيف تشغل GlitchTip بديلاً خفيفاً لـ Sentry يستقبل أخطاء تطبيقك بالـ SDK نفسه.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, DevOps وCI/CD, الاستضافة الذاتية, GlitchTip

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

وقد شرحنا في [دليل السجلات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#exceptions) كيف تسجل الاستثناء مرة واحدة مع ال Stack Trace، وفي [مقال السجلات التي تساعدك فعلاً](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-%D8%AA%D8%B3%D8%A7%D8%B9%D8%AF%D9%83-%D9%81%D8%B9%D9%84%D8%A7-%D9%85%D9%86-%D8%AD%D8%AF%D8%AB-%D8%AE%D8%B7%D8%A7-%D8%A7%D9%84%D9%89-%D8%B3%D8%A8%D8%A8-%D8%A7%D9%84%D9%85%D8%B4%D9%83%D9%84%D8%A9/) كيف تحقق في حادثة بالسجلات، وهذا المقال يكمل الصورة من جهة المطور. وسوف نتناول فيه:

- ماذا يضيف تتبع الأخطاء فوق السجلات: التجميع Grouping، وال Stack Trace مع الكود، والإصدارات Releases، وخرائط المصدر Source Maps، والتنبيهات.
- Sentry المستضاف ذاتياً وترخيصه FSL ومتطلباته، مقابل GlitchTip الأخف الذي يقبل SDK الخاص بـ Sentry نفسه.
- أسعار الخدمات المستضافة Sentry وRollbar وBugsnag وHoneybadger وRaygun ومكان بياناتها، ومتى تكون الاستضافة الذاتية أنسب.
- تجربة عملية كاملة مع GlitchTip في Docker: خدمة Python وصفحة JavaScript ترسلان أخطاء حقيقية عبر Sentry SDK.
- ماذا يحدث إذا وضعت قيمة متغيرة في رسالة الخطأ، وكيف تصلح التجميع ببصمة Fingerprint.
- تتبع الإصدارات وعودة الخطأ بعد إغلاقه Regression، وإخفاء البيانات الشخصية، وخرائط المصدر، والتنبيه بالبريد.

## ماذا يضيف تتبع الأخطاء فوق السجلات؟

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

![على اليمين سجل داكن فيه ثلاثة استثناءات KeyError متتالية لقيم item-0 وitem-1 وitem-2 مع Traceback لكل منها ثم 37 أخرى، وتحته أسئلة: هل هو خطأ جديد؟ في أي إصدار بدأ؟ هل عاد بعد إصلاحه؟ وعلى اليسار بعد سهم التجميع Grouping بطاقة مشكلة واحدة KeyError في app.py داخل price بعدد 40 حدثاً، مع أول ظهور وآخر ظهور في الإصدار shop-api@1.4.2، وسطر الكود 40 الذي سبب الخطأ، وتنبيه واحد بالبريد عند 10 أحداث في دقيقة](https://arabroot.io/content/images/2026/10/error-tracking-01-log-vs-issue.webp)

أربعون سطراً في السجل، ومشكلة واحدة بعددها وإصدارها في أداة تتبع الأخطاء

وفيما يلي ما تضيفه الأداة بالتفصيل:

- **التجميع Grouping:** أربعون استثناء من السطر نفسه تصبح مشكلة واحدة بعدد 40، فترى أن هناك ثلاث مشكلات وليس أربعمئة سطر، وترتبها بعدد مرات حدوثها أو بعدد المستخدمين المتأثرين.
- **ال Stack Trace مع الكود والمتغيرات:** كل إطار Frame في ال Stack Trace يعرض سطور الكود حوله، وقيم المتغيرات المحلية Local Variables لحظة الخطأ، وهذا ما يحتاجه المطور ليفهم الخطأ دون أن يعيد إنتاجه.
- **السياق Context:** الإصدار والبيئة Environment والمتصفح ونظام التشغيل، والخطوات التي سبقت الخطأ Breadcrumbs، مثل النقر على زر أو طلب HTTP.
- **الإصدارات Releases:** متى ظهرت المشكلة أول مرة، وفي أي إصدار، وهل عادت بعد أن أغلقتها، وهذا ما يجعل الأداة جزءاً من عملية النشر.
- **خرائط المصدر Source Maps:** كود JavaScript في المتصفح مضغوط Minified في سطر واحد، والأداة تستخدم خريطة المصدر لترجع كل إطار إلى الملف والسطر الأصلي.
- **التنبيه على المشكلة وليس على السطر:** تنبيه عند ظهور مشكلة جديدة، أو عندما تتكرر مشكلة أكثر من عدد محدد في مدة محددة.

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

## Sentry المستضاف ذاتياً أم GlitchTip؟

أشهر أداة في هذا المجال هي Sentry، وهي التي وضعت صيغة الأحداث والمكتبات SDK التي تستخدمها أغلب الأدوات الأخرى، وتقدمها الشركة كخدمة سحابية وكنسخة تستضيفها بنفسك Self-hosted. وGlitchTip مشروع مفتوح المصدر يستقبل الأحداث بصيغة Sentry نفسها، فتستخدم في تطبيقك [أي SDK متوافق مع Sentry](https://glitchtip.com/sdkdocs?ref=arabroot.io) وتغير فقط عنوان ال DSN. والصورة التالية تبين ما تشغله على السيرفر في كل حالة:

![على اليمين Sentry self-hosted 26.10.0 وملف docker-compose.yml يعرف 53 خدمة منها nginx وrelay وweb وtaskworker وkafka وclickhouse وpostgres وredis وsnuba-api وsymbolicator وseaweedfs وmemcached و18 خدمة snuba أخرى، والحد الأدنى 4 أنوية و16 GB RAM و16 GB Swap و20 GB مساحة، والترخيص FSL-1.1-Apache-2.0، وعلى اليسار GlitchTip 6.2.6 بثلاث Containers: glitchtip web وworker بذاكرة 273 MiB وpostgres 18.6 بذاكرة 132 MiB وvalkey 9.0.6 بذاكرة 12 MiB، والتوثيق يوصي بـ 512 MB RAM، والترخيص MIT](https://arabroot.io/content/images/2026/10/error-tracking-02-architectures.webp)

الفرق في الحجم كبير، وكذلك الفرق في الميزات

### Sentry المستضاف ذاتياً

آخر إصدار من Sentry المستضاف ذاتياً عند كتابة المقال هو [26.10.0 الصادر في 16 أكتوبر 2026](https://github.com/getsentry/self-hosted/releases/tag/26.10.0?ref=arabroot.io)، و[ملف docker-compose.yml](https://github.com/getsentry/self-hosted/blob/26.10.0/docker-compose.yml?ref=arabroot.io) فيه 53 خدمة، منها Kafka وClickHouse وPostgreSQL وRedis وMemcached وRelay وSymbolicator وعدد كبير من خدمات Snuba. والمتطلبات الدنيا بحسب [توثيق Sentry](https://develop.sentry.dev/self-hosted/?ref=arabroot.io) هي 4 أنوية للمعالج و16 GB من الذاكرة RAM مع 16 GB من ال Swap و20 GB مساحة حرة، والذاكرة الموصى بها 32 GB، ويذكر التوثيق أن هذا إعداد بسيط يعمل للحالات البسيطة دون ضمانات أو دعم مخصص. وإذا كنت تحتاج الأخطاء فقط فهناك [وضع الأخطاء فقط errors-only](https://develop.sentry.dev/self-hosted/optional-features/errors-only/?ref=arabroot.io) منذ الإصدار 24.8.0، تفعله بالمتغير `COMPOSE_PROFILES=errors-only`، ويحذف التتبع وال Profiling وإعادة الجلسات Replays وغيرها، ويبقي المشكلات والتنبيهات والإصدارات.

أما الترخيص فهو [Functional Source License, Version 1.1, Apache 2.0 Future License](https://github.com/getsentry/sentry/blob/master/LICENSE.md?ref=arabroot.io) واختصاراً FSL-1.1-Apache-2.0، وتصفه Sentry بأنه [مصدر عادل Fair Source](https://develop.sentry.dev/self-hosted/?ref=arabroot.io) وليس مفتوح المصدر بتعريف OSI. والمسموح فيه كل استخدام عدا الاستخدام المنافس Competing Use، أي أن تقدم البرنامج للآخرين في منتج أو خدمة تجارية تحل محله أو تقدم وظيفة مشابهة له، وبالتالي تستطيع تشغيله داخل شركتك لتطبيقاتك دون قيود، ولا تستطيع بيعه كخدمة لعملائك. وكل إصدار يتحول إلى ترخيص Apache 2.0 بعد سنتين من تاريخ نشره، كما يشرح [موقع الترخيص](https://fsl.software/?ref=arabroot.io).

- **نقاط القوة:** أكمل الميزات في المجال، من التتبع وال Profiling إلى إعادة الجلسات Session Replay ومراقبة المهام المجدولة Crons، والواجهة نفسها التي يعرفها أغلب المطورين، والتوثيق الأوسع.
- **نقاط الضعف:** متطلبات كبيرة لسيرفر واحد، وعشرات الخدمات التي يجب أن تفهمها عند أي مشكلة، والتحديث يمر بسكربت التثبيت، والترخيص ليس مفتوح المصدر.

### GlitchTip

GlitchTip مشروع بلغة Python وDjango بترخيص [MIT](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/LICENSE?ref=arabroot.io)، وآخر إصدار من ال Docker Image عند كتابة المقال [6.2.6](https://hub.docker.com/r/glitchtip/glitchtip?ref=arabroot.io). ويحتاج بحسب [صفحة التثبيت](https://glitchtip.com/documentation/install?ref=arabroot.io) إلى PostgreSQL 14 أو أحدث، وValkey أو Redis 7 أو أحدث اختيارياً، والإعداد الموصى به 512 MB من الذاكرة، والحد الأدنى 256 MB في وضع all\_in\_one الذي يشغل الواجهة والمعالج الخلفي Worker في Container واحد. وفي تجربتنا كانت الذاكرة بعد نحو 250 حدثاً 273 MiB لـ GlitchTip و132 MiB لـ PostgreSQL و12 MiB لـ Valkey. وإلى جانب الأخطاء فيه مراقبة الأداء Performance ومراقبة التوفر Uptime Monitoring واستقبال السجلات.

- **نقاط القوة:** خفيف جداً، وثلاث Containers يفهمها أي شخص يعرف Django وPostgreSQL، وترخيص MIT، ويقبل SDK الخاص بـ Sentry وأداة sentry-cli لرفع خرائط المصدر كما سوف نرى.
- **نقاط الضعف:** ميزات أقل من Sentry، والتجميع أبسط ويعتمد على عنوان الخطأ كما سوف نرى في التجربة، والواجهة أبسط، ومجتمعه أصغر.

| المعيار                   | Sentry self-hosted 26.10.0                      | GlitchTip 6.2.6                              |
| ------------------------- | ----------------------------------------------- | -------------------------------------------- |
| الترخيص                   | FSL-1.1-Apache-2.0                              | MIT                                          |
| الخدمات في Docker Compose | 53 خدمة                                         | 3 في وضع all\_in\_one                        |
| الذاكرة                   | 16 GB حد أدنى مع 16 GB Swap                     | 512 MB موصى بها                              |
| SDK                       | Sentry SDK                                      | Sentry SDK نفسه                              |
| طريقة التجميع الافتراضية  | ال Stack Trace أولاً                            | العنوان والمكان ونوع الحدث                   |
| يناسب                     | فريق كبير يحتاج كل الميزات ويملك سيرفراً كبيراً | فريق يحتاج الأخطاء والتنبيهات على سيرفر صغير |

ولأن الكود في تطبيقك واحد في الحالتين، فالانتقال من أحدهما إلى الآخر يعني تغيير ال DSN فقط، وهذا يجعل البدء بـ GlitchTip قراراً قليل المخاطرة، ولذلك اخترناه للتجربة.

## أم خدمة مستضافة Hosted؟ الأسعار ومتى يربح GlitchTip

الخيار الثالث ألا تشغل شيئاً، وأن تشتري الخدمة مستضافة من Sentry نفسها أو من منافسيها. والجدول التالي من صفحات الأسعار والأمان الرسمية لكل خدمة كما ظهرت في 23 أكتوبر 2026، وهي أسعار قوائم قبل الضرائب وأي خصم:

| الخدمة                                                           | الخطة المجانية                                  | أول خطة مدفوعة                                                                                                                                                | مدة الحفظ Retention                     | منطقة البيانات                                                                                                                                                                                             | SDK                                                                          |
| ---------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [Sentry](https://sentry.io/pricing/?ref=arabroot.io)             | 5 آلاف خطأ شهرياً لمستخدم واحد، وحفظ 30 يوماً   | Team بـ 26 دولاراً شهرياً بالدفع السنوي، فيها 50 ألف خطأ ومستخدمون بلا حد، ثم 0.0003625 دولار للخطأ بين 50 و100 ألف                                           | حتى 90 يوماً في Team وBusiness          | الولايات المتحدة (Iowa) أو الاتحاد الأوروبي (Frankfurt)، تختارها عند إنشاء المنظمة ولا تتغير بعدها، كما في [صفحة مكان التخزين](https://docs.sentry.io/organization/data-storage-location/?ref=arabroot.io) | Sentry SDK                                                                   |
| [Rollbar](https://rollbar.com/pricing/?ref=arabroot.io)          | 5 آلاف حدث و1,000 Session Replay، وحفظ 30 يوماً | Essentials من 9 دولارات شهرياً لعشرة آلاف حدث، و29 دولاراً لخمسين ألفاً، كما في [جدول الأسعار الكامل](https://rollbar.com/pricing-for-agents?ref=arabroot.io) | 90 يوماً في Essentials و180 في Advanced | Google Cloud في منطقة Iowa، ولا تذكر [صفحة الأمان](https://docs.rollbar.com/docs/security?ref=arabroot.io) منطقة أخرى                                                                                      | [SDK خاص](https://docs.rollbar.com/docs/sdk-overview?ref=arabroot.io)        |
| [Bugsnag](https://www.bugsnag.com/pricing/?ref=arabroot.io)      | 7,500 حدث شهرياً لمستخدم واحد، وحفظ 7 أيام      | Select من 20 دولاراً شهرياً، وPreferred من 33 دولاراً، ويرتفع السعر بعدد الأحداث                                                                              | 60 يوماً                                | لا تذكر صفحة الأسعار منطقة، وفي خطة Enterprise خيار [التثبيت على سيرفراتك On-Premises](https://docs.bugsnag.com/on-premise/?ref=arabroot.io)                                                               | [SDK خاص](https://docs.bugsnag.com/platforms/?ref=arabroot.io)               |
| [Honeybadger](https://www.honeybadger.io/plans/?ref=arabroot.io) | 5 آلاف خطأ شهرياً لمستخدم واحد، وحفظ 15 يوماً   | Team بـ 26 دولاراً شهرياً، فيها 50 ألف خطأ ومستخدمون بلا حد                                                                                                   | 90 يوماً في Team و180 في Business       | الولايات المتحدة (AWS us-east-1) افتراضياً، أو الاتحاد الأوروبي (Frankfurt) بحساب منفصل، كما في [صفحة مكان البيانات](https://docs.honeybadger.io/resources/data-residency/?ref=arabroot.io)                | [SDK خاص](https://docs.honeybadger.io/lib/?ref=arabroot.io)                  |
| [Raygun](https://raygun.com/pricing?ref=arabroot.io)             | لا توجد، وإنما تجربة 14 يوماً                   | Basic بـ 40 دولاراً شهرياً لكل 100 ألف خطأ بالدفع السنوي، وما زاد بـ 0.001 دولار للخطأ                                                                        | 180 يوماً                               | AWS في منطقة US-EAST-1 كما تقول صفحة الأسعار                                                                                                                                                               | [SDK خاص](https://raygun.com/documentation/language-guides/?ref=arabroot.io) |
| GlitchTip على سيرفرك                                             | لا حد إلا القرص                                 | تكلفة السيرفر ووقت تشغيله                                                                                                                                     | 90 يوماً افتراضياً وتغيرها بمتغير       | حيث تضع السيرفر                                                                                                                                                                                            | Sentry SDK                                                                   |

لاحظ أن Sentry هي الخدمة الوحيدة في الجدول التي تستخدم SDK الخاص بـ Sentry، أي أن تطبيقاً يرسل إلى GlitchTip اليوم ينتقل إلى Sentry المستضافة أو يعود منها بتغيير ال DSN فقط، وأما Rollbar وBugsnag وHoneybadger وRaygun فلكل منها مكتباتها، والانتقال إليها أو منها تعديل في كود كل خدمة لديك. ولاحظ أيضاً أن الحد الشهري في هذه الخدمات حقيقي، فعند بلوغه يتوقف Rollbar افتراضياً عن معالجة الأحداث الجديدة حتى الشهر التالي، أو يستمر بسعر الحدث إذا فعلت الأحداث عند الطلب On-demand، وهذا يعني أن خطأً واحداً في حلقة Loop قد يستهلك حصة الشهر في ساعة فتفقد الأخطاء التي تأتي بعده، أو يرفع الفاتورة، ولذلك ضع حداً لعدد الأحداث Rate Limit في كل مشروع أياً كانت الأداة.

متى يربح GlitchTip أو أي استضافة ذاتية إذاً؟ أولاً عندما يجب أن تبقى البيانات داخل المملكة، فلا توجد لأي من الخدمات الخمس منطقة في الشرق الأوسط في صفحاتها الرسمية، والأحداث تحمل بيانات شخصية كما سوف نرى في فقرة [البيانات الشخصية](#pii). وثانياً عندما يكبر عدد الأحداث، لأن الخدمة المستضافة تحاسب على كل حدث، وأما GlitchTip فتكلفته القرص، وصفحة التثبيت تقدر مليون حدث في الشهر بنحو 30 GB. وأما الخدمة المستضافة فهي الأنسب لفريق صغير تكفيه 5 آلاف خطأ في الشهر ولا يريد أن يدير سيرفراً، أو لمن يحتاج ميزات لا يقدمها GlitchTip مثل إعادة الجلسات Session Replay، ففي هذه الحالة ابدأ بـ Sentry المستضافة، لأنك تستطيع الانتقال منها إلى GlitchTip لاحقاً دون تغيير الكود. وإذا كان السؤال أكبر من تتبع الأخطاء، أي منصة مراقبة كاملة بالسجلات والمقاييس وال Traces، فقد حسبنا فاتورتها الشهرية في Datadog والمنصات التجارية الأخرى مقابل الاستضافة الذاتية في مقال [كم تكلف منصات المراقبة Observability؟](https://arabroot.io/articles/%D8%AA%D9%83%D9%84%D9%81%D8%A9-%D8%A7%D9%84%D9%85%D8%B1%D8%A7%D9%82%D8%A8%D8%A9-observability-datadog-%D8%A7%D9%85-%D8%A7%D9%84%D8%A7%D8%B3%D8%AA%D8%B6%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D8%B0%D8%A7%D8%AA%D9%8A%D8%A9/).

## تجربة عملية: GlitchTip مع خدمة Python وصفحة JavaScript

سوف نشغل GlitchTip مع PostgreSQL وValkey، ثم خدمة Flask صغيرة اسمها `shop-api` ترسل أخطاءها عبر `sentry-sdk`، وصفحة ويب اسمها `shop-web` مبنية بـ esbuild ترسل أخطاءها عبر `@sentry/browser`. وكل الأسماء تبدأ بالبادئة `obs1-`، والمنافذ على `127.0.0.1` فقط.

### تشغيل GlitchTip

الملف `compose.yaml` كما يلي:

```yaml
name: obs1-et
x-gt-env: &gt-env
  DATABASE_URL: postgres://postgres:lab-only-pass@obs1-gt-db:5432/postgres
  VALKEY_URL: redis://obs1-gt-valkey:6379
  SECRET_KEY: lab-only-0b7c2f6e4d9a41c8b3e5f1a2c6d8e0f4
  EMAIL_URL: consolemail://
  GLITCHTIP_DOMAIN: http://127.0.0.1:33800
  DEFAULT_FROM_EMAIL: glitchtip@example.com
  SERVER_ROLE: all_in_one
services:
  db:
    image: postgres:18.6
    container_name: obs1-gt-db
    environment:
      POSTGRES_PASSWORD: lab-only-pass
    volumes:
      - obs1-gt-pg:/var/lib/postgresql
  valkey:
    image: valkey/valkey:9.0.6-alpine
    container_name: obs1-gt-valkey
  web:
    image: glitchtip/glitchtip:6.2.6
    container_name: obs1-gt-web
    depends_on: [db, valkey]
    environment: *gt-env
    ports:
      - "127.0.0.1:33800:8000"
    volumes:
      - obs1-gt-uploads:/code/uploads
  shop-api:
    build: ./py
    image: obs1-shop-api:1
    container_name: obs1-shop-api
    environment:
      SENTRY_DSN: ${API_DSN:-}
      SENTRY_RELEASE: ${API_RELEASE:-shop-api@1.4.1}
      FIX_GROUPING: ${FIX_GROUPING:-0}
      SCRUB: ${SCRUB:-0}
  shop-web:
    image: python:3.13.17-slim
    container_name: obs1-shop-web
    command: python -m http.server 8080 --directory /site
    volumes:
      - ./web/site:/site:ro
    ports:
      - "127.0.0.1:33880:8080"
volumes:
  obs1-gt-pg:
    name: obs1-gt-pg
  obs1-gt-uploads:
    name: obs1-gt-uploads
```

في الإعداد أعلاه لاحظ التالي:

- `SERVER_ROLE: all_in_one` يشغل الواجهة والمعالج الخلفي في Container واحد، وفي الإنتاج الكبير تفصلهما لتزيد عدد ال Workers وحدها.
- `GLITCHTIP_DOMAIN` هو العنوان الذي يراه المستخدم، ومنه تبنى روابط البريد وقيم ال DSN، فإذا وضعت عنواناً خاطئاً ستجد روابط لا تفتح في كل تنبيه.
- `EMAIL_URL: consolemail://` يطبع رسائل البريد في سجل ال Container بدل إرسالها، وهو للتجربة فقط، وفي الإنتاج تضع عنوان SMTP بالصيغة `smtp://user:pass@smtp.example.com:587`.
- ال `SECRET_KEY` وكلمة مرور PostgreSQL هنا للتجربة، وفي الإنتاج تولدهما بالأمر `openssl rand -hex 32` وتضعهما في ملف `.env`.

⚠️

توثيق GlitchTip يقول إن Valkey اختياري، ولكن حذف المتغير `VALKEY_URL` لا يلغيه، لأن القيمة الافتراضية في [ملف الإعدادات](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/glitchtip/settings.py?ref=arabroot.io) هي `redis://redis:6379/0`، فإذا حذفت خدمة Valkey وحذفت المتغير معها ستجد في السجل `ConnectionError: Connection failed: timed out` متكرراً. وبحسب ملف الإعدادات نفسه فالقيمة الفارغة `VALKEY_URL: ""` هي التي تلغيه وتجعل PostgreSQL يقوم بعمله.

نشغل قاعدة البيانات وValkey وGlitchTip:

```bash
docker compose up -d db valkey web
docker logs obs1-gt-web 2>&1 | tail -3
```

```text
Maintaining weekly partitions for uptime_uptimecheckhourlystatistic...
Cleaning up old weekly partitions for uptime_uptimecheckhourlystatistic...
Partition maintenance complete.
```

بعد ذلك نحتاج حساباً ومنظمة Organization وفريقاً Team ومشروعين Projects، واحد للخدمة وواحد للصفحة، ومفتاح API نستخدمه من سطر الأوامر. ويمكن إنشاء ذلك كله من واجهة الويب، ولكننا كتبناه في سكربت `setup_glitchtip.py` يعمل داخل GlitchTip حتى تكون التجربة قابلة للتكرار:

```python
# Creates the lab user, organization, team, two projects and an API token (run with: manage.py shell < setup_glitchtip.py).
from apps.users.models import User
from apps.organizations_ext.models import Organization, OrganizationUser, OrganizationUserRole
from apps.projects.models import Project, ProjectKey
from apps.teams.models import Team
from apps.api_tokens.models import APIToken

user = User.objects.create_user(email="lab@example.com", password="lab-only-Passw0rd")
org = Organization.objects.create(name="lab")
org.add_user(user, role=OrganizationUserRole.OWNER)
team = Team.objects.create(slug="backend", organization=org)
team.members.add(OrganizationUser.objects.get(organization=org, user=user))
for name, platform in (("shop-api", "python-flask"), ("shop-web", "javascript")):
    p = Project.objects.create(name=name, organization=org, platform=platform)
    team.projects.add(p)
    key = ProjectKey.objects.filter(project=p).first() or ProjectKey.objects.create(project=p)
    print(name, key.get_dsn())
token = APIToken.objects.create(user=user, label="lab")
token.add_permissions(["project:read", "project:write", "project:admin", "project:releases",
                       "event:read", "event:write", "event:admin", "org:read"])
token.save()
print("token", token.token)
```

```bash
docker exec -i obs1-gt-web ./manage.py shell < setup_glitchtip.py
```

```text
shop-api http://7dd17a64…@127.0.0.1:33800/1
shop-web http://57923a3b…@127.0.0.1:33800/2
token 736e0c0b…
```

ال DSN هو العنوان الذي يرسل إليه ال SDK أحداثه، والجزء قبل `@` مفتاح عام Public Key يعرف المشروع، وهو ليس سراً بالمعنى الكامل لأنه يظهر في كود المتصفح، ولكنه يسمح لأي أحد بإرسال أحداث إلى مشروعك، لذلك لا تنشره في مكان عام دون حاجة. أما مفتاح API فهو سر، ونضعه في متغير `TOKEN` في الأوامر التالية.

### خدمة Python ترسل أخطاءها

الخدمة `py/app.py` فيها أربعة أخطاء مقصودة، ولكل منها درس: `/price` ترفع `KeyError` لأي صنف غير موجود، و`/checkout` تقسم على عدد الأصناف وقد يكون صفراً، و`/pay` تكتب سطر ERROR فيه رقم الطلب، و`/login` ترفع خطأ فيه بريد المستخدم. والمتغيران `FIX_GROUPING` و`SCRUB` يفعلان الإصلاحات التي سوف نشرحها بعد قليل:

```python
import logging, os, re
import sentry_sdk
from sentry_sdk.scrubber import EventScrubber, DEFAULT_DENYLIST
from flask import Flask, request

EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")

def before_send(event, hint):
    if os.environ.get("FIX_GROUPING") == "1":
        exc = (event.get("exception") or {}).get("values") or []
        msg = (event.get("logentry") or {}).get("message") or ""
        if exc:
            # group by exception type and the function that raised it, not by the value
            top = exc[-1]["stacktrace"]["frames"][-1]
            event["fingerprint"] = [exc[-1]["type"], top.get("module") or "", top.get("function") or ""]
        elif msg.startswith("payment gateway timeout"):
            event["fingerprint"] = ["payment-gateway-timeout"]
    if os.environ.get("SCRUB") == "1":
        for exc in (event.get("exception") or {}).get("values", []):
            exc["value"] = EMAIL.sub("[email]", exc.get("value") or "")
    return event

sentry_sdk.init(
    dsn=os.environ["SENTRY_DSN"],
    release=os.environ.get("SENTRY_RELEASE"),
    environment="lab",
    send_default_pii=False,
    before_send=before_send,
    event_scrubber=EventScrubber(denylist=DEFAULT_DENYLIST + ["email", "card_number"], recursive=True)
    if os.environ.get("SCRUB") == "1" else None,
)

app = Flask(__name__)
log = logging.getLogger("payments")
PRICES = {"tea": 12, "coffee": 18}

@app.get("/price")
def price():
    sku = request.args["sku"]
    return {"sku": sku, "price": PRICES[sku]}          # KeyError for unknown sku

@app.get("/checkout")
def checkout():
    items = int(request.args.get("items", 1))
    total = 120
    per_item = total / items                            # 1.4.1: the cart can be empty
    return {"per_item": per_item}

@app.get("/pay")
def pay():
    order = request.args["order"]
    log.error(f"payment gateway timeout for order {order}")
    return {"queued": order}, 202

@app.post("/login")
def login():
    email = request.form["email"]
    password = request.form["password"]
    card_number = "4111111111111111"
    raise ValueError(f"no account for {email}")
```

و`py/Dockerfile`:

```dockerfile
FROM python:3.13.17-slim
RUN pip install --no-cache-dir flask==3.1.3 sentry-sdk[flask]==2.72.0
WORKDIR /app
COPY app.py traffic.py ./
CMD ["flask", "--app", "app", "run", "--host", "0.0.0.0", "--port", "8080"]
```

والملف `py/traffic.py` يرسل الطلبات من داخل ال Container نفسه:

```python
# Sends the lab traffic to the app. usage: python traffic.py <scenario>
import sys, urllib.request, urllib.parse
BASE = "http://127.0.0.1:8080"
def get(path):
    try: urllib.request.urlopen(BASE + path).read()
    except Exception: pass
def post(path, data):
    try: urllib.request.urlopen(BASE + path, data=urllib.parse.urlencode(data).encode()).read()
    except Exception: pass
s = sys.argv[1]
if s == "prices":
    for i in range(40): get(f"/price?sku=item-{i}")
elif s == "checkout":
    for i in range(10): get("/checkout?items=0")
elif s == "pay":
    for i in range(30): get(f"/pay?order=ord-{1000 + i}")
elif s == "login":
    post("/login", {"email": "ali@example.com", "password": "S3cret-pass"})
print("done", s)
```

في الكود أعلاه لاحظ التالي:

- لا يوجد في الكود أي سطر يرسل الأخطاء بنفسه، فـ `sentry_sdk.init` يفعل [تكامل Flask](https://docs.sentry.io/platforms/python/integrations/flask/?ref=arabroot.io) تلقائياً ويلتقط كل استثناء لم يعالج، ويفعل [تكامل logging](https://docs.sentry.io/platforms/python/integrations/logging/?ref=arabroot.io) الذي يرسل كل سطر بمستوى ERROR كحدث، ولذلك يصل سطر `/pay` رغم أنه ليس استثناء.
- `release` و`environment` يدخلان في كل حدث، وهما ما يسمح بتتبع الإصدارات وفصل الإنتاج عن الاختبار.
- ال DSN داخل شبكة Docker يجب أن يشير إلى اسم ال Container `obs1-gt-web:8000`، وليس إلى `127.0.0.1:33800`، لأن `127.0.0.1` داخل ال Container هو ال Container نفسه.

نبني الخدمة ونشغلها بالإصدار 1.4.1، ثم نرسل الطلبات:

```bash
export TOKEN=736e0c0b…        # the API token printed by setup_glitchtip.py
API_DSN=http://7dd17a64…@obs1-gt-web:8000/1 docker compose up -d --build shop-api
for s in prices pay checkout; do docker exec obs1-shop-api python traffic.py $s; done
```

والصورة التالية تبين واحدة من المشكلات في GlitchTip، مع الإصدار والبيئة وال Stack Trace وسطر الكود الذي سبب الخطأ:

![صفحة مشكلة في GlitchTip 6.2.6 بعنوان KeyError item-0 للمشروع shop-api، فيها معرف الحدث وCPython 3.13.17 والعلامات Tags: release shop-api@1.4.3 وenvironment lab وserver_name، والاستثناء KeyError بقيمة item-39 مع آلية flask وhandled no، وإطار app.py في الدالة price عند السطر 40 مع الكود، وعلى اليمين رقم المشكلة SHOP-API-28 وعدد الأحداث 120 وأول ظهور وآخر ظهور](https://arabroot.io/content/images/2026/10/error-tracking-03-py-issue.webp)

كل حدث يصل مع إصداره وبيئته وال Stack Trace وسطور الكود حوله

### نجرب العطل: قيمة متغيرة في رسالة الخطأ

أرسلنا ثلاثة أخطاء فقط من حيث الكود: `KeyError` واحد في `price`، وسطر ERROR واحد في `pay`، و`ZeroDivisionError` واحد في `checkout`. والمتوقع ثلاث مشكلات، فلنسأل GlitchTip عن عدد المشكلات بالواجهة البرمجية API:

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:33800/api/0/projects/lab/shop-api/issues/?limit=100"
```

وبعد تلخيص الرد بحسب نمط العنوان كانت النتيجة كما يلي:

```text
issues: 71
'ZeroDivisionError: division by zero':          1 issue,  10 events
'payment gateway timeout for order ord-N':     30 issues, 30 events
"KeyError: 'item-N'":                          40 issues, 40 events
```

واحد وسبعون مشكلة من ثلاثة أخطاء، وكل مشكلة فيها حدث واحد، فالقائمة لا تقول شيئاً عن التكرار، وهي الحالة نفسها التي أردنا الهروب منها في السجل. والسبب في طريقة التجميع: GlitchTip يحسب بصمة كل حدث من [العنوان والمكان Culprit ونوع الحدث](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/apps/event%5Fingest/utils.py?ref=arabroot.io)، والعنوان في الاستثناء هو النوع مع القيمة، مثل `KeyError: 'item-7'`، وفي سطر السجل هو الرسالة بعد التعويض، فكل قيمة مختلفة تصنع مشكلة جديدة. أما Sentry فيقول [توثيق التجميع](https://docs.sentry.io/concepts/data-management/event-grouping/?ref=arabroot.io) فيه إنه ينظر إلى البصمة Fingerprint أولاً، ثم ال Stack Trace، ثم الاستثناء، ثم الرسالة، وإذا وجد Stack Trace فالتجميع يعتمد عليه تقريباً بالكامل، وبالتالي كان سيجمع الأربعين `KeyError` في مشكلة واحدة. والصورة التالية تبين ما حدث والإصلاح:

![في الأعلى طريقتا التجميع: GlitchTip ببصمة md5 من العنوان والمكان والنوع، وSentry بالترتيب fingerprint ثم stack trace ثم exception ثم message، وفي الأسفل على اليمين قبل الإصلاح: KeyError item-0 بحدث واحد وKeyError item-1 بحدث واحد و38 أخرى، وpayment gateway timeout for order ord-1000 بحدث واحد و29 أخرى، والمجموع 71 مشكلة من ثلاثة أخطاء حقيقية، وعلى اليسار بعد البصمة الثابتة: KeyError app price بعدد 40، وpayment-gateway-timeout بعدد 30، وZeroDivisionError app checkout بعدد 10، والمجموع 3 مشكلات](https://arabroot.io/content/images/2026/10/error-tracking-04-grouping.webp)

القيمة المتغيرة في العنوان تحول الخطأ الواحد إلى عشرات المشكلات

والإصلاح الأول في الكود نفسه: لا تضع القيم المتغيرة في رسالة الاستثناء أو سطر السجل إذا كانت الأداة تجمع بالرسالة، وضعها في حقول مستقلة. ولكن `KeyError` تضع القيمة في الرسالة بنفسها، وفي GlitchTip حتى الرسالة بصيغة `log.error("... %s", order)` تعوض قبل حساب البصمة كما في [الكود نفسه](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/apps/event%5Fingest/utils.py?ref=arabroot.io)، لذلك الإصلاح الأضمن هو البصمة Fingerprint التي يضعها ال SDK في الحدث، وهي ما تفعله الدالة `before_send` في `app.py` عند `FIX_GROUPING=1`: الاستثناء يجمع بنوعه والوحدة Module والدالة التي رفعته، وسطر انتهاء مهلة الدفع يجمع ببصمة ثابتة `payment-gateway-timeout`. وتشرح Sentry الطريقة نفسها في [توثيق البصمة من ال SDK](https://docs.sentry.io/platforms/python/usage/sdk-fingerprinting/?ref=arabroot.io)، وهي تعمل في الأداتين. نعيد تشغيل الخدمة بالإصلاح وبإصدار جديد 1.4.2، ونرسل الطلبات نفسها:

```bash
API_DSN=http://7dd17a64…@obs1-gt-web:8000/1 FIX_GROUPING=1 API_RELEASE=shop-api@1.4.2 docker compose up -d shop-api
for s in prices pay; do docker exec obs1-shop-api python traffic.py $s; done
```

وأحدث المشكلات في الرد، بالمعرف والرقم القصير والعدد والعنوان والدالة والإصدار الأول:

```text
issues: 73
73 SHOP-API-29 30 payment gateway timeout for order ord-1000 | shop-api@1.4.2
72 SHOP-API-28 40 KeyError: 'item-0' | price | shop-api@1.4.2
```

مشكلتان جديدتان فقط، الأولى فيها 30 حدثاً والثانية 40، وهذا ما نريده. ولاحظ أن عنوان المشكلة بقي عنوان أول حدث فيها `KeyError: 'item-0'`، فلا تعتمد على العنوان لمعرفة القيمة، وإنما افتح الأحداث نفسها. والصورة التالية تبين القائمة بعد التجربة، وفيها المشكلات المجمعة بأعدادها في الأعلى، والمشكلات المتفرقة القديمة تحتها:

![قائمة المشكلات في GlitchTip للمشروع shop-api بعدد 75 مشكلة، في الأعلى KeyError item-0 بعدد 120 حدثاً، وValueError no account for ali@example.com بعدد 2، وZeroDivisionError بعدد 20، وpayment gateway timeout for order ord-1000 بعدد 30، وZeroDivisionError بعدد 10، ثم مشكلات payment gateway timeout for order ord-1029 وord-1028 وما بعدها بحدث واحد لكل منها](https://arabroot.io/content/images/2026/10/error-tracking-05-issues-list.webp)

بعد البصمة: مشكلة واحدة بعدد أحداثها، وتحتها المشكلات المتفرقة من قبل الإصلاح

وقد يتساءل البعض: ماذا نفعل بالمشكلات المتفرقة القديمة؟ والإجابة أن تحددها وتدمجها Merge في مشكلة واحدة من الواجهة، أو تغلقها، فالبصمة الجديدة لا تغير المشكلات السابقة وإنما تؤثر على الأحداث التي تصل بعدها، ولذلك ظهر `ZeroDivisionError` مرتين في القائمة: مرة بالبصمة القديمة ومرة بالجديدة.

### الإصدارات Releases وعودة الخطأ Regression

لنفرض أن الفريق أصلح القسمة على صفر في الإصدار 1.4.2، فنغلق المشكلة 74 من الواجهة أو بالأمر التالي:

```bash
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  "http://127.0.0.1:33800/api/0/organizations/lab/issues/?id=74" -d '{"status":"resolved"}'
```

```text
{"status": "resolved", "status_details": null, "merge": null, "assigned_to": null}
```

ثم ننشر الإصدار 1.4.3 والخطأ ما زال فيه، ونرسل عشرة طلبات بسلة فارغة:

```bash
API_DSN=http://7dd17a64…@obs1-gt-web:8000/1 FIX_GROUPING=1 API_RELEASE=shop-api@1.4.3 docker compose up -d shop-api
docker exec obs1-shop-api python traffic.py checkout
curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:33800/api/0/issues/74/"
```

والحقول المهمة من الرد هي الحالة `status` والعدد `count` وأول إصدار `firstRelease` وآخر إصدار `lastRelease`:

```text
unresolved 20 shop-api@1.4.2 shop-api@1.4.3
```

المشكلة رجعت من `resolved` إلى `unresolved` وحدها، وعددها صار 20، وأول إصدار ظهرت فيه 1.4.2 وآخر إصدار 1.4.3، وهذا ما لا يعطيك إياه السجل: خطأ أغلقته عاد في الإصدار الأخير، ويظهر في أعلى القائمة. ولذلك اجعل قيمة `release` في كل نشر هي رقم الإصدار أو ال Commit نفسه الذي نشرته، فإذا كانت ثابتة أو فارغة فقدت هذه المعلومة كلها.

### البيانات الشخصية في الأحداث

أداة تتبع الأخطاء تأخذ من التطبيق أكثر مما يأخذ السجل: المتغيرات المحلية وجسم الطلب والترويسات، وكل ذلك يخزن على سيرفر آخر. وشرحنا في [دليل السجلات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#never-log) ما لا يدخل السجل أبداً، والقاعدة نفسها هنا. نرسل طلب الدخول الذي يرفع خطأ فيه بريد المستخدم، والخدمة تعمل بالإعداد الافتراضي `send_default_pii=False`:

```bash
docker exec obs1-shop-api python traffic.py login
```

وهذا ما وصل إلى GlitchTip في الحدث، من آخر إطار في ال Stack Trace ومن جسم الطلب:

```text
ValueError: no account for ali@example.com
frame login 60 {"email":"'ali@example.com'","password":"[Filtered]","card_number":"'4111111111111111'"}
request {"data":{"email":"ali@example.com","password":"[Filtered]"}, …}
```

لاحظ أن كلمة المرور حجبت تلقائياً لأن اسمها في القائمة الافتراضية للحجب، أما البريد ورقم البطاقة فوصلا كما هما، في رسالة الخطأ وفي المتغيرات المحلية وفي جسم الطلب، رغم أن `send_default_pii` مغلق. والسبب أن هذا الخيار يتحكم في بيانات المستخدم التي يضيفها ال SDK بنفسه مثل عنوان IP، ولا يعرف أن متغيراً اسمه `card_number` فيه رقم بطاقة. ويوصي [توثيق Sentry للبيانات الحساسة](https://docs.sentry.io/platforms/python/data-management/sensitive-data/?ref=arabroot.io) بأن تحجب البيانات في ال SDK قبل أن تخرج من بيئتك، لأن الحجب في الخادم يمنع التخزين فقط ولا يمنع الإرسال. والإعداد `SCRUB=1` في `app.py` يضيف `email` و`card_number` إلى قائمة الحجب `EventScrubber`، ويستبدل أي بريد في رسالة الاستثناء بـ `[email]` داخل `before_send`:

```bash
API_DSN=http://7dd17a64…@obs1-gt-web:8000/1 FIX_GROUPING=1 SCRUB=1 API_RELEASE=shop-api@1.4.3 docker compose up -d shop-api
docker exec obs1-shop-api python traffic.py login
```

```text
ValueError: no account for [email]
frame {"email":"[Filtered]","password":"[Filtered]","card_number":"[Filtered]"}
request data {"email":"[Filtered]","password":"[Filtered]"}
```

والحدث الجديد نظيف، ولكن لاحظ شيئاً مهماً: الحدث ذهب إلى المشكلة نفسها، وعنوان المشكلة في القائمة بقي `ValueError: no account for ali@example.com` كما رأيت في صورة القائمة أعلاه، لأن العنوان يؤخذ من أول حدث. لذلك فعل الحجب قبل أول حدث يصل من الإنتاج، وإذا وصلت بيانات شخصية قبل ذلك فاحذف المشكلات التي تحملها، ولا يكفي أن تصلح الكود.

### صفحة JavaScript وخرائط المصدر Source Maps

الصفحة `shop-web` فيها زر يعرض مجموع السلة، والواجهة البرمجية ترجع `items: null` للسلة الفارغة، والكود يفترض أنها مصفوفة دائماً. الملف `web/src/cart.js`:

```javascript
// The API returns { items: null } for an empty cart; this code assumes an array.
export function cartTotal(cart) {
  return cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

export function renderCart(cart) {
  const total = cartTotal(cart);
  document.querySelector("#total").textContent = `${total} SAR`;
}
```

والملف `web/src/main.js` يهيئ ال SDK ويربط الزر:

```javascript
import * as Sentry from "@sentry/browser";
import { renderCart } from "./cart.js";

Sentry.init({
  dsn: window.LAB_DSN,
  release: RELEASE,
  environment: "lab",
});

document.querySelector("#load").addEventListener("click", () => {
  renderCart({ items: null });
});
```

والصفحة `web/index.html`، وتضع فيها DSN المشروع `shop-web` الذي طبعه سكربت الإعداد، وهنا العنوان هو `127.0.0.1:33800` لأن المتصفح يعمل خارج Docker:

```html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>shop-web</title>
<p>Total: <span id="total">-</span></p>
<button id="load">Load cart</button>
<script>window.LAB_DSN = "http://PUBLIC_KEY@127.0.0.1:33800/2";</script>
<script src="app.min.js"></script>
</html>
```

والملف `web/package.json` يبني الكود مضغوطاً مع خريطة المصدر، ويثبت رقم الإصدار داخله:

```json
{
  "name": "shop-web",
  "version": "2.0.0",
  "private": true,
  "scripts": {
    "build": "esbuild src/main.js --bundle --minify --sourcemap --target=es2020 --define:RELEASE='\"shop-web@2.0.0\"' --outfile=dist/app.min.js"
  },
  "dependencies": { "@sentry/browser": "11.8.0" },
  "devDependencies": { "esbuild": "0.28.2", "@sentry/cli": "3.8.1" }
}
```

نبني الصفحة وننشر الملف المضغوط وحده، دون الخريطة، كما يحدث في الإنتاج عادة:

```bash
cd web
npm install
npm run build
mkdir -p site && cp index.html dist/app.min.js site/
cd .. && docker compose up -d shop-web
```

```text
  dist\app.min.js      88.4kb
  dist\app.min.js.map   2.0mb

⚡ Done in 14971ms
```

ثم نفتح `http://127.0.0.1:33880/` وننقر الزر ثلاث مرات، والنتيجة في GlitchTip مشكلة واحدة بحدث واحد، وال Stack Trace فيها كما في الصورة التالية:

![الاستثناء TypeError Cannot read properties of null reading reduce في GlitchTip، وال Stack Trace فيه أربعة إطارات كلها في app.min.js عند السطر 15: الدالة qp عند العمود 30455 والدالة oa عند 30509 وHTMLButtonElement.anonymous عند 30701 وHTMLButtonElement.r عند 14298، وتحته Breadcrumbs فيها ui.click على body ثم button#load](https://arabroot.io/content/images/2026/10/error-tracking-07-js-minified.webp)

بدون خريطة المصدر: أسماء دوال مختصرة وأعمدة في سطر واحد طوله 88 KB

ولاحظ أمرين: الأول أن النقرات الثلاث أنتجت حدثاً واحداً، لأن [تكامل Dedupe](https://docs.sentry.io/platforms/javascript/configuration/integrations/dedupe/?ref=arabroot.io) مفعل افتراضياً في SDK المتصفح ويحذف الحدث المكرر الذي له ال Stack Trace نفسه، فلا تحسب عدد الأحداث عدداً للنقرات. والثاني أن ال Stack Trace لا يفيد المطور بشيء، فـ `qp` عند العمود 30455 لا تعني أي شيء، وهنا يأتي دور خريطة المصدر. والصورة التالية تبين الطريقة:

![أربع خطوات من اليمين: البناء Build بالأمر esbuild minify sourcemap ينتج app.min.js بحجم 88 KB وapp.min.js.map بحجم 2 MB، ثم المعرف والرفع بالأمرين sourcemaps inject وdebug id 8953a275 ثم sourcemaps upload والخريطة إلى الخادم فقط، ثم الخطأ في المتصفح app.min.js:15:30455 في qp مع debug id في الحدث، ثم GlitchTip يترجم إلى src/cart.js:3 في cartTotal مع سطور الكود حوله، وفي الأسفل السطر قبل وبعد](https://arabroot.io/content/images/2026/10/error-tracking-06-sourcemaps.webp)

المعرف Debug ID يربط الملف المنشور بخريطته على الخادم

الطريقة الحالية التي يشرحها [توثيق Sentry لخرائط المصدر](https://docs.sentry.io/platforms/javascript/sourcemaps/?ref=arabroot.io) تعتمد على معرف Debug ID: أداة `sentry-cli` تضيف المعرف نفسه إلى الملف المضغوط وإلى خريطته، ثم ترفع الخريطة إلى الخادم، والمتصفح يرسل المعرف مع كل خطأ فيعرف الخادم أي خريطة يستخدم، دون أن تنشر الخريطة للعامة. وGlitchTip يقبل هذه الطريقة، وتوثيقه يذكر [أداته glitchtip-cli](https://glitchtip.com/documentation/cli?ref=arabroot.io) للغرض نفسه، أما نحن فاستخدمنا `sentry-cli` 3.8.1 كما هي مع عنوان GlitchTip:

```bash
cd web
export SENTRY_URL=http://127.0.0.1:33800 SENTRY_AUTH_TOKEN=$TOKEN SENTRY_ORG=lab SENTRY_PROJECT=shop-web
npx sentry-cli sourcemaps inject dist
npx sentry-cli sourcemaps upload --release shop-web@2.0.0 dist
```

```text
Source Map Debug ID Injection Report
  Modified: The following source files have been modified to have debug ids
    8953a275-21f2-524a-a04a-fa012b0608ed - dist\app.min.js
  Modified: The following sourcemap files have been modified to have debug ids
    8953a275-21f2-524a-a04a-fa012b0608ed - dist\app.min.js.map
…
> Organization: lab
> Projects: shop-web
> Release: shop-web@2.0.0
> Upload type: artifact bundle

Source Map Upload Report
  Scripts
    ~/app.min.js (sourcemap at app.min.js.map, debug id 8953a275-21f2-524a-a04a-fa012b0608ed)
  Source Maps
    ~/app.min.js.map (debug id 8953a275-21f2-524a-a04a-fa012b0608ed)
```

لاحظ أن `inject` عدل الملف `app.min.js` نفسه، لذلك يجب أن تنشر الملف بعد هذه الخطوة وليس قبلها، وإلا وصل الخطأ بدون معرف ولم يجد الخادم خريطته. نعيد نسخ الملف إلى `site/` وننقر الزر مرة واحدة:

```bash
cp dist/app.min.js site/
```

![الاستثناء نفسه في GlitchTip بعد رفع الخريطة، وأول إطار فيه ../src/cart.js في cartTotal عند السطر 3 العمود 15 مع الكود الأصلي من cart.js وسطر return cart.items.reduce مظللاً، ثم إطار renderCart عند السطر 7، ثم src/main.js عند السطر 10، ثم إطار sentryWrapped من مكتبة Sentry](https://arabroot.io/content/images/2026/10/error-tracking-08-js-sourcemap.webp)

مع خريطة المصدر: الملف الأصلي والدالة والسطر والكود حوله

والآن يقول الخطأ بوضوح إن `cart.items.reduce` في السطر 3 من `cart.js` فشلت، ومن السطر الأول في الملف نفسه يعرف المطور السبب. ولاحظ أن الحدث الجديد صار مشكلة جديدة منفصلة عن الأولى، لأن المكان Culprit تغير بعد الترجمة، لذلك ارفع الخرائط في خط النشر Pipeline نفسه مع كل إصدار، حتى لا تنقسم المشكلة الواحدة بين نسخة مترجمة وأخرى غير مترجمة.

### التنبيه عند تكرار الخطأ

التنبيه في GlitchTip قاعدة على مستوى المشروع: إذا وصل إلى مشكلة واحدة عدد معين من الأحداث خلال عدد معين من الدقائق، يرسل إشعاراً إلى البريد أو إلى Webhook مثل Slack وDiscord. ننشئ قاعدة تنبه عند 10 أحداث في دقيقة، وترسل البريد إلى أعضاء الفريق:

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  http://127.0.0.1:33800/api/0/projects/lab/shop-api/alerts/ \
  -d '{"name":"shop-api errors","timespanMinutes":1,"quantity":10,"uptime":false,"alertRecipients":[{"recipientType":"email","url":""}]}'
```

```text
{"alertRecipients": [{"id": 1, "recipientType": "email", "url": "", "config": {}, "tagsToAdd": []}], "id": 1, "name": "shop-api errors", "timespanMinutes": 1, "quantity": 10, "uptime": false}
```

ثم نرسل الأربعين طلباً إلى `/price` وننتظر، فيظهر البريد في سجل GlitchTip لأننا استخدمنا `consolemail://`:

```bash
docker exec obs1-shop-api python traffic.py prices
docker logs obs1-gt-web 2>&1 | grep -A27 "Subject:"
```

```text
Subject: Error in shop-api: KeyError: &#x27;item-0&#x27;
From: glitchtip@example.com
To: lab@example.com
Date: Fri, 23 Oct 2026 00:25:21 +0000
…
X-GlitchTip-Organization: lab
X-GlitchTip-Project: shop-api

Issue in shop-api

KeyError: &#x27;item-0&#x27; http://127.0.0.1:33800/lab/issues/72

View project shop-api at http://127.0.0.1:33800/lab/issues?project=1
```

في المخرج أعلاه لاحظ التالي:

- البريد عن مشكلة واحدة وليس عن أربعين حدثاً، والرابط فيه مبني من `GLITCHTIP_DOMAIN`.
- التنبيه لا يتكرر للمشكلة نفسها في القاعدة نفسها، لأن المهمة التي تفحص القواعد كل دقيقة تستبعد المشكلات التي أرسل لها إشعار من قبل، كما في [كود التنبيهات](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/apps/alerts/tasks.py?ref=arabroot.io)، فإذا أردت أن تعرف أن المشكلة ما زالت مستمرة فهذا عمل لوحة المقاييس وتنبيهاتها كما شرحنا في مقال مفاهيم المراقبة من هذه المرحلة.
- عنوان البريد ظهر بعلامات الاقتباس مرمزة `&#x27;` في نص الرسالة، وهذا ما ستراه في عميل البريد أيضاً مع الأخطاء التي فيها علامة اقتباس.
- البريد لا يصل إذا لم يكن المستخدم عضواً في فريق مرتبط بالمشروع، فإذا أنشأت القاعدة ولم يصل شيء فابدأ بالتحقق من الفريق.

💡

إذا أردت التنبيه إلى Webhook داخل شبكتك، مثل خدمة على `192.168.x.x` أو Container في الشبكة نفسها، فسوف يرفض GlitchTip العنوان بالرسالة `URLs targeting private or internal IPs are not allowed`، وهذه حماية من استغلال الخادم لطلب عناوين داخلية Server-Side Request Forgery. والمتغير `GLITCHTIP_ALLOW_PRIVATE_IPS` في [ملف الإعدادات](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/glitchtip/settings.py?ref=arabroot.io) يسمح بذلك، فلا تفعله إلا إذا كنت تحتاجه فعلاً وتعرف من يستطيع إنشاء القواعد.

### إيقاف التجربة

```bash
docker compose down -v
docker rmi obs1-shop-api:1
```

## قبل الإنتاج

ما سبق يعمل على جهازك، وقبل أن تضع GlitchTip أمام فريقك تحتاج النقاط التالية:

- **النطاق والتشفير TLS:** ضع GlitchTip خلف Reverse Proxy على نطاق مثل `errors.example.com` بشهادة TLS، واجعل `GLITCHTIP_DOMAIN` هو هذا العنوان مع `https://`، لأن SDK المتصفح سوف يرسل الأحداث إليه من أجهزة العملاء.
- **النسخ الاحتياطي Backup:** كل البيانات في PostgreSQL، والملفات المرفوعة مثل خرائط المصدر في ال Volume `/code/uploads`، فخذ نسخة من الاثنين، مثلاً بالأمر `docker exec obs1-gt-db pg_dump -U postgres -Fc postgres > glitchtip.dump` مع نسخة من ال Volume.
- **التحديث Upgrade:** ثبت رقم الإصدار في ال Image كما فعلنا، واقرأ ملاحظات الإصدار، وخذ نسخة احتياطية قبل تغيير الرقم، لأن GlitchTip في وضع all\_in\_one يطبق تعديلات قاعدة البيانات Migrations عند التشغيل، ولا رجوع عنها إلا من النسخة الاحتياطية.
- **مدة الاحتفاظ وحجم القاعدة:** صفحة التثبيت تذكر أن مليون حدث في الشهر قد يحتاج 30 GB من القرص، وفي تجربتنا كانت القاعدة 22 MB بعد نحو 250 حدثاً، فراقب حجمها، ومدة الاحتفاظ يحددها المتغير `GLITCHTIP_RETENTION_DAYS` وقيمته الافتراضية 90 يوماً في [ملف الإعدادات](https://gitlab.com/glitchtip/glitchtip-backend/-/blob/master/glitchtip/settings.py?ref=arabroot.io).
- **البريد:** استخدم عنوان SMTP حقيقياً، واختبر وصول التنبيه قبل أن تعتمد عليه.

## أخطاء شائعة

- **إرسال كل شيء:** لا تقم بإرسال الأخطاء المتوقعة، مثل 404 أو فشل التحقق من مدخلات المستخدم، إلى أداة تتبع الأخطاء، والسبب أنها تملأ القائمة بما لا يحتاج إصلاحاً وتخفي الأخطاء الحقيقية، واستخدم `before_send` لاستبعادها كما في [توثيق التصفية](https://docs.sentry.io/platforms/python/configuration/filtering/?ref=arabroot.io).
- **بدون release:** عندها لا تعرف في أي إصدار ظهر الخطأ، ولا تعمل عودة المشكلة بعد إغلاقها بالشكل الصحيح، ولا تعمل خرائط المصدر المرتبطة بالإصدار.
- **القيم المتغيرة في رسالة الخطأ:** كما رأينا، أربعون مشكلة بدل واحدة، والحل بصمة ثابتة أو قيم في حقول مستقلة.
- **نشر خرائط المصدر للعامة:** الخريطة تحتوي على الكود الأصلي كاملاً بتعليقاته، لذلك ارفعها إلى الأداة واحذفها من الملفات المنشورة.
- **الثقة في send\_default\_pii وحده:** رأينا البريد ورقم البطاقة يصلان معه، لذلك أضف قائمة الحجب الخاصة بتطبيقك.
- **الاعتماد على الأداة بدل السجلات:** أداة تتبع الأخطاء ترى الأخطاء فقط، والسجلات والمقاييس ترى الصورة كاملة.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- أداة تتبع الأخطاء تجمع الاستثناءات المتشابهة في مشكلة واحدة، مع ال Stack Trace والكود والسياق والإصدار، وتنبهك عند مشكلة جديدة أو عائدة، وهذا ما لا يفعله السجل.
- Sentry المستضاف ذاتياً أكمل الميزات بترخيص FSL ومتطلبات تبدأ من 16 GB RAM، وGlitchTip بترخيص MIT يعمل بثلاث Containers وأقل من نصف GB في تجربتنا، ويقبل SDK الخاص بـ Sentry نفسه، فالانتقال بينهما تغيير DSN فقط.
- GlitchTip يجمع بالعنوان، فالقيمة المتغيرة في رسالة الخطأ حولت ثلاثة أخطاء إلى 71 مشكلة، والبصمة Fingerprint في `before_send` أعادتها إلى ثلاث.
- اجعل `release` رقم الإصدار الحقيقي، وارفع خرائط المصدر مع كل نشر بعد خطوة `inject`، وفعل الحجب قبل أول حدث من الإنتاج.

[![](https://arabroot.io/content/images/2026/10/observability-intro-cover-cover.webp)دليلمفاهيم المراقبة Observability: من الإشارة إلى التنبيهما الذي تجيب عنه المقاييس والسجلات والتتبع، وكيف تبني تنبيهاً على ما يشعر به المستخدم وعلى ميزانية الأخطاء بدل كل رقم.](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D9%85%D8%B1%D8%A7%D9%82%D8%A8%D8%A9-observability-%D9%85%D9%86-%D8%A7%D9%84%D8%A7%D8%B4%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%89-%D8%A7%D9%84%D8%AA%D9%86%D8%A8%D9%8A%D9%87/)[![](https://arabroot.io/content/images/2026/10/logging-basics-cover-cover.webp)دليلالسجلات Logging للمطورين: ماذا تسجل وماذا لا تسجلكيف تكتب سجلات Logs تساعدك على فهم ما حدث في الإنتاج، وما الذي لا يدخل السجل أبداً، وكيف تجمعها في مكان واحد.](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/logs-that-help-cover-cover.webp)دليلسجلات تساعدك فعلاً: من «حدث خطأ» إلى سبب المشكلةكيف تكتب سجلات تجيب عن أسئلة الحادثة، بتحقيق في عطل حقيقي بسجلات سيئة ثم بحدث واسع Wide Event لكل طلب، مع قياس الفرق.](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-%D8%AA%D8%B3%D8%A7%D8%B9%D8%AF%D9%83-%D9%81%D8%B9%D9%84%D8%A7-%D9%85%D9%86-%D8%AD%D8%AF%D8%AB-%D8%AE%D8%B7%D8%A7-%D8%A7%D9%84%D9%89-%D8%B3%D8%A8%D8%A8-%D8%A7%D9%84%D9%85%D8%B4%D9%83%D9%84%D8%A9/)[![](https://arabroot.io/content/images/2026/10/distributed-tracing-cover-cover.webp)دليلالتتبع الموزع Distributed Tracing مع OpenTelemetryكيف ترى طلباً واحداً يمر بعدة خدمات في شجرة زمنية واحدة، فتعرف أين أبطأ وأين فشل دون أن تخمن من السجلات.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%AA%D8%A8%D8%B9-%D8%A7%D9%84%D9%85%D9%88%D8%B2%D8%B9-distributed-tracing-%D9%85%D8%B9-opentelemetry/)[![](https://arabroot.io/content/images/2026/10/error-handling-cover-cover.webp)دليلكيف تتعامل لغات البرمجة مع الأخطاء؟لماذا ترمي Java وPython الاستثناءات بينما ترجع Go وRust الخطأ كقيمة، وكيف تمنع الخطأ من أن يختفي بصمت في كودك.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AA%D8%B9%D8%A7%D9%85%D9%84-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%85%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%B7%D8%A7%D8%A1/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال واختباره على GlitchTip 6.2.6 مع PostgreSQL 18.6 وValkey 9.0.6، وخدمة Python 3.13.17 مع Flask 3.1.3 وsentry-sdk 2.72.0، وصفحة مبنية بـ esbuild 0.28.2 مع @sentry/browser 11.8.0، ورفع خرائط المصدر بـ sentry-cli 3.8.1.
- أكتوبر 2026: إضافة مقارنة الخدمات المستضافة Sentry وRollbar وBugsnag وHoneybadger وRaygun بأسعارها ومدد الحفظ ومناطق البيانات كما ظهرت في صفحاتها الرسمية في 23 أكتوبر 2026.