العودة إلى المدونة

المدونة

أحداث التجارة الإلكترونية دون منصة متجر: كيف تجعل صفحة دفع بنيتها بنفسك تبلّغ GA4

نُشر · 5 دقائق قراءة

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

العقد: اسم وكائن ecommerce

كل ما هنا يصل إلى Analytics بالطريقة نفسها. تدفع صفحتك حدثًا إلى window.dataLayer باسمه في GA4، يحمل كائن ecommerce بشكل GA4. مشغّل في Tag Manager يطابق الاسم، ووسم حدث GA4 يقرأ الكائن ويمرّره إلى حاوي الخادم لديك. إن أخبرت SignalHost بأن موقعك متجر — أو ضغطت «إعداد» تحت «أحداث التحويل» في صفحة الحاوي — فهذه المشغّلات والوسوم موجودة بالفعل في حاوي الويب لديك، بأسماء تبدأ بالبادئة SH. ما يتبقى هو الدفع.

قاعدتان تلتقطان معظم الأخطاء. امسح كائن ecommerce السابق أولًا، وإلا ركب view_item من ثلاث صفحات مضت داخل الحدث التالي. وفي كل حدث يحمل مالًا، أرسل value كرقم و currency كرمز ISO: فقيمة value النصية أو غياب currency يُقبلان دون أي اعتراض، ويُستبعدان من الإيرادات بصمت.

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
  event: 'begin_checkout',
  ecommerce: {
    currency: 'EUR',
    value: 49.00,
    items: [
      { item_id: 'plan-pro', item_name: 'Pro', price: 49.00, quantity: 1 }
    ]
  }
});

أي حدث، وأين مكانه

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

  • view_item_list — صفحة أو مكوّن يعرض عدة منتجات أو خطط. مرة واحدة عند عرض القائمة، لا عند كل تمرير.
  • view_item — صفحة منتج واحد أو خطة واحدة.
  • add_to_cart و remove_from_cart — في معالج نقرة الزر، بعد أن تتغير سلتك فعلًا.
  • begin_checkout — لحظة دخول الزائر إلى الدفع: أول عرض لصفحة الدفع، أو النقرة التي تفتح نافذة الدفع.
  • add_payment_info — عندما تُقبل بيانات الدفع، لا عندما يظهر النموذج.
  • purchase — مرة واحدة، عند تأكيد الطلب. معظم هذا المقال يدور حول ضبط هذا الحدث تحديدًا.
  • sign_up و start_trial — لنشاط قائم على الاشتراكات، هما التحويلان اللذان يسبقان انتقال أي مال من يد إلى يد. تبني SignalHost وسومًا لكليهما إلى جانب أحداث المتجر.

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

الشراء في صفحة التأكيد: اقرأ الطلب من خادمك

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

// On your confirmation page. The order comes from YOUR server, by an id the
// page already knows — never from price or status parameters in the URL.
const order = await fetch(`/api/orders/${orderId}`).then((r) => r.json());

const key = `purchase-sent-${order.id}`;
if (order.status === 'paid' && !localStorage.getItem(key)) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: order.id,
      value: order.total,
      tax: order.tax,
      currency: order.currency,
      items: order.items.map((i) => ({
        item_id: i.sku, item_name: i.name, price: i.price, quantity: i.quantity
      }))
    }
  });
  localStorage.setItem(key, '1');
}

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

الدفع على صفحة غيرك: Stripe Checkout وPayPal وإعادة التوجيه إلى البنك

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

لذا قرّر، لكل طريقة دفع، أين تكمن الحقيقة. إن كانت واجهتك الخلفية تعلم بالدفع من webhook، فالـ webhook هو المكان الموثوق للإبلاغ عنه — انظر القسم التالي — ويجب ألا تدفع صفحة التأكيد purchase أيضًا، وإلا احتُسب كل دفع بالبطاقة مرتين. وإن كان المكان الوحيد الذي تعلم فيه بالدفع هو صفحة العودة، فأبلغ عنه هناك وتقبّل الفجوة.

مشتريات لا يحضرها أي متصفح: أرسلها من خادمك

الاشتراكات تجعل هذا أمرًا لا مفر منه. تتحول الفترة التجريبية إلى اشتراك مدفوع بعد سبعة أيام، ويحدث التجديد كل شهر، وتُدفع فاتورة بتحويل بنكي الأسبوع المقبل. كل منها شراء، ولا يحدث أي منها أمام متصفح. الحل أن ترسل الحدث من خادم إلى خادم، من الـ webhook الذي يعلم بالدفع، إلى نقطة نهاية الوسم نفسها التي تستخدمها صفحاتك — اسم مضيفك في SignalHost أو نطاقك الخاص — فيمر عبر حاوي الخادم لديك مثل أي نقرة أخرى.

