Hãy tham khảo hướng dẫn này để làm quen với quy trình gửi sự kiện từ nhiều nguồn của Google Analytics bằng Data Manager API.
Trong hướng dẫn này, bạn sẽ hoàn tất các bước sau:
- Chuẩn bị một
Destinationđể nhận dữ liệu sự kiện. - Chuẩn bị dữ liệu sự kiện để gửi.
- Tạo một yêu cầu
IngestionServicecho các sự kiện. - Gửi yêu cầu bằng Google APIs Explorer.
- Hiểu rõ các phản hồi thành công và không thành công.
Chuẩn bị đích đến
Trước khi có thể gửi dữ liệu, bạn cần chuẩn bị ít nhất một Destination cho dữ liệu. Sau đây là các trường của Destination. Hãy xem bài viết Thiết lập vị trí xuất hiện để biết thêm thông tin và ví dụ về vị trí xuất hiện cho nhiều trường hợp.
operatingAccountTài sản Google Analytics nhận được các sự kiện. Đặt
accountTypethànhGOOGLE_ANALYTICS_PROPERTYvàaccountIdthành Mã nhận dạng tài sản Google Analytics (ví dụ:123456789).Thông tin đăng nhập của yêu cầu phải là của một Tài khoản Google có vai trò Người chỉnh sửa hoặc Quản trị viên đối với tài sản.
loginAccountTài khoản mà người dùng của thông tin đăng nhập có quyền truy cập. Đặt
accountTypethànhGOOGLE_ANALYTICS_PROPERTYvàaccountIdthành mã nhận dạng của tài sản Google Analytics.productDestinationIdGiá trị nhận dạng của luồng dữ liệu nhận các sự kiện.
Luồng dữ liệu web
Đặt thành Mã đo lường của một luồng dữ liệu web Google Analytics (theo định dạng
G-XXXXXXXXXX).Luồng dữ liệu ứng dụng
Đặt thành Mã ứng dụng Firebase của một luồng dữ liệu ứng dụng Google Analytics dành cho iOS hoặc Android.
{
"operatingAccount": {
"accountType": "GOOGLE_ANALYTICS_PROPERTY",
"accountId": "PROPERTY_ID"
},
"loginAccount": {
"accountType": "GOOGLE_ANALYTICS_PROPERTY",
"accountId": "LOGIN_ACCOUNT_ID"
},
"productDestinationId": "PRODUCT_DESTINATION_ID"
}
Ví dụ trong hướng dẫn này cho thấy cách tạo một yêu cầu gửi mọi sự kiện đến cùng một đích đến. Nếu bạn muốn gửi sự kiện cho nhiều đích đến trong cùng một yêu cầu, hãy xem bài viết gửi sự kiện cho nhiều đích đến.
Chuẩn bị dữ liệu sự kiện
Trước khi gửi dữ liệu sự kiện trong một yêu cầu, bạn phải chuẩn bị dữ liệu:
- Định dạng các giá trị thô theo quy định trong hướng dẫn định dạng.
- Giá trị nhận dạng người dùng (chẳng hạn như địa chỉ email, tên và họ) phải được băm bằng thuật toán SHA-256 và được mã hoá bằng phương thức mã hoá thập lục phân (hex) hoặc Base64.
- Tạo tải trọng
Eventbằng các giá trị đã định dạng và băm.
Các thẻ sau đây cho thấy dữ liệu sự kiện mẫu tiến triển từ dữ liệu đầu vào thô, thông qua định dạng, đến giá trị tải trọng cuối cùng:
Sự kiện 1
| Thuộc tính sự kiện | Giá trị thô | Đã định dạng | Giá trị tải trọng |
|---|---|---|---|
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 (Mã ISO gồm 3 chữ cái) |
USD |
ad_unit_name |
Banner_01 |
Banner_01 |
Banner_01 |
| Dữ liệu giỏ hàng | |||
item_id |
SKU_12345 |
SKU_12345 |
SKU_12345 |
unit_price |
10.01 |
10.01 |
10.01 |
quantity |
3 |
3 |
3 |
| Tham số mặt hàng bổ sung | |||
item_name |
Stan and Friends Tee |
Stan and Friends Tee |
Stan and Friends Tee |
item_category |
Apparel |
Apparel |
Apparel |
item_brand |
Google |
Google |
Google |
Sự kiện 2
| Thuộc tính sự kiện | Giá trị thô | Đã định dạng | Giá trị tải trọng |
|---|---|---|---|
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 (Mã ISO gồm 3 chữ cái) |
EUR |
email_address |
|
|
|
given_name |
zoë |
zoë (đã cắt bớt, chữ thường) |
2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450 (SHA-256 hex) |
family_name |
pérez |
pérez (đã cắt bớt, chữ thường) |
6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F (SHA-256 hex) |
address_line |
1800 Amphibious Blvd. |
1800 amphibious blvd (chữ thường, không có dấu chấm câu) |
FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579 (SHA-256 hex) |
city |
Mountain View |
mountain view (đã cắt bớt, chữ thường) |
mountain view |
administrative_area |
CA |
ca (đã cắt bớt, chữ thường) |
ca |
region_code |
US |
US (Mã ISO gồm 2 chữ cái) |
US |
postal_code |
94043 |
94043 |
94043 |
customer_type |
RETURNING |
RETURNING |
RETURNING |
ad_unit_name |
Banner_02 |
Banner_02 |
Banner_02 |
| Dữ liệu giỏ hàng | |||
item_id |
SKU_12346 |
SKU_12346 |
SKU_12346 |
unit_price |
21.01 |
21.01 |
21.01 |
quantity |
2 |
2 |
2 |
| Tham số mặt hàng bổ sung | |||
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 |
Chuyển đổi dữ liệu thành các đối tượng Event
Chuyển đổi dữ liệu đã băm và được định dạng của mỗi sự kiện thành một Event.
Yêu cầu về trường sự kiện
Tạo sự kiện theo các yêu cầu trong bảng sau.
Hãy tham khảo tài liệu tham khảo Event để biết danh sách đầy đủ các trường có sẵn.
| Trường | Trạng thái | Mô tả |
|---|---|---|
eventName |
Bắt buộc | Tên của sự kiện Google Analytics. Nếu tên sự kiện là tên dành riêng, thì Data Manager API sẽ từ chối sự kiện đó và trả về lỗi INVALID_EVENT_NAME. |
eventTimestamp |
Bắt buộc | Thời gian xảy ra sự kiện. Xem Định dạng dấu thời gian. Các sự kiện cho Google Analytics phải có eventTimestamp trong vòng 72 giờ qua. Ngoài ra, bạn phải gửi các sự kiện Google Analytics mà bạn muốn kết hợp hoặc xử lý cùng với các sự kiện mà Google Analytics cho Firebase SDK hoặc gtag.js thu thập trong vòng 48 giờ kể từ dấu thời gian sự kiện phía máy khách ban đầu. Google Analytics có thể không xử lý các sự kiện được gửi muộn hơn thời gian này như dự kiến, đặc biệt là đối với mô hình phân bổ lượt chuyển đổi. |
transactionId |
Bắt buộc | Giá trị nhận dạng riêng biệt của giao dịch. |
eventSource |
Không bắt buộc | Nguồn gốc của sự kiện. Nếu được đặt, phải là WEB đối với luồng dữ liệu web hoặc APP đối với luồng dữ liệu ứng dụng. |
conversionValue |
Không bắt buộc | Giá trị bằng tiền liên kết với sự kiện. |
currency |
Không bắt buộc | Mã tiền tệ (chẳng hạn như USD hoặc EUR) được liên kết với các giá trị bằng tiền trong sự kiện này. |
cartData |
Bắt buộc trong một số điều kiện | Bắt buộc nếu được chỉ định trong tài liệu tham khảo về sự kiện Google Analytics cho sự kiện. Xem phần Thêm dữ liệu giỏ hàng. |
additionalEventParameters |
Không bắt buộc | Không bắt buộc, nhưng nên dùng. Điền vào danh sách này mọi thông số sự kiện Google Analytics chưa được ghi nhận trong các trường Event khác. Xem bài viết Thêm thông số sự kiện. |
userProperties |
Không bắt buộc | Thuộc tính người dùng cho người dùng. Xem phần Thêm thuộc tính người dùng. |
destinationReferences |
Bắt buộc trong một số điều kiện | Bắt buộc nếu danh sách destinations ở cấp yêu cầu chứa nhiều Google Analytics Destination. Xem bài viết Gửi sự kiện cho nhiều đích đến. |
consent |
Không bắt buộc | Chế độ cài đặt về sự đồng ý theo Đạo luật thị trường kỹ thuật số (DMA) cho người dùng, chỉ định xem người dùng có đồng ý cho adUserData và adPersonalization hay không. |
Yêu cầu đối với giá trị nhận dạng
Sau đây là các yêu cầu về giá trị nhận dạng cho từng loại luồng dữ liệu:
Luồng dữ liệu web
Bạn phải thêm ít nhất một trong số các giá trị nhận dạng sau khi gửi một sự kiện đa nguồn Google Analytics đến một luồng dữ liệu web:
[
clientId]: Giá trị nhận dạng riêng biệt cho một thực thể người dùng của ứng dụng web. Xem hướng dẫn về Measurement Protocol.[
userId]: Giá trị nhận dạng riêng biệt cho một người dùng. Xem bài viết Đo lường hoạt động trên nhiều nền tảng bằng tính năng User-ID để biết thêm thông tin.[
userData]: Dữ liệu do người dùng cung cấp, chẳng hạn như địa chỉ email, số điện thoại và địa chỉ, thể hiện người dùng được liên kết với sự kiện. Hãy xem phần Định dạng dữ liệu người dùng để biết hướng dẫn về cách chuẩn hoá và băm các giá trị.
Luồng dữ liệu ứng dụng
Hãy tuân thủ các yêu cầu sau khi gửi một sự kiện có nhiều nguồn của Google Analytics đến một luồng dữ liệu ứng dụng:
- [
appInstanceId]: Bắt buộc. Đặt thành giá trị nhận dạng riêng biệt cho phiên bản người dùng của ứng dụng khách. Xem hướng dẫn về Measurement Protocol. - [
userId]: Không bắt buộc. Giá trị nhận dạng riêng biệt cho một người dùng. Hãy xem bài viết Đo lường hoạt động trên nhiều nền tảng bằng tính năng User-ID để biết thêm thông tin. adIdentifiers: Không bắt buộc. Giá trị nhận dạng lượt nhấp cógbraidhoặcgclid.- [
userData]: Không bắt buộc. Dữ liệu do người dùng cung cấp, chẳng hạn như địa chỉ email, số điện thoại và địa chỉ, thể hiện người dùng được liên kết với sự kiện. Hãy xem phần Định dạng dữ liệu người dùng để biết hướng dẫn về cách chuẩn hoá và băm các giá trị.
Cách Google xử lý dữ liệu từ nhiều nguồn
Trong cùng một sự kiện và tài sản, Google Analytics sử dụng transactionId để loại bỏ sự kiện trùng lặp từ nhiều nguồn (chẳng hạn như thẻ trang web và yêu cầu tiếp nhận Data Manager API). Google Analytics sử dụng thông tin từ phiên bản đầu tiên của cùng một sự kiện mà hệ thống nhận được.
Thêm dữ liệu giỏ hàng
Điền thông tin về các mặt hàng được liên kết với sự kiện vào trường cartData của Event. Hãy sử dụng trường này khi gửi sự kiện được đề xuất của Google Analytics (chẳng hạn như purchase) yêu cầu danh sách items.
Sau đây là các trường của đối tượng CartData:
items- Bắt buộc. Thêm ít nhất một
Itemvào danh sách này.
Trường mặt hàng
Thêm một hoặc nhiều đối tượng Item vào danh sách items của CartData.
Điền vào các trường sau cho mỗi Item:
items.itemId- Bắt buộc. Giá trị nhận dạng riêng biệt của mặt hàng.
items.unitPriceKhông bắt buộc. Giá của từng đơn vị hàng, chưa bao gồm thuế, phí vận chuyển và các khoản chiết khấu ở phạm vi sự kiện (cấp giao dịch) cho mặt hàng này.
Nếu mặt hàng có chiết khấu ở phạm vi mặt hàng, hãy sử dụng giá chiết khấu theo đơn vị. Ví dụ: nếu một mặt hàng có đơn giá là
27.67và chiết khấu theo đơn vị là6.66, hãy đặtunitPricethành21.01.items.quantityBắt buộc. Số lượng đơn vị của mặt hàng cụ thể này.
items.additionalItemParametersKhông bắt buộc. Điền vào danh sách này mọi thông số ở phạm vi mặt hàng không được ghi lại trong các trường
Itemkhác. Sử dụng tên tham số trong tài liệu của Google Analytics làmparameterNamecủaItemParameter.Ví dụ: đối với Google Analytics, nếu bạn có thương hiệu và danh mục cho một mặt hàng cho sự kiện
purchase, hãy thêm một mục vàoadditionalItemParameterscủa mặt hàng vớiparameterNameđược đặt thànhitem_brandvàvalueđược đặt thành tên thương hiệu.Bạn không nên thêm các mục vào
additionalItemParameterscho các thông số mụcquantity,pricehoặcitem_id. Thay vào đó, hãy điền các trườngitemId,unitPricevàquantitycủaItem. Các trường này sẽ được ưu tiên hơn mọi mục trongadditionalItemParameters.
Thêm thông số sự kiện
Điền sẵn danh sách additionalEventParameters của Event bằng mọi thông số sự kiện Google Analytics chưa được ghi nhận trong các trường Event khác. Các thông số này có thể bao gồm các thông số được đề xuất khác cho sự kiện hoặc bất kỳ thông số nào khác mà bạn muốn thu thập. Sử dụng tên tham số Google Analytics cho parameterName của EventParameter.
Ví dụ: nếu bạn có thuế liên kết với một giao dịch, hãy thêm một mục vào additionalEventParameters với parameterName được đặt thành tax và value được đặt thành chi phí thuế.
Bạn không nên thêm các mục vào additionalEventParameters cho các thông số sự kiện transactionId, currency hoặc value của Google Analytics. Thay vào đó, hãy điền các trường transactionId, currency và conversionValue của Event. Các trường này sẽ được ưu tiên hơn mọi mục trong additionalEventParameters.
Thêm thuộc tính người dùng
Thuộc tính người dùng mô tả người dùng tại thời điểm xảy ra sự kiện. Thêm một mục riêng vào danh sách additionalUserProperties cho từng thuộc tính người dùng.
Yêu cầu mẫu
Sau đây là các đối tượng Event mẫu cho dữ liệu được định dạng từ mỗi sự kiện:
Sự kiện 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"
}
Sự kiện 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"
}
]
}
}
Tạo nội dung yêu cầu
Để tạo nội dung yêu cầu, hãy kết hợp destinations và events, đặt trường encoding và thêm mọi trường yêu cầu khác mà bạn muốn đưa vào, chẳng hạn như validateOnly và consent.
Gửi yêu cầu
Sau đây là các bước để thử gửi yêu cầu từ trình duyệt:
- Chọn thẻ REST rồi nhấp vào Open in API Explorer (Mở trong Trình khám phá API) để mở Trình khám phá API trong một thẻ hoặc cửa sổ mới.
- Trong phần nội dung yêu cầu trong API Explorer, hãy thay thế từng chuỗi bắt đầu bằng
REPLACE_WITH, chẳng hạn nhưREPLACE_WITH_OPERATING_ACCOUNT_TYPE, bằng giá trị có liên quan. - Nhấp vào Thực thi ở cuối trang API Explorer và hoàn tất các lời nhắc uỷ quyền để gửi yêu cầu.
- Đặt
validateOnlythànhtrueđể xác thực yêu cầu mà không áp dụng các thay đổi. Khi bạn đã sẵn sàng áp dụng các thay đổi, hãy đặtvalidateOnlythànhfalse.
Nếu bạn đã cài đặt một thư viện ứng dụng, hãy chọn thẻ cho ngôn ngữ lập trình bạn đã chọn để xem mẫu mã hoàn chỉnh về cách tạo và gửi yêu cầu.
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>(); } } }
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); } } }
Nút
#!/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, )
Phản hồi thành công
Yêu cầu thành công sẽ trả về một phản hồi có chứa một đối tượng requestId.
Nếu có trường không bắt buộc nào không được xác thực, thì phản hồi cũng sẽ bao gồm một danh sách fieldWarnings.
Phản hồi tiêu chuẩn
Sau đây là một phản hồi mẫu cho yêu cầu truyền dữ liệu thành công mà không có cảnh báo:
{
"requestId": "126365e1-16d0-4c81-9de9-f362711e250a"
}
Phản hồi kèm theo cảnh báo
Sau đây là một phản hồi mẫu cho yêu cầu truyền dữ liệu thành công có chứa một cảnh báo:
{
"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"
}
]
}
Ghi lại requestId được trả về để bạn có thể truy xuất thông tin chẩn đoán khi mỗi đích đến trong yêu cầu được xử lý. Bạn cũng nên kiểm tra mọi fieldWarnings để đảm bảo mọi trường không bắt buộc mà bạn đã gửi đều được chấp nhận. Hãy xem phần Cảnh báo về việc truyền dữ liệu để biết thêm thông tin chi tiết.
Phản hồi thất bại
Yêu cầu không thành công sẽ dẫn đến mã trạng thái phản hồi lỗi, chẳng hạn như 400 Bad
Request và phản hồi có thông tin chi tiết về lỗi.
Ví dụ: emailAddress chứa một chuỗi văn bản thuần tuý thay vì một giá trị được mã hoá hex sẽ tạo ra phản hồi sau:
{
"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"
}
]
}
]
}
}
Một emailAddress không được băm và chỉ được mã hoá theo hệ thập lục phân sẽ tạo ra phản hồi sau:
{
"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"
}
]
}
]
}
}
Gửi sự kiện cho nhiều đích đến
Nếu dữ liệu của bạn chứa các sự kiện cho nhiều đích đến, thì bạn có thể gửi các sự kiện đó trong cùng một yêu cầu bằng cách sử dụng thông tin tham chiếu về đích đến. Hãy xem phần Giới hạn và hạn mức để biết số lượng đích đến tối đa cho mỗi yêu cầu.
Ví dụ: nếu bạn có một sự kiện cho mã hành động chuyển đổi 123456789 và một sự kiện khác cho mã hành động chuyển đổi 777111122, hãy gửi cả hai sự kiện trong một yêu cầu bằng cách đặt reference của mỗi Destination. reference do người dùng xác định. Yêu cầu duy nhất là mỗi Destination phải có một reference riêng biệt. Sau đây là danh sách destinations đã sửa đổi cho yêu cầu:
"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"
}
]
Đặt destinationReferences của mỗi Event để gửi đến một hoặc nhiều đích đến cụ thể. Ví dụ: sau đây là một Event chỉ dành cho
Destination đầu tiên, nên danh sách destinationReferences của này chỉ chứa
reference của Destination đầu tiên:
{
"adIdentifiers": {
"gclid": "GCLID_1"
},
"conversionValue": 1.99,
"currency": "USD",
"eventTimestamp": "2025-06-10T20:07:01Z",
"transactionId": "ABC798654321",
"eventSource": "WEB",
"destinationReferences": [
"destination_a"
]
}
Trường destinationReferences là một danh sách, vì vậy bạn có thể chỉ định nhiều đích đến cho một sự kiện. Nếu bạn không đặt destinationReferences của một Event, Data Manager API sẽ gửi sự kiện đến tất cả đích đến trong yêu cầu.
Nếu một sự kiện có nhiều đích đến, Data Manager API sẽ gửi các trường có liên quan đến từng đích đến. Ví dụ: nếu một sự kiện có đích đến là Google Ads và đích đến là Google Analytics, thì API sẽ bao gồm các trường Google Analytics như clientId, appInstanceId hoặc eventName khi gửi sự kiện đến đích đến Google Analytics và bao gồm các trường Google Ads như customVariables khi gửi sự kiện đến đích đến Google Ads.
Các bước tiếp theo
- Định cấu hình quy trình xác thực và thiết lập môi trường bằng một thư viện ứng dụng.
- Tìm hiểu về các yêu cầu về định dạng, băm và mã hoá đối với từng loại dữ liệu.
- Tìm hiểu cách mã hoá dữ liệu người dùng.
- Tìm hiểu cách truy xuất thông tin chẩn đoán cho các yêu cầu của bạn.
- Tìm hiểu về các phương pháp hay nhất.
- Tìm hiểu về hạn mức và giới hạn.