بعد أن يطلب العميل نسخة تجريبية ويبدأ الاستخدام، يبقى السؤال الذي لا تجيب عنه أرقام الزيارات ولا نقاط التقييم: لماذا؟ لماذا رشح منصتك لزميله، ولماذا توقف عند الخطوة الثالثة، ومن أين عرف عنها أصلاً؟ ولا يجيب عن ذلك إلا العميل نفسه، والطريقة العملية لسؤاله استبيان قصير في الوقت المناسب. وفي دليل تثبيت Formbricks شرحنا التثبيت والاستبيان العربي بالرابط، وفي هذا الدليل نكمل من حيث توقفنا باستخدام تسويقي: استبيان رضا NPS يحمل مصدر العميل في حقول مخفية، واستبيان داخل التطبيق يظهر بعد خطوة محددة، ثم قراءة النتائج من اللوحة ومن الـ API.
والمقالات المرتبطة في السلسلة:
دليلFormbricks: استبيانات ونماذج حديثة على سيرفرككيف تثبت Formbricks وتنشئ استبياناً عربياً بالرابط أو داخل تطبيقك، وتبقى الإجابات في قاعدة بياناتك على سيرفرك.
دليلأتمتة التسويق Marketing Automation وتقييم العملاء Lead Scoringكيف تتابع كل عميل محتمل في وقته بقواعد من محفز وشرط وإجراء، وكيف تقيمه بالنقاط لكي يبدأ فريق المبيعات بالأقرب للشراء.
دليلMautic على سيرفرك: حملة مؤتمتة من النموذج إلى البريدنشغل Mautic في Docker ونبني نموذجاً عربياً ونقاطاً للتقييم وحملة ترسل الترحيب وتنبه المبيعات، ونختبرها بزائرين تجريبيين.
دليلمنظومة تسويق كاملة على سيرفرك: من الرابط إلى العميلنجمع Umami وShlink وMautic في ملف Compose واحد، ونتتبع عميلاً من رابط مختصر في منشور حتى يظهر تحويله ومصدره في التقارير.وفي هذا الدليل سوف نقوم بما يلي:
- نشغل Formbricks 6.1.1 بالإصدارات المثبتة، ونرى ما الذي تغير في ملف Compose منذ الإصدار 6.0.
- ننشئ استبياناً عربياً بسؤال NPS وسؤال مفتوح وسؤال عن مصدر العميل، ونمرر له مصدر الزيارة
utm_sourceفي حقل مخفي Hidden Field. - نضع استبياناً داخل صفحة تطبيق تجريبية، يظهر عندما يرفع المستخدم أول فاتورة.
- نقرأ الإجابات من لوحة الملخص، ثم نحسب NPS لكل مصدر بسكربت صغير على الـ API.
- نختبر ما يحتاج ترخيصاً تجارياً في النسخة المجتمعية، مثل التعرف على المستخدم.
والصورة التالية تبين الطريقتين:

