@google-cloud/nodejs-common 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -17
- package/src/apis/base/google_api_client.js +1 -1
- package/src/apis/doubleclick_bidmanager.js +3 -3
- package/src/apis/gmail.js +113 -0
- package/src/apis/index.js +10 -15
- package/src/apis/sendgrid.js +62 -1
- package/src/apis/youtube.js +0 -284
- package/src/components/firestore/data_access_object.js +3 -3
- package/src/apis/google_ads.js +0 -1145
package/src/apis/google_ads.js
DELETED
|
@@ -1,1145 +0,0 @@
|
|
|
1
|
-
// Copyright 2019 Google Inc.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @fileoverview Google Ads API (unofficial) Wrapper.
|
|
17
|
-
*/
|
|
18
|
-
'use strict';
|
|
19
|
-
|
|
20
|
-
const {protos: {google: {ads: {googleads}}}} = require('google-ads-node');
|
|
21
|
-
const googleAdsLib = googleads[Object.keys(googleads)[0]];
|
|
22
|
-
const {
|
|
23
|
-
common: {
|
|
24
|
-
CustomerMatchUserListMetadata,
|
|
25
|
-
StoreSalesMetadata,
|
|
26
|
-
TransactionAttribute,
|
|
27
|
-
UserAttribute,
|
|
28
|
-
UserData,
|
|
29
|
-
UserIdentifier,
|
|
30
|
-
},
|
|
31
|
-
resources: {
|
|
32
|
-
GoogleAdsField,
|
|
33
|
-
OfflineUserDataJob,
|
|
34
|
-
UserList,
|
|
35
|
-
},
|
|
36
|
-
services: {
|
|
37
|
-
AddOfflineUserDataJobOperationsRequest,
|
|
38
|
-
CreateOfflineUserDataJobRequest,
|
|
39
|
-
RunOfflineUserDataJobRequest,
|
|
40
|
-
SearchGoogleAdsFieldsRequest,
|
|
41
|
-
UploadCallConversionsRequest,
|
|
42
|
-
UploadCallConversionsResponse,
|
|
43
|
-
UploadClickConversionsRequest,
|
|
44
|
-
UploadClickConversionsResponse,
|
|
45
|
-
UploadConversionAdjustmentsRequest,
|
|
46
|
-
UploadConversionAdjustmentsResponse,
|
|
47
|
-
UploadUserDataRequest,
|
|
48
|
-
UploadUserDataResponse,
|
|
49
|
-
UserDataOperation,
|
|
50
|
-
},
|
|
51
|
-
errors: {
|
|
52
|
-
GoogleAdsFailure,
|
|
53
|
-
},
|
|
54
|
-
enums: {
|
|
55
|
-
OfflineUserDataJobFailureReasonEnum: { OfflineUserDataJobFailureReason },
|
|
56
|
-
OfflineUserDataJobTypeEnum: { OfflineUserDataJobType },
|
|
57
|
-
OfflineUserDataJobStatusEnum: { OfflineUserDataJobStatus },
|
|
58
|
-
UserListMembershipStatusEnum: { UserListMembershipStatus },
|
|
59
|
-
UserListTypeEnum: { UserListType },
|
|
60
|
-
CustomerMatchUploadKeyTypeEnum: { CustomerMatchUploadKeyType },
|
|
61
|
-
},
|
|
62
|
-
} = googleAdsLib;
|
|
63
|
-
const {GoogleAdsApi} = require('google-ads-api');
|
|
64
|
-
const lodash = require('lodash');
|
|
65
|
-
|
|
66
|
-
const AuthClient = require('./auth_client.js');
|
|
67
|
-
const {getLogger, BatchResult,} = require('../components/utils.js');
|
|
68
|
-
|
|
69
|
-
/** @type {!ReadonlyArray<string>} */
|
|
70
|
-
const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* List of properties that will be taken from the data file as elements of a
|
|
74
|
-
* conversion or a conversion adjustment.
|
|
75
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
76
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CallConversion
|
|
77
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ConversionAdjustment
|
|
78
|
-
* @type {Array<string>}
|
|
79
|
-
*/
|
|
80
|
-
const PICKED_PROPERTIES = [
|
|
81
|
-
'external_attribution_data',
|
|
82
|
-
'cart_data',
|
|
83
|
-
'user_identifiers',
|
|
84
|
-
'gbraid',
|
|
85
|
-
'wbraid',
|
|
86
|
-
'gclid',
|
|
87
|
-
'caller_id',
|
|
88
|
-
'call_start_date_time',
|
|
89
|
-
'conversion_action',
|
|
90
|
-
'conversion_date_time',
|
|
91
|
-
'conversion_value',
|
|
92
|
-
'currency_code',
|
|
93
|
-
'order_id',
|
|
94
|
-
'adjustment_type',
|
|
95
|
-
'restatement_value',
|
|
96
|
-
'adjustment_date_time',
|
|
97
|
-
'user_agent',
|
|
98
|
-
'gclid_date_time_pair',
|
|
99
|
-
'consent',
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Kinds of UserIdentifier.
|
|
104
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
105
|
-
* @type {Array<string>}
|
|
106
|
-
*/
|
|
107
|
-
const IDENTIFIERS = [
|
|
108
|
-
'hashed_email',
|
|
109
|
-
'hashed_phone_number',
|
|
110
|
-
'mobile_id',
|
|
111
|
-
'third_party_user_id',
|
|
112
|
-
'address_info',
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Additional attributes in user data for store sales data or customer match.
|
|
117
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
118
|
-
* @type {Array<string>}
|
|
119
|
-
*/
|
|
120
|
-
const USERDATA_ADDITIONAL_ATTRIBUTES = [
|
|
121
|
-
'transaction_attribute',
|
|
122
|
-
'user_attribute',
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Maximum number of user identifiers in single UserData.
|
|
127
|
-
* @see https://ads-developers.googleblog.com/2021/10/userdata-enforcement-in-google-ads-api.html
|
|
128
|
-
* @type {number}
|
|
129
|
-
*/
|
|
130
|
-
const MAX_IDENTIFIERS_PER_USER = 20;
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Configuration for uploading click conversions, call converions or conversion
|
|
134
|
-
* adjustments for Google Ads, includes:
|
|
135
|
-
* gclid, conversion_action, conversion_date_time, conversion_value,
|
|
136
|
-
* currency_code, order_id, external_attribution_data,
|
|
137
|
-
* caller_id, call_start_date_time,
|
|
138
|
-
* adjustment_type, adjustment_date_time, user_agent, gclid_date_time_pair, etc.
|
|
139
|
-
* @see PICKED_PROPERTIES
|
|
140
|
-
*
|
|
141
|
-
* Other properties that will be used to build the conversions but not picked by
|
|
142
|
-
* the value directly including:
|
|
143
|
-
* 1. 'user_identifier_source', source of the user identifier. If there is user
|
|
144
|
-
* identifiers information in the conversion, this property should be set as
|
|
145
|
-
* 'FIRST_PARTY'.
|
|
146
|
-
* @see IDENTIFIERS
|
|
147
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier?hl=en
|
|
148
|
-
* 2. 'custom_variable_tags', the tags of conversion custom variables. To upload
|
|
149
|
-
* custom variables, 'conversion_custom_variable_id' is required rather than the
|
|
150
|
-
* 'tag'. So the invoker is expected to use the function
|
|
151
|
-
* 'getConversionCustomVariableId' to get the ids and pass in as a
|
|
152
|
-
* map(customVariables) of <tag, id> pairs before uploading conversions.
|
|
153
|
-
*
|
|
154
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
155
|
-
* @typedef {{
|
|
156
|
-
* external_attribution_data: (GoogleAdsApi.ExternalAttributionData|undefined),
|
|
157
|
-
* cart_data: (object|undefined),
|
|
158
|
-
* gclid: (string|undefined),
|
|
159
|
-
* caller_id: (string|undefined),
|
|
160
|
-
* call_start_date_time: (string|undefined),
|
|
161
|
-
* conversion_action: string,
|
|
162
|
-
* conversion_date_time: string,
|
|
163
|
-
* conversion_value: number,
|
|
164
|
-
* currency_code:(string|undefined),
|
|
165
|
-
* order_id: (string|undefined),
|
|
166
|
-
* adjustment_type: (string|undefined),
|
|
167
|
-
* adjustment_date_time: (!ConversionAdjustmentType|undefined),
|
|
168
|
-
* user_agent: (string|undefined),
|
|
169
|
-
* user_identifier_source:(!UserIdentifierSource|undefined),
|
|
170
|
-
* custom_variable_tags:(!Array<string>|undefined),
|
|
171
|
-
* customVariables:(!Object<string,string>|undefined),
|
|
172
|
-
* }}
|
|
173
|
-
*/
|
|
174
|
-
let ConversionConfig;
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Configuration for uploading customer match to Google Ads, includes:
|
|
178
|
-
* customer_id, login_customer_id, list_id and operation.
|
|
179
|
-
* If audience list_id is not present, 'list_name' and 'upload_key_type' need to
|
|
180
|
-
* be there so they can be used to create a customer match user list.
|
|
181
|
-
* operation must be one of the two: 'create' or 'remove'.
|
|
182
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
183
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUploadKeyTypeEnum.CustomerMatchUploadKeyType
|
|
184
|
-
* @typedef {{
|
|
185
|
-
* customer_id: (string|number),
|
|
186
|
-
* login_customer_id: (string|number),
|
|
187
|
-
* list_id: (string|undefined),
|
|
188
|
-
* list_name: (string|undefined),
|
|
189
|
-
* upload_key_type: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
|
|
190
|
-
* operation: ('create'|'remove'),
|
|
191
|
-
* }}
|
|
192
|
-
*/
|
|
193
|
-
let CustomerMatchConfig;
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Configuration for offline user data job, includes:
|
|
197
|
-
* customer_id, login_customer_id, list_id, operation and type.
|
|
198
|
-
* 'operation' should be one of the two: 'create' or 'remove',
|
|
199
|
-
* 'type' is OfflineUserDataJobType, it can be 'CUSTOMER_MATCH_USER_LIST',
|
|
200
|
-
* 'CUSTOMER_MATCH_WITH_ATTRIBUTES' or 'STORE_SALES_UPLOAD_FIRST_PARTY'.
|
|
201
|
-
* For job type 'CUSTOMER_MATCH_USER_LIST', if `list_id` is not present,
|
|
202
|
-
* 'list_name' and 'upload_key_type' need to be there so they can be used to
|
|
203
|
-
* create a customer match user list.
|
|
204
|
-
* For job type 'CUSTOMER_MATCH_WITH_ATTRIBUTES', 'user_attribute' can be used
|
|
205
|
-
* to store shared additional user attributes.
|
|
206
|
-
* For job type 'STORE_SALES_UPLOAD_FIRST_PARTY', `store_sales_metadata` is
|
|
207
|
-
* required to offer StoreSalesMetadata. Besides that, for the store sales data,
|
|
208
|
-
* common data (e.g. `currency_code`, `conversion_action`) in
|
|
209
|
-
* `transaction_attribute` can be put here as well.
|
|
210
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/OfflineUserDataJob
|
|
211
|
-
* @typedef {{
|
|
212
|
-
* customer_id: (string|number),
|
|
213
|
-
* login_customer_id: (string|number),
|
|
214
|
-
* list_id: (string|undefined),
|
|
215
|
-
* list_name: (string|undefined),
|
|
216
|
-
* upload_key_type: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
|
|
217
|
-
* operation: ('create'|'remove'),
|
|
218
|
-
* type: !OfflineUserDataJobType,
|
|
219
|
-
* store_sales_metadata: (undefined|StoreSalesMetadata),
|
|
220
|
-
* transaction_attribute: (undefined|TransactionAttribute),
|
|
221
|
-
* user_attribute: (undefined|UserAttribute),
|
|
222
|
-
* }}
|
|
223
|
-
*/
|
|
224
|
-
let OfflineUserDataJobConfig;
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Configuration for uploading customer match data for Google Ads.
|
|
228
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
229
|
-
* @typedef {{
|
|
230
|
-
* hashed_email: (string|Array<string>|undefined),
|
|
231
|
-
* hashed_phone_number: (string|Array<string>|undefined),
|
|
232
|
-
* mobile_id: (string|Array<string>|undefined),
|
|
233
|
-
* third_party_user_id: (string|Array<string>|undefined),
|
|
234
|
-
* address_info: (GoogleAdsApi.OfflineUserAddressInfo|undefined),
|
|
235
|
-
* }}
|
|
236
|
-
*/
|
|
237
|
-
let CustomerMatchRecord;
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Configuration for querying report from Google Ads, includes:
|
|
241
|
-
* entity, attributes, metrics, and constraints etc.
|
|
242
|
-
* For other properties, see
|
|
243
|
-
* https://opteo.com/dev/google-ads-api/#report
|
|
244
|
-
* https://developers.google.com/google-ads/api/docs/query/grammar
|
|
245
|
-
* @typedef {{
|
|
246
|
-
* entity:string,
|
|
247
|
-
* attributes:(!Array<string>|undefined),
|
|
248
|
-
* metrics:(!Array<string>|undefined),
|
|
249
|
-
* segments:(!Array<string>|undefined),
|
|
250
|
-
* constraints:(!Array<{
|
|
251
|
-
* key:string,
|
|
252
|
-
* op:string,
|
|
253
|
-
* val:string,
|
|
254
|
-
* }|{string,string}>|undefined),
|
|
255
|
-
* date_constant:(string|undefined),
|
|
256
|
-
* from_date:(string|undefined),
|
|
257
|
-
* to_date:(string|undefined),
|
|
258
|
-
* limit:(number|undefined),
|
|
259
|
-
* order_by:(string|undefined),
|
|
260
|
-
* sort_order:('DESC'|'ASC'|undefined),
|
|
261
|
-
* page_size:(number|undefined),
|
|
262
|
-
* }}
|
|
263
|
-
*/
|
|
264
|
-
let ReportQueryConfig;
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Google Ads API class based on Opteo's Nodejs library.
|
|
268
|
-
* see https://opteo.com/dev/google-ads-api/#features
|
|
269
|
-
*/
|
|
270
|
-
class GoogleAds {
|
|
271
|
-
/**
|
|
272
|
-
* Note: Rate limits is set by the access level of Developer token.
|
|
273
|
-
* @param {string} developerToken Developer token to access the API.
|
|
274
|
-
* @param {boolean=} debugMode This is used to set ONLY validate conversions
|
|
275
|
-
* but not real uploading.
|
|
276
|
-
* @param {!Object<string,string>=} env The environment object to hold env
|
|
277
|
-
* variables.
|
|
278
|
-
*/
|
|
279
|
-
constructor(developerToken, debugMode = false, env = process.env) {
|
|
280
|
-
this.developerToken = developerToken;
|
|
281
|
-
this.debugMode = debugMode;
|
|
282
|
-
this.authClient = new AuthClient(API_SCOPES, env);
|
|
283
|
-
this.logger = getLogger('API.ADS');
|
|
284
|
-
this.logger.debug(`Init ${this.constructor.name} with Debug Mode.`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Gets a report synchronously from a given Customer account.
|
|
289
|
-
* The enum fields are present as index number.
|
|
290
|
-
* @param {string} customerId
|
|
291
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
292
|
-
* @param {!ReportQueryConfig} reportQueryConfig
|
|
293
|
-
* @return {!ReadableStream}
|
|
294
|
-
*/
|
|
295
|
-
async getReport(customerId, loginCustomerId, reportQueryConfig) {
|
|
296
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
297
|
-
loginCustomerId, customerId);
|
|
298
|
-
return customer.report(reportQueryConfig);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Gets report as generator of a given Customer account.
|
|
303
|
-
* @param {string} customerId
|
|
304
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
305
|
-
* @param {!ReportQueryConfig} reportQueryConfig
|
|
306
|
-
* @return {!ReadableStream}
|
|
307
|
-
*/
|
|
308
|
-
async generatorReport(customerId, loginCustomerId, reportQueryConfig) {
|
|
309
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
310
|
-
loginCustomerId, customerId);
|
|
311
|
-
return customer.reportStream(reportQueryConfig);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Gets stream report of a given Customer account.
|
|
316
|
-
* @param {string} customerId
|
|
317
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
318
|
-
* @param {!ReportQueryConfig} reportQueryConfig
|
|
319
|
-
* @return {!ReadableStream}
|
|
320
|
-
*/
|
|
321
|
-
async streamReport(customerId, loginCustomerId, reportQueryConfig) {
|
|
322
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
323
|
-
loginCustomerId, customerId);
|
|
324
|
-
return customer.reportStreamRaw(reportQueryConfig);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Returns resources information from Google Ads API. see:
|
|
329
|
-
* https://developers.google.com/google-ads/api/docs/concepts/field-service
|
|
330
|
-
* Note, it looks like this function doesn't check the CID, just using
|
|
331
|
-
* developer token and OAuth.
|
|
332
|
-
* @param {string|number} loginCustomerId Login customer account ID.
|
|
333
|
-
* @param {Array<string>} adFields Array of Ad fields.
|
|
334
|
-
* @param {Array<string>} metadata Select fields, default values are:
|
|
335
|
-
* name, data_type, is_repeated, type_url.
|
|
336
|
-
* @return {!Promise<!Array<GoogleAdsField>>}
|
|
337
|
-
*/
|
|
338
|
-
async searchMetaData(loginCustomerId, adFields, metadata = [
|
|
339
|
-
'name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
340
|
-
const customer = await this.getGoogleAdsApiCustomer_(loginCustomerId);
|
|
341
|
-
const selectClause = metadata.join(',');
|
|
342
|
-
const fields = adFields.join('","');
|
|
343
|
-
const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
|
|
344
|
-
const request = new SearchGoogleAdsFieldsRequest({query});
|
|
345
|
-
const [results] = await customer.googleAdsFields
|
|
346
|
-
.searchGoogleAdsFields(request);
|
|
347
|
-
return results;
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Returns the function to send out a request to Google Ads API with a batch
|
|
351
|
-
* of call conversions.
|
|
352
|
-
* @param {string} customerId
|
|
353
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
354
|
-
* @param {!ConversionConfig} adsConfig Default call conversion params
|
|
355
|
-
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
356
|
-
* Google Ads API.
|
|
357
|
-
*/
|
|
358
|
-
getUploadCallConversionFn(customerId, loginCustomerId, adsConfig) {
|
|
359
|
-
return this.getUploadConversionFnBase_(customerId, loginCustomerId,
|
|
360
|
-
adsConfig, 'uploadCallConversions', 'caller_id');
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Returns the function to send out a request to Google Ads API with a batch
|
|
365
|
-
* of click conversions.
|
|
366
|
-
* @param {string} customerId
|
|
367
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
368
|
-
* @param {!ConversionConfig} adsConfig Default click conversion params
|
|
369
|
-
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
370
|
-
* Google Ads API.
|
|
371
|
-
*/
|
|
372
|
-
getUploadClickConversionFn(customerId, loginCustomerId, adsConfig) {
|
|
373
|
-
return this.getUploadConversionFnBase_(customerId, loginCustomerId,
|
|
374
|
-
adsConfig, 'uploadClickConversions', 'gclid');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Returns the function to send out a request to Google Ads API with a batch
|
|
379
|
-
* of conversion adjustments.
|
|
380
|
-
* @param {string} customerId
|
|
381
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
382
|
-
* @param {!ConversionConfig} adsConfig Default conversion adjustments
|
|
383
|
-
* params.
|
|
384
|
-
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
385
|
-
* Google Ads API.
|
|
386
|
-
*/
|
|
387
|
-
getUploadConversionAdjustmentFn(customerId, loginCustomerId, adsConfig) {
|
|
388
|
-
return this.getUploadConversionFnBase_(customerId, loginCustomerId,
|
|
389
|
-
adsConfig, 'uploadConversionAdjustments', 'order_id');
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Returns the function to send call conversions, click conversions or
|
|
394
|
-
* conversion adjustment (enhanced conversions).
|
|
395
|
-
* @param {string} customerId
|
|
396
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
397
|
-
* @param {!ConversionConfig} adsConfig Default click conversion params
|
|
398
|
-
* @param {string} functionName The name of sending converions function, could
|
|
399
|
-
* be `uploadClickConversions`, `uploadCallConversions` or
|
|
400
|
-
* `uploadConversionAdjustments`.
|
|
401
|
-
* @param {string} propertyForDebug The name of property for debug info.
|
|
402
|
-
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
403
|
-
* Google Ads API.
|
|
404
|
-
* @private
|
|
405
|
-
*/
|
|
406
|
-
getUploadConversionFnBase_(customerId, loginCustomerId, adsConfig,
|
|
407
|
-
functionName, propertyForDebug) {
|
|
408
|
-
/**
|
|
409
|
-
* Sends a batch of hits to Google Ads API.
|
|
410
|
-
* @param {!Array<string>} lines Data for single request. It should be
|
|
411
|
-
* guaranteed that it doesn't exceed quota limitation.
|
|
412
|
-
* @param {string} batchId The tag for log.
|
|
413
|
-
* @return {!BatchResult}
|
|
414
|
-
*/
|
|
415
|
-
return async (lines, batchId) => {
|
|
416
|
-
/** @type {!Array<ConversionConfig>} */
|
|
417
|
-
const conversions = lines.map(
|
|
418
|
-
(line) => buildClickConversionFromLine(line, adsConfig, customerId));
|
|
419
|
-
/** @const {BatchResult} */
|
|
420
|
-
const batchResult = {
|
|
421
|
-
result: true,
|
|
422
|
-
numberOfLines: lines.length,
|
|
423
|
-
};
|
|
424
|
-
try {
|
|
425
|
-
const response = await this[functionName](conversions, customerId,
|
|
426
|
-
loginCustomerId);
|
|
427
|
-
const { results, partial_failure_error: failed } = response;
|
|
428
|
-
if (this.logger.isDebugEnabled()) {
|
|
429
|
-
const id = results.map((conversion) => conversion[propertyForDebug]);
|
|
430
|
-
this.logger.debug(`Uploaded ${propertyForDebug}:`, id);
|
|
431
|
-
}
|
|
432
|
-
if (failed) {
|
|
433
|
-
this.logger.info('partial_failure_error:', failed.message);
|
|
434
|
-
const failures = failed.details.map(
|
|
435
|
-
({ value }) => GoogleAdsFailure.decode(value));
|
|
436
|
-
this.extraFailedLines_(batchResult, failures, lines, 0);
|
|
437
|
-
}
|
|
438
|
-
return batchResult;
|
|
439
|
-
} catch (error) {
|
|
440
|
-
this.logger.error(
|
|
441
|
-
`Error in ${functionName} batch: ${batchId}`, error);
|
|
442
|
-
this.updateBatchResultWithError(batchResult, error, lines, 0);
|
|
443
|
-
return batchResult;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Updates the BatchResult based on errors.
|
|
450
|
-
*
|
|
451
|
-
* There are 2 types of errors here:
|
|
452
|
-
* 1. Normal JavaScript Error object. It happens when the whole process fails
|
|
453
|
-
* (not partial failure), so there is no detailed failed lines.
|
|
454
|
-
* 2. GoogleAdsFailure. It is a Google Ads' own error object which has an
|
|
455
|
-
* array of GoogleAdsError (property name 'errors'). GoogleAdsError contains
|
|
456
|
-
* the detailed failed data if it is a line-error. For example, a wrong
|
|
457
|
-
* encoded user identifier is a line-error, while a wrong user list id is not.
|
|
458
|
-
* GoogleAdsFailure: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsFailure
|
|
459
|
-
* GoogleAdsError: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsError
|
|
460
|
-
*
|
|
461
|
-
* For Customer Match data uploading, there is not partial failure, so the
|
|
462
|
-
* result can be either succeeded or a thrown error. The thrown error will be
|
|
463
|
-
* used to build the returned result here.
|
|
464
|
-
* For Conversions uploading (partial failure enabled), if there is an error
|
|
465
|
-
* fails the whole process, the error will also be thrown and handled here.
|
|
466
|
-
* Otherwise, the errors will be wrapped in the response as the property named
|
|
467
|
-
* 'partial_failure_error' which contains an array of GoogleAdsFailure. This
|
|
468
|
-
* kind of failure doesn't fail the process, while line-errors can be
|
|
469
|
-
* extracted from it.
|
|
470
|
-
* For more information, see the function `extraFailedLines_`.
|
|
471
|
-
*
|
|
472
|
-
* An example of 'GoogleAdsFailure' is:
|
|
473
|
-
* GoogleAdsFailure {
|
|
474
|
-
* errors: [
|
|
475
|
-
* GoogleAdsError {
|
|
476
|
-
* error_code: ErrorCode { offline_user_data_job_error: 25 },
|
|
477
|
-
* message: 'The SHA256 encoded value is malformed.',
|
|
478
|
-
* location: ErrorLocation {
|
|
479
|
-
* field_path_elements: [
|
|
480
|
-
* FieldPathElement { field_name: 'operations', index: 0 },
|
|
481
|
-
* FieldPathElement { field_name: 'create' },
|
|
482
|
-
* FieldPathElement { field_name: 'user_identifiers', index: 0 },
|
|
483
|
-
* FieldPathElement { field_name: 'hashed_email' }
|
|
484
|
-
* ]
|
|
485
|
-
* }
|
|
486
|
-
* }
|
|
487
|
-
* ],
|
|
488
|
-
* request_id: 'xxxxxxxxxxxxxxx'
|
|
489
|
-
* }
|
|
490
|
-
*
|
|
491
|
-
* @param {!BatchResult} batchResult
|
|
492
|
-
* @param {(!GoogleAdsFailure|!Error)} error
|
|
493
|
-
* @param {!Array<string>} lines The original input data.
|
|
494
|
-
* @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
|
|
495
|
-
* 'field_path_elements'. This is used to get the original line related to
|
|
496
|
-
* this GoogleAdsError.
|
|
497
|
-
*/
|
|
498
|
-
updateBatchResultWithError(batchResult, error, lines, fieldPathIndex) {
|
|
499
|
-
batchResult.result = false;
|
|
500
|
-
if (error.errors) { //GoogleAdsFailure
|
|
501
|
-
this.extraFailedLines_(batchResult, [error], lines, fieldPathIndex);
|
|
502
|
-
} else {
|
|
503
|
-
batchResult.errors = [error.message || error.toString()];
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Extras failed lines based on the GoogleAdsFailures.
|
|
509
|
-
*
|
|
510
|
-
* Different errors have different 'fieldPathIndex' which is the index of
|
|
511
|
-
* failed lines in original input data (an array of a string).
|
|
512
|
-
*
|
|
513
|
-
* For conversions, the ErrorLocation is like:
|
|
514
|
-
* ErrorLocation {
|
|
515
|
-
* field_path_elements: [
|
|
516
|
-
* FieldPathElement { field_name: 'operations', index: 0 },
|
|
517
|
-
* FieldPathElement { field_name: 'create' }
|
|
518
|
-
* ]
|
|
519
|
-
* }
|
|
520
|
-
* So the index is 0, index of 'operations'.
|
|
521
|
-
*
|
|
522
|
-
* For customer match upload, the ErrorLocation is like:
|
|
523
|
-
* ErrorLocation {
|
|
524
|
-
* field_path_elements: [
|
|
525
|
-
* FieldPathElement { field_name: 'operations', index: 0 },
|
|
526
|
-
* FieldPathElement { field_name: 'create' },
|
|
527
|
-
* FieldPathElement { field_name: 'user_identifiers', index: 0 },
|
|
528
|
-
* FieldPathElement { field_name: 'hashed_email' }
|
|
529
|
-
* ]
|
|
530
|
-
* }
|
|
531
|
-
* The index should be 2, index of 'user_identifiers'.
|
|
532
|
-
*
|
|
533
|
-
* With this we can get errors and failed lines. The function will set
|
|
534
|
-
* following for the given BatchResult object:
|
|
535
|
-
* result - false
|
|
536
|
-
* errors - de-duplicated error reasons
|
|
537
|
-
* failedLines - failed lines, an array of string. Without the reason of
|
|
538
|
-
* failure.
|
|
539
|
-
* groupedFailed - a hashmap of failed the lines. The key is the reason, the
|
|
540
|
-
* value is the array of failed lines due to this reason.
|
|
541
|
-
* @param {!BatchResult} batchResult
|
|
542
|
-
* @param {!Array<!GoogleAdsFailure>} failures
|
|
543
|
-
* @param {!Array<string>} lines The original input data.
|
|
544
|
-
* @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
|
|
545
|
-
* 'field_path_elements'. This is used to get the original line related to
|
|
546
|
-
* this GoogleAdsError.
|
|
547
|
-
* @private
|
|
548
|
-
*/
|
|
549
|
-
extraFailedLines_(batchResult, failures, lines, fieldPathIndex) {
|
|
550
|
-
batchResult.result = false;
|
|
551
|
-
batchResult.failedLines = [];
|
|
552
|
-
batchResult.groupedFailed = {};
|
|
553
|
-
const errors = new Set();
|
|
554
|
-
failures.forEach((failure) => {
|
|
555
|
-
failure.errors.forEach(({message, location}) => {
|
|
556
|
-
errors.add(message);
|
|
557
|
-
if (location && location.field_path_elements[fieldPathIndex]) {
|
|
558
|
-
const {index} = location.field_path_elements[fieldPathIndex];
|
|
559
|
-
if (typeof index === 'undefined') {
|
|
560
|
-
this.logger.warn(`Unknown field path index: ${fieldPathIndex}`,
|
|
561
|
-
location.field_path_elements);
|
|
562
|
-
} else {
|
|
563
|
-
const groupedFailed = batchResult.groupedFailed[message] || [];
|
|
564
|
-
const failedLine = lines[index];
|
|
565
|
-
batchResult.failedLines.push(failedLine);
|
|
566
|
-
groupedFailed.push(failedLine);
|
|
567
|
-
if (groupedFailed.length === 1) {
|
|
568
|
-
batchResult.groupedFailed[message] = groupedFailed;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
batchResult.errors = Array.from(errors);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Uploads call conversions to google ads account.
|
|
579
|
-
* It requires an array of call conversions and customer id.
|
|
580
|
-
* In DEBUG mode, this function will only validate the conversions.
|
|
581
|
-
* @param {Array<ConversionConfig>} callConversions Call Conversions
|
|
582
|
-
* @param {string} customerId
|
|
583
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
584
|
-
* @return {!Promise<!UploadCallConversionsResponse>}
|
|
585
|
-
*/
|
|
586
|
-
async uploadCallConversions(callConversions, customerId, loginCustomerId) {
|
|
587
|
-
this.logger.debug('Upload call conversions for customerId:', customerId);
|
|
588
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
589
|
-
loginCustomerId, customerId);
|
|
590
|
-
const request = new UploadCallConversionsRequest({
|
|
591
|
-
conversions: callConversions,
|
|
592
|
-
customer_id: customerId,
|
|
593
|
-
validate_only: this.debugMode, // when true makes no changes
|
|
594
|
-
partial_failure: true, // Will still create the non-failed entities
|
|
595
|
-
});
|
|
596
|
-
return customer.conversionUploads.uploadCallConversions(request);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Uploads click conversions to google ads account.
|
|
601
|
-
* It requires an array of click conversions and customer id.
|
|
602
|
-
* In DEBUG mode, this function will only validate the conversions.
|
|
603
|
-
* @param {Array<ConversionConfig>} clickConversions ClickConversions
|
|
604
|
-
* @param {string} customerId
|
|
605
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
606
|
-
* @return {!Promise<!UploadClickConversionsResponse>}
|
|
607
|
-
*/
|
|
608
|
-
async uploadClickConversions(clickConversions, customerId, loginCustomerId) {
|
|
609
|
-
this.logger.debug('Upload click conversions for customerId:', customerId);
|
|
610
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
611
|
-
loginCustomerId, customerId);
|
|
612
|
-
const request = new UploadClickConversionsRequest({
|
|
613
|
-
conversions: clickConversions,
|
|
614
|
-
customer_id: customerId,
|
|
615
|
-
validate_only: this.debugMode, // when true makes no changes
|
|
616
|
-
partial_failure: true, // Will still create the non-failed entities
|
|
617
|
-
});
|
|
618
|
-
return customer.conversionUploads.uploadClickConversions(request);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Uploads conversion adjustments to google ads account.
|
|
623
|
-
* It requires an array of conversion adjustments and customer id.
|
|
624
|
-
* In DEBUG mode, this function will only validate the conversion adjustments.
|
|
625
|
-
* @param {Array<ConversionAdjustment>} conversionAdjustments Conversion
|
|
626
|
-
* adjustments.
|
|
627
|
-
* @param {string} customerId
|
|
628
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
629
|
-
* @return {!Promise<!UploadConversionAdjustmentsResponse>}
|
|
630
|
-
*/
|
|
631
|
-
async uploadConversionAdjustments(conversionAdjustments, customerId,
|
|
632
|
-
loginCustomerId) {
|
|
633
|
-
this.logger.debug('Upload conversion adjustments for customerId:',
|
|
634
|
-
customerId);
|
|
635
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
636
|
-
loginCustomerId, customerId);
|
|
637
|
-
const request = new UploadConversionAdjustmentsRequest({
|
|
638
|
-
conversion_adjustments: conversionAdjustments,
|
|
639
|
-
customer_id: customerId,
|
|
640
|
-
validate_only: this.debugMode, // when true makes no changes
|
|
641
|
-
partial_failure: true, // Will still create the non-failed entities
|
|
642
|
-
});
|
|
643
|
-
return customer.conversionAdjustmentUploads.uploadConversionAdjustments(
|
|
644
|
-
request);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Returns the id of Conversion Custom Variable with the given tag.
|
|
649
|
-
* @param {string} tag Custom Variable tag.
|
|
650
|
-
* @param {string} customerId
|
|
651
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
652
|
-
* @return {Promise<number|undefined>} Returns undefined if can't find tag.
|
|
653
|
-
*/
|
|
654
|
-
async getConversionCustomVariableId(tag, customerId, loginCustomerId) {
|
|
655
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
656
|
-
loginCustomerId, customerId);
|
|
657
|
-
const customVariables = await customer.query(`
|
|
658
|
-
SELECT conversion_custom_variable.id,
|
|
659
|
-
conversion_custom_variable.tag
|
|
660
|
-
FROM conversion_custom_variable
|
|
661
|
-
WHERE conversion_custom_variable.tag = "${tag}" LIMIT 1
|
|
662
|
-
`);
|
|
663
|
-
if (customVariables.length > 0) {
|
|
664
|
-
return customVariables[0].conversion_custom_variable.id;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Gets the user list_id of a given list name and upload key type. It
|
|
670
|
-
* only looks for a CRM_BASED and OPEN list.
|
|
671
|
-
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
672
|
-
* @return {number|undefined} User list_id if it exists.
|
|
673
|
-
*/
|
|
674
|
-
async getCustomerMatchUserListId(customerMatchConfig) {
|
|
675
|
-
const customerId = this.getCleanCid_(customerMatchConfig.customer_id);
|
|
676
|
-
const loginCustomerId = this.getCleanCid_(
|
|
677
|
-
customerMatchConfig.login_customer_id);
|
|
678
|
-
const listName = customerMatchConfig.list_name;
|
|
679
|
-
const uploadKeyType = customerMatchConfig.upload_key_type;
|
|
680
|
-
const reportConfig = {
|
|
681
|
-
entity: 'user_list',
|
|
682
|
-
attributes: [
|
|
683
|
-
'user_list.id',
|
|
684
|
-
'user_list.resource_name',
|
|
685
|
-
],
|
|
686
|
-
constraints: {
|
|
687
|
-
'user_list.name': listName,
|
|
688
|
-
'customer.id': customerId,
|
|
689
|
-
'user_list.type': UserListType.CRM_BASED,
|
|
690
|
-
'user_list.membership_status': UserListMembershipStatus.OPEN,
|
|
691
|
-
'user_list.crm_based_user_list.upload_key_type': uploadKeyType,
|
|
692
|
-
},
|
|
693
|
-
};
|
|
694
|
-
const userlists =
|
|
695
|
-
await this.getReport(customerId, loginCustomerId, reportConfig);
|
|
696
|
-
return userlists.length === 0 ? undefined : userlists[0].user_list.id;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Creates the user list based on a given customerMatchConfig and returns the
|
|
701
|
-
* Id. The user list would be a CRM_BASED type.
|
|
702
|
-
* Trying to create a list with an used name will fail.
|
|
703
|
-
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
704
|
-
* @return {number} The created user list id. Note this is not the resource
|
|
705
|
-
* name.
|
|
706
|
-
*/
|
|
707
|
-
async createCustomerMatchUserList(customerMatchConfig) {
|
|
708
|
-
const customerId = this.getCleanCid_(customerMatchConfig.customer_id);
|
|
709
|
-
const loginCustomerId = this.getCleanCid_(
|
|
710
|
-
customerMatchConfig.login_customer_id);
|
|
711
|
-
const listName = customerMatchConfig.list_name;
|
|
712
|
-
const uploadKeyType = customerMatchConfig.upload_key_type;
|
|
713
|
-
const userList = UserList.create({
|
|
714
|
-
name: listName,
|
|
715
|
-
type: UserListType.CRM_BASED,
|
|
716
|
-
crm_based_user_list: { upload_key_type: uploadKeyType },
|
|
717
|
-
});
|
|
718
|
-
const options = {
|
|
719
|
-
validate_only: this.debugMode, // when true makes no changes
|
|
720
|
-
partial_failure: true, // Will still create the non-failed entities
|
|
721
|
-
};
|
|
722
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
723
|
-
loginCustomerId, customerId);
|
|
724
|
-
const response = await customer.userLists.create([userList], options);
|
|
725
|
-
const { results, partial_failure_error: failed } = response;
|
|
726
|
-
if (this.logger.isDebugEnabled()) {
|
|
727
|
-
this.logger.debug(`Created crm userlist from`, customerMatchConfig);
|
|
728
|
-
}
|
|
729
|
-
if (failed) {
|
|
730
|
-
const failures = failed.errors.map(({ message }) => message).join(' ');
|
|
731
|
-
this.logger.info('partial_failure_error:', failures);
|
|
732
|
-
throw new Error(failures);
|
|
733
|
-
}
|
|
734
|
-
if (!results[0]) {
|
|
735
|
-
if (this.debugMode) {
|
|
736
|
-
throw new Error('No UserList was created in DEBUG mode.');
|
|
737
|
-
} else {
|
|
738
|
-
throw new Error('No UserList was created.');
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
const resourceName = results[0].resource_name;
|
|
742
|
-
const splitted = resourceName.split('/');
|
|
743
|
-
return splitted[splitted.length - 1];
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
/**
|
|
747
|
-
* Returns the function to send out a request to Google Ads API with
|
|
748
|
-
* user ids for Customer Match upload
|
|
749
|
-
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
750
|
-
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
751
|
-
* Google Ads API.
|
|
752
|
-
*/
|
|
753
|
-
getUploadCustomerMatchFn(customerMatchConfig) {
|
|
754
|
-
/**
|
|
755
|
-
* Sends a batch of hits to Google Ads API.
|
|
756
|
-
* @param {!Array<string>} lines Data for single request. It should be
|
|
757
|
-
* guaranteed that it doesn't exceed quota limitation.
|
|
758
|
-
* @param {string} batchId The tag for log.
|
|
759
|
-
* @return {!Promise<BatchResult>}
|
|
760
|
-
*/
|
|
761
|
-
return async (lines, batchId) => {
|
|
762
|
-
/** @type {Array<CustomerMatchRecord>} */
|
|
763
|
-
const userIds = lines.map((line) => JSON.parse(line));
|
|
764
|
-
/** @const {BatchResult} */ const batchResult = {
|
|
765
|
-
result: true,
|
|
766
|
-
numberOfLines: lines.length,
|
|
767
|
-
};
|
|
768
|
-
try {
|
|
769
|
-
const response = await this.uploadUserDataToUserList(userIds,
|
|
770
|
-
customerMatchConfig);
|
|
771
|
-
this.logger.debug(`Customer Match upload batch[${batchId}]`, response);
|
|
772
|
-
return batchResult;
|
|
773
|
-
} catch (error) {
|
|
774
|
-
this.logger.error(
|
|
775
|
-
`Error in Customer Match upload batch[${batchId}]`, error);
|
|
776
|
-
this.updateBatchResultWithError(batchResult, error, lines, 2);
|
|
777
|
-
return batchResult;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Uploads a user data to a user list (aka customer match).
|
|
784
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataService
|
|
785
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
786
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
787
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
788
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
789
|
-
* Please note: The UserDataService has a limit of 10 UserDataOperations
|
|
790
|
-
* and 100 user IDs per request
|
|
791
|
-
* @see https://developers.google.com/google-ads/api/docs/migration/user-data-service#rate_limits
|
|
792
|
-
* @param {!Array<CustomerMatchRecord>} customerMatchRecords user Ids
|
|
793
|
-
* @param {CustomerMatchConfig} customerMatchConfig Customer Match config containing
|
|
794
|
-
* customer_id, login_customer_id, list_id, list_type which can be one of the following
|
|
795
|
-
* hashed_email, hashed_phone_number, mobile_id, third_party_user_id or address_info and
|
|
796
|
-
* operation which can be either 'create' or 'remove'
|
|
797
|
-
* @return {!Promise<UploadUserDataResponse>}
|
|
798
|
-
*/
|
|
799
|
-
async uploadUserDataToUserList(customerMatchRecords, customerMatchConfig) {
|
|
800
|
-
const customerId = this.getCleanCid_(customerMatchConfig.customer_id);
|
|
801
|
-
const loginCustomerId = this.getCleanCid_(
|
|
802
|
-
customerMatchConfig.login_customer_id);
|
|
803
|
-
const userListId = customerMatchConfig.list_id;
|
|
804
|
-
const operation = customerMatchConfig.operation;
|
|
805
|
-
|
|
806
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
807
|
-
loginCustomerId, customerId);
|
|
808
|
-
const operationsList = this.buildOperationsList_(operation,
|
|
809
|
-
customerMatchRecords);
|
|
810
|
-
const metadata = this.buildCustomerMatchUserListMetadata_(customerId,
|
|
811
|
-
userListId);
|
|
812
|
-
const request = UploadUserDataRequest.create({
|
|
813
|
-
customer_id: customerId,
|
|
814
|
-
operations: operationsList,
|
|
815
|
-
customer_match_user_list_metadata: metadata,
|
|
816
|
-
});
|
|
817
|
-
const response = await customer.userData.uploadUserData(request);
|
|
818
|
-
return response;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Builds a list of UserDataOperations.
|
|
823
|
-
* Since v6 you can set a user_attribute
|
|
824
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
825
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
826
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
827
|
-
* @param {string} operationType either 'create' or 'remove'
|
|
828
|
-
* @param {Array<CustomerMatchRecord>} customerMatchRecords userIds
|
|
829
|
-
* @param {{
|
|
830
|
-
* transaction_attribute: TransactionAttribute|undefined,
|
|
831
|
-
* user_attribute: UserAttribute,
|
|
832
|
-
* }} additionalAttributes Additional attributes for 'UserData', includes
|
|
833
|
-
* 'transaction_attribute' or 'user_attribute'.
|
|
834
|
-
* @return {Array<UserDataOperation>}
|
|
835
|
-
* @private
|
|
836
|
-
*/
|
|
837
|
-
buildOperationsList_(operationType, customerMatchRecords,
|
|
838
|
-
additionalAttributes = {}) {
|
|
839
|
-
return customerMatchRecords.map((customerMatchRecord) => {
|
|
840
|
-
const userIdentifiers = [];
|
|
841
|
-
IDENTIFIERS.forEach((idType) => {
|
|
842
|
-
const idValue = customerMatchRecord[idType];
|
|
843
|
-
if (idValue) {
|
|
844
|
-
if (Array.isArray(idValue)) {
|
|
845
|
-
idValue.forEach((user) => {
|
|
846
|
-
userIdentifiers.push(UserIdentifier.create({[idType]: user}));
|
|
847
|
-
});
|
|
848
|
-
} else {
|
|
849
|
-
userIdentifiers.push(UserIdentifier.create({[idType]: idValue}));
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
});
|
|
853
|
-
const userData = {};
|
|
854
|
-
if (userIdentifiers.length <= MAX_IDENTIFIERS_PER_USER) {
|
|
855
|
-
userData.user_identifiers = userIdentifiers;
|
|
856
|
-
} else {
|
|
857
|
-
this.logger.warn(
|
|
858
|
-
`Too many user identifiers, will only send ${MAX_IDENTIFIERS_PER_USER}:`,
|
|
859
|
-
JSON.stringify(customerMatchRecord));
|
|
860
|
-
userData.user_identifiers =
|
|
861
|
-
userIdentifiers.slice(0, MAX_IDENTIFIERS_PER_USER);
|
|
862
|
-
}
|
|
863
|
-
USERDATA_ADDITIONAL_ATTRIBUTES.forEach((attribute) => {
|
|
864
|
-
if (additionalAttributes[attribute] || customerMatchRecord[attribute]) {
|
|
865
|
-
userData[attribute] = lodash.merge(
|
|
866
|
-
{}, additionalAttributes[attribute], customerMatchRecord[attribute]);
|
|
867
|
-
}
|
|
868
|
-
})
|
|
869
|
-
return UserDataOperation.create(
|
|
870
|
-
{ [operationType]: UserData.create(userData) });
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Creates CustomerMatchUserListMetadata.
|
|
876
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
877
|
-
* @param {string} customerId part of the ResourceName to be mutated
|
|
878
|
-
* @param {string} userListId part of the ResourceName to be mutated
|
|
879
|
-
* @return {!CustomerMatchUserListMetadata}
|
|
880
|
-
* @private
|
|
881
|
-
*/
|
|
882
|
-
buildCustomerMatchUserListMetadata_(customerId, userListId) {
|
|
883
|
-
const resourceName = `customers/${customerId}/userLists/${userListId}`;
|
|
884
|
-
return CustomerMatchUserListMetadata.create({
|
|
885
|
-
user_list: resourceName,
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Returns a integer format CID by removing dashes.
|
|
891
|
-
* @param {string} cid
|
|
892
|
-
* @return {string}
|
|
893
|
-
* @private
|
|
894
|
-
*/
|
|
895
|
-
getCleanCid_(cid) {
|
|
896
|
-
return cid.replace(/-/g, '');
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/**
|
|
900
|
-
* Get OfflineUserDataJob status.
|
|
901
|
-
* @param {OfflineUserDataJobConfig} config Offline user data job config.
|
|
902
|
-
* @param {string} resourceName
|
|
903
|
-
* @return {!OfflineUserDataJobStatus} Job status
|
|
904
|
-
*/
|
|
905
|
-
async getOfflineUserDataJob(config, resourceName) {
|
|
906
|
-
const loginCustomerId = this.getCleanCid_(config.login_customer_id);
|
|
907
|
-
const customerId = this.getCleanCid_(config.customer_id);
|
|
908
|
-
const reportConfig = {
|
|
909
|
-
entity: 'offline_user_data_job',
|
|
910
|
-
attributes: [
|
|
911
|
-
'offline_user_data_job.id',
|
|
912
|
-
'offline_user_data_job.status',
|
|
913
|
-
'offline_user_data_job.type',
|
|
914
|
-
'offline_user_data_job.customer_match_user_list_metadata.user_list',
|
|
915
|
-
'offline_user_data_job.failure_reason',
|
|
916
|
-
],
|
|
917
|
-
constraints: {
|
|
918
|
-
'offline_user_data_job.resource_name': resourceName,
|
|
919
|
-
},
|
|
920
|
-
};
|
|
921
|
-
const jobs = await this.getReport(customerId, loginCustomerId, reportConfig);
|
|
922
|
-
if (jobs.length === 0) {
|
|
923
|
-
throw new Error(`Can't find the OfflineUserDataJob: ${resourceName}`);
|
|
924
|
-
}
|
|
925
|
-
const { failure_reason: failure, status } = jobs[0].offline_user_data_job;
|
|
926
|
-
if (failure > 0) {
|
|
927
|
-
this.logger.warn(`Offline UserData Job [${resourceName}] failed: `,
|
|
928
|
-
OfflineUserDataJobFailureReason[failure])
|
|
929
|
-
}
|
|
930
|
-
return OfflineUserDataJobStatus[status];
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Creates a OfflineUserDataJob and returns resource name.
|
|
935
|
-
* @param {OfflineUserDataJobConfig} config Offline user data job config.
|
|
936
|
-
* @return {string} The resouce name of the creaed job.
|
|
937
|
-
*/
|
|
938
|
-
async createOfflineUserDataJob(config) {
|
|
939
|
-
const loginCustomerId = this.getCleanCid_(config.login_customer_id);
|
|
940
|
-
const customerId = this.getCleanCid_(config.customer_id);
|
|
941
|
-
const { list_id: userListId, type } = config;
|
|
942
|
-
this.logger.debug('Creating OfflineUserDataJob for CID:', customerId);
|
|
943
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
944
|
-
loginCustomerId, customerId);
|
|
945
|
-
const job = OfflineUserDataJob.create({
|
|
946
|
-
type,
|
|
947
|
-
});
|
|
948
|
-
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/customers.offlineUserDataJobs?hl=en#CustomerMatchUserListMetadata
|
|
949
|
-
if (type.startsWith('CUSTOMER_MATCH')) {
|
|
950
|
-
const metadata = this.buildCustomerMatchUserListMetadata_(customerId,
|
|
951
|
-
userListId);
|
|
952
|
-
job.customer_match_user_list_metadata = metadata;
|
|
953
|
-
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/customers.offlineUserDataJobs?hl=en#StoreSalesMetadata
|
|
954
|
-
} else if (type.startsWith('STORE_SALES')) {
|
|
955
|
-
// Support previous property 'StoreSalesMetadata' for compatibility.
|
|
956
|
-
if (config.store_sales_metadata || config.StoreSalesMetadata) {
|
|
957
|
-
job.store_sales_metadata = config.store_sales_metadata
|
|
958
|
-
|| config.StoreSalesMetadata;
|
|
959
|
-
}
|
|
960
|
-
} else {
|
|
961
|
-
throw new Error(`UNSUPPORTED OfflineUserDataJobType: ${type}.`);
|
|
962
|
-
}
|
|
963
|
-
const request = CreateOfflineUserDataJobRequest.create({
|
|
964
|
-
customer_id: customerId,
|
|
965
|
-
job,
|
|
966
|
-
validate_only: this.debugMode, // when true makes no changes
|
|
967
|
-
enable_match_rate_range_preview: true,
|
|
968
|
-
});
|
|
969
|
-
const { resource_name: resourceName } =
|
|
970
|
-
await customer.offlineUserDataJobs.createOfflineUserDataJob(request);
|
|
971
|
-
this.logger.info('Created OfflineUserDataJob:', resourceName);
|
|
972
|
-
return resourceName;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Adds user data in to the OfflineUserDataJob.
|
|
977
|
-
* @param {OfflineUserDataJobConfig} config Offline user data job config.
|
|
978
|
-
* @param {string} jobResourceName
|
|
979
|
-
* @param {!Array<CustomerMatchRecord>} customerMatchRecords user Ids
|
|
980
|
-
* @return {!Promise<AddOfflineUserDataJobOperationsResponse>}
|
|
981
|
-
*/
|
|
982
|
-
async addOperationsToOfflineUserDataJob(config, jobResourceName, records) {
|
|
983
|
-
const start = new Date().getTime();
|
|
984
|
-
const loginCustomerId = this.getCleanCid_(config.login_customer_id);
|
|
985
|
-
const customerId = this.getCleanCid_(config.customer_id);
|
|
986
|
-
const operation = config.operation;
|
|
987
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
988
|
-
loginCustomerId, customerId);
|
|
989
|
-
const operationsList = this.buildOperationsList_(operation, records, config);
|
|
990
|
-
const request = AddOfflineUserDataJobOperationsRequest.create({
|
|
991
|
-
resource_name: jobResourceName,
|
|
992
|
-
operations: operationsList,
|
|
993
|
-
validate_only: false,//this.debugMode,
|
|
994
|
-
enable_partial_failure: true,
|
|
995
|
-
enable_warnings: true,
|
|
996
|
-
});
|
|
997
|
-
const response = await customer.
|
|
998
|
-
offlineUserDataJobs.addOfflineUserDataJobOperations(request);
|
|
999
|
-
this.logger.debug(`Added ${records.length} records in (ms):`,
|
|
1000
|
-
new Date().getTime() - start);
|
|
1001
|
-
return response;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
/**
|
|
1005
|
-
* Starts the OfflineUserDataJob.
|
|
1006
|
-
* @param {OfflineUserDataJobConfig} config Offline user data job config.
|
|
1007
|
-
* @param {string} jobResourceName
|
|
1008
|
-
* @returns
|
|
1009
|
-
*/
|
|
1010
|
-
async runOfflineUserDataJob(config, jobResourceName) {
|
|
1011
|
-
const loginCustomerId = this.getCleanCid_(config.login_customer_id);
|
|
1012
|
-
const customerId = this.getCleanCid_(config.customer_id);
|
|
1013
|
-
const customer = await this.getGoogleAdsApiCustomer_(
|
|
1014
|
-
loginCustomerId, customerId);
|
|
1015
|
-
const request = RunOfflineUserDataJobRequest.create({
|
|
1016
|
-
resource_name: jobResourceName,
|
|
1017
|
-
validate_only: false,//this.debugMode,
|
|
1018
|
-
});
|
|
1019
|
-
const rawResponse = await customer.
|
|
1020
|
-
offlineUserDataJobs.runOfflineUserDataJob(request);
|
|
1021
|
-
const response = lodash.pick(rawResponse, ['name', 'done', 'error']);
|
|
1022
|
-
this.logger.debug('runOfflineUserDataJob response: ', response);
|
|
1023
|
-
return response;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Returns the function to send out a request to Google Ads API with
|
|
1028
|
-
* user data as operations in OfflineUserDataJob.
|
|
1029
|
-
* @param {!OfflineUserDataJobConfig} config
|
|
1030
|
-
* @param {string} jobResourceName
|
|
1031
|
-
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
1032
|
-
* Google Ads API.
|
|
1033
|
-
*/
|
|
1034
|
-
getAddOperationsToOfflineUserDataJobFn(config, jobResourceName) {
|
|
1035
|
-
/**
|
|
1036
|
-
* Sends a batch of hits to Google Ads API.
|
|
1037
|
-
* @param {!Array<string>} lines Data for single request. It should be
|
|
1038
|
-
* guaranteed that it doesn't exceed quota limitation.
|
|
1039
|
-
* @param {string} batchId The tag for log.
|
|
1040
|
-
* @return {!Promise<BatchResult>}
|
|
1041
|
-
*/
|
|
1042
|
-
return async (lines, batchId) => {
|
|
1043
|
-
/** @type {Array<CustomerMatchRecord>} */
|
|
1044
|
-
const records = lines.map((line) => JSON.parse(line));
|
|
1045
|
-
/** @const {BatchResult} */ const batchResult = {
|
|
1046
|
-
result: true,
|
|
1047
|
-
numberOfLines: lines.length,
|
|
1048
|
-
};
|
|
1049
|
-
try {
|
|
1050
|
-
const response = await this.addOperationsToOfflineUserDataJob(config,
|
|
1051
|
-
jobResourceName, records);
|
|
1052
|
-
this.logger.debug(`Add operation to job batch[${batchId}]`, response);
|
|
1053
|
-
const { results, partial_failure_error: failed } = response;
|
|
1054
|
-
if (failed) {
|
|
1055
|
-
this.logger.info('partial_failure_error:', failed.message);
|
|
1056
|
-
const failures = failed.details.map(
|
|
1057
|
-
({ value }) => GoogleAdsFailure.decode(value));
|
|
1058
|
-
this.extraFailedLines_(batchResult, failures, lines, 0);
|
|
1059
|
-
}
|
|
1060
|
-
return batchResult;
|
|
1061
|
-
} catch (error) {
|
|
1062
|
-
this.logger.error(
|
|
1063
|
-
`Error in OfflineUserDataJob add operations batch[${batchId}]`, error);
|
|
1064
|
-
this.updateBatchResultWithError(batchResult, error, lines, 2);
|
|
1065
|
-
return batchResult;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
/**
|
|
1071
|
-
* Returns an instance of GoogleAdsApi.Customer on google-ads-api.
|
|
1072
|
-
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
1073
|
-
* @param {string=} customerId Customer account ID, default is the same as
|
|
1074
|
-
* the login customer account ID.
|
|
1075
|
-
* @return {GoogleAdsApi.Customer}
|
|
1076
|
-
* @private
|
|
1077
|
-
*/
|
|
1078
|
-
async getGoogleAdsApiCustomer_(loginCustomerId, customerId = loginCustomerId) {
|
|
1079
|
-
if (this.googleAds) return this.googleAds;
|
|
1080
|
-
await this.authClient.prepareCredentials();
|
|
1081
|
-
const oauthClient = await this.authClient.getOAuth2Token();
|
|
1082
|
-
/** @const {GoogleAdsApi} */
|
|
1083
|
-
const googleAdsApiClient = new GoogleAdsApi({
|
|
1084
|
-
client_id: oauthClient.clientId,
|
|
1085
|
-
client_secret: oauthClient.clientSecret,
|
|
1086
|
-
developer_token: this.developerToken,
|
|
1087
|
-
});
|
|
1088
|
-
this.googleAds = googleAdsApiClient.Customer({
|
|
1089
|
-
customer_id: customerId,
|
|
1090
|
-
login_customer_id: loginCustomerId,
|
|
1091
|
-
refresh_token: oauthClient.refreshToken,
|
|
1092
|
-
});
|
|
1093
|
-
return this.googleAds;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
/**
|
|
1099
|
-
* Returns a conversion object based the given config and line data.
|
|
1100
|
-
* @param {string} line A JSON string of a conversion data.
|
|
1101
|
-
* @param {ConversionConfig} config Default click conversion params
|
|
1102
|
-
* @param {string} customerId
|
|
1103
|
-
* @return {object} A conversion
|
|
1104
|
-
*/
|
|
1105
|
-
const buildClickConversionFromLine = (line, config, customerId) => {
|
|
1106
|
-
const {customVariables, user_identifier_source} = config;
|
|
1107
|
-
const record = JSON.parse(line);
|
|
1108
|
-
const conversion = lodash.merge(lodash.pick(config, PICKED_PROPERTIES),
|
|
1109
|
-
lodash.pick(record, PICKED_PROPERTIES));
|
|
1110
|
-
if (customVariables) {
|
|
1111
|
-
const tags = Object.keys(customVariables);
|
|
1112
|
-
conversion.custom_variables = tags.map((tag) => {
|
|
1113
|
-
return {
|
|
1114
|
-
conversion_custom_variable:
|
|
1115
|
-
`customers/${customerId}/conversionCustomVariables/${customVariables[tag]}`,
|
|
1116
|
-
value: record[tag],
|
|
1117
|
-
};
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
const user_identifiers = [];
|
|
1121
|
-
IDENTIFIERS.forEach((identifier) => {
|
|
1122
|
-
if (record[identifier]) {
|
|
1123
|
-
user_identifiers.push({
|
|
1124
|
-
user_identifier_source,
|
|
1125
|
-
[identifier]: record[identifier],
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
});
|
|
1129
|
-
if (user_identifiers.length > 0) {
|
|
1130
|
-
conversion.user_identifiers = user_identifiers;
|
|
1131
|
-
}
|
|
1132
|
-
return conversion;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
module.exports = {
|
|
1136
|
-
ConversionConfig,
|
|
1137
|
-
CustomerMatchRecord,
|
|
1138
|
-
CustomerMatchConfig,
|
|
1139
|
-
OfflineUserDataJobType,
|
|
1140
|
-
OfflineUserDataJobConfig,
|
|
1141
|
-
GoogleAds,
|
|
1142
|
-
ReportQueryConfig,
|
|
1143
|
-
GoogleAdsField,
|
|
1144
|
-
buildClickConversionFromLine,
|
|
1145
|
-
};
|