您可以完成本快速入門導覽課程,熟悉如何傳送事件資料。
在下列任一情境中,使用 Data Manager API:
將 Google Ads 代碼轉換或 Google Analytics
purchase事件做為代碼轉換的額外資料來源,提升廣告互動信號、強化資料和整體成效。所有 Google Ads 帳戶都能使用這項功能,但只有在許可清單上的 Google Analytics 資源才能使用。如有意將 Google Analytics 資源加入允許清單,請填寫這份表單。
選擇要查看的指南版本:
在本快速入門導覽課程中,您將完成下列步驟:
- 準備
Destination接收事件資料。 - 準備要傳送的事件資料。
- 為事件建立
IngestionService要求。 - 使用 Google APIs Explorer 傳送要求。
- 瞭解成功和失敗的回應。
準備目的地
傳送資料前,您必須先準備至少一個資料的 Destination。以下是可供使用的 Destination 範例:
{
"operatingAccount": {
"accountType": "OPERATING_ACCOUNT_TYPE",
"accountId": "OPERATING_ACCOUNT_ID"
},
"loginAccount": {
"accountType": "LOGIN_ACCOUNT_TYPE",
"accountId": "LOGIN_ACCOUNT_ID"
},
"productDestinationId": "PRODUCT_DESTINATION_ID"
}
Destination 的欄位如下:如需更多詳細資料和不同情境的目標範例,請參閱「設定目的地」。
operatingAccount接收事件的帳戶。
如果是以額外資料來源傳送的事件,作業帳戶可以是 Google Ads 帳戶或 Google Analytics 資源。
如果
accountType為GOOGLE_ANALYTICS_PROPERTY,則要求憑證必須屬於 Google Analytics 使用者,且該使用者具備資源的編輯者或管理員角色。如果是離線轉換和待開發客戶強化轉換,則運作中的帳戶必須是 Google Ads 帳戶。
loginAccount- 憑證的 Google 帳戶是使用者的帳戶。
productDestinationId接收事件的
operatingAccount中實體的 ID。如果事件是做為額外資料來源傳送,
productDestinationId必須是下列其中一項:Google Ads 轉換的 ID,其中
type設為WEBPAGE。在 Google Ads 使用者介面中,WEBPAGE轉換動作的「轉換來源」為「網站」。Google Analytics 網站串流的評估 ID。您無法將事件做為額外資料來源,傳送至 Google Analytics iOS 應用程式或 Android 應用程式串流。
如果是離線轉換或待開發客戶強化轉換,
productDestinationId必須是 Google Ads 轉換動作的 ID,且type設為UPLOAD_CLICKS。在 Google Ads 使用者介面中,UPLOAD_CLICKS轉換動作的「轉換來源」為「網站 (從點擊匯入)」。
本指南中的範例說明如何建構要求,將所有事件傳送至相同目的地。如要在同一個要求中傳送多個目的地的事件,請參閱「傳送多個目的地的事件」。
準備事件資料
請參考下列事件資料。每個表格都對應一個轉換事件。每個轉換事件都有事件的時間戳記、轉換動作和轉換價值。
每個事件可能都有廣告 ID (例如 gclid) 或使用者 ID (例如電子郵件地址、電話號碼和地址資訊)。事件也可以有:
- 在事件發生時評估使用者相關資訊,例如顧客價值,或是新顧客、回訪顧客或重新參與的顧客。
- 購物車資料。
- 目的地適用的其他事件參數或使用者屬性,例如 Google Analytics 的
client_id或user_id。
活動資料如下:
事件 1
| 活動 #1 | |
|---|---|
conversion_time |
2025-06-10 15:07:01-05:00 |
conversion_action_id |
123456789 |
transaction_id |
ABC798654321 |
conversion_value |
30.03 |
currency |
USD |
gclid |
GCLID_1 |
emails |
|
given_name |
John |
family_name |
Smith-Jones |
region_code |
us |
postal_code |
94045 |
customer_type |
NEW |
customer_value_bucket |
HIGH |
client_id |
1234567890.1761581763 |
user_id |
user_ABC12345 |
ad_unit_name |
Banner_01 |
event_name |
purchase |
| 購物車商品 | |
item_id |
SKU_12345 |
item_name |
Stan and Friends Tee |
item_affiliation |
Google Merchandise Store |
item_coupon |
SUMMER_FUN |
item_discount |
2.22 |
item_index |
0 |
item_brand |
Google |
item_category |
Apparel |
item_category2 |
Adult |
item_category3 |
Shirts |
item_category4 |
Crew |
item_category5 |
Short sleeve |
item_list_id |
related_products |
item_list_name |
Related Products |
item_price |
10.01 |
item_quantity |
3 |
事件 2
| 活動 2 | |
|---|---|
conversion_time |
June 10, 2025 11:42:33PM America/New_York |
conversion_action_id |
123456789 |
transaction_id |
DEF999911111 |
conversion_value |
42.02 |
currency |
eur |
gclid |
GCLID_2 |
emails |
|
given_name |
zoë |
family_name |
pérez |
region_code |
PT |
postal_code |
1229-076 |
customer_type |
RETURNING |
client_id |
9876543210.1761582117 |
user_id |
user_DEF9876 |
ad_unit_name |
Banner_02 |
event_name |
purchase |
| 購物車商品 | |
item_id |
SKU_12346 |
item_name |
Google Grey Women's Tee |
item_affiliation |
Google Merchandise Store |
item_coupon |
SUMMER_FUN |
item_discount |
3.33 |
item_index |
1 |
item_brand |
Google |
item_category |
Apparel |
item_category2 |
Adult |
item_category3 |
Shirts |
item_category4 |
Crew |
item_category5 |
Short sleeve |
item_list_id |
related_products |
item_list_name |
Related Products |
item_price |
21.01 |
item_quantity |
2 |
設定資料格式
按照格式設定指南的規定設定欄位格式。以下是格式化後的事件資料:
事件 1
| 活動 #1 | |
|---|---|
conversion_time |
2025-06-10T15:07:01-05:00 |
conversion_action_id |
123456789 |
transaction_id |
ABC798654321 |
conversion_value |
30.03 |
currency |
USD |
gclid |
GCLID_1 |
emails |
|
given_name |
john |
family_name |
smith-jones |
region_code |
US |
postal_code |
94045 |
customer_type |
NEW |
customer_value_bucket |
HIGH |
client_id |
1234567890.1761581763 |
user_id |
user_ABC12345 |
ad_unit_name |
Banner_01 |
event_name |
purchase |
| 購物車商品 | |
item_id |
SKU_12345 |
item_name |
Stan and Friends Tee |
item_affiliation |
Google Merchandise Store |
item_coupon |
SUMMER_FUN |
item_discount |
2.22 |
item_index |
0 |
item_brand |
Google |
item_category |
Apparel |
item_category2 |
Adult |
item_category3 |
Shirts |
item_category4 |
Crew |
item_category5 |
Short sleeve |
item_list_id |
related_products |
item_list_name |
Related Products |
item_price |
10.01 |
item_quantity |
3 |
事件 2
| 活動 2 | |
|---|---|
conversion_time |
2025-06-10T23:42:33-05:00 |
conversion_action_id |
123456789 |
transaction_id |
DEF999911111 |
conversion_value |
42.02 |
currency |
EUR |
gclid |
GCLID_2 |
emails |
|
given_name |
zoë |
family_name |
pérez |
region_code |
PT |
postal_code |
1229-076 |
customer_type |
RETURNING |
client_id |
9876543210.1761582117 |
user_id |
user_DEF9876 |
ad_unit_name |
Banner_02 |
event_name |
purchase |
| 購物車商品 | |
item_id |
SKU_12346 |
item_name |
Google Grey Women's Tee |
item_affiliation |
Google Merchandise Store |
item_coupon |
SUMMER_FUN |
item_discount |
3.33 |
item_index |
1 |
item_brand |
Google |
item_category |
Apparel |
item_category2 |
Adult |
item_category3 |
Shirts |
item_category4 |
Crew |
item_category5 |
Short sleeve |
item_list_id |
related_products |
item_list_name |
Related Products |
item_price |
21.01 |
item_quantity |
2 |
雜湊並編碼資料
此外,格式化的電子郵件地址、名字和姓氏必須使用 SHA-256 演算法進行雜湊處理,並以十六進位或 Base64 編碼。以下是經過格式化、雜湊處理和編碼 (使用十六進位編碼) 的事件資料:
事件 1
| 活動 #1 | |
|---|---|
conversion_time |
2025-06-10T15:07:01-05:00 |
conversion_action_id |
123456789 |
transaction_id |
ABC798654321 |
conversion_value |
30.03 |
currency |
USD |
gclid |
GCLID_1 |
emails |
|
given_name |
96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A |
family_name |
DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081 |
region_code |
US |
postal_code |
94045 |
customer_type |
NEW |
customer_value_bucket |
HIGH |
client_id |
1234567890.1761581763 |
user_id |
user_ABC12345 |
ad_unit_name |
Banner_01 |
event_name |
purchase |
| 購物車商品 | |
item_id |
SKU_12345 |
item_name |
Stan and Friends Tee |
item_affiliation |
Google Merchandise Store |
item_coupon |
SUMMER_FUN |
item_discount |
2.22 |
item_index |
0 |
item_brand |
Google |
item_category |
Apparel |
item_category2 |
Adult |
item_category3 |
Shirts |
item_category4 |
Crew |
item_category5 |
Short sleeve |
item_list_id |
related_products |
item_list_name |
Related Products |
item_price |
10.01 |
item_quantity |
3 |
事件 2
| 活動 2 | |
|---|---|
conversion_time |
2025-06-10T23:42:33-05:00 |
conversion_action_id |
123456789 |
transaction_id |
DEF999911111 |
conversion_value |
42.02 |
currency |
EUR |
gclid |
GCLID_2 |
emails |
|
given_name |
2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450 |
family_name |
6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F |
region_code |
PT |
postal_code |
1229-076 |
customer_type |
RETURNING |
client_id |
9876543210.1761582117 |
user_id |
user_DEF9876 |
ad_unit_name |
Banner_02 |
event_name |
purchase |
| 購物車商品 | |
item_id |
SKU_12346 |
item_name |
Google Grey Women's Tee |
item_affiliation |
Google Merchandise Store |
item_coupon |
SUMMER_FUN |
item_discount |
3.33 |
item_index |
1 |
item_brand |
Google |
item_category |
Apparel |
item_category2 |
Adult |
item_category3 |
Shirts |
item_category4 |
Crew |
item_category5 |
Short sleeve |
item_list_id |
related_products |
item_list_name |
Related Products |
item_price |
21.01 |
item_quantity |
2 |
將資料轉換為 Event 物件
將每個事件的格式化和雜湊處理資料轉換為 Event。請按照指示填入下列欄位:
將
eventTimestamp設為事件發生的時間。Google Analytics 事件的
eventTimestamp必須在過去 72 小時內。根據用途設定必填欄位。
用途 ID transactionIdeventSource離線轉換或待開發客戶強化轉換 必填。設定至少下列其中一項: adIdentifiers至少設定一個gclid、gbraid或wbraid- 工作階段屬性
userData
選用 必填。設為 EventSource的其中一個列舉值。傳送至 Google Ads 目的地做為額外資料來源的事件 必填。設定至少下列其中一項: adIdentifiers至少要設定gclid、gbraid或wbraiduserData
必要 (選用步驟) 如要設定,必須為 WEB。傳送至 Google Analytics 目的地的事件,做為額外資料來源 必填。設定至少下列其中一項: clientIdadIdentifiers已設定gcliduserId
必要 (選用步驟) 如要設定,必須為 WEB。如果您將事件做為額外資料來源傳送至 Google Ads 目的地,請參閱「Google 如何處理額外資料來源的資料」。
填入您有事件值的任何其他欄位。如需可用欄位的完整清單,請參閱
Event參考說明文件。
Google 如何處理額外資料來源的資料
在同一個轉換動作中,Google 會使用 transactionId,將不同來源 (例如網站代碼和 Data Manager API 擷取要求) 傳送的轉換事件進行去重複處理。下表說明系統如何處理擷取要求中的資料。
| 情境 | 資料欄位 | 處理方式 |
|---|---|---|
transactionId 與現有代碼事件相符
|
conversionValue (含 currencyCode) |
已更新。 注意:轉換動作的初始 14 天試用期內,系統會停用價值更新功能。試用期結束前,系統不會在 Google Ads 報表中覆寫代碼價值。 |
transactionId 符合現有代碼事件 |
conversionValue 或 currencyCode 以外的其他欄位 (例如 adIdentifiers.gclid) |
已忽略。其他資料來源的其他欄位值不會覆寫 Google 代碼原本為比對相符交易記錄的欄位值。 |
transactionId 與任何現有事件都「不」相符 |
所有提供的資料 (例如 userData、
conversionValue、currencyCode)
|
用於建立新的轉換事件。Google 接著會嘗試使用您提供的 ID (例如 注意:在最初的 14 天試用期內,這些新建立的轉換會顯示在報表中,但不會用於出價。試用期結束後,這些轉換就會自動用於出價。 |
新增工作階段屬性
如果您要傳送離線轉換或待開發客戶強化轉換,請在沒有其他廣告 ID (例如 Google 點擊 ID 或 WBRAID) 時,加入工作階段屬性。除了其他廣告 ID 之外,您也可以納入工作階段屬性。
工作階段屬性可提供使用者與網站互動的額外背景資訊和信號,有助於提升轉換評估、報表和出價的準確度。
在 Data Manager API 中,您可以使用兩種方法傳送工作階段屬性:
建議:將
adIdentifiers的sessionAttributes欄位設為採用 Base64 編碼的工作階段屬性字串。請按照「如何擷取 session_attributes」一文中的操作說明,修改表單提交頁面,擷取編碼字串。如果無法使用 JavaScript,請擷取個別工作階段屬性欄位,並將每個欄位以個別
ExperimentalField的形式新增至experimentalFields清單:gad_campaignidsession_start_time_usecgad_sourcelanding_page_urllanding_page_referrer
如果
landing_page_user_agent工作階段屬性有值,請在adIdentifiers.landingPageDeviceInfo的userAgent欄位中傳送。傳送個別鍵值組時,請遵循下列最佳做法:
- 持續傳送
gad_campaignid和session_start_time_usec。這些欄位對準確歸因至關重要。 - 請勿提供不準確或不完整的
landing_page_url值,例如預留位置字串、內部應用程式路徑或不完整的網址。如果沒有準確的完整網址,請省略landing_page_url。
以下是範例事件的部分內容,其中包含
experimentalFields、gad_campaignid和session_start_time_usec的項目,以及landingPageDeviceInfo欄位中的使用者代理程式:{ ..., "experimentalFields": [ { "field": "gad_campaignid", "value": "21288051566" }, { "field": "session_start_time_usec", "value": "1767711548052000" } ], "adIdentifiers": { "landingPageDeviceInfo": { "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" } } }
新增 Google Analytics 資訊
如果以額外資料來源傳送的事件目的地包含 Google Analytics 資源,請按照指示填寫下列欄位:
eventName必填。Google Analytics 事件的名稱。
transactionId必要。事件的專屬 ID。
- 至少一個 ID
必須設定下列至少其中一個欄位:
clientId:網頁用戶端使用者執行個體的專屬 ID。請參閱「將事件傳送至 Measurement Protocol」。userId:使用者的專屬 ID。詳情請參閱「利用 User-ID 評估跨平台活動」一文。
destinationReferences如果要求層級的
destinations清單包含多個 Google AnalyticsDestination,則為必填。在destinationReferences中新增項目,指定應接收事件的 Google Analytics 目的地。如要進一步瞭解目的地參照,請參閱「將事件傳送至多個目的地」。如果未設定
destinationReferences,或有多個參照 Google Analytics 目的地的項目,Data Manager API 會拒絕事件,並傳回MULTIPLE_DESTINATIONS_FOR_GOOGLE_ANALYTICS_EVENT錯誤。userId(選用步驟) 使用者的 User-ID。
additionalEventParameters(選填,但建議使用) 填入其他
Event欄位未擷取的任何 Google Analytics 事件參數。參數可包含purchase事件中的其他建議參數,或您想擷取的其他參數。使用EventParameter的parameterNameGoogle Analytics 參數名稱。舉例來說,如果交易有相關稅金,請在
additionalEventParameters中新增項目,並將parameterName設為tax,以及將value設為稅金費用。我們不建議為
transactionId、currency或valueGoogle Analytics 事件參數新增項目。請改為填入Event的transactionId、currency和conversionValue,這些屬性的優先順序高於additionalEventParameters中的任何項目。
為購買事件新增購物車資料
在 Event 的 cartData 欄位中,填入所購商品的相關資訊。為每項購買的商品,在 CartData 的 items 清單中新增 Item 物件,並填入下列欄位:
itemId- 必要。商品的專屬 ID。
unitPrice必填。此為單位價格,不含稅金、運費和事件範圍 (交易層級) 折扣。
如果商品有商品範圍折扣,請使用折扣單價。 舉例來說,如果商品的單價為
27.67,單一折扣為6.66,則請將unitPrice設為21.01。quantity必填。這項商品購買的單位數。
additionalItemParameters請在其他
Item欄位中未擷取的任何項目範圍參數,填入這份清單。使用parameterName的 Google Analytics 項目參數名稱,適用於ItemParameter。舉例來說,如果項目有品牌和類別,請在項目的
additionalItemParameters中新增項目,並將parameterName設為item_brand,將value設為品牌名稱;另一個項目則將parameterName設為item_category,將value設為項目的類別。我們不建議為
quantity、price或item_idGoogle Analytics 項目參數新增項目。請改為填入Item的itemId、unitPrice和quantity,這些屬性的優先順序高於additionalItemParameters中的任何項目。
以下是第二個事件的格式化、雜湊處理和編碼資料範例,其中包含 Google Analytics 的額外資料:Event
{
"adIdentifiers": {
"gclid": "GCLID_2"
},
"conversionValue": 42.02,
"currency": "EUR",
"eventTimestamp": "2025-06-10T23:42:33-05:00",
"transactionId": "DEF999911111",
"eventSource": "WEB",
"userData": {
"userIdentifiers": [
{
"emailAddress": "3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250"
},
{
"emailAddress": "223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4"
},
{
"address": {
"givenName": "2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450",
"familyName": "6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F",
"regionCode": "PT",
"postalCode": "1229-076"
}
}
],
},
"userProperties": {
"customerType": "RETURNING"
},
"eventName": "purchase",
"clientId": "9876543210.1761582117",
"userId": "user_DEF9876",
"additionalEventParameters": [
{
"parameterName": "ad_unit_name",
"value": "Banner_02"
}
],
"cartData": {
"transactionDiscount": 6.66,
"items": [
{
"itemId": "SKU_12346",
"quantity": 2,
"unitPrice": 21.01,
"additionalItemParameters": [
{
"parameterName": "item_name",
"value": "Google Grey Women's Tee"
},
{
"parameterName": "affiliation",
"value": "Google Merchandise Store"
},
{
"parameterName": "coupon",
"value": "SUMMER_FUN"
},
{
"parameterName": "discount",
"value": "3.33"
},
{
"parameterName": "index",
"value": "1"
},
{
"parameterName": "item_brand",
"value": "Google"
},
{
"parameterName": "item_category",
"value": "Apparel"
},
{
"parameterName": "item_category2",
"value": "Adult"
},
{
"parameterName": "item_category3",
"value": "Shirts"
},
{
"parameterName": "item_category4",
"value": "Crew"
},
{
"parameterName": "item_category5",
"value": "Short sleeve"
},
{
"parameterName": "item_list_id",
"value": "related_products"
},
{
"parameterName": "item_list_name",
"value": "Related Products"
}
]
}
]
}
}
建立要求主體
如要建構要求主體,請合併 destinations 和 events,設定 encoding 欄位,並新增您想加入的任何其他要求欄位,例如 validateOnly 和 consent。
本指南中的範例不會使用加密功能,但您可以按照「加密使用者資料」一文中的說明,在程序中加入加密功能。
傳送要求
如要透過瀏覽器提出要求,請按照下列步驟操作:
- 選取「REST」分頁,然後按一下「Open in API Explorer」,在新分頁或視窗中開啟 API Explorer。
- 在 API 探索工具的要求內容中,將開頭為
REPLACE_WITH的每個字串 (例如REPLACE_WITH_OPERATING_ACCOUNT_TYPE) 替換為相關值。 - 按一下 API Explorer 頁面底部的「執行」,然後完成授權提示,即可傳送要求。
- 將
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": "CONVERSION_ACTION_ID" } ], "encoding": "HEX", "events": [ { "adIdentifiers": { "gclid": "GCLID_1" }, "conversionValue": 30.03, "currency": "USD", "eventTimestamp": "2025-06-10T20:07:01Z", "transactionId": "ABC798654321", "eventSource": "WEB", "userData": { "userIdentifiers": [ { "address": { "givenName": "96D9632F363564CC3032521409CF22A852F2032EEC099ED5967C0D000CEC607A", "familyName": "DB98D2607EFFFA28AFF66975868BF54C075ECA7157E35064DCE08E20B85B1081", "regionCode": "US", "postalCode": "94045" } } ] }, "userProperties": { "customerType": "NEW", "customerValueBucket": "HIGH" }, "eventName": "purchase", "clientId": "1234567890.1761581763", "userId": "user_ABC12345", "additionalEventParameters": [ { "parameterName": "ad_unit_name", "value": "Banner_01" } ], "cartData": { "transactionDiscount": 6.66, "items": [ { "itemId": "SKU_12345", "quantity": 3, "unitPrice": 10.01, "additionalItemParameters": [ { "parameterName": "item_name", "value": "Stan and Friends Tee" }, { "parameterName": "affiliation", "value": "Google Merchandise Store" }, { "parameterName": "coupon", "value": "SUMMER_FUN" }, { "parameterName": "discount", "value": "2.22" }, { "parameterName": "index", "value": "0" }, { "parameterName": "item_brand", "value": "Google" }, { "parameterName": "item_category", "value": "Apparel" }, { "parameterName": "item_category2", "value": "Adult" }, { "parameterName": "item_category3", "value": "Shirts" }, { "parameterName": "item_category4", "value": "Crew" }, { "parameterName": "item_category5", "value": "Short sleeve" }, { "parameterName": "item_list_id", "value": "related_products" }, { "parameterName": "item_list_name", "value": "Related Products" } ] } ] } }, { "adIdentifiers": { "gclid": "GCLID_2" }, "conversionValue": 42.02, "currency": "EUR", "eventTimestamp": "2025-06-11T04:42:33Z", "transactionId": "DEF999911111", "eventSource": "WEB", "userData": { "userIdentifiers": [ { "emailAddress": "3E693CF7E5B67880BFF33B2D2626DADB7BF1D4BC737192E47CF8BAA89ACF2250" }, { "emailAddress": "223EBDA6F6889B1494551BA902D9D381DAF2F642BAE055888E96343D53E9F9C4" }, { "address": { "givenName": "2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450", "familyName": "6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F", "regionCode": "PT", "postalCode": "1229-076" } } ] }, "userProperties": { "customerType": "RETURNING" }, "eventName": "purchase", "clientId": "9876543210.1761582117", "userId": "user_DEF9876", "additionalEventParameters": [ { "parameterName": "ad_unit_name", "value": "Banner_02" } ], "cartData": { "transactionDiscount": 6.66, "items": [ { "itemId": "SKU_12346", "quantity": 2, "unitPrice": 21.01, "additionalItemParameters": [ { "parameterName": "item_name", "value": "Google Grey Women's Tee" }, { "parameterName": "affiliation", "value": "Google Merchandise Store" }, { "parameterName": "coupon", "value": "SUMMER_FUN" }, { "parameterName": "discount", "value": "3.33" }, { "parameterName": "index", "value": "1" }, { "parameterName": "item_brand", "value": "Google" }, { "parameterName": "item_category", "value": "Apparel" }, { "parameterName": "item_category2", "value": "Adult" }, { "parameterName": "item_category3", "value": "Shirts" }, { "parameterName": "item_category4", "value": "Crew" }, { "parameterName": "item_category5", "value": "Short sleeve" }, { "parameterName": "item_list_id", "value": "related_products" }, { "parameterName": "item_list_name", "value": "Related Products" } ] } ] } } ], "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}"); } 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>(); } } }
Java
// 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 #:%n%s", requestCount, response)); } 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/data-manager-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); } 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"; } 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) _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 的物件。
{
"requestId": "126365e1-16d0-4c81-9de9-f362711e250a"
}
記錄傳回的 requestId,以便在處理要求中的每個目的地時,擷取診斷資訊。
失敗回應
如果要求失敗,系統會傳回錯誤回應狀態碼 (例如 400 Bad
Request),以及含有錯誤詳細資料的回應。
舉例來說,如果 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"
}
]
}
]
}
}
將事件傳送至多個目的地
如果資料包含不同目的地的事件,您可以使用目的地參照,在同一個要求中傳送這些事件。
舉例來說,如果您有轉換動作 ID 123456789 的事件,以及轉換動作 ID 777111122 的另一個事件,請設定每個 Destination 的 reference,在單一要求中傳送這兩個事件。reference 是由使用者定義。唯一規定是每個 Destination 都必須有專屬的 reference。以下是要求中修改後的 destinations 清單:
"destinations": [
{
"operatingAccount": {
"accountType": "OPERATING_ACCOUNT_TYPE",
"accountId": "OPERATING_ACCOUNT_ID"
},
"loginAccount": {
"accountType": "LOGIN_ACCOUNT_TYPE",
"accountId": "LOGIN_ACCOUNT_ID"
},
"productDestinationId": "PRODUCT_DESTINATION_ID",
"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"
}
]
設定每個 Event 的 destinationReferences,將其傳送至一或多個特定目的地。舉例來說,以下 Event 僅適用於第一個 Destination,因此其 destinationReferences 清單只包含第一個 Destination 的 reference:
{
"adIdentifiers": {
"gclid": "GCLID_1"
},
"conversionValue": 1.99,
"currency": "USD",
"eventTimestamp": "2025-06-10T20:07:01Z",
"transactionId": "ABC798654321",
"eventSource": "WEB",
"destinationReferences": [
"destination_a"
]
}
destinationReferences 欄位是清單,因此您可以為活動指定多個目的地。如未設定 Event 的 destinationReferences,Data Manager API 會將事件傳送至要求中的所有目的地。
如果事件有多個目的地,Data Manager API 會將相關欄位傳送至每個目的地。舉例來說,如果事件有 Google Ads 目的地和 Google Analytics 目的地,當 API 將事件傳送至 Google Analytics 目的地時,會加入 Google Analytics 欄位,例如 clientId 或 eventName;當 API 將事件傳送至 Google Ads 目的地時,則會加入 Google Ads 欄位,例如 customVariables。