المتطلبات
- Formbricks مثبت كما في دليل التثبيت، وقد أعدنا التثبيت هنا على الإصدار 6.1.1 بنفس الخطوات.
- ذاكرة: بعد التجربة كانت الخدمات تستهلك حوالي 1.16 جيجابايت، منها 761 ميجابايت للتطبيق و207 لقاعدة البيانات و100 لخدمة cube، فلا تقم بتشغيله على سيرفر بذاكرة أقل من 2 جيجابايت.
- مساحة: الـ Image الخاصة بالتطبيق وحدها 2.6 جيجابايت.
- عنوان HTTPS عام للتشغيل الفعلي، لأن الموقع الذي يعمل على HTTPS لن يحمل سكربت الاستبيان من عنوان HTTP.
التثبيت على الإصدار 6.1.1
نزلنا ملفات الإصدار نفسه من المستودع كما في دليل التثبيت، مع تغيير رقم الإصدار:
V=6.1.1
RAW=https://raw.githubusercontent.com/formbricks/formbricks/$V/docker
curl -fsSLO $RAW/docker-compose.yml
curl -fsSLO $RAW/authzed-postgres-bootstrap.sh
mkdir -p cube/schema
curl -fsSL -o cube/cube.js $RAW/cube/cube.js
curl -fsSL -o cube/schema/FeedbackRecords.js $RAW/cube/schema/FeedbackRecords.jsثم ثبتنا الإصدارات ووضعنا العنوان والأسرار:
sed -i 's|image: pgvector/pgvector:pg18|image: pgvector/pgvector:0.8.7-pg18|; s|ghcr.io/formbricks/formbricks:latest|ghcr.io/formbricks/formbricks:6.1.1|; s|ghcr.io/formbricks/hub${HUB_IMAGE_REF:-:latest}|ghcr.io/formbricks/hub:0.14.1|; s|- 3000:3000|- "127.0.0.1:18801:3000"|' docker-compose.yml
sed -i '/^ WEBAPP_URL:$/s|WEBAPP_URL:.*|WEBAPP_URL: "https://forms.example.com"|; /^ BETTER_AUTH_URL:$/s|BETTER_AUTH_URL:.*|BETTER_AUTH_URL: "https://forms.example.com"|' docker-compose.yml
for k in BETTER_AUTH_SECRET ENCRYPTION_KEY CRON_SECRET; do
v=$(openssl rand -hex 32); sed -i "/^ $k:\$/s/$k:.*/$k: $v/" docker-compose.yml
doneوملف .env هو نفسه في دليل التثبيت، ومعه المتغيران AUTHZED_ENDPOINT=spicedb:50051 وAUTHZED_SYSTEM_KEY=formbricks من البداية لكي لا تتوقف خدمة الترحيل، ثم:
docker compose -p mke-fb up -dولاحظ ما تغير في ملف Compose بين 6.0 و6.1:
- أصبح اسم سر المصادقة
BETTER_AUTH_SECRETوعنوانهاBETTER_AUTH_URL، وبقيNEXTAUTH_SECRETوNEXTAUTH_URLكأسماء قديمة للتثبيتات السابقة، فإذا كنت تحدث تثبيتاً قديماً فلا تغير قيمة السر القديم. - خدمة
hubأصبحت على الإصدار 0.14.1 بدل 0.8.7، ومعها خدمة جديدةhub-worker، وقد تأكدنا أنlatestيشير إلى 0.14.1 بمقارنة البصمة Digest في سجل ghcr.io. - ظهرت خدمتان اختياريتان
taxonomyوvllmتحت ملفات تعريف Profiles، أي أنهما لا تعملان إلا إذا طلبتهما، ولم نشغلهما.
وبدأت كل الخدمات من أول مرة، ثم أنشأنا الحساب الأول والمنظمة من المتصفح، وتخطينا دعوة الأعضاء.
العربية لغة افتراضية قبل أي استبيان
من Settings ثم Survey Languages أضفنا Arabic (Saudi Arabia) بالرمز ar-SA وجعلناها اللغة الافتراضية، كما في توثيق تعدد اللغات. ثم أنشأنا الاستبيانين من Management API، ولكن الـ API رفض إرسال اللغة مع الاستبيان بهذا الخطأ:
'languages.0.language.createdAt': 'Invalid input: expected date, received string'والسبب أن الـ API ينتظر كائن تاريخ Date في حقل لا يمكن كتابته في JSON إلا كنص، لذلك أنشأنا الاستبيان دون لغة، ثم فتحناه في المحرر، ومن تبويب Language فعلنا Activate translations واخترنا العربية لغة افتراضية:

