Pelajari panduan ini untuk memahami proses pengiriman peristiwa multi-sumber Google Analytics dengan Data Manager API.
Dalam panduan ini, Anda akan menyelesaikan langkah-langkah berikut:
- Siapkan
Destinationuntuk menerima data peristiwa. - Siapkan data peristiwa untuk dikirim.
- Membangun permintaan
IngestionServiceuntuk peristiwa. - Kirim permintaan dengan Google APIs Explorer.
- Pahami respons keberhasilan dan kegagalan.
Menyiapkan tujuan
Sebelum dapat mengirim data, Anda harus menyiapkan setidaknya satu
Destination untuk data tersebut. Berikut kolom
Destination. Lihat Mengonfigurasi tujuan
untuk mengetahui detail dan contoh tujuan untuk berbagai skenario.
operatingAccountProperti Google Analytics yang menerima peristiwa. Tetapkan
accountTypekeGOOGLE_ANALYTICS_PROPERTYdanaccountIdke ID Properti Google Analytics (misalnya,123456789).Kredensial permintaan harus untuk Akun Google dengan peran Editor atau Administrator untuk properti.
loginAccountAkun tempat pengguna kredensial memiliki akses. Tetapkan
accountTypekeGOOGLE_ANALYTICS_PROPERTYdanaccountIdke ID properti Google Analytics.productDestinationIdID aliran data yang menerima peristiwa.
Aliran data web
Disetel ke ID Pengukuran aliran data web Google Analytics (dalam format
G-XXXXXXXXXX).Aliran data aplikasi
Disetel ke ID Aplikasi Firebase dari aliran data aplikasi iOS atau Android Google Analytics.
{
"operatingAccount": {
"accountType": "GOOGLE_ANALYTICS_PROPERTY",
"accountId": "PROPERTY_ID"
},
"loginAccount": {
"accountType": "GOOGLE_ANALYTICS_PROPERTY",
"accountId": "LOGIN_ACCOUNT_ID"
},
"productDestinationId": "PRODUCT_DESTINATION_ID"
}
Contoh dalam panduan ini menunjukkan cara membuat permintaan yang mengirim setiap peristiwa ke tujuan yang sama. Jika Anda ingin mengirim peristiwa untuk beberapa tujuan dalam permintaan yang sama, lihat mengirim peristiwa untuk beberapa tujuan.
Menyiapkan data peristiwa
Sebelum mengirim data peristiwa dalam permintaan, Anda harus menyiapkan data:
- Format nilai mentah seperti yang ditentukan dalam panduan pemformatan.
- ID pengguna seperti alamat email, nama depan, dan nama belakang harus di-hash menggunakan algoritma SHA-256 dan dienkode menggunakan encoding heksadesimal (hex) atau Base64.
- Buat payload
Eventmenggunakan nilai yang diformat dan di-hash.
Tab berikut menampilkan contoh data peristiwa yang berkembang dari input mentah, melalui pemformatan, hingga nilai payload akhir:
Acara 1
| Atribut peristiwa | Nilai mentah | Diformat | Nilai payload |
|---|---|---|---|
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 (kode ISO 3 huruf) |
USD |
ad_unit_name |
Banner_01 |
Banner_01 |
Banner_01 |
| Data keranjang | |||
item_id |
SKU_12345 |
SKU_12345 |
SKU_12345 |
unit_price |
10.01 |
10.01 |
10.01 |
quantity |
3 |
3 |
3 |
| Parameter item tambahan | |||
item_name |
Stan and Friends Tee |
Stan and Friends Tee |
Stan and Friends Tee |
item_category |
Apparel |
Apparel |
Apparel |
item_brand |
Google |
Google |
Google |
Acara 2
| Atribut peristiwa | Nilai mentah | Diformat | Nilai payload |
|---|---|---|---|
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 (kode ISO 3 huruf) |
EUR |
email_address |
|
|
|
given_name |
zoë |
zoë (dipangkas, huruf kecil) |
2752B88686847FA5C86F47B94CE652B7B3F22A91C37617D451A4DB9AFA431450 (hex SHA-256) |
family_name |
pérez |
pérez (dipangkas, huruf kecil) |
6654977D57DDDD3C0329CA741B109EF6CD6430BEDD00008AAD213DF25683D77F (hex SHA-256) |
address_line |
1800 Amphibious Blvd. |
1800 amphibious blvd (huruf kecil, tanpa tanda baca) |
FF75E73A0E768CC1FA28A64FAEBBCECCB562D7C05F2FFCDD8D100ABAD73E4579 (hex SHA-256) |
city |
Mountain View |
mountain view (dipangkas, huruf kecil) |
mountain view |
administrative_area |
CA |
ca (dipangkas, huruf kecil) |
ca |
region_code |
US |
US (kode ISO 2 huruf) |
US |
postal_code |
94043 |
94043 |
94043 |
customer_type |
RETURNING |
RETURNING |
RETURNING |
ad_unit_name |
Banner_02 |
Banner_02 |
Banner_02 |
| Data keranjang | |||
item_id |
SKU_12346 |
SKU_12346 |
SKU_12346 |
unit_price |
21.01 |
21.01 |
21.01 |
quantity |
2 |
2 |
2 |
| Parameter item tambahan | |||
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 |
Mengonversi data ke objek Event
Konversi data yang diformat dan di-hash dari setiap peristiwa menjadi Event.
Persyaratan kolom acara
Buat peristiwa Anda sesuai dengan persyaratan dalam tabel berikut.
Lihat dokumentasi referensi Event untuk mengetahui daftar lengkap kolom yang tersedia.
| Kolom | Status | Deskripsi |
|---|---|---|
eventName |
Wajib | Nama peristiwa Google Analytics. Jika nama peristiwa adalah nama yang dicadangkan, Data Manager API akan menolak peristiwa dengan error INVALID_EVENT_NAME. |
eventTimestamp |
Wajib | Waktu terjadinya peristiwa. Lihat Format stempel waktu. Peristiwa untuk Google Analytics harus memiliki eventTimestamp dalam 72 jam terakhir. Selain itu, Anda harus mengirim peristiwa Google Analytics yang ingin Anda gabungkan atau proses bersama peristiwa yang dikumpulkan oleh Google Analytics for Firebase SDK atau gtag.js dalam waktu 48 jam setelah stempel waktu peristiwa sisi klien asli. Google Analytics mungkin tidak memproses peristiwa yang dikirim setelah waktu ini seperti yang diharapkan, terutama untuk atribusi konversi. |
transactionId |
Wajib | ID unik untuk transaksi. |
eventSource |
Opsional | Asal peristiwa. Jika disetel, harus WEB untuk aliran data web atau APP untuk aliran data aplikasi. |
conversionValue |
Opsional | Nilai uang yang terkait dengan peristiwa. |
currency |
Opsional | Kode mata uang (seperti USD atau EUR) yang terkait dengan nilai uang dalam peristiwa ini. |
cartData |
Wajib Bersyarat | Wajib jika ditunjukkan dalam referensi peristiwa Google Analytics untuk peristiwa tersebut. Lihat Menambahkan data keranjang. |
additionalEventParameters |
Opsional | Opsional, tetapi direkomendasikan. Isi daftar ini dengan parameter peristiwa Google Analytics yang tidak tercatat di kolom Event lainnya. Lihat Menambahkan parameter peristiwa. |
userProperties |
Opsional | Properti pengguna untuk pengguna. Lihat Menambahkan properti pengguna. |
destinationReferences |
Wajib Bersyarat | Wajib jika daftar destinations tingkat permintaan berisi lebih dari satu Destination Google Analytics. Lihat Mengirim peristiwa untuk beberapa tujuan. |
consent |
Opsional | Setelan izin Digital Markets Act (DMA) untuk pengguna, yang menentukan apakah izin diberikan untuk adUserData dan adPersonalization. |
Persyaratan ID
Berikut persyaratan ID untuk setiap jenis aliran data:
Aliran data web
Anda harus menyertakan setidaknya satu ID berikut saat mengirimkan peristiwa multi-sumber Google Analytics ke aliran data web:
[
clientId]: ID unik untuk instance pengguna dari klien web. Lihat petunjuk Measurement Protocol.[
userId]: ID unik untuk pengguna. Lihat artikel Mengukur aktivitas di berbagai platform dengan User-ID untuk mengetahui informasi selengkapnya.[
userData]: Data yang disediakan pengguna, seperti alamat email, nomor telepon, dan alamat, yang merepresentasikan pengguna yang terkait dengan peristiwa. Lihat Memformat data pengguna untuk mendapatkan panduan tentang cara menormalisasi dan melakukan hashing pada nilai.
Aliran data aplikasi
Ikuti persyaratan berikut saat mengirim peristiwa multi-sumber Google Analytics ke aliran data aplikasi:
- [
appInstanceId]: Wajib diisi. Disetel ke ID unik untuk instance pengguna klien aplikasi. Lihat petunjuk Measurement Protocol. - [
userId]: Opsional. ID unik untuk pengguna. Lihat artikel Mengukur aktivitas di berbagai platform dengan User-ID untuk mengetahui informasi selengkapnya. adIdentifiers: Opsional. ID klik dengangbraidataugclid.- [
userData]: Opsional. Data yang disediakan pengguna, seperti alamat email, nomor telepon, dan alamat, yang merepresentasikan pengguna yang terkait dengan peristiwa. Lihat Memformat data pengguna untuk mendapatkan panduan tentang cara menormalisasi dan melakukan hashing pada nilai.
Cara Google menangani data multi-sumber
Dalam peristiwa dan properti yang sama, Google Analytics menggunakan
transactionId
untuk menghapus duplikat peristiwa dari berbagai sumber (seperti tag situs dan
permintaan penyerapan Data Manager API). Google Analytics menggunakan informasi dari instance pertama
dari peristiwa yang sama yang diterimanya.
Menambahkan data keranjang
Isi kolom cartData dari Event dengan informasi tentang item yang terkait dengan peristiwa. Gunakan kolom ini saat mengirim peristiwa yang direkomendasikan Google Analytics seperti purchase yang memerlukan daftar items.
Berikut adalah kolom objek CartData:
items- Wajib. Sertakan setidaknya satu
Itemdalam daftar ini.
Kolom item
Tambahkan satu atau beberapa objek Item ke daftar items dari CartData.
Isi kolom berikut untuk setiap Item:
items.itemId- Wajib. ID unik untuk item.
items.unitPriceOpsional. Harga satuan tidak termasuk pajak, pengiriman, dan diskon cakupan peristiwa (tingkat transaksi) untuk item ini.
Jika item memiliki diskon cakupan item, gunakan harga satuan diskon. Misalnya, jika item memiliki harga satuan
27.67dan diskon satuan6.66, tetapkanunitPriceke21.01.items.quantityWajib diisi. Jumlah unit untuk item tertentu ini.
items.additionalItemParametersOpsional. Isi daftar ini dengan parameter cakupan item yang tidak tercatat di kolom
Itemlainnya. Gunakan nama parameter dari dokumentasi Google Analytics sebagaiparameterNamedariItemParameter.Misalnya, untuk Google Analytics, jika Anda memiliki merek dan kategori item untuk peristiwa
purchase, tambahkan entri keadditionalItemParametersitem denganparameterNamedisetel keitem_branddanvaluedisetel ke nama merek.Sebaiknya jangan menambahkan entri ke
additionalItemParametersuntuk parameter itemquantity,price, atauitem_id. Sebagai gantinya, isi kolomitemId,unitPrice, danquantitydariItem, yang lebih diprioritaskan daripada entri apa pun diadditionalItemParameters.
Tambahkan parameter peristiwa
Isi daftar additionalEventParameters dari Event dengan parameter peristiwa Google Analytics yang tidak tercatat di kolom Event lainnya. Parameter dapat mencakup parameter yang direkomendasikan untuk peristiwa
atau parameter lain yang ingin Anda ambil. Gunakan nama parameter Google Analytics
untuk parameterName dari EventParameter.
Misalnya, jika Anda memiliki pajak yang terkait dengan transaksi, tambahkan entri ke additionalEventParameters dengan parameterName ditetapkan ke tax,
dan value ditetapkan ke biaya pajak.
Sebaiknya jangan menambahkan entri ke additionalEventParameters untuk parameter peristiwa Google Analytics transactionId, currency, atau value. Sebagai gantinya, isi kolom transactionId, currency, dan conversionValue dari Event, yang lebih diprioritaskan daripada entri apa pun di additionalEventParameters.
Menambahkan properti pengguna
Properti pengguna
mendeskripsikan pengguna pada saat peristiwa terjadi. Tambahkan entri terpisah ke daftar
additionalUserProperties untuk setiap properti pengguna.
Permintaan contoh
Berikut adalah contoh objek Event untuk data yang diformat dari setiap peristiwa:
Acara 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"
}
Acara 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"
}
]
}
}
Buat isi permintaan
Untuk membuat isi permintaan, gabungkan destinations dan events, tetapkan kolom
encoding, dan tambahkan kolom permintaan lain yang ingin Anda sertakan seperti
validateOnly dan consent.
Kirim permintaan
Berikut langkah-langkah untuk mencoba permintaan dari browser Anda:
- Pilih tab REST, lalu klik Open in API Explorer untuk membuka API Explorer di tab atau jendela baru.
- Di isi permintaan di API Explorer, ganti setiap string yang diawali dengan
REPLACE_WITH, sepertiREPLACE_WITH_OPERATING_ACCOUNT_TYPE, dengan nilai yang relevan. - Klik Execute di bagian bawah halaman API Explorer dan selesaikan perintah otorisasi untuk mengirim permintaan.
- Tetapkan
validateOnlyketrueuntuk memvalidasi permintaan tanpa menerapkan perubahan. Jika Anda sudah siap untuk menerapkan perubahan, tetapkanvalidateOnlykefalse.
Jika Anda menginstal library klien, pilih tab untuk bahasa pemrograman yang Anda pilih untuk melihat contoh kode lengkap tentang cara membuat dan mengirim permintaan.
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); } } }
Node
#!/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, )
Respons keberhasilan
Permintaan yang berhasil akan menampilkan respons dengan objek yang berisi requestId.
Jika ada kolom opsional yang gagal divalidasi, respons juga akan menyertakan daftar
fieldWarnings.
Respons standar
Berikut adalah contoh respons untuk permintaan penyerapan yang berhasil tanpa peringatan:
{
"requestId": "126365e1-16d0-4c81-9de9-f362711e250a"
}
Respons dengan peringatan
Berikut adalah contoh respons untuk permintaan penyerapan yang berhasil yang berisi peringatan:
{
"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"
}
]
}
Catat requestId yang ditampilkan sehingga Anda dapat mengambil
diagnostik saat setiap tujuan dalam permintaan diproses. Anda juga harus memeriksa apakah ada
fieldWarnings
untuk memastikan kolom opsional yang Anda kirimkan diterima. Lihat Peringatan
penyerapan untuk mengetahui detail selengkapnya.
Respons kegagalan
Permintaan yang gagal akan menghasilkan kode status respons error seperti 400 Bad
Request, dan respons dengan detail error.
Misalnya, emailAddress yang berisi string teks biasa, bukan nilai yang dienkode hex, akan menghasilkan respons berikut:
{
"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 yang tidak di-hash dan hanya dienkode hex akan menghasilkan respons berikut:
{
"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"
}
]
}
]
}
}
Mengirim peristiwa untuk beberapa tujuan
Jika data Anda berisi peristiwa untuk tujuan yang berbeda, Anda dapat mengirimkannya dalam permintaan yang sama menggunakan referensi tujuan. Lihat Batas dan kuota untuk mengetahui jumlah maksimum tujuan per permintaan.
Misalnya, jika Anda memiliki peristiwa untuk ID tindakan konversi 123456789 dan
peristiwa lain untuk ID tindakan konversi 777111122, kirim kedua peristiwa dalam satu
permintaan dengan menyetel reference setiap
Destination. reference ditentukan pengguna. Satu-satunya persyaratan adalah setiap Destination memiliki reference unik. Berikut daftar destinations yang diubah untuk permintaan:
"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"
}
]
Tetapkan destinationReferences setiap
Event untuk mengirimkannya ke satu atau beberapa
tujuan tertentu. Misalnya, berikut Event yang hanya untuk Destination pertama, sehingga daftar destinationReferences-nya hanya berisi reference dari Destination pertama:
{
"adIdentifiers": {
"gclid": "GCLID_1"
},
"conversionValue": 1.99,
"currency": "USD",
"eventTimestamp": "2025-06-10T20:07:01Z",
"transactionId": "ABC798654321",
"eventSource": "WEB",
"destinationReferences": [
"destination_a"
]
}
Kolom destinationReferences adalah daftar, sehingga Anda dapat menentukan beberapa
tujuan untuk suatu peristiwa. Jika Anda tidak menetapkan destinationReferences dari
Event, Data Manager API akan mengirimkan peristiwa ke semua tujuan dalam
permintaan.
Jika suatu peristiwa memiliki beberapa tujuan, Data Manager API akan mengirimkan kolom yang relevan ke setiap tujuan. Misalnya, jika suatu peristiwa memiliki tujuan Google Ads dan tujuan Google Analytics, API akan menyertakan kolom Google Analytics seperti clientId, appInstanceId, atau eventName saat mengirimkan peristiwa ke tujuan Google Analytics, dan menyertakan kolom Google Ads seperti customVariables saat mengirimkan peristiwa ke tujuan Google Ads.
Langkah berikutnya
- Konfigurasi autentikasi dan siapkan lingkungan Anda dengan library klien.
- Pelajari persyaratan pemformatan, hashing, dan encoding untuk setiap jenis data.
- Pelajari cara mengenkripsi data pengguna.
- Pelajari cara mengambil diagnostik untuk permintaan Anda.
- Pelajari praktik terbaik.
- Pelajari batas dan kuota.