ويحتاج ذلك إلى شيء واحد من صفحة الدفع: معرّف العميل الخاص بالزائر في Analytics، كي يُنسب الشراء إلى الشخص الذي قادت زياراته إليه. هذا المعرّف موجود في ملف تعريف الارتباط _ga، الذي يحمله طلب الدفع لديك أصلًا حين يذهب إلى نطاقك الخاص. احفظه مع الطلب. غياب ملف _ga يعني أن الزائر لم يمنح موافقة التحليلات: أرسل النقرة بحالة موافقة مرفوضة ومعرّف مؤقت يُرمى بعد الاستعمال، أو لا ترسلها إطلاقًا — ولا تخترع له هوية أبدًا. وإن كنت تمرّر التحويلات أيضًا إلى Meta أو TikTok أو Google Ads، فاحفظ اختيار الزائر بشأن الإعلانات مع الطلب، وأرسل G111 حين يكون قد وافق عليها: فالشراء الموسوم بـ G101 يصل إلى Analytics لكنه لا يصل إلى Meta ولا TikTok، ولا يستطيع Google Ads ربطه بنقرة على إعلان.

// In your checkout request handler: keep the visitor's Analytics id with the order.
const ga = req.cookies._ga;                        // "GA1.1.1234567890.1700000000"
order.gaClientId = ga ? ga.split('.').slice(-2).join('.') : null;

// Later, in your payment webhook — a trial converting, an invoice paid:
const params = new URLSearchParams({
  v: '2',
  tid: 'G-XXXXXXXXXX',                              // your GA4 measurement ID
  cid: order.gaClientId ?? `${Date.now()}.${Math.floor(Math.random() * 1e9)}`,
  gcs: order.gaClientId ? 'G101' : 'G100',          // no _ga cookie = no consent
  en: 'purchase',
  'ep.transaction_id': invoice.id,
  'epn.value': '49.00',
  cu: 'EUR',
  pr1: 'idplan-pro~nmPro~pr49.00~qt1',
});
await fetch(`https://metrics.example.com/g/collect?${params}`, { method: 'POST' });

اختر مصدرًا واحدًا لكل حدث والتزم به. هكذا بالضبط تقيس signalhost.io مبيعاتها: المتصفح يدفع sign_up و begin_checkout و start_trial؛ والـ webhook من Stripe يرسل purchase حين تُدفع الفاتورة الأولى؛ ولا يأتي أي حدث من المصدرين معًا. كان Measurement Protocol من Google سيؤدي الغرض أيضًا، لكنه يذهب مباشرة إلى Google متجاوزًا حاوي الخادم لديك، فلا يرى أي من وسومك في جانب الخادم عملية البيع أبدًا.

المشتريات المتأخرة سبب من أربعة أسباب تجعل GA4 يحتسب أقل من قائمة طلباتك: الأسباب الثلاثة الأخرى، وأيّها يمكنك إصلاحه.

الموافقة لا تطلب منك شيئًا إضافيًا

ادفع الأحداث أيًا كان اختيار الزائر. الوسوم تلتزم بـ Consent Mode: مع الموافقة ترسل كالمعتاد؛ ومن دونها ترسل إشارات بلا ملفات تعريف ارتباط يعالجها Analytics بالنمذجة بدلًا من الإبلاغ عنها. حجب الدفعات بنفسك لا يفعل سوى إخفاء التحويلات عن هذه النمذجة. ما يهم فعلًا هو الترتيب — مقطع Tag Manager يأتي بعد سكربت لافتة الموافقة لديك، كي تكون القيم الافتراضية للموافقة موجودة قبل إرسال الحدث الأول.

التحقق من أن كل شيء يعمل

  • افتح الحاوي في SignalHost. بطاقة الأحداث تسرد ما وصل، حسب الاسم، مع الحصة التي حملت الموافقة. أي اسم دفعته ولا تراه هناك لم يصل أبدًا إلى خادم الوسم لديك.
  • امشِ في مسار الدفع لديك في وضع المعاينة في Tag Manager. يعرض كل دفعة، وأي وسم أُطلق، وكائن ecommerce الذي أُرسل.
  • في Analytics، يعرض DebugView الأحداث فور وصولها. أما التقارير القياسية فقد تستغرق يومًا.
  • أعد تحميل صفحة التأكيد مرتين وعُدّ المشتريات. ينبغي أن تبقى واحدة.

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

كفى خسارةً للبيانات أمام أدوات الحجب

عشرة آلاف طلب شهريًا، مجانًا، ما شئت من الوقت. أربع دقائق لتعرف إن كان ذلك يفيدك.