وقبل هذه الخطوة كان الاستبيان يظهر من اليمين إلى اليسار لأن النص عربي، وهذا تغير عن الإصدار 6.0 الذي وجدناه في دليل التثبيت من اليسار إلى اليمين، ولكن الصفحة بقيت lang="en-US" وكلمة Required بالإنجليزية. ولاحظ أيضاً أن المحرر يحذرك قبل التأكيد: اللغة الافتراضية للاستبيان لا تتغير بعد ذلك إلا بإيقاف التعدد وحذف كل الترجمات، فاخترها مرة واحدة بانتباه.
استبيان NPS برابط ومعه مصدر العميل
الاستبيان الأول ثلاث خطوات: سؤال NPS من 0 إلى 10، ثم «ما السبب الرئيسي لتقييمك؟» اختياري، ثم «من أين عرفت عن منصة الفواتير؟» باختيار واحد. وهذا جزء من ملف الاستبيان الذي أرسلناه إلى الـ API، والملف كاملاً في حزمة التجربة:
{
"name": "رأيك في النسخة التجريبية", "type": "link", "status": "inProgress",
"hiddenFields": {"enabled": true, "fieldIds": ["utm_source", "plan"]},
"isCaptureIpEnabled": false,
"blocks": [
{"id": "b1", "elements": [{"id": "nps", "type": "nps", "headline": {"default": "ما احتمال أن ترشح منصة الفواتير لزميل في العمل؟"},
"lowerLabel": {"default": "غير محتمل أبداً"}, "upperLabel": {"default": "محتمل جداً"}, "required": true}],
"buttonLabel": {"default": "التالي"}}
]
}curl -s -X POST https://forms.example.com/api/v1/management/surveys -H "x-api-key: $FB_API_KEY" \
-H 'Content-Type: application/json' --data-binary @survey-link.jsonوظهرت لنا أثناء الإنشاء ثلاث قواعد في الـ API:
- إما
questionsوإماblocks، والإصدار 6 يستخدم الكتل Blocks، فإذا أرسلت الاثنين رفض الطلب برسالةSurvey cannot have both questions and blocks. - بعض المعرفات محجوزة، فقد سمينا سؤال المصدر
sourceفرفض برسالةQuestion id is not allowed، فغيرناه إلىheard_from. - مفتاح الـ API في الإصدار 6 مربوط بمساحة عمل وصلاحية Read أو Write أو Manage، فأنشئ مفتاحاً بصلاحية Write لمساحة العمل التي تحتاجها فقط، ولاحظ أن المفتاح يظهر مرة واحدة عند الإنشاء.
والحقول المخفية utm_source وplan لا يراها المشارك، وتمتلئ من الرابط نفسه كما في توثيق الحقول المخفية، فالرابط الذي تضعه في رسالة لعملاء جاؤوا من X يكون هكذا:
https://forms.example.com/s/cmvnv5v7m000c01nyygxnzeed?utm_source=x&plan=trial
ولاحظ أن المقياس انقلب مع الاتجاه، فالصفر على اليمين، وهذا ما يتوقعه القارئ العربي. ثم سؤال المصدر بأزرار اختيار على يمين النص:

وأجبنا بثلاثة مشاركين من روابط مختلفة، ورابع فتح الاستبيان واختار رقماً ثم أغلق الصفحة دون أن يضغط التالي، وهذه الإجابات كما حفظت في قاعدة البيانات:
finished | language | data
----------+----------+------------------------------------------------------------------------------------------------
t | ar-SA | {"nps": 9, "why": "رفع الفواتير أسرع من النظام القديم، وأحتاج ربطاً مع Shopify", "plan": "trial", "heard_from": "منشور في X", "utm_source": "x"}
t | ar-SA | {"nps": 6, "why": "جيدة لكن التقارير بالإنجليزية فقط", "plan": "trial", "heard_from": "منشور في LinkedIn", "utm_source": "linkedin"}
t | ar-SA | {"nps": 10, "plan": "trial", "heard_from": "زميل أو صديق", "utm_source": "x"}في المخرج أعلاه لاحظ التالي:
- المشارك الرابع غير موجود، لأن Formbricks ينشئ الإجابة عند الضغط على التالي في أول خطوة، فهو في الإحصاءات مشاهدة Impression دون بداية.
- الحقول المخفية محفوظة مع الإجابة نفسها، أي أن كل رأي مرتبط بالمصدر الذي جاء منه صاحبه دون أن يسأل عنه.
- المشارك الثالث جاء من رابط X، ولكنه قال إنه عرف عنا من زميل، وكلا المعلومتين صحيح: الزميل أرسل له المنشور. وهذا هو السبب في أن تجمع المصدر المتتبع ومصدر العميل كما يقوله هو، فالأول يقول من أين ضغط، والثاني من أين اقتنع.
- الخيار محفوظ بنصه العربي وليس بمعرفه، فإذا غيرت نص الخيار بعد بدء الاستبيان أصبح لنفس الخيار قيمتان في التحليل، لذلك ثبت النصوص قبل النشر.
استبيان داخل التطبيق بعد أول فاتورة
الاستبيان الثاني يسأل سؤال سهولة Customer Effort بخمسة وجوه، ثم «ما الذي أخرك أو أربكك؟»، ويظهر في لحظة واحدة فقط: بعد أن يرفع المستخدم أول فاتورة. ولكي يحدث ذلك نحتاج ثلاثة أشياء، كما في دليل البداية للاستبيانات داخل التطبيق:
- فعل Action من نوع Code بالمفتاح
first-invoice-uploaded، أنشأناه من الـ API، والأنواع الأخرى في توثيق الأفعال. - استبيان من نوع
appومعه هذا الفعل فيtriggers، وبدونه يرفض الـ API نشره برسالةAn app survey must have at least one trigger. - سكربت Formbricks في صفحات التطبيق، ثم استدعاء
formbricks.trackعندما ينجح الرفع.
وهذا هو الجزء المهم من صفحة التطبيق التجريبية:
<script type="text/javascript">
!function(){
var appUrl = "https://forms.example.com";
var workspaceId = "cmvnuudym000101ny03wnr6ey";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.setup({workspaceId: workspaceId, appUrl: appUrl})},500)}();
</script>
<script>
document.getElementById('upload').addEventListener('click', async function () {
document.getElementById('status').textContent = 'تم رفع الفاتورة INV-0001';
// tell Formbricks that the user reached this step; the in-app survey is triggered by this action
if (window.formbricks) await window.formbricks.track('first-invoice-uploaded');
});
</script>وعند الضغط على الزر ظهر الاستبيان في زاوية الصفحة، بالعربية ومن اليمين إلى اليسار:

