إرسال الأحداث

يمكنك الاطّلاع على هذا الدليل للتعرّف على عملية إرسال الأحداث المتعدّدة المصادر في "إحصاءات Google" باستخدام Data Manager API.

في هذا الدليل، ستكمل الخطوات التالية:

  1. إعداد Destination لتلقّي بيانات الأحداث
  2. جهِّز بيانات الأحداث لإرسالها.
  3. أنشئ طلب IngestionService للأحداث.
  4. أرسِل الطلب باستخدام Google APIs Explorer.
  5. التعرّف على الاستجابات الناجحة وغير الناجحة

إعداد الوجهات

قبل أن تتمكّن من إرسال البيانات، عليك إعداد Destination واحدة على الأقل للبيانات. في ما يلي حقول Destination. يمكنك الاطّلاع على ضبط الوجهات لمزيد من التفاصيل والأمثلة على الوجهات لسيناريوهات مختلفة.

operatingAccount

الموقع على "إحصاءات Google" الذي يتلقّى الأحداث اضبط accountType على GOOGLE_ANALYTICS_PROPERTY وaccountId على رقم تعريف الموقع على "إحصاءات Google" (على سبيل المثال، 123456789).

يجب أن تكون بيانات اعتماد الطلب لحساب Google يملك دور المحرِّر أو المشرف للموقع.

loginAccount

الحساب الذي يمكن لمستخدم بيانات الاعتماد الوصول إليه اضبط accountType على GOOGLE_ANALYTICS_PROPERTY وaccountId على رقم تعريف الموقع على "إحصاءات Google".

productDestinationId

معرّف مصدر البيانات الذي يتلقّى الأحداث

مصدر بيانات الويب

يجب ضبط هذه السمة على رقم تعريف القياس لمصدر بيانات موقع إلكتروني في "إحصاءات Google" (بالتنسيق G-XXXXXXXXXX).

مصدر بيانات التطبيق

يجب ضبط هذه المَعلمة على رقم تعريف تطبيق Firebase لمصدر بيانات تطبيق iOS أو Android في "إحصاءات Google".

{
  "operatingAccount": {
    "accountType": "GOOGLE_ANALYTICS_PROPERTY",
    "accountId": "PROPERTY_ID"
  },
  "loginAccount": {
    "accountType": "GOOGLE_ANALYTICS_PROPERTY",
    "accountId": "LOGIN_ACCOUNT_ID"
  },
  "productDestinationId": "PRODUCT_DESTINATION_ID"
}

يوضّح المثال في هذا الدليل كيفية إنشاء طلب يرسل كل حدث إلى الوجهة نفسها. إذا أردت إرسال أحداث لوجهات متعددة في الطلب نفسه، يمكنك الاطّلاع على إرسال أحداث لوجهات متعددة.

إعداد بيانات الأحداث

قبل إرسال بيانات الأحداث في طلب، عليك إعداد البيانات:

  1. يجب تنسيق القيم الأولية كما هو موضّح في دليل التنسيق.
  2. يجب تجزئة معرّفات المستخدمين، مثل عناوين البريد الإلكتروني والأسماء الأولى وأسماء العائلة، باستخدام خوارزمية SHA-256 وترميزها باستخدام نظام العد الستة عشري (hex) أو ترميز Base64.
  3. أنشئ حمولة Event باستخدام القيم المنسَّقة والمجزّأة.

تعرض علامات التبويب التالية أمثلة على بيانات الأحداث التي تنتقل من الإدخال الأولي، من خلال التنسيق، إلى قيمة الحمولة النهائية:

الحدث 1

سمة الحدث قيمة أساسية ناتج منسَّق قيمة حمولة البيانات
event_timestamp June 10, 2025 3:07:01PM America/Chicago 2025-06-10T15:07:01-05:00 (ISO 8601) 2025-06-10T15:07:01-05:00
product_destination_id G-1234567890 G-1234567890 G-1234567890
event_name purchase purchase purchase
client_id 1234567890.1761581763 1234567890.1761581763 1234567890.1761581763
user_id user_ABC12345 user_ABC12345 user_ABC12345
transaction_id XYZ987654321 XYZ987654321 XYZ987654321
conversion_value 30.03 30.03 30.03
currency usd USD (رمز ISO مكوّن من 3 أحرف) USD
ad_unit_name Banner_01 Banner_01 Banner_01
بيانات سلّة التسوّق
item_id SKU_12345 SKU_12345 SKU_12345
unit_price 10.01 10.01 10.01
quantity 3 3 3
مَعلمات إضافية للسلع
item_name Stan and Friends Tee Stan and Friends Tee Stan and Friends Tee
item_category Apparel Apparel Apparel
item_brand Google Google Google

الحدث 2

سمة الحدث قيمة أساسية ناتج منسَّق قيمة حمولة البيانات
event_timestamp June 10, 2025 11:42:33PM America/New_York 2025-06-10T23:42:33-05:00 (ISO 8601) 2025-06-10T23:42:33-05:00
product_destination_id G-1234567890 G-1234567890 G-1234567890
event_name purchase purchase purchase
client_id 9876543210.1761582117 9876543210.1761582117 9876543210.1761582117
user_id user_DEF9876 user_DEF9876 user_DEF9876
transaction_id DEF999911111 DEF999911111 DEF999911111
conversion_value 42.02 42.02 42.02
currency eur EUR (رمز ISO مكوّن من 3 أحرف) EUR
email_address

zoe@EXAMPLE.COM

cloudy.sanfrancisco@gmail.com

zoe@example.com (تم اقتطاعه، أحرف صغيرة)

cloudysanfrancisco@gmail.com (تم اقتطاعها وتحويلها إلى أحرف صغيرة وإزالة النقطة قبل ‎ @gmail.com)

3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250 (SHA-256 hex)

223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4 (SHA-256 hex)

given_name zoë zoë (تم اقتطاعه، أحرف صغيرة) 2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450 (SHA-256 hex)
family_name pérez pérez (تم اقتطاعه، أحرف صغيرة) 6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F (SHA-256 hex)
address_line 1800 Amphibious Blvd. 1800 amphibious blvd (أحرف صغيرة، بدون علامات ترقيم) FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579 (SHA-256 hex)
city Mountain View mountain view (تم اقتطاعه، أحرف صغيرة) mountain view
administrative_area CA ca (تم اقتطاعه، أحرف صغيرة) ca
region_code US US (رمز ISO مكوّن من حرفَين) US
postal_code 94043 94043 94043
customer_type RETURNING RETURNING RETURNING
ad_unit_name Banner_02 Banner_02 Banner_02
بيانات سلّة التسوّق
item_id SKU_12346 SKU_12346 SKU_12346
unit_price 21.01 21.01 21.01
quantity 2 2 2
مَعلمات إضافية للسلع
item_name Google Grey Women's Tee Google Grey Women's Tee Google Grey Women's Tee
item_category Apparel Apparel Apparel
item_brand Google Google Google

تحويل البيانات إلى عناصر Event

حوِّل البيانات المنسَّقة والمجزّأة لكل حدث إلى Event.

متطلبات حقول الأحداث

أنشئ أحداثك وفقًا للمتطلبات الواردة في الجدول التالي. يُرجى الرجوع إلى المستندات المرجعية لـ Event للاطّلاع على القائمة الكاملة للحقول المتاحة.

الحقل الحالة الوصف
eventName مطلوب اسم حدث "إحصاءات Google" إذا كان اسم الحدث اسمًا محجوزًا، ترفض واجهة Data Manager API الحدث وتعرض الخطأ INVALID_EVENT_NAME.
eventTimestamp مطلوب وقت وقوع الحدث يُرجى الاطّلاع على تنسيق الطابع الزمني. يجب أن تتضمّن الأحداث في "إحصاءات Google" مَعلمة eventTimestamp تعود إلى آخر 72 ساعة. بالإضافة إلى ذلك، يجب إرسال أحداث "إحصاءات Google" التي تريد دمجها أو معالجتها مع الأحداث التي تجمعها حزمة تطوير البرامج (SDK) لخدمة "إحصاءات Google لبرنامج Firebase" أو gtag.js خلال 48 ساعة من الطابع الزمني الأصلي للحدث من جهة العميل. قد لا تعالج "إحصاءات Google" الأحداث التي يتم إرسالها بعد هذا الوقت على النحو المتوقّع، لا سيما في ما يتعلق بتحديد مصدر الإحالة الناجحة.
transactionId مطلوب المعرّف الفريد للمعاملة
eventSource اختياري مصدر الحدث في حال ضبط هذه السمة، يجب أن تكون قيمتها WEB لمصادر بيانات المواقع الإلكترونية أو APP لمصادر بيانات التطبيقات.
conversionValue اختياري القيمة المالية المرتبطة بالحدث
currency اختياري رمز العملة (مثل USD أو EUR) المرتبط بالقيم النقدية في هذا الحدث.
cartData مطلوبة بشروط مطلوبة إذا كانت محدّدة في مرجع أحداث "إحصاءات Google" للحدث. يُرجى الاطّلاع على المقالة إضافة بيانات سلة التسوّق.
additionalEventParameters اختياري اختيارية، ولكن ننصح بها. املأ هذه القائمة بأي مَعلمات خاصة بأحداث "إحصاءات Google" لم يتم تسجيلها في حقول Event الأخرى. اطّلِع على مقالة إضافة مَعلمات خاصة بالأحداث.
userProperties اختياري خصائص المستخدم للمستخدم يُرجى الاطّلاع على إضافة خصائص المستخدمين.
destinationReferences مطلوبة بشروط مطلوبة إذا كانت قائمة destinations على مستوى الطلب تحتوي على أكثر من Destination واحد في "إحصاءات Google". يُرجى الاطّلاع على مقالة إرسال الأحداث إلى وجهات متعددة.
consent اختياري إعدادات الموافقة بموجب قانون الأسواق الرقمية (DMA) للمستخدم، والتي تحدّد ما إذا تم منح الموافقة لكل من adUserData وadPersonalization.

