مستوى المقال: متوسط. يفترض إنك تعرف Node.js و Express على مستوى أساسي، وتقدر تقرأ كود JavaScript بسيط.
هدف المقال واضح: تفصل النشر (deploy) عن الإطلاق (release). يعني تحط الكود الجديد على الإنتاج وهو مقفول، وتفتحه لـ 5% من المستخدمين بس، ولو ظبط تكمّل لحد 100%، ولو باظ تطفيه في أقل من ثانية من غير ما تعمل rollback أو redeploy. هنبني الأداة دي بنفسنا في أقل من 40 سطر، من غير أي مكتبة خارجية.
المشكلة باختصار
الطريقة الشائعة: تدمج الميزة الجديدة، تعمل deploy، وفي نفس اللحظة كل المستخدمين بيشوفوها. الطريقة دي بتفشل وقت الضغط: لو فيه باج ظهر بس عند 50 ألف مستخدم في الساعة، خيارك الوحيد هو rollback كامل — بناء جديد، نشر جديد، و10 دقايق الخدمة فيها مشكلة. المشكلة إنك ربطت "الكود موجود على السيرفر" بـ "كل الناس بتستخدمه"، وهما لازم يكونوا حاجتين منفصلتين.
يعني إيه Feature Flag؟ (مثال بسيط الأول)
تخيّل مفتاح النور في الصالة، بس مش مفتاح عادي. ده مفتاح بيقدر يوزّع النور بالتقسيط: يضيء لـ 5 ضيوف بس من أصل 100 في الأوضة، والباقي لسه قاعدين في نص الإضاءة القديمة. لو الـ 5 عجبهم، تلفّ المفتاح شوية كمان فيوصل لـ 25، بعدين 50، لحد ما الكل يشوف النور الجديد. ولو حد اشتكى، ترجّع المفتاح لصفر في ثانية والكل يرجع زي الأول.
ده بالظبط الـ Feature Flag. تقنيًا هو شرط منطقي (boolean) بيتقيّم وقت التشغيل لكل طلب، وبيقرّر أي مسار كود يشتغل: الجديد ولا القديم. المفتاح مش مكتوب في الكود ثابت، هو بيتقرا من مكان تقدر تغيّره من بره (ملف أو Redis)، فتقدر تبدّل السلوك من غير ما تلمس الكود.
الخطوات: نبني المُقيّم (evaluator)
محتاجين حاجتين: مصدر للـ flags (نبدأ بملف بسيط)، ودالة تقرّر لكل مستخدم هو داخل الطرح ولا لأ. السر في "التوزيع الثابت": لازم نفس المستخدم ياخد نفس القرار كل مرة، عشان ميشوفش الميزة تظهر وتختفي مع كل refresh.
- عرّف الـ flags مع نسبة الطرح ومفتاح الإيقاف (kill).
- حوّل معرّف المستخدم لرقم ثابت بين 0 و100 باستخدام hash.
- قارن الرقم بنسبة الطرح: أقل منها يعني "داخل".
- احترم مفتاح الـ kill قبل أي حاجة تانية.
// flags.js — صفر اعتماديات خارجية
const crypto = require('crypto');
// المصدر: ابدأ بكائن بسيط، وبعدين انقله لملف JSON أو Redis
const flags = {
new_checkout: { enabled: true, rollout: 5, kill: false },
};
// توزيع ثابت: نفس (flag + user) بيدّي دايمًا نفس الرقم 0..100
function bucket(flagKey, userId) {
const h = crypto.createHash('sha256')
.update(`${flagKey}:${userId}`)
.digest();
return (h.readUInt32BE(0) / 0xffffffff) * 100;
}
function isEnabled(flagKey, userId) {
const f = flags[flagKey];
if (!f || f.kill || !f.enabled) return false; // مفتاح الطوارئ أولًا
if (f.rollout >= 100) return true;
return bucket(flagKey, userId) < f.rollout; // داخل الطرح؟
}
module.exports = { flags, isEnabled };
ليه الـ hash مش Math.random()؟ لأن random بيدّي قرار مختلف كل نداء، فالمستخدم هيتنطط بين الجديد والقديم. الـ hash بيثبّت القرار على مستوى المستخدم، وده اللي بيحصل فعلاً في أدوات زي LaunchDarkly و OpenFeature.
ربطه بالطلب في Express
دلوقتي كل طلب بيتفرّع لمسارين حسب قرار المفتاح: الكود الجديد لنسبة صغيرة، والقديم للباقي — من نفس النشر.
const express = require('express');
const { isEnabled } = require('./flags');
const app = express();
app.get('/checkout', (req, res) => {
const userId = req.user?.id ?? req.ip; // ثابت لكل مستخدم
if (isEnabled('new_checkout', userId)) {
return res.json({ version: 'new', total: newCheckout(req) });
}
return res.json({ version: 'old', total: oldCheckout(req) });
});
app.listen(3000);
مفتاح الإيقاف (Kill Switch) من غير redeploy
عشان تقفل الميزة فورًا، اقرا الـ flags من ملف JSON بتراقبه، أو من Redis. كده تغيّر kill: true في المصدر، والخدمة تلتقطه في أقل من ثانية بدون إعادة نشر:
const fs = require('fs');
// أعد تحميل الإعدادات كل ما الملف يتغيّر
fs.watch('./flags.json', () => {
try {
const next = JSON.parse(fs.readFileSync('./flags.json', 'utf8'));
Object.assign(flags, next); // تحديث حي بدون restart
} catch (_) { /* تجاهل ملف نص نصّه */ }
});
سيناريو واقعي بالأرقام
افترض متجر بـ 50 ألف طلب/يوم، وعايز تطلّع صفحة checkout جديدة. بدل الإطلاق الكامل، تبدأ بـ rollout: 5 (حوالي 2500 طلب فقط يشوفوا الجديد). تراقب نسبة أخطاء 5xx وزمن الاستجابة على المجموعتين. لو الأخطاء فضلت تحت 2%، ترفع النسبة على 8 خطوات: 5 ← 10 ← 25 ← 50 ← 75 ← 100 خلال يوم أو اتنين. لو قفزت الأخطاء فوق الحد، kill: true بيرجّع الـ 50 ألف كلهم للمسار القديم في أقل من ثانية — من غير بناء ولا نشر.
الـ trade-offs وما يجب الانتباه له
كل flag بيضيف مسارين للكود، يعني بيضاعف مساحة الاختبار. لو عندك 10 flags مفتوحة في نفس الوقت، نظريًا عندك 1024 تركيبة سلوك محتملة. الـ trade-off هنا: بتكسب أمان الطرح التدريجي، بتخسر بساطة الكود وسهولة الاختبار. القاعدة: الـ flag ده مؤقت. بعد ما توصل 100% وتستقر أسبوع، امسح المفتاح والمسار القديم. الـ flags المنسية (flag debt) بتتحول لألغام في الكود.
الافتراض اللي المقال مبني عليه: عندك خدمة واحدة أو خدمات قليلة، والـ flags بتتقيّم داخل العملية (in-process). لو عندك عشرات الخدمات محتاجة تشوف نفس المفتاح لحظيًا، هتحتاج مصدر مركزي (Redis أو أداة تدعم معيار OpenFeature) بدل ملف محلي.
متى لا تستخدم هذه الطريقة
لو مشروعك صغير، مستخدمينه قليلين، والنشر عندك رخيص وسريع (ثواني)، الـ Feature Flags بتضيف تعقيد من غير عائد حقيقي — الـ rollback العادي كفاية. كمان متستخدمش flag لإخفاء إعدادات حساسة أو صلاحيات أمان؛ دي مسؤولية طبقة الأذونات مش المفاتيح. الـ flags للطرح التدريجي والتجارب، مش للتحكم في الوصول.
التحقق من أنه يعمل
اختبر التوزيع الثابت بنفسك: نادِ isEnabled('new_checkout', 'user-123') ألف مرة لنفس المستخدم — لازم ترجّع نفس النتيجة كل مرة. بعدين جرّب 10 آلاف مستخدم مختلف واحسب كام واحد رجع true: المفروض قريب من 5% (بين 4.7% و5.3% تقريبًا). لو النسبة بعيدة، غالبًا الـ hash مش متوزّع صح أو بتستخدم مصدر متغيّر للـ userId.
الخطوة التالية
افتح أقرب endpoint فيه ميزة قدامك، لفّها بـ isEnabled وابدأها بـ rollout: 1. راقب أخطاء الـ 5xx لمدة ساعة على المسار الجديد. لو فضلت نضيفة، اطلع لـ 10%. لو لأ، جرّب مفتاح الـ kill وشوفه بيرجّع الكل للمسار القديم في كام مللي ثانية بالظبط.
المصادر
- Martin Fowler — Feature Toggles (Pete Hodgson): martinfowler.com/articles/feature-toggles.html — تصنيف أنواع المفاتيح ومشكلة flag debt.
- OpenFeature (معيار مفتوح للـ feature flags): openfeature.dev — مصدر مركزي وموحّد للمفاتيح عبر الخدمات.
- Node.js Crypto (دالة الـ hash المستخدمة): nodejs.org/api/crypto.html.
- LaunchDarkly — Percentage rollouts & bucketing: docs.launchdarkly.com/home/flags/rollouts — منطق التوزيع الثابت بالـ hash.