واخترنا الوجه الرابع، ثم كتبنا إجابة تجمع العربية والإنجليزية:

وحفظت الإجابة هكذا:
finished | language | data
----------+----------+------------------------------------------------------------------
t | ar-SA | {"ces": 4, "hard": "لم أعرف أن رقم الضريبة VAT مطلوب قبل الرفع"}ثم ضغطنا الزر مرة ثانية فلم يظهر الاستبيان، لأن خيار العرض displayOnce يمنع عرضه لنفس المستخدم مرتين، وهذا ما تريده في استبيان عن «أول» فاتورة. وهذا النوع من الإجابات هو ما تحتاجه في التسويق للمنتج: سبب واضح يمكن إصلاحه في الصفحة، أو كتابته في رسالة الترحيب التي ترسلها أداة الأتمتة.
التعرف على المستخدم: ميزة تجارية
الخطوة الطبيعية التالية أن تربط الإجابة بالمستخدم، أي أن تستدعي formbricks.setUserId بعد تسجيل الدخول، فتعرف أن صاحب الإجابة هو العميل الفلاني في باقة كذا، وتوجه استبيانات لشريحة محددة. وجربنا ذلك، فظهر في Console المتصفح:
🧱 Formbricks - [ERROR] - Failed to send updates: User identification is only available for enterprise users.ولم ينشأ أي سطر في جدول جهات الاتصال Contact. وهذا متسق مع صفحة الترخيص، فإدارة جهات الاتصال والشرائح Contact Management & Segments في النسخة التجارية Enterprise، بينما الاستبيانات داخل التطبيق والحقول المخفية وتعدد اللغات متاحة في النسخة المجتمعية Community. وإخفاء عبارة «مشغل بواسطة Formbricks» أيضاً من الميزات التجارية. والحل العملي دون ترخيص أن تمرر ما تحتاجه في الحقول المخفية للاستبيانات بالرابط، مثل الباقة ومعرف مجهول للعميل، وأن تبقي الاستبيان داخل التطبيق مجهول الهوية.
قراءة النتائج
صفحة الملخص Summary تعرض لكل سؤال نتيجته، ولسؤال NPS التوزيع بين المروجين Promoters والمحايدين Passives والمنتقدين Detractors، ثم الرقم النهائي:

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