متطلبات المعرّف

في ما يلي متطلبات المعرّفات لكل نوع من مصادر البيانات:

مصدر بيانات الويب

يجب تضمين معرّف واحد على الأقل من المعرّفات التالية عند إرسال حدث متعدّد المصادر في "إحصاءات Google" إلى مصدر بيانات موقع إلكتروني:

مصدر بيانات التطبيق

يُرجى اتّباع هذه المتطلبات عند إرسال حدث متعدّد المصادر في "إحصاءات Google" إلى مصدر بيانات تطبيق:

كيفية معالجة Google للبيانات المتعدّدة المصادر

ضمن الحدث والموقع نفسهما، تستخدم "إحصاءات Google" transactionId لإزالة تكرار الحدث من مصادر مختلفة (مثل علامة موقعك الإلكتروني وطلبات نقل البيانات في واجهة برمجة التطبيقات Data Manager API). تستخدم "إحصاءات Google" المعلومات من الحالة الأولى للحدث نفسه الذي تلقّته.

إضافة بيانات سلة التسوّق

املأ حقل cartData في Event بمعلومات عن السلع المرتبطة بالحدث. استخدِم هذا الحقل عند إرسال الأحداث المقترَحة في "إحصاءات Google"، مثل purchase، التي تتطلّب قائمة items.

في ما يلي حقول الكائن CartData:

items
مطلوبة. يجب تضمين Item واحد على الأقل في هذه القائمة.

حقول المنتجات

أضِف عنصرًا واحدًا أو أكثر من عناصر Item إلى قائمة items في CartData.

املأ الحقول التالية لكل Item:

items.itemId
مطلوبة. معرّف فريد للسلعة
items.unitPrice

اختيارية: سعر الوحدة غير شامل للضرائب أو تكاليف الشحن أو الخصومات على مستوى الحدث (على مستوى المعاملة) لهذه السلعة.

إذا كان المنتج يتضمّن خصمًا على مستوى المنتج أو الخدمة، استخدِم سعر الوحدة بعد الخصم. على سبيل المثال، إذا كان سعر الوحدة لمنتج معيّن هو 27.67 والخصم على الوحدة هو 6.66، اضبط السمة unitPrice على 21.01.

items.quantity

مَعلمة مطلوبة. عدد الوحدات لهذه السلعة المحدّدة

items.additionalItemParameters

اختيارية: يمكنك تعبئة هذه القائمة بأي مَعلمات على مستوى المنتج أو الخدمة لم يتم تسجيلها في حقول Item الأخرى. استخدِم اسم المَعلمة من مستندات "إحصاءات Google" كـ parameterName للسمة ItemParameter.

على سبيل المثال، في "إحصاءات Google"، إذا كانت لديك العلامة التجارية والفئة لسلعة معيّنة في حدث purchase، أضِف إدخالاً إلى additionalItemParameters السلعة مع ضبط parameterName على item_brand وضبط value على اسم العلامة التجارية.

لا ننصح بإضافة إدخالات إلى additionalItemParameters لمَعلمات العناصر quantity أو price أو item_id. بدلاً من ذلك، املأ الحقول itemId وunitPrice وquantity في Item، والتي لها الأولوية على أي إدخالات في additionalItemParameters.

إضافة معلمات أحداث

املأ قائمة additionalEventParameters الخاصة بـ Event بأي مَعلمات خاصة بالحدث في "إحصاءات Google" لم يتم تسجيلها في حقول Event الأخرى. يمكن أن تتضمّن المَعلمات مَعلمات إضافية مقترَحة للحدث أو أي مَعلمات أخرى تريد تسجيلها. استخدِم اسم مَعلمة "إحصاءات Google" لـ parameterName EventParameter.

على سبيل المثال، إذا كانت لديك الضرائب المرتبطة بمعاملة معيّنة، يمكنك إضافة إدخال إلى additionalEventParameters مع ضبط parameterName على tax، وضبط value على تكلفة الضريبة.

لا ننصح بإضافة إدخالات إلى additionalEventParameters لمَعلمات الأحداث transactionId أو currency أو value في "إحصاءات Google". بدلاً من ذلك، املأ الحقول transactionId وcurrency وconversionValue في Event، والتي لها الأولوية على أي إدخالات في additionalEventParameters.

إضافة البيانات المقدَّمة من المستخدم

املأ حقل userData في Event بمعلومات عملاء الطرف الأول (مثل عنوان البريد الإلكتروني ورقم الهاتف والعنوان) لتحسين قياس الإحالات الناجحة وتحديد مصدرها.

  • التنسيق والتجزئة: يجب تسوية المعرّفات الشخصية (مثل عنوان البريد الإلكتروني ورقم الهاتف والاسم وعنوان الشارع) وتجزئتها باستخدام خوارزمية SHA-256 قبل إرسالها. أما الحقول الأخرى (مثل المدينة والولاية أو المقاطعة والرمز البريدي ورمز البلد)، فيتم تسويتها ولكن لا تتم تجزئتها. يُرجى الاطّلاع على تنسيق بيانات المستخدمين لمعرفة المتطلبات المحدّدة لكل معرّف.

  • التشفير: يمكنك اختياريًا تشفير معرّفات المستخدمين المجزّأة.

  • رقم تعريف المستخدم: عند تقديم userData، ننصح أيضًا بتقديم الحقل userId إذا كان هناك معرّف مستخدم متّسق متاحًا.

  • متطلبات الفعالية: في Data Manager API، يجب إرسال البيانات المقدَّمة من المستخدم كجزء من Event. لا يمكن تحميل بيانات المستخدمين بشكل مستقل بدون حدث.

إضافة خصائص المستخدمين

خصائص المستخدمين تصف المستخدم في وقت الحدث. أضِف إدخالاً منفصلاً إلى قائمة additionalUserProperties لكل سمة مستخدم.

عند إرسال خصائص المستخدمين، لا تستخدِم أسماء خصائص المستخدمين المحجوزة التالية:

  • first_open_time
  • first_visit_time
  • last_deep_link_referrer
  • user_id
  • first_open_after_install

بالإضافة إلى ذلك، لا تستخدِم البادئات التالية لأسماء خصائص المستخدمين:

  • google_
  • ga_
  • firebase_

أمثلة على الطلبات

في ما يلي نماذج لكائنات Event للبيانات المنسَّقة من كل حدث:

الحدث 1

{
  "additionalEventParameters": [
    {
      "parameterName": "ad_unit_name",
      "value": "Banner_01"
    }
  ],
  "cartData": {
    "items": [
      {
        "additionalItemParameters": [
          {
            "parameterName": "item_name",
            "value": "Stan and Friends Tee"
          },
          {
            "parameterName": "item_category",
            "value": "Apparel"
          },
          {
            "parameterName": "item_brand",
            "value": "Google"
          }
        ],
        "itemId": "SKU_12345",
        "quantity": 3,
        "unitPrice": 10.01
      }
    ]
  },
  "clientId": "1234567890.1761581763",
  "conversionValue": 30.03,
  "currency": "USD",
  "eventName": "purchase",
  "eventTimestamp": "2025-06-10T15:07:01-05:00",
  "transactionId": "XYZ987654321",
  "userId": "user_ABC12345"
}

الحدث 2

{
  "additionalEventParameters": [
    {
      "parameterName": "ad_unit_name",
      "value": "Banner_02"
    }
  ],
  "cartData": {
    "items": [
      {
        "additionalItemParameters": [
          {
            "parameterName": "item_name",
            "value": "Google Grey Women's Tee"
          },
          {
            "parameterName": "item_category",
            "value": "Apparel"
          },
          {
            "parameterName": "item_brand",
            "value": "Google"
          }
        ],
        "itemId": "SKU_12346",
        "quantity": 2,
        "unitPrice": 21.01
      }
    ]
  },
  "clientId": "9876543210.1761582117",
  "conversionValue": 42.02,
  "currency": "EUR",
  "eventName": "purchase",
  "eventTimestamp": "2025-06-10T23:42:33-05:00",
  "transactionId": "DEF999911111",
  "userData": {
    "userIdentifiers": [
      {
        "emailAddress": "3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250"
      },
      {
        "emailAddress": "223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4"
      },
      {
        "address": {
          "addressLine": "FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579",
          "administrativeArea": "ca",
          "city": "mountain view",
          "familyName": "6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F",
          "givenName": "2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450",
          "postalCode": "94043",
          "regionCode": "US"
        }
      }
    ]
  },
  "userId": "user_DEF9876",
  "userProperties": {
    "additionalUserProperties": [
      {
        "propertyName": "customer_type",
        "value": "RETURNING"
      }
    ]
  }
}

إنشاء نص الطلب

لإنشاء نص الطلب، ادمج destinations وevents، واضبط الحقل encoding، وأضِف أي حقول طلب أخرى تريد تضمينها، مثل validateOnly وconsent.

إرسال الطلب

في ما يلي خطوات إرسال طلب من المتصفّح:

  1. اختَر علامة التبويب REST وانقر على فتح في "مستكشف واجهة برمجة التطبيقات" لفتح "مستكشف واجهة برمجة التطبيقات" في علامة تبويب أو نافذة جديدة.
  2. في نص الطلب في "مستكشف واجهة برمجة التطبيقات"، استبدِل كل سلسلة تبدأ بـ REPLACE_WITH، مثل REPLACE_WITH_OPERATING_ACCOUNT_TYPE، بالقيمة ذات الصلة.
  3. انقر على تنفيذ في أسفل صفحة "مستكشف واجهة برمجة التطبيقات" وأكمِل طلبات التفويض لإرسال الطلب.
  4. اضبط validateOnly على true للتحقّق من صحة الطلب بدون تطبيق التغييرات. عندما تكون مستعدًا لتطبيق التغييرات، اضبط validateOnly على false.

إذا ثبّتّ مكتبة برامج، اختَر علامة التبويب للغة البرمجة التي اخترتها للاطّلاع على عيّنة تعليمات برمجية كاملة توضّح كيفية إنشاء طلب وإرساله.

REST

{
    "destinations": [
        {
            "operatingAccount": {
                "accountType": "OPERATING_ACCOUNT_TYPE",
                "accountId": "OPERATING_ACCOUNT_ID"
            },
            "loginAccount": {
                "accountType": "LOGIN_ACCOUNT_TYPE",
                "accountId": "LOGIN_ACCOUNT_ID"
            },
            "productDestinationId": "PRODUCT_DESTINATION_ID"
        }
    ],
    "encoding": "HEX",
    "consent": {
        "adUserData": "CONSENT_GRANTED",
        "adPersonalization": "CONSENT_GRANTED"
    },
    "events": [
        {
            "additionalEventParameters": [
                {
                    "parameterName": "ad_unit_name",
                    "value": "Banner_01"
                }
            ],
            "cartData": {
                "items": [
                    {
                        "additionalItemParameters": [
                            {
                                "parameterName": "item_name",
                                "value": "Stan and Friends Tee"
                            },
                            {
                                "parameterName": "item_category",
                                "value": "Apparel"
                            },
                            {
                                "parameterName": "item_brand",
                                "value": "Google"
                            }
                        ],
                        "itemId": "SKU_12345",
                        "quantity": 3,
                        "unitPrice": 10.01
                    }
                ]
            },
            "clientId": "1234567890.1761581763",
            "conversionValue": 30.03,
            "currency": "USD",
            "eventName": "purchase",
            "eventTimestamp": "2025-06-10T15:07:01-05:00",
            "transactionId": "XYZ987654321",
            "userId": "user_ABC12345"
        },
        {
            "additionalEventParameters": [
                {
                    "parameterName": "ad_unit_name",
                    "value": "Banner_02"
                }
            ],
            "cartData": {
                "items": [
                    {
                        "additionalItemParameters": [
                            {
                                "parameterName": "item_name",
                                "value": "Google Grey Women's Tee"
                            },
                            {
                                "parameterName": "item_category",
                                "value": "Apparel"
                            },
                            {
                                "parameterName": "item_brand",
                                "value": "Google"
                            }
                        ],
                        "itemId": "SKU_12346",
                        "quantity": 2,
                        "unitPrice": 21.01
                    }
                ]
            },
            "clientId": "9876543210.1761582117",
            "conversionValue": 42.02,
            "currency": "EUR",
            "eventName": "purchase",
            "eventTimestamp": "2025-06-10T23:42:33-05:00",
            "transactionId": "DEF999911111",
            "userData": {
                "userIdentifiers": [
                    {
                        "emailAddress": "3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250"
                    },
                    {
                        "emailAddress": "223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4"
                    },
                    {
                        "address": {
                            "addressLine": "FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579",
                            "administrativeArea": "ca",
                            "city": "mountain view",
                            "familyName": "6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F",
                            "givenName": "2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450",
                            "postalCode": "94043",
                            "regionCode": "US"
                        }
                    }
                ]
            },
            "userId": "user_DEF9876",
            "userProperties": {
                "additionalUserProperties": [
                    {
                        "propertyName": "customer_type",
                        "value": "RETURNING"
                    }
                ]
            }
        }
    ],
    "validateOnly": true
}

NET.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Text.Json;
using CommandLine;
using Google.Ads.DataManager.Util;
using Google.Ads.DataManager.V1;
using Google.Protobuf.WellKnownTypes;
using static Google.Ads.DataManager.V1.ProductAccount.Types;

namespace Google.Ads.DataManager.Samples
{
    // <summary>
    // Sends an <see cref="IngestEventsRequest" /> without using encryption.
    //
    // Event data is read from a data file. See the <c>events_1.json</c> file in the
    // <c>sampledata</c> directory for an example.
    // </summary>
    public class IngestEvents
    {
        private static readonly int MaxEventsPerRequest = 2_000;

        [Verb("ingest-events", HelpText = "Sends an IngestEventsRequest without using encryption.")]
        public class Options
        {
            [Option(
                "operatingAccountType",
                Required = true,
                HelpText = "Account type of the operating account"
            )]
            public AccountType OperatingAccountType { get; set; }

            [Option(
                "operatingAccountId",
                Required = true,
                HelpText = "ID of the operating account"
            )]
            public string OperatingAccountId { get; set; } = null!;

            [Option(
                "loginAccountType",
                Required = false,
                HelpText = "Account type of the login account"
            )]
            public AccountType? LoginAccountType { get; set; }

            [Option("loginAccountId", Required = false, HelpText = "ID of the login account")]
            public string? LoginAccountId { get; set; }

            [Option(
                "linkedAccountProduct",
                Required = false,
                HelpText = "Account type of the linked account"
            )]
            public AccountType? LinkedAccountType { get; set; }

            [Option("linkedAccountId", Required = false, HelpText = "ID of the linked account")]
            public string? LinkedAccountId { get; set; }

            [Option(
                "conversionActionId",
                Required = true,
                HelpText = "ID of the conversion action"
            )]
            public string ConversionActionId { get; set; } = null!;

            [Option(
                "jsonFile",
                Required = true,
                HelpText = "JSON file containing user data to ingest"
            )]
            public string JsonFile { get; set; } = null!;

            [Option(
                "validateOnly",
                Default = true,
                HelpText = "Whether to enable validateOnly on the request"
            )]
            public bool ValidateOnly { get; set; }
        }

        public void Run(Options options)
        {
            RunExample(
                options.OperatingAccountType,
                options.OperatingAccountId,
                options.LoginAccountType,
                options.LoginAccountId,
                options.LinkedAccountType,
                options.LinkedAccountId,
                options.ConversionActionId,
                options.JsonFile,
                options.ValidateOnly
            );
        }