ولكن الملخص لا يعطيك NPS لكل مصدر، وهذا هو السؤال التسويقي: أي قناة تجلب عملاء راضين؟ لذلك كتبنا سكربتاً صغيراً يقرأ الإجابات من Management API ويحسب المؤشر لكل قيمة في utm_source:
// Reads link-survey responses from the Formbricks Management API and prints NPS per utm_source hidden field.
// FB_URL=http://localhost:18801 FB_API_KEY=... SURVEY_ID=... node nps-by-source.mjs
const { FB_URL, FB_API_KEY, SURVEY_ID } = process.env;
const r = await fetch(`${FB_URL}/api/v1/management/responses?surveyId=${SURVEY_ID}&limit=500`, { headers: { 'x-api-key': FB_API_KEY } });
const { data } = await r.json();
const groups = {};
for (const resp of data) {
if (typeof resp.data.nps !== 'number') continue;
const src = resp.data.utm_source || '(none)';
(groups[src] ??= []).push(resp.data.nps);
}
const nps = (a) => Math.round(100 * (a.filter(s => s >= 9).length - a.filter(s => s <= 6).length) / a.length);
console.log('responses:', data.length);
for (const [src, scores] of Object.entries({ all: Object.values(groups).flat(), ...groups }))
console.log(src.padEnd(10), 'n =', scores.length, 'scores =', JSON.stringify(scores), 'NPS =', nps(scores));وشغلناه في Container من node:24.22.1-bookworm على نفس شبكة Formbricks، والمخرج سوف يكون كما يلي:
responses: 3
all n = 3 scores = [10,9,6] NPS = 33
x n = 2 scores = [10,9] NPS = 100
linkedin n = 1 scores = [6] NPS = -100والمؤشر العام 33 هو نفس الرقم في لوحة الملخص، وهذا يؤكد أن الحساب صحيح. أما الأرقام لكل مصدر فلا معنى لها مع ثلاث إجابات، وقد وضعناها لتبين الطريقة فقط، فلا تحكم على قناة بأقل من بضع عشرات من الإجابات. ومعادلة NPS المستخدمة هي نسبة من أعطى 9 أو 10 ناقص نسبة من أعطى 6 أو أقل.
الخصوصية وأين تبقى الإجابات
- عطلنا حفظ عنوان IP في الاستبيان بالخيار
isCaptureIpEnabled: false، وما يحفظ فيmetaهو رابط الصفحة ونوع الجهاز والمتصفح، مثل{"os": "Windows", "device": "desktop", "browser": "Chrome"}. - الرابط المحفوظ في
metaيحمل الحقول المخفية كما هي، فلا تضع في الرابط بريد العميل أو رقم هاتفه، وضع معرفاً لا يدل على شخص. - الإجابات في قاعدة بياناتك، ولا تخرج إلا بتكامل Integration تضيفه أنت، والتفاصيل في قسم أين تذهب الإجابات من دليل التثبيت.
التحقق من النجاح
- رابط الاستبيان يفتح بالعربية وفيه «مطلوب» و«مشغل بواسطة»، والصفحة
lang="ar-SA". - الإجابة المحفوظة فيها
utm_sourceبنفس القيمة التي في الرابط. - في صفحة التطبيق يظهر الاستبيان بعد
formbricks.trackمرة واحدة فقط، وإذا لم يظهر فافتح Console وابحث عن رسائل Formbricks. - سكربت NPS يعطي نفس الرقم العام الذي في لوحة الملخص.
مشكلات شائعة وحلولها
الـ API يرفض اللغة مع الاستبيان
أنشئ الاستبيان دون languages، ثم اختر اللغة الافتراضية من تبويب Language في المحرر كما في قسم اللغة.
الاستبيان داخل التطبيق لا يظهر
تأكد أن مفتاح الفعل في track يطابق المفتاح في Formbricks حرفاً بحرف، وأن الاستبيان منشور Active، وأنه لم يعرض لهذا المتصفح من قبل مع displayOnce، فإذا أردت إعادة التجربة فامسح التخزين المحلي Local Storage للصفحة، ففيه المفتاح formbricks-js.
رسالة User identification is only available for enterprise users
هذه ميزة تجارية كما في قسم التعرف على المستخدم، فاحذف استدعاء setUserId أو احصل على ترخيص.
المشاهدات أكبر بكثير من الإجابات
كل فتح للرابط يحسب مشاهدة، بما فيها فتحك أنت ومعاينات الروابط، فاحكم بالبدايات والإكمال.
إزالة التجربة
docker compose -p mke-fb down -v
docker rm -f mke-fb-site
docker rmi ghcr.io/formbricks/formbricks:6.1.1 ghcr.io/formbricks/hub:0.14.1 ghcr.io/formbricks/cube:1.7.50-1 \
pgvector/pgvector:0.8.7-pg18 authzed/spicedb:v1.52.0الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- Formbricks 6.1.1 يعمل بنفس خطوات 6.0 مع أسماء جديدة لمتغيرات المصادقة وإصدار أحدث من hub، ويستهلك حوالي 1.16 جيجابايت من الذاكرة.
- اجعل العربية اللغة الافتراضية لمساحة العمل وللاستبيان، فتظهر العبارات الثابتة بالعربية والمقياس من اليمين، وانتبه أن الـ API لا يقبل اللغة عند الإنشاء.
- الحقول المخفية تربط كل رأي بمصدره دون سؤال إضافي، والسؤال الصريح عن المصدر يكشف ما لا يكشفه الرابط.
- الاستبيان داخل التطبيق يعمل في النسخة المجتمعية بفعل
track، أما ربط الإجابة بمستخدم معروف فميزة تجارية. - لوحة الملخص لا تعطي NPS لكل قناة، وسكربت صغير على الـ API يحسبه في ثوان.
سجل التحديثات
- أكتوبر 2026: كتابة الدليل واختباره على Formbricks 6.1.1 (ghcr.io/formbricks/formbricks:6.1.1) مع hub 0.14.1 وcube 1.7.50-1 وauthzed/spicedb:v1.52.0 وpgvector/pgvector:0.8.7-pg18، وسكربت NPS على node:24.22.1-bookworm، على Docker Engine 29.8.0.