        private void RunExample(
            AccountType operatingAccountType,
            string operatingAccountId,
            AccountType? loginAccountType,
            string? loginAccountId,
            AccountType? linkedAccountType,
            string? linkedAccountId,
            string conversionActionId,
            string jsonFile,
            bool validateOnly
        )
        {
            if (loginAccountId == null ^ loginAccountType == null)
            {
                throw new ArgumentException(
                    "Must specify either both or neither of login account ID and login account "
                        + "type"
                );
            }
            if (linkedAccountId == null ^ linkedAccountType == null)
            {
                throw new ArgumentException(
                    "Must specify either both or neither of linked account ID and linked account "
                        + "type"
                );
            }

            // Reads member data from the data file.
            List<EventRecord> eventRecords = ReadEventData(jsonFile);
            // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
            UserDataFormatter userDataFormatter = new UserDataFormatter();

            // Builds the events collection for the request.
            var events = new List<Event>();
            foreach (var eventRecord in eventRecords)
            {
                var eventBuilder = new Event();

                try
                {
                    eventBuilder.EventTimestamp = Timestamp.FromDateTime(
                        DateTime.Parse(eventRecord.Timestamp ?? "").ToUniversalTime()
                    );
                }
                catch (FormatException)
                {
                    Console.WriteLine(
                        $"Skipping event with invalid timestamp: {eventRecord.Timestamp}"
                    );
                    continue;
                }

                if (string.IsNullOrEmpty(eventRecord.TransactionId))
                {
                    Console.WriteLine("Skipping event with no transaction ID");
                    continue;
                }
                eventBuilder.TransactionId = eventRecord.TransactionId;

                if (!string.IsNullOrEmpty(eventRecord.EventSource))
                {
                    if (
                        System.Enum.TryParse(
                            eventRecord.EventSource,
                            true,
                            out EventSource eventSource
                        )
                    )
                    {
                        eventBuilder.EventSource = eventSource;
                    }
                    else
                    {
                        Console.WriteLine(
                            $"Skipping event with invalid event source: {eventRecord.EventSource}"
                        );
                        continue;
                    }
                }

                if (!string.IsNullOrEmpty(eventRecord.Gclid))
                {
                    eventBuilder.AdIdentifiers = new AdIdentifiers { Gclid = eventRecord.Gclid };
                }

                if (!string.IsNullOrEmpty(eventRecord.Currency))
                {
                    eventBuilder.Currency = eventRecord.Currency;
                }

                if (eventRecord.Value.HasValue)
                {
                    eventBuilder.ConversionValue = eventRecord.Value.Value;
                }

                var userDataBuilder = new UserData();

                // Adds a UserIdentifier for each valid email address for the eventRecord.
                if (eventRecord.Emails != null)
                {
                    foreach (var email in eventRecord.Emails)
                    {
                        try
                        {
                            string preparedEmail = userDataFormatter.ProcessEmailAddress(
                                email,
                                UserDataFormatter.Encoding.Hex
                            );
                            // Adds an email address identifier with the encoded email hash.
                            userDataBuilder.UserIdentifiers.Add(
                                new UserIdentifier { EmailAddress = preparedEmail }
                            );
                        }
                        catch (ArgumentException)
                        {
                            // Skips invalid input.
                            continue;
                        }
                    }
                }

                // Adds a UserIdentifier for each valid phone number for the eventRecord.
                if (eventRecord.PhoneNumbers != null)
                {
                    foreach (var phoneNumber in eventRecord.PhoneNumbers)
                    {
                        try
                        {
                            string preparedPhoneNumber = userDataFormatter.ProcessPhoneNumber(
                                phoneNumber,
                                UserDataFormatter.Encoding.Hex
                            );
                            // Adds a phone number identifier with the encoded phone hash.
                            userDataBuilder.UserIdentifiers.Add(
                                new UserIdentifier { PhoneNumber = preparedPhoneNumber }
                            );
                        }
                        catch (ArgumentException)
                        {
                            // Skips invalid input.
                            continue;
                        }
                    }
                }

                if (userDataBuilder.UserIdentifiers.Any())
                {
                    eventBuilder.UserData = userDataBuilder;
                }
                events.Add(eventBuilder);
            }

            // Builds the Destination for the request.
            var destinationBuilder = new Destination
            {
                OperatingAccount = new ProductAccount
                {
                    AccountType = operatingAccountType,
                    AccountId = operatingAccountId,
                },
                ProductDestinationId = conversionActionId,
            };

            if (loginAccountType.HasValue && loginAccountId != null)
            {
                destinationBuilder.LoginAccount = new ProductAccount
                {
                    AccountType = loginAccountType.Value,
                    AccountId = loginAccountId,
                };
            }

            if (linkedAccountType.HasValue && linkedAccountId != null)
            {
                destinationBuilder.LinkedAccount = new ProductAccount
                {
                    AccountType = linkedAccountType.Value,
                    AccountId = linkedAccountId,
                };
            }

            IngestionServiceClient ingestionServiceClient = IngestionServiceClient.Create();

            int requestCount = 0;

            // Batches requests to send up to the maximum number of events per request.
            for (var i = 0; i < events.Count; i += MaxEventsPerRequest)
            {
                IEnumerable<Event> batch = events.Skip(i).Take(MaxEventsPerRequest);
                requestCount++;
                var request = new IngestEventsRequest
                {
                    Destinations = { destinationBuilder },
                    // Adds events from the current batch.
                    Events = { batch },
                    Consent = new Consent
                    {
                        AdPersonalization = ConsentStatus.ConsentGranted,
                        AdUserData = ConsentStatus.ConsentGranted,
                    },
                    // Sets validate_only. If true, then the Data Manager API only validates the
                    // request but doesn't apply changes.
                    ValidateOnly = validateOnly,
                    Encoding = V1.Encoding.Hex,
                };

                // Sends the data to the Data Manager API.
                IngestEventsResponse response = ingestionServiceClient.IngestEvents(request);
                Console.WriteLine($"Response for request #{requestCount}:\n{response}");

                if (response.FieldWarnings.Any())
                {
                    Console.WriteLine(
                        "Request ingested successfully, but field warnings were returned. "
                            + "Review warning details and update your implementation as needed."
                    );
                }
            }
            Console.WriteLine($"# of requests sent: {requestCount}");
        }

        private class EventRecord
        {
            public List<string>? Emails { get; set; }
            public List<string>? PhoneNumbers { get; set; }
            public string? Timestamp { get; set; }
            public string? TransactionId { get; set; }
            public string? EventSource { get; set; }
            public double? Value { get; set; }
            public string? Currency { get; set; }
            public string? Gclid { get; set; }
        }

        private List<EventRecord> ReadEventData(string jsonFile)
        {
            string jsonString = File.ReadAllText(jsonFile);
            var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
            return JsonSerializer.Deserialize<List<EventRecord>>(jsonString, options)
                ?? new List<EventRecord>();
        }
    }
}

جافا

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.ads.datamanager.samples;

import com.beust.jcommander.Parameter;
import com.google.ads.datamanager.samples.common.BaseParamsConfig;
import com.google.ads.datamanager.util.UserDataFormatter;
import com.google.ads.datamanager.util.UserDataFormatter.Encoding;
import com.google.ads.datamanager.v1.AdIdentifiers;
import com.google.ads.datamanager.v1.Consent;
import com.google.ads.datamanager.v1.ConsentStatus;
import com.google.ads.datamanager.v1.Destination;
import com.google.ads.datamanager.v1.Event;
import com.google.ads.datamanager.v1.EventSource;
import com.google.ads.datamanager.v1.IngestEventsRequest;
import com.google.ads.datamanager.v1.IngestEventsResponse;
import com.google.ads.datamanager.v1.IngestionServiceClient;
import com.google.ads.datamanager.v1.ProductAccount;
import com.google.ads.datamanager.v1.ProductAccount.AccountType;
import com.google.ads.datamanager.v1.UserData;
import com.google.ads.datamanager.v1.UserIdentifier;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.reflect.TypeToken;
import com.google.gson.GsonBuilder;
import com.google.protobuf.util.Timestamps;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Sends an {@link IngestEventsRequest} without using encryption.
 *
 * <p>Event data is read from a data file. See the {@code events_1.json} file in the {@code
 * resources/sampledata} directory for a sample file.
 */
public class IngestEvents {
  private static final Logger LOGGER = Logger.getLogger(IngestEvents.class.getName());

  /** The maximum number of events allowed per request. */
  private static final int MAX_EVENTS_PER_REQUEST = 2_000;

  private static final class ParamsConfig extends BaseParamsConfig<ParamsConfig> {

    @Parameter(
        names = "--operatingAccountType",
        required = true,
        description = "Account type of the operating account")
    AccountType operatingAccountType;

    @Parameter(
        names = "--operatingAccountId",
        required = true,
        description = "ID of the operating account")
    String operatingAccountId;

    @Parameter(
        names = "--loginAccountType",
        required = false,
        description = "Account type of the login account")
    AccountType loginAccountType;

    @Parameter(
        names = "--loginAccountId",
        required = false,
        description = "ID of the login account")
    String loginAccountId;

    @Parameter(
        names = "--linkedAccountType",
        required = false,
        description = "Account type of the linked account")
    AccountType linkedAccountType;

    @Parameter(
        names = "--linkedAccountId",
        required = false,
        description = "ID of the linked account")
    String linkedAccountId;

    @Parameter(
        names = "--conversionActionId",
        required = true,
        description = "ID of the conversion action")
    String conversionActionId;

    @Parameter(
        names = "--jsonFile",
        required = true,
        description = "JSON file containing user data to ingest")
    String jsonFile;

    @Parameter(
        names = "--validateOnly",
        required = false,
        arity = 1,
        description = "Whether to enable validateOnly on the request")
    boolean validateOnly = true;
  }

  public static void main(String[] args) throws IOException {
    ParamsConfig paramsConfig = new ParamsConfig().parseOrExit(args);
    if ((paramsConfig.loginAccountId == null) != (paramsConfig.loginAccountType == null)) {
      throw new IllegalArgumentException(
          "Must specify either both or neither of login account ID and login account type");
    }
    if ((paramsConfig.linkedAccountId == null) != (paramsConfig.linkedAccountType == null)) {
      throw new IllegalArgumentException(
          "Must specify either both or neither of linked account ID and linked account type");
    }
    new IngestEvents().runExample(paramsConfig);
  }

  /**
   * Runs the example. This sample assumes that the login and operating account are the same.
   *
   * @param params the parameters for the example
   */
  private void runExample(ParamsConfig params) throws IOException {
    // Reads event data from the JSON file.
    List<EventRecord> eventRecords = readEventData(params.jsonFile);

    // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
    UserDataFormatter userDataFormatter = UserDataFormatter.create();

    // Builds the events collection for the request.
    List<Event> events = new ArrayList<>();
    for (EventRecord eventRecord : eventRecords) {
      Event.Builder eventBuilder = Event.newBuilder();
      try {
        eventBuilder.setEventTimestamp(Timestamps.parse(eventRecord.timestamp));
      } catch (ParseException pe) {
        LOGGER.warning(
            () ->
                String.format("Skipping event with invalid timestamp: %s", eventRecord.timestamp));
        continue;
      }

      if (Strings.isNullOrEmpty(eventRecord.transactionId)) {
        LOGGER.warning("Skipping event with no transaction ID");
        continue;
      }
      eventBuilder.setTransactionId(eventRecord.transactionId);
      if (!Strings.isNullOrEmpty(eventRecord.eventSource)) {
        try {
          eventBuilder.setEventSource(EventSource.valueOf(eventRecord.eventSource));
        } catch (IllegalArgumentException iae) {
          LOGGER.warning("Skipping event with invalid event source: " + eventRecord.eventSource);
          continue;
        }
      }

      if (!Strings.isNullOrEmpty(eventRecord.gclid)) {
        eventBuilder.setAdIdentifiers(AdIdentifiers.newBuilder().setGclid(eventRecord.gclid));
      }

      if (!Strings.isNullOrEmpty(eventRecord.currency)) {
        eventBuilder.setCurrency(eventRecord.currency);
      }

      if (eventRecord.value != null) {
        eventBuilder.setConversionValue(eventRecord.value);
      }

      UserData.Builder userDataBuilder = UserData.newBuilder();

      // Adds a UserIdentifier for each valid email address for the eventRecord.
      if (eventRecord.emails != null) {
        for (String email : eventRecord.emails) {
          String preparedEmail;
          try {
            preparedEmail = userDataFormatter.processEmailAddress(email, Encoding.HEX);
          } catch (IllegalArgumentException iae) {
            // Skips invalid input.
            continue;
          }
          // Sets the email address identifier to the encoded email hash.
          userDataBuilder.addUserIdentifiers(
              UserIdentifier.newBuilder().setEmailAddress(preparedEmail));
        }
      }

      // Adds a UserIdentifier for each valid phone number for the eventRecord.
      if (eventRecord.phoneNumbers != null) {
        for (String phoneNumber : eventRecord.phoneNumbers) {
          String preparedPhoneNumber;
          try {
            preparedPhoneNumber = userDataFormatter.processPhoneNumber(phoneNumber, Encoding.HEX);
          } catch (IllegalArgumentException iae) {
            // Skips invalid input.
            continue;
          }
          // Sets the phone number identifier to the encoded phone number hash.
          userDataBuilder.addUserIdentifiers(
              UserIdentifier.newBuilder().setPhoneNumber(preparedPhoneNumber));
        }
      }

      if (userDataBuilder.getUserIdentifiersCount() > 0) {
        eventBuilder.setUserData(userDataBuilder);
      }
      events.add(eventBuilder.build());
    }

    // Builds the Destination for the request.
    Destination.Builder destinationBuilder =
        Destination.newBuilder()
            .setOperatingAccount(
                ProductAccount.newBuilder()
                    .setAccountType(params.operatingAccountType)
                    .setAccountId(params.operatingAccountId))
            .setProductDestinationId(params.conversionActionId);
    if (params.loginAccountType != null && params.loginAccountId != null) {
      destinationBuilder.setLoginAccount(
          ProductAccount.newBuilder()
              .setAccountType(params.loginAccountType)
              .setAccountId(params.loginAccountId));
    }
    if (params.linkedAccountType != null && params.linkedAccountId != null) {
      destinationBuilder.setLinkedAccount(
          ProductAccount.newBuilder()
              .setAccountType(params.linkedAccountType)
              .setAccountId(params.linkedAccountId));
    }

    try (IngestionServiceClient ingestionServiceClient = IngestionServiceClient.create()) {
      int requestCount = 0;
      // Batches requests to send up to the maximum number of events per request.
      for (List<Event> eventsBatch : Lists.partition(events, MAX_EVENTS_PER_REQUEST)) {
        requestCount++;
        // Builds the request.
        IngestEventsRequest request =
            IngestEventsRequest.newBuilder()
                .addDestinations(destinationBuilder)
                // Adds events from the current batch.
                .addAllEvents(eventsBatch)
                .setConsent(
                    Consent.newBuilder()
                        .setAdPersonalization(ConsentStatus.CONSENT_GRANTED)
                        .setAdUserData(ConsentStatus.CONSENT_GRANTED))
                // Sets validate_only. If true, then the Data Manager API only validates the request
                // but doesn't apply changes.
                .setValidateOnly(params.validateOnly)
                // Sets encoding to match the encoding used.
                .setEncoding(com.google.ads.datamanager.v1.Encoding.HEX)
                .build();

        LOGGER.info(() -> String.format("Request:%n%s", request));
        IngestEventsResponse response = ingestionServiceClient.ingestEvents(request);
        LOGGER.info(String.format("Response for request #%d:%n%s", requestCount, response));

        if (response.getFieldWarningsCount() > 0) {
          LOGGER.warning(
              "Request ingested successfully, but field warnings were returned. Review warning"
                  + " details and update your implementation as needed.");
        }
      }

      LOGGER.info("# of requests sent: " + requestCount);
    }
  }

  /** Data object for a single row of input data. */
  @SuppressWarnings("unused")
  private static class EventRecord {
    private List<String> emails;
    private List<String> phoneNumbers;
    private String timestamp;
    private String transactionId;
    private String eventSource;
    private Double value;
    private String currency;
    private String gclid;
  }

  /** Reads the data file and parses each line into a {@link EventRecord} object. */
  private List<EventRecord> readEventData(String jsonFile) throws IOException {
    try (BufferedReader jsonReader =
        Files.newBufferedReader(Paths.get(jsonFile), StandardCharsets.UTF_8)) {
      // Define the type for Gson to deserialize into (List of EventRecord objects)
      Type recordListType = new TypeToken<ArrayList<EventRecord>>() {}.getType();

      // Parse the JSON string from the file into a List of EventRecord objects
      return new GsonBuilder().create().fromJson(jsonReader, recordListType);
    }
  }
}

العقدة

#!/usr/bin/env node
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use strict';

import {IngestionServiceClient} from '@google-ads/datamanager';
import {protos} from '@google-ads/datamanager';
const {
  Event: DataManagerEvent,
  Destination,
  Encoding: DataManagerEncoding,
  EventSource,
  Consent,
  ConsentStatus,
  IngestEventsRequest,
  ProductAccount,
  UserData,
  UserIdentifier,
} = protos.google.ads.datamanager.v1;
import {UserDataFormatter, Encoding} from '@google-ads/datamanager-util';
import * as fs from 'fs';
import * as yargs from 'yargs';

const MAX_EVENTS_PER_REQUEST = 10000;

interface Arguments {
  operating_account_type: string;
  operating_account_id: string;
  conversion_action_id: string;
  json_file: string;
  validate_only: boolean;
  login_account_type?: string;
  login_account_id?: string;
  linked_account_type?: string;
  linked_account_id?: string;
  [x: string]: unknown;
}

interface EventRow {
  timestamp: string;
  transactionId: string;
  eventSource?: string;
  gclid?: string;
  currency?: string;
  value?: number;
  emails?: string[];
  phoneNumbers?: string[];
}

/**
 * The main function for the IngestEvents sample.
 */
async function main() {
  const argv: Arguments = yargs
    .option('operating_account_type', {
      describe: 'The account type of the operating account.',
      type: 'string',
      required: true,
    })
    .option('operating_account_id', {
      describe: 'The ID of the operating account.',
      type: 'string',
      required: true,
    })
    .option('conversion_action_id', {
      describe: 'The ID of the conversion action.',
      type: 'string',
      required: true,
    })
    .option('json_file', {
      describe: 'JSON file containing user data to ingest.',
      type: 'string',
      required: true,
    })
    .option('validate_only', {
      describe: 'Whether to enable validate_only on the request.',
      type: 'boolean',
      default: true,
    })
    .option('login_account_type', {
      describe: 'The account type of the login account.',
      type: 'string',
    })
    .option('login_account_id', {
      describe: 'The ID of the login account.',
      type: 'string',
    })
    .option('linked_account_type', {
      describe: 'The account type of the linked account.',
      type: 'string',
    })
    .option('linked_account_id', {
      describe: 'The ID of the linked account.',
      type: 'string',
    })
    .option('config', {
      describe: 'Path to a JSON file with arguments.',
      type: 'string',
    })
    .config('config')
    .check((args: Arguments) => {
      if (
        (args.login_account_type && !args.login_account_id) ||
        (!args.login_account_type && args.login_account_id)
      ) {
        throw new Error(
          'Must specify either both or neither of login account type ' +
            'and login account ID',
        );
      }
      if (
        (args.linked_account_type && !args.linked_account_id) ||
        (!args.linked_account_type && args.linked_account_id)
      ) {
        throw new Error(
          'Must specify either both or neither of linked account type ' +
            'and linked account ID',
        );
      }
      return true;
    })
    .parseSync();

  // Reads event data from the JSON file.
  const eventRows: EventRow[] = readEventDataFile(argv.json_file);

  // Builds the events collection for the request.
  const events = [];
  const formatter = new UserDataFormatter();
  for (const eventRow of eventRows) {
    const event = DataManagerEvent.create();
    try {
      const date = new Date(eventRow.timestamp);
      event.eventTimestamp = {
        seconds: Math.floor(date.getTime() / 1000),
        nanos: (date.getTime() % 1000) * 1e6,
      };
    } catch (e) {
      console.warn(
        `Invalid timestamp format: ${eventRow.timestamp}. Skipping row.`,
      );
      continue;
    }

    if (!eventRow.transactionId) {
      console.warn('Skipping event with no transaction ID');
      continue;
    }
    event.transactionId = eventRow.transactionId;

    if (eventRow.eventSource) {
      const eventSourceEnumValue: number | undefined =
        EventSource[eventRow.eventSource as keyof typeof EventSource];
      if (eventSourceEnumValue === undefined) {
        console.warn(
          `Skipping event with invalid event_source: ${eventRow.eventSource}`,
        );
        continue;
      }
      event.eventSource = eventSourceEnumValue;
    }

    if (eventRow.gclid) {
      event.adIdentifiers = {gclid: eventRow.gclid};
    }

    if (eventRow.currency) {
      event.currency = eventRow.currency;
    }

    if (eventRow.value) {
      event.conversionValue = eventRow.value;
    }

    const userData = UserData.create();
    // Adds a UserIdentifier for each valid email address for the eventRecord.
    if (eventRow.emails) {
      for (const email of eventRow.emails) {
        try {
          const processedEmail = formatter.processEmailAddress(
            email,
            Encoding.HEX,
          );
          userData.userIdentifiers.push(
            UserIdentifier.create({emailAddress: processedEmail}),
          );
        } catch (e) {
          console.warn(`Invalid email address: ${email}. Skipping.`);
        }
      }
    }

    // Adds a UserIdentifier for each valid phone number for the eventRecord.
    if (eventRow.phoneNumbers) {
      for (const phoneNumber of eventRow.phoneNumbers) {
        try {
          const processedPhone = formatter.processPhoneNumber(
            phoneNumber,
            Encoding.HEX,
          );
          userData.userIdentifiers.push(
            UserIdentifier.create({phoneNumber: processedPhone}),
          );
        } catch (e) {
          console.warn(`Invalid phone: ${phoneNumber}. Skipping.`);
        }
      }
    }

    if (userData.userIdentifiers.length > 0) {
      event.userData = userData;
    }

    events.push(event);
  }

  // Sets up the Destination.
  const operatingAccountType = convertToAccountType(
    argv.operating_account_type,
    'operating_account_type',
  );

  const destination = Destination.create({
    operatingAccount: ProductAccount.create({
      accountType: operatingAccountType,
      accountId: argv.operating_account_id,
    }),
    productDestinationId: argv.conversion_action_id,
  });

  // The login account is optional.
  if (argv.login_account_type) {
    const loginAccountType = convertToAccountType(
      argv.login_account_type,
      'login_account_type',
    );
    destination.loginAccount = ProductAccount.create({
      accountType: loginAccountType,
      accountId: argv.login_account_id,
    });
  }

  // The linked account is optional.
  if (argv.linked_account_type) {
    const linkedAccountType = convertToAccountType(
      argv.linked_account_type,
      'linked_account_type',
    );
    destination.linkedAccount = ProductAccount.create({
      accountType: linkedAccountType,
      accountId: argv.linked_account_id,
    });
  }

  const client = new IngestionServiceClient();

  let requestCount = 0;
  // Batches requests to send up to the maximum number of events per request.
  for (let i = 0; i < events.length; i += MAX_EVENTS_PER_REQUEST) {
    requestCount++;
    const eventsBatch = events.slice(i, i + MAX_EVENTS_PER_REQUEST);

    // Builds the request.
    const request = IngestEventsRequest.create({
      destinations: [destination],
      // Adds events from the current batch.
      events: eventsBatch,
      consent: Consent.create({
        adUserData: ConsentStatus.CONSENT_GRANTED,
        adPersonalization: ConsentStatus.CONSENT_GRANTED,
      }),
      // Sets encoding to match the encoding used.
      encoding: DataManagerEncoding.HEX,
      // Sets validate_only. If true, then the Data Manager API only validates the request
      validateOnly: argv.validate_only,
    });

    const [response] = await client.ingestEvents(request);
    console.log(`Response for request #${requestCount}:\n`, response);

    if (response.fieldWarnings && response.fieldWarnings.length > 0) {
      console.warn(
        'Request ingested successfully, but field warnings were returned. ' +
          'Review warning details and update your implementation as needed.',
      );
    }
  }
  console.log(`# of requests sent: ${requestCount}`);
}

/**
 * Reads the event data from the given JSON file.
 * @param {string} jsonFile The path to the JSON file.
 * @return {EventRow[]} An array of event data.
 */
function readEventDataFile(jsonFile: string): EventRow[] {
  const fileContent = fs.readFileSync(jsonFile, 'utf8');
  return JSON.parse(fileContent);
}

/**
 * Validates that a given string is an enum value for the AccountType enum, and
 * if validation passes, returns the AccountType enum value.
 * @param proposedValue the name of an AccountType enum value
 * @param paramName the name of the parameter to use in the error message if validation fails
 * @returns {protos.google.ads.datamanager.v1.ProductAccount.AccountType} The corresponding enum value.
 * @throws {Error} If the string is not an AccountType enum value.
 */
function convertToAccountType(
  proposedValue: string,
  paramName: string,
): protos.google.ads.datamanager.v1.ProductAccount.AccountType {
  const AccountType = ProductAccount.AccountType;
  const accountTypeEnumNames = Object.keys(AccountType).filter(key =>
    isNaN(Number(key)),
  );
  if (!accountTypeEnumNames.includes(proposedValue)) {
    throw new Error(`Invalid ${paramName}: ${proposedValue}`);
  }
  return AccountType[proposedValue as keyof typeof AccountType];
}

if (require.main === module) {
  main().catch(console.error);
}

PHP

<?php
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * Sample of sending an IngestEventsRequest without encryption.
 */

require_once dirname(__DIR__, 1) . '/vendor/autoload.php';

use Google\Ads\DataManager\V1\AdIdentifiers;
use Google\Ads\DataManager\V1\Client\IngestionServiceClient;
use Google\Ads\DataManager\V1\Consent;
use Google\Ads\DataManager\V1\ConsentStatus;
use Google\Ads\DataManager\V1\Destination;
use Google\Ads\DataManager\V1\Encoding as DataManagerEncoding;
use Google\Ads\DataManager\V1\Event;
use Google\Ads\DataManager\V1\EventSource;
use Google\Ads\DataManager\V1\IngestEventsRequest;
use Google\Ads\DataManager\V1\ProductAccount;
use Google\Ads\DataManager\V1\ProductAccount\AccountType;
use Google\Ads\DataManager\V1\UserData;
use Google\Ads\DataManager\V1\UserIdentifier;
use Google\Ads\DataManagerUtil\Encoding;
use Google\Ads\DataManagerUtil\Formatter;
use Google\ApiCore\ApiException;
use Google\Protobuf\Timestamp;

// The maximum number of events allowed per request.
const MAX_EVENTS_PER_REQUEST = 2000;

/**
 * Reads the JSON-formatted event data file.
 *
 * @param string $jsonFile The event data file.
 * @return array A list of associative arrays, each representing an event.
 */
function readEventDataFile(string $jsonFile): array
{
    $jsonContent = file_get_contents($jsonFile);
    if ($jsonContent === false) {
        throw new \RuntimeException(sprintf('Could not read JSON file: %s', $jsonFile));
    }
    $events = json_decode($jsonContent, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new \RuntimeException(sprintf('Invalid JSON in file: %s', $jsonFile));
    }
    return $events;
}

/**
 * Runs the sample.
 *
 * @param int $operatingAccountType The account type of the operating account.
 * @param string $operatingAccountId The ID of the operating account.
 * @param string $conversionActionId The ID of the conversion action.
 * @param string $jsonFile The JSON file containing event data.
 * @param bool $validateOnly Whether to enable validateOnly on the request.
 * @param int|null $loginAccountType The account type of the login account.
 * @param string|null $loginAccountId The ID of the login account.
 * @param int|null $linkedAccountType The account type of the linked account.
 * @param string|null $linkedAccountId The ID of the linked account.
 */
function main(
    int $operatingAccountType,
    string $operatingAccountId,
    string $conversionActionId,
    string $jsonFile,
    bool $validateOnly,
    ?int $loginAccountType = null,
    ?string $loginAccountId = null,
    ?int $linkedAccountType = null,
    ?string $linkedAccountId = null
): void {
    // Reads event data from the data file.
    $eventRecords = readEventDataFile($jsonFile);

    // Gets an instance of the UserDataFormatter for normalizing and formatting the data.
    $formatter = new Formatter();

    // Builds the events collection for the request.
    $events = [];
    foreach ($eventRecords as $eventRecord) {
        $event = new Event();

        if (empty($eventRecord['timestamp'])) {
            error_log('Skipping event with no timestamp.');
            continue;
        }
        try {
            $dateTime = new DateTime($eventRecord['timestamp']);
            $timestamp = new Timestamp();
            $timestamp->fromDateTime($dateTime);
            $event->setEventTimestamp($timestamp);
        } catch (\Exception $e) {
            error_log(sprintf('Skipping event with invalid timestamp: %s', $eventRecord['timestamp']));
            continue;
        }

        if (empty($eventRecord['transactionId'])) {
            error_log('Skipping event with no transaction ID');
            continue;
        }
        $event->setTransactionId($eventRecord['transactionId']);

        if (!empty($eventRecord['eventSource'])) {
            try {
                $event->setEventSource(EventSource::value($eventRecord['eventSource']));
            } catch (\UnexpectedValueException $e) {
                error_log('Skipping event with invalid event source: ' . $eventRecord['eventSource']);
                continue;
            }
        }

        if (!empty($eventRecord['gclid'])) {
            $event->setAdIdentifiers((new AdIdentifiers())->setGclid($eventRecord['gclid']));
        }

        if (!empty($eventRecord['currency'])) {
            $event->setCurrency($eventRecord['currency']);
        }

        if (isset($eventRecord['value'])) {
            $event->setConversionValue($eventRecord['value']);
        }

        $userData = new UserData();
        $identifiers = [];

        if (!empty($eventRecord['emails'])) {
            foreach ($eventRecord['emails'] as $email) {
                try {
                    $preparedEmail = $formatter->processEmailAddress($email, Encoding::Hex);
                    $identifiers[] = (new UserIdentifier())->setEmailAddress($preparedEmail);
                } catch (\InvalidArgumentException $e) {
                    // Skips invalid input.
                    error_log(sprintf('Skipping invalid email: %s', $e->getMessage()));
                    continue;
                }
            }
        }

        if (!empty($eventRecord['phoneNumbers'])) {
            foreach ($eventRecord['phoneNumbers'] as $phoneNumber) {
                try {
                    $preparedPhoneNumber = $formatter->processPhoneNumber($phoneNumber, Encoding::Hex);
                    $identifiers[] = (new UserIdentifier())->setPhoneNumber($preparedPhoneNumber);
                } catch (\InvalidArgumentException $e) {
                    // Skips invalid input.
                    error_log(sprintf('Skipping invalid phone number: %s', $e->getMessage()));
                    continue;
                }
            }
        }

        if (!empty($identifiers)) {
            $userData->setUserIdentifiers($identifiers);
            $event->setUserData($userData);
        }
        $events[] = $event;
    }

    // Builds the destination for the request.
    $destination = (new Destination())
        ->setOperatingAccount((new ProductAccount())
            ->setAccountType($operatingAccountType)
            ->setAccountId($operatingAccountId))
        ->setProductDestinationId($conversionActionId);

    if ($loginAccountType !== null && $loginAccountId !== null) {
        $destination->setLoginAccount((new ProductAccount())
            ->setAccountType($loginAccountType)
            ->setAccountId($loginAccountId));
    }

    if ($linkedAccountType !== null && $linkedAccountId !== null) {
        $destination->setLinkedAccount((new ProductAccount())
            ->setAccountType($linkedAccountType)
            ->setAccountId($linkedAccountId));
    }

    $client = new IngestionServiceClient();
    try {
        $requestCount = 0;
        // Batches requests to send up to the maximum number of events per request.
        foreach (array_chunk($events, MAX_EVENTS_PER_REQUEST) as $eventsBatch) {
            $requestCount++;
            // Builds the request.
            $request = (new IngestEventsRequest())
                ->setDestinations([$destination])
                ->setEvents($eventsBatch)
                ->setConsent((new Consent())
                    ->setAdUserData(ConsentStatus::CONSENT_GRANTED)
                    ->setAdPersonalization(ConsentStatus::CONSENT_GRANTED)
                )
                ->setValidateOnly($validateOnly)
                ->setEncoding(DataManagerEncoding::HEX);

            echo "Request:\n" . json_encode(json_decode($request->serializeToJsonString()), JSON_PRETTY_PRINT) . "\n";
            $response = $client->ingestEvents($request);
            echo "Response for request #{$requestCount}:\n" . json_encode(json_decode($response->serializeToJsonString()), JSON_PRETTY_PRINT) . "\n";

            if (count($response->getFieldWarnings()) > 0) {
                echo 'Request ingested successfully, but field warnings were returned. '
                    . "Review warning details and update your implementation as needed.\n";
            }
        }
        echo "# of requests sent: {$requestCount}\n";
    } catch (ApiException $e) {
        echo 'Error sending request: ' . $e->getMessage() . "\n";
    } finally {
        $client->close();
    }
}

// Command-line argument parsing
$options = getopt(
    '',
    [
        'operating_account_type:',
        'operating_account_id:',
        'login_account_type::',
        'login_account_id::',
        'linked_account_type::',
        'linked_account_id::',
        'conversion_action_id:',
        'json_file:',
        'validate_only::'
    ]
);

$operatingAccountType = $options['operating_account_type'] ?? null;
$operatingAccountId = $options['operating_account_id'] ?? null;
$conversionActionId = $options['conversion_action_id'] ?? null;
$jsonFile = $options['json_file'] ?? null;

// Only validates requests by default.
$validateOnly = true;
if (array_key_exists('validate_only', $options)) {
    $value = $options['validate_only'];
    // `getopt` with `::` returns boolean `false` if the option is passed without a value.
    if ($value === false || !in_array($value, ['true', 'false'], true)) {
        echo "Error: --validate_only requires a value of 'true' or 'false'.\n";
        exit(1);
    }
    $validateOnly = ($value === 'true');
}

if (empty($operatingAccountType) || empty($operatingAccountId) || empty($conversionActionId) || empty($jsonFile)) {
    echo 'Usage: php ingest_events.php ' .
        '--operating_account_type=<account_type> ' .
        '--operating_account_id=<account_id> ' .
        '--conversion_action_id=<conversion_action_id> ' .
        "--json_file=<path_to_json>\n" .
        'Optional: --login_account_type=<account_type> --login_account_id=<account_id> ' .
        '--linked_account_type=<account_type> --linked_account_id=<account_id> ' .
        "--validate_only=<true|false>\n";
    exit(1);
}

// Converts the operating account type string to an AccountType enum.
$parsedOperatingAccountType = AccountType::value($operatingAccountType);

if (isset($options['login_account_type']) != isset($options['login_account_id'])) {
    throw new \InvalidArgumentException(
        'Must specify either both or neither of login account type and login account ID'
    );
}

$parsedLoginAccountType = null;
if (isset($options['login_account_type'])) {
    // Converts the login account type string to an AccountType enum.
    $parsedLoginAccountType = AccountType::value($options['login_account_type']);
}

if (isset($options['linked_account_type']) != isset($options['linked_account_id'])) {
    throw new \InvalidArgumentException(
        'Must specify either both or neither of linked account type and linked account ID'
    );
}

$parsedLinkedAccountType = null;
if (isset($options['linked_account_type'])) {
    // Converts the linked account type string to an AccountType enum.
    $parsedLinkedAccountType = AccountType::value($options['linked_account_type']);
}

main(
    $parsedOperatingAccountType,
    $operatingAccountId,
    $conversionActionId,
    $jsonFile,
    $validateOnly,
    $parsedLoginAccountType,
    $options['login_account_id'] ?? null,
    $parsedLinkedAccountType,
    $options['linked_account_id'] ?? null
);

Python

#!/usr/bin/env python
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sample of sending an IngestEventsRequest without encryption."""

import argparse
import json
import logging
from typing import Any, Dict, List, Optional

from google.ads import datamanager_v1
from google.ads.datamanager_util import Formatter
from google.ads.datamanager_util.format import Encoding
from google.protobuf.timestamp_pb2 import Timestamp

_logger = logging.getLogger(__name__)

# The maximum number of events allowed per request.
_MAX_EVENTS_PER_REQUEST = 10_000


def main(
    operating_account_type: datamanager_v1.ProductAccount.AccountType,
    operating_account_id: str,
    conversion_action_id: str,
    json_file: str,
    validate_only: bool,
    login_account_type: Optional[
        datamanager_v1.ProductAccount.AccountType
    ] = None,
    login_account_id: Optional[str] = None,
    linked_account_type: Optional[
        datamanager_v1.ProductAccount.AccountType
    ] = None,
    linked_account_id: Optional[str] = None,
) -> None:
    """Runs the sample.
    Args:
     operating_account_type: the account type of the operating account.
     operating_account_id: the ID of the operating account.
     json_file: the JSON file containing event data.
     validate_only: whether to enable validate_only on the request.
     login_account_type: the account type of the login account.
     login_account_id: the ID of the login account.
     linked_account_type: the account type of the linked account.
     linked_account_id: the ID of the linked account.
    """

    # Gets an instance of the formatter.
    formatter: Formatter = Formatter()

    # Reads the input file.
    event_rows: List[Dict[str, Any]] = read_event_data_file(json_file)
    events: List[datamanager_v1.Event] = []
    for event_row in event_rows:
        event = datamanager_v1.Event()
        try:
            event_timestamp = Timestamp()
            event_timestamp.FromJsonString(str(event_row["timestamp"]))
            event.event_timestamp = event_timestamp
        except ValueError:
            _logger.warning(
                "Invalid timestamp format: %s. Skipping row.",
                event_row["timestamp"],
            )
            continue

        if "transactionId" not in event_row:
            _logger.warning("Skipping event with no transaction ID")
            continue
        event.transaction_id = event_row["transactionId"]

        if "eventSource" in event_row:
            event.event_source = event_row["eventSource"]

        if "gclid" in event_row:
            event.ad_identifiers = datamanager_v1.AdIdentifiers(
                gclid=event_row["gclid"]
            )

        if "currency" in event_row:
            event.currency = event_row["currency"]

        if "value" in event_row:
            event.conversion_value = event_row["value"]

        user_data = datamanager_v1.UserData()
        # Adds a UserIdentifier for each valid email address for the event row.
        if "emails" in event_row:
            for email in event_row["emails"]:
                try:
                    processed_email: str = formatter.process_email_address(
                        email, Encoding.HEX
                    )
                    user_data.user_identifiers.append(
                        datamanager_v1.UserIdentifier(
                            email_address=processed_email
                        )
                    )
                except ValueError:
                    # Skips invalid input.
                    _logger.warning(
                        "Invalid email address: %s. Skipping.",
                        event_row["email_address"],
                    )

        # Adds a UserIdentifier for each valid phone number for the event row.
        if "phoneNumbers" in event_row:
            for phone_number in event_row["phoneNumbers"]:
                try:
                    processed_phone: str = formatter.process_phone_number(
                        phone_number, Encoding.HEX
                    )
                    user_data.user_identifiers.append(
                        datamanager_v1.UserIdentifier(
                            phone_number=processed_phone
                        )
                    )
                except ValueError:
                    # Skips invalid input.
                    _logger.warning(
                        "Invalid phone: %s. Skipping.",
                        event_row["phone_number"],
                    )

        if user_data.user_identifiers:
            event.user_data = user_data

        # Adds the event to the list of events to send in the request.
        events.append(event)

    # Configures the destination.
    destination: datamanager_v1.Destination = datamanager_v1.Destination()
    destination.operating_account.account_type = operating_account_type
    destination.operating_account.account_id = operating_account_id
    destination.product_destination_id = str(conversion_action_id)
    if login_account_type or login_account_id:
        if bool(login_account_type) != bool(login_account_id):
            raise ValueError(
                "Must specify either both or neither of login "
                + "account type and login account ID"
            )
        destination.login_account.account_type = login_account_type
        destination.login_account.account_id = login_account_id
    if linked_account_type or linked_account_id:
        if bool(linked_account_type) != bool(linked_account_id):
            raise ValueError(
                "Must specify either both or neither of linked account "
                + "type and linked account ID"
            )
        destination.linked_account.account_type = linked_account_type
        destination.linked_account.account_id = linked_account_id

    # Creates a client for the ingestion service.
    client: datamanager_v1.IngestionServiceClient = (
        datamanager_v1.IngestionServiceClient()
    )

    # Batches requests to send up to the maximum number of events per
    # request.
    request_count = 0
    for i in range(0, len(events), _MAX_EVENTS_PER_REQUEST):
        request_count += 1
        events_batch = events[i : i + _MAX_EVENTS_PER_REQUEST]
        # Sends the request.
        request: datamanager_v1.IngestEventsRequest = (
            datamanager_v1.IngestEventsRequest(
                destinations=[destination],
                # Adds events from the current batch.
                events=events_batch,
                consent=datamanager_v1.Consent(
                    ad_user_data=datamanager_v1.ConsentStatus.CONSENT_GRANTED,
                    ad_personalization=datamanager_v1.ConsentStatus.CONSENT_GRANTED,
                ),
                # Sets encoding to match the encoding used.
                encoding=datamanager_v1.Encoding.HEX,
                # Sets validate_only. If true, then the Data Manager API only
                # validates the request but doesn't apply changes.
                validate_only=validate_only,
            )
        )

        # Sends the request.
        response: datamanager_v1.IngestEventsResponse = client.ingest_events(
            request=request
        )

        # Logs the response.
        _logger.info("Response for request #%d:\n%s", request_count, response)

        if response.field_warnings:
            _logger.warning(
                "Request ingested successfully, but field warnings were returned. "
                "Review warning details and update your implementation as needed."
            )

    _logger.info("# of requests sent: %d", request_count)


def read_event_data_file(json_file: str) -> List[Dict[str, Any]]:
    """Reads the JSON-formatted event data file.
    Args:
      json_file: the event data file.
    """
    with open(json_file, "r") as f:
        return json.load(f)


if __name__ == "__main__":
    # Configures logging.
    logging.basicConfig(level=logging.INFO)

    parser = argparse.ArgumentParser(
        description=("Sends events from a JSON file to a destination."),
        fromfile_prefix_chars="@",
    )
    # The following argument(s) should be provided to run the example.
    parser.add_argument(
        "--operating_account_type",
        type=str,
        required=True,
        help="The account type of the operating account.",
    )
    parser.add_argument(
        "--operating_account_id",
        type=str,
        required=True,
        help="The ID of the operating account.",
    )
    parser.add_argument(
        "--conversion_action_id",
        type=int,
        required=True,
        help="The ID of the conversion action",
    )
    parser.add_argument(
        "--login_account_type",
        type=str,
        required=False,
        help="The account type of the login account.",
    )
    parser.add_argument(
        "--login_account_id",
        type=str,
        required=False,
        help="The ID of the login account.",
    )
    parser.add_argument(
        "--linked_account_type",
        type=str,
        required=False,
        help="The account type of the linked account.",
    )
    parser.add_argument(
        "--linked_account_id",
        type=str,
        required=False,
        help="The ID of the linked account.",
    )
    parser.add_argument(
        "--json_file",
        type=str,
        required=True,
        help="JSON file containing user data to ingest.",
    )
    parser.add_argument(
        "--validate_only",
        choices=["true", "false"],
        default="true",
        help="""Whether to enable validate_only on the request. Must be
        'true' or 'false'. Defaults to 'true'.""",
    )
    args = parser.parse_args()

    main(
        args.operating_account_type,
        args.operating_account_id,
        args.conversion_action_id,
        args.json_file,
        args.validate_only == "true",
        args.login_account_type,
        args.login_account_id,
        args.linked_account_type,
        args.linked_account_id,
    )

الردود الناجحة

يعرض الطلب الناجح استجابةً تتضمّن عنصرًا يحتوي على requestId. إذا تعذّر التحقّق من صحة أي حقول اختيارية، ستتضمّن الاستجابة أيضًا قائمة fieldWarnings.

ردّ عادي

في ما يلي نموذج ردّ على طلب نقل بيانات ناجح بدون تحذيرات:

{
  "requestId": "126365e1-16d0-4c81-9de9-f362711e250a"
}

الردّ مع ظهور تحذيرات

في ما يلي نموذج ردّ على طلب نقل ناجح يتضمّن تحذيرًا:

{
  "requestId": "126365e1-16d0-4c81-9de9-f362711e250a",
  "fieldWarnings": [
    {
      "field": "events.events[0].cart_data.items[0].merchant_product_id",
      "description": "The merchant product ID is missing in the cart item.",
      "reason": "WARNING_REASON_CART_DATA_ITEM_MERCHANT_PRODUCT_ID_MISSING"
    }
  ]
}

سجِّل requestId الذي تم عرضه لتتمكّن من استرداد بيانات التشخيص أثناء معالجة كل وجهة في الطلب. يجب أيضًا التحقّق من أيّ fieldWarnings للتأكّد من قبول أيّ حقول اختيارية أرسلتها. يُرجى الاطّلاع على تحذيرات الاستيعاب لمزيد من التفاصيل.

الردود على حالات تعذُّر التسليم

يؤدي الطلب الذي يتعذّر إكماله إلى ظهور رمز حالة استجابة خطأ، مثل 400 Bad Request، واستجابة تتضمّن تفاصيل الخطأ.

تستخدم واجهة Data Manager API نموذج الفشل السريع: إذا تعذّر التحقّق من صحة أي حدث، يتعذّر تنفيذ الطلب بالكامل ولا تتم معالجة أي من الأحداث. أصلِح الأخطاء وأعِد إرسال الطلب بالكامل. للتحقّق من هذه الأخطاء بدون نقل البيانات، أرسِل الطلب مع ضبط validateOnly على true.

على سبيل المثال، إذا كان emailAddress يحتوي على سلسلة نصية عادية بدلاً من قيمة مرمّزة بنظام سداسي عشري، سيؤدي ذلك إلى ظهور الاستجابة التالية:

{
  "error": {
    "code": 400,
    "message": "There was a problem with the request.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "INVALID_ARGUMENT",
        "domain": "datamanager.googleapis.com"
      },
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "events.events[0].user_data.user_identifiers",
            "description": "Email is not hex encoded.",
            "reason": "INVALID_HEX_ENCODING"
          }
        ]
      }
    ]
  }
}

إذا لم يتم تجزئة emailAddress وكان بترميز ستة عشري فقط، سيتم عرض الاستجابة التالية:

{
  "error": {
    "code": 400,
    "message": "There was a problem with the request.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
        "reason": "INVALID_ARGUMENT",
        "domain": "datamanager.googleapis.com"
      },
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "field": "events.events[0]",
            "reason": "INVALID_SHA256_FORMAT"
          }
        ]
      }
    ]
  }
}

إرسال الأحداث إلى وجهات متعددة

إذا كانت بياناتك تحتوي على أحداث لوجهات مختلفة، يمكنك إرسالها في الطلب نفسه باستخدام مراجع الوجهات. يُرجى الاطّلاع على الحدود القصوى والحصص لمعرفة الحدّ الأقصى لعدد الوجهات لكل طلب.

على سبيل المثال، إذا كان لديك حدث لرقم تعريف إجراء الإحالة الناجحة 123456789 وحدث آخر لرقم تعريف إجراء الإحالة الناجحة 777111122، أرسِل الحدثَين في طلب واحد من خلال ضبط reference لكل Destination. يتم تحديد reference من قِبل المستخدم. الشرط الوحيد هو أن يكون لكل Destination سمة reference فريدة. إليك قائمة destinations المعدَّلة للطلب:

  "destinations": [
    {
      "operatingAccount": {
        "accountType": "OPERATING_ACCOUNT_TYPE",
        "accountId": "OPERATING_ACCOUNT_ID"
      },
      "loginAccount": {
        "accountType": "LOGIN_ACCOUNT_TYPE",
        "accountId": "LOGIN_ACCOUNT_ID"
      },
      "productDestinationId": "123456789",
      "reference": "destination_a"
    },
    {
      "operatingAccount": {
        "accountType": "OPERATING_ACCOUNT_2_TYPE",
        "accountId": "OPERATING_ACCOUNT_2_ID"
      },
      "loginAccount": {
        "accountType": "LOGIN_ACCOUNT_2_TYPE",
        "accountId": "LOGIN_ACCOUNT_2_ID"
      },
      "productDestinationId": "777111122",
      "reference": "destination_b"
    }
  ]

اضبط السمة destinationReferences لكل Event لإرسالها إلى وجهة واحدة أو أكثر محدّدة. على سبيل المثال، إليك Event مخصّص فقط لأول Destination، لذا تحتوي قائمة destinationReferences الخاصة به على reference لأول Destination فقط:

{
   "adIdentifiers": {
      "gclid": "GCLID_1"
   },
   "conversionValue": 1.99,
   "currency": "USD",
   "eventTimestamp": "2025-06-10T20:07:01Z",
   "transactionId": "ABC798654321",
   "eventSource": "WEB",
   "destinationReferences": [
      "destination_a"
   ]
}

الحقل destinationReferences هو قائمة، لذا يمكنك تحديد وجهات متعددة لحدث معيّن. إذا لم تضبط السمة destinationReferences لـ Event، سترسل Data Manager API الحدث إلى جميع الوجهات في الطلب.

إذا كان للحدث وجهات متعددة، ترسل واجهة Data Manager API الحقول ذات الصلة إلى كل وجهة. على سبيل المثال، إذا كان الحدث يتضمّن وجهة على "إعلانات Google" ووجهة على "إحصاءات Google"، تتضمّن واجهة برمجة التطبيقات حقول "إحصاءات Google"، مثل clientId أو appInstanceId أو eventName، عند إرسال الحدث إلى وجهة "إحصاءات Google"، وتتضمّن حقول "إعلانات Google"، مثل customVariables، عند إرسال الحدث إلى وجهة "إعلانات Google".

الخطوات التالية