@google-cloud/nodejs-common 0.9.4-beta3 → 0.9.9-alpha
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/README.md +69 -60
- package/bin/install_functions.sh +18 -19
- package/package.json +17 -16
- package/src/apis/auth_client.js +15 -0
- package/src/apis/dfa_reporting.js +67 -19
- package/src/apis/doubleclick_search.js +161 -89
- package/src/apis/google_ads.js +310 -51
- package/src/apis/index.js +10 -0
- package/src/apis/measurement_protocol.js +45 -25
- package/src/apis/measurement_protocol_ga4.js +7 -1
- package/src/apis/spreadsheets.js +13 -13
- package/src/apis/youtube.js +356 -0
- package/src/components/utils.js +44 -9
package/src/apis/google_ads.js
CHANGED
|
@@ -30,31 +30,88 @@ const {
|
|
|
30
30
|
},
|
|
31
31
|
services: {
|
|
32
32
|
UploadClickConversionsRequest,
|
|
33
|
+
UploadClickConversionsResponse,
|
|
33
34
|
UploadUserDataRequest,
|
|
35
|
+
UploadUserDataResponse,
|
|
34
36
|
UserDataOperation,
|
|
35
37
|
SearchGoogleAdsFieldsRequest,
|
|
36
38
|
},
|
|
39
|
+
errors: {
|
|
40
|
+
GoogleAdsFailure,
|
|
41
|
+
},
|
|
37
42
|
} = googleAdsLib;
|
|
38
43
|
const {GoogleAdsApi} = require('google-ads-api');
|
|
44
|
+
const lodash = require('lodash');
|
|
45
|
+
|
|
39
46
|
const AuthClient = require('./auth_client.js');
|
|
40
|
-
const {getLogger} = require('../components/utils.js');
|
|
47
|
+
const {getLogger, BatchResult,} = require('../components/utils.js');
|
|
41
48
|
|
|
42
49
|
/** @type {!ReadonlyArray<string>} */
|
|
43
50
|
const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* List of properties that will be taken from the data file as elements of a
|
|
54
|
+
* conversion.
|
|
55
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
56
|
+
* @type {Array<string>}
|
|
57
|
+
*/
|
|
58
|
+
const PICKED_PROPERTIES = [
|
|
59
|
+
'external_attribution_data',
|
|
60
|
+
'cart_data',
|
|
61
|
+
'user_identifiers',
|
|
62
|
+
'gclid',
|
|
63
|
+
'conversion_action',
|
|
64
|
+
'conversion_date_time',
|
|
65
|
+
'conversion_value',
|
|
66
|
+
'currency_code',
|
|
67
|
+
'order_id',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Kinds of UserIdentifier.
|
|
72
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
73
|
+
* @type {Array<string>}
|
|
74
|
+
*/
|
|
75
|
+
const IDENTIFIERS = [
|
|
76
|
+
'hashed_email',
|
|
77
|
+
'hashed_phone_number',
|
|
78
|
+
'mobile_id',
|
|
79
|
+
'third_party_user_id',
|
|
80
|
+
'address_info',
|
|
81
|
+
];
|
|
82
|
+
|
|
45
83
|
/**
|
|
46
84
|
* Configuration for uploading click conversions for Google Ads, includes:
|
|
47
85
|
* gclid, conversion_action, conversion_date_time, conversion_value,
|
|
48
|
-
* currency_code, order_id, external_attribution_data
|
|
49
|
-
* @see
|
|
86
|
+
* currency_code, order_id, external_attribution_data, etc.
|
|
87
|
+
* @see PICKED_PROPERTIES
|
|
88
|
+
*
|
|
89
|
+
* Other properties that will be used to build the conversions but not picked by
|
|
90
|
+
* the value directly including:
|
|
91
|
+
* 1. 'user_identifier_source', source of the user identifier. If there is user
|
|
92
|
+
* identifiers information in the conversion, this property should be set as
|
|
93
|
+
* 'FIRST_PARTY'.
|
|
94
|
+
* @see IDENTIFIERS
|
|
95
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier?hl=en
|
|
96
|
+
* 2. 'custom_variable_tags', the tags of conversion custom variables. To upload
|
|
97
|
+
* custom variables, 'conversion_custom_variable_id' is required rather than the
|
|
98
|
+
* 'tag'. So the invoker is expected to use the function
|
|
99
|
+
* 'getConversionCustomVariableId' to get the ids and pass in as a
|
|
100
|
+
* map(customVariables) of <tag, id> pairs before uploading conversions.
|
|
101
|
+
*
|
|
102
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
50
103
|
* @typedef {{
|
|
104
|
+
* external_attribution_data: (GoogleAdsApi.ExternalAttributionData|undefined),
|
|
105
|
+
* cart_data: (object|undefined),
|
|
51
106
|
* gclid: string,
|
|
52
107
|
* conversion_action: string,
|
|
53
108
|
* conversion_date_time: string,
|
|
54
109
|
* conversion_value: number,
|
|
55
110
|
* currency_code:(string|undefined),
|
|
56
111
|
* order_id: (string|undefined),
|
|
57
|
-
*
|
|
112
|
+
* user_identifier_source:(UserIdentifierSource|undefined),
|
|
113
|
+
* custom_variable_tags:(!Array<string>|undefined),
|
|
114
|
+
* customVariables:(!object<string,string>|undefined),
|
|
58
115
|
* }}
|
|
59
116
|
*/
|
|
60
117
|
let ClickConversionConfig;
|
|
@@ -65,7 +122,7 @@ let ClickConversionConfig;
|
|
|
65
122
|
* list_type must be one of the following: hashed_email,
|
|
66
123
|
* hashed_phone_number, mobile_id, third_party_user_id or address_info;
|
|
67
124
|
* operation must be one of the two: 'create' or 'remove';
|
|
68
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
125
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
69
126
|
* @typedef {{
|
|
70
127
|
* customer_id: string,
|
|
71
128
|
* login_customer_id: string,
|
|
@@ -80,7 +137,7 @@ let CustomerMatchConfig;
|
|
|
80
137
|
/**
|
|
81
138
|
* Configuration for uploading customer match data for Google Ads, includes one of:
|
|
82
139
|
* hashed_email, hashed_phone_number, mobile_id, third_party_user_id or address_info
|
|
83
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
140
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
84
141
|
* @typedef {{
|
|
85
142
|
* hashed_email: string,
|
|
86
143
|
* }|{
|
|
@@ -123,17 +180,21 @@ let CustomerMatchRecord;
|
|
|
123
180
|
let ReportQueryConfig;
|
|
124
181
|
|
|
125
182
|
/**
|
|
126
|
-
* Google Ads API
|
|
183
|
+
* Google Ads API class based on Opteo's Nodejs library.
|
|
127
184
|
* see https://opteo.com/dev/google-ads-api/#features
|
|
128
185
|
*/
|
|
129
186
|
class GoogleAds {
|
|
130
187
|
/**
|
|
131
188
|
* Note: Rate limits is set by the access level of Developer token.
|
|
132
189
|
* @param {string} developerToken Developer token to access the API.
|
|
190
|
+
* @param {boolean=} debugMode This is used to set ONLY validate conversions
|
|
191
|
+
* but not real uploading.
|
|
192
|
+
* @param {!Object<string,string>=} env The environment object to hold env
|
|
193
|
+
* variables.
|
|
133
194
|
*/
|
|
134
|
-
constructor(developerToken) {
|
|
135
|
-
this.
|
|
136
|
-
const oauthClient = new AuthClient(API_SCOPES).getOAuth2Token();
|
|
195
|
+
constructor(developerToken, debugMode = false, env = process.env) {
|
|
196
|
+
this.debugMode = debugMode;
|
|
197
|
+
const oauthClient = new AuthClient(API_SCOPES, env).getOAuth2Token();
|
|
137
198
|
/** @const {GoogleAdsApi} */ this.apiClient = new GoogleAdsApi({
|
|
138
199
|
client_id: oauthClient.clientId,
|
|
139
200
|
client_secret: oauthClient.clientSecret,
|
|
@@ -206,25 +267,171 @@ class GoogleAds {
|
|
|
206
267
|
* @param {!Array<string>} lines Data for single request. It should be
|
|
207
268
|
* guaranteed that it doesn't exceed quota limitation.
|
|
208
269
|
* @param {string} batchId The tag for log.
|
|
209
|
-
* @return {!
|
|
270
|
+
* @return {!BatchResult}
|
|
210
271
|
*/
|
|
211
272
|
return async (lines, batchId) => {
|
|
212
273
|
/** @type {!Array<ClickConversionConfig>} */
|
|
213
|
-
const conversions = lines.map(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
274
|
+
const conversions = lines.map(
|
|
275
|
+
(line) => buildClickConversionFromLine(line, adsConfig, customerId));
|
|
276
|
+
/** @const {BatchResult} */
|
|
277
|
+
const batchResult = {
|
|
278
|
+
result: true,
|
|
279
|
+
numberOfLines: lines.length,
|
|
280
|
+
};
|
|
217
281
|
try {
|
|
218
|
-
|
|
219
|
-
loginCustomerId);
|
|
282
|
+
const response = await this.uploadClickConversions(conversions,
|
|
283
|
+
customerId, loginCustomerId);
|
|
284
|
+
const {results, partial_failure_error: failed} = response;
|
|
285
|
+
if (this.logger.isDebugEnabled()) {
|
|
286
|
+
const gclids = results.map((conversion) => conversion.gclid);
|
|
287
|
+
this.logger.debug('Uploaded gclids:', gclids);
|
|
288
|
+
}
|
|
289
|
+
if (failed) {
|
|
290
|
+
this.logger.info('partial_failure_error:', failed.message);
|
|
291
|
+
const failures = failed.details.map(
|
|
292
|
+
({value}) => GoogleAdsFailure.decode(value));
|
|
293
|
+
this.extraFailedLines_(batchResult, failures, lines, 0);
|
|
294
|
+
}
|
|
295
|
+
return batchResult;
|
|
220
296
|
} catch (error) {
|
|
221
|
-
this.logger.
|
|
222
|
-
`Error in
|
|
223
|
-
|
|
297
|
+
this.logger.error(
|
|
298
|
+
`Error in upload conversions batch: ${batchId}`, error);
|
|
299
|
+
this.updateBatchResultWithError_(batchResult, error, lines, 0);
|
|
300
|
+
return batchResult;
|
|
224
301
|
}
|
|
225
302
|
}
|
|
226
303
|
}
|
|
227
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Updates the BatchResult based on errors.
|
|
307
|
+
*
|
|
308
|
+
* There are 2 types of errors here:
|
|
309
|
+
* 1. Normal JavaScript Error object. It happens when the whole process fails
|
|
310
|
+
* (not partial failure), so there is no detailed failed lines.
|
|
311
|
+
* 2. GoogleAdsFailure. It is a Google Ads' own error object which has an
|
|
312
|
+
* array of GoogleAdsError (property name 'errors'). GoogleAdsError contains
|
|
313
|
+
* the detailed failed data if it is a line-error. For example, a wrong
|
|
314
|
+
* encoded user identifier is a line-error, while a wrong user list id is not.
|
|
315
|
+
* GoogleAdsFailure: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsFailure
|
|
316
|
+
* GoogleAdsError: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsError
|
|
317
|
+
*
|
|
318
|
+
* For Customer Match data uploading, there is not partial failure, so the
|
|
319
|
+
* result can be either succeeded or a thrown error. The thrown error will be
|
|
320
|
+
* used to build the returned result here.
|
|
321
|
+
* For Conversions uploading (partial failure enabled), if there is an error
|
|
322
|
+
* fails the whole process, the error will also be thrown and handled here.
|
|
323
|
+
* Otherwise, the errors will be wrapped in the response as the property named
|
|
324
|
+
* 'partial_failure_error' which contains an array of GoogleAdsFailure. This
|
|
325
|
+
* kind of failure doesn't fail the process, while line-errors can be
|
|
326
|
+
* extracted from it.
|
|
327
|
+
* For more information, see the function `extraFailedLines_`.
|
|
328
|
+
*
|
|
329
|
+
* An example of 'GoogleAdsFailure' is:
|
|
330
|
+
* GoogleAdsFailure {
|
|
331
|
+
* errors: [
|
|
332
|
+
* GoogleAdsError {
|
|
333
|
+
* error_code: ErrorCode { offline_user_data_job_error: 25 },
|
|
334
|
+
* message: 'The SHA256 encoded value is malformed.',
|
|
335
|
+
* location: ErrorLocation {
|
|
336
|
+
* field_path_elements: [
|
|
337
|
+
* FieldPathElement { field_name: 'operations', index: 0 },
|
|
338
|
+
* FieldPathElement { field_name: 'create' },
|
|
339
|
+
* FieldPathElement { field_name: 'user_identifiers', index: 0 },
|
|
340
|
+
* FieldPathElement { field_name: 'hashed_email' }
|
|
341
|
+
* ]
|
|
342
|
+
* }
|
|
343
|
+
* }
|
|
344
|
+
* ],
|
|
345
|
+
* request_id: 'xxxxxxxxxxxxxxx'
|
|
346
|
+
* }
|
|
347
|
+
*
|
|
348
|
+
* @param {!BatchResult} batchResult
|
|
349
|
+
* @param {(!GoogleAdsFailure|!Error)} error
|
|
350
|
+
* @param {!Array<string>} lines The original input data.
|
|
351
|
+
* @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
|
|
352
|
+
* 'field_path_elements'. This is used to get the original line related to
|
|
353
|
+
* this GoogleAdsError.
|
|
354
|
+
* @private
|
|
355
|
+
*/
|
|
356
|
+
updateBatchResultWithError_(batchResult, error, lines, fieldPathIndex) {
|
|
357
|
+
batchResult.result = false;
|
|
358
|
+
if (error.errors) { //GoogleAdsFailure
|
|
359
|
+
this.extraFailedLines_(batchResult, [error], lines, fieldPathIndex);
|
|
360
|
+
} else {
|
|
361
|
+
batchResult.errors = [error.message || error.toString()];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Extras failed lines based on the GoogleAdsFailures.
|
|
367
|
+
*
|
|
368
|
+
* Different errors have different 'fieldPathIndex' which is the index of
|
|
369
|
+
* failed lines in original input data (an array of a string).
|
|
370
|
+
*
|
|
371
|
+
* For conversions, the ErrorLocation is like:
|
|
372
|
+
* ErrorLocation {
|
|
373
|
+
* field_path_elements: [
|
|
374
|
+
* FieldPathElement { field_name: 'operations', index: 0 },
|
|
375
|
+
* FieldPathElement { field_name: 'create' }
|
|
376
|
+
* ]
|
|
377
|
+
* }
|
|
378
|
+
* So the index is 0, index of 'operations'.
|
|
379
|
+
*
|
|
380
|
+
* For customer match upload, the ErrorLocation is like:
|
|
381
|
+
* ErrorLocation {
|
|
382
|
+
* field_path_elements: [
|
|
383
|
+
* FieldPathElement { field_name: 'operations', index: 0 },
|
|
384
|
+
* FieldPathElement { field_name: 'create' },
|
|
385
|
+
* FieldPathElement { field_name: 'user_identifiers', index: 0 },
|
|
386
|
+
* FieldPathElement { field_name: 'hashed_email' }
|
|
387
|
+
* ]
|
|
388
|
+
* }
|
|
389
|
+
* The index should be 2, index of 'user_identifiers'.
|
|
390
|
+
*
|
|
391
|
+
* With this we can get errors and failed lines. The function will set
|
|
392
|
+
* following for the given BatchResult object:
|
|
393
|
+
* result - false
|
|
394
|
+
* errors - de-duplicated error reasons
|
|
395
|
+
* failedLines - failed lines, an array of string. Without the reason of
|
|
396
|
+
* failure.
|
|
397
|
+
* groupedFailed - a hashmap of failed the lines. The key is the reason, the
|
|
398
|
+
* value is the array of failed lines due to this reason.
|
|
399
|
+
* @param {!BatchResult} batchResult
|
|
400
|
+
* @param {!Array<!GoogleAdsFailure>} failures
|
|
401
|
+
* @param {!Array<string>} lines The original input data.
|
|
402
|
+
* @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
|
|
403
|
+
* 'field_path_elements'. This is used to get the original line related to
|
|
404
|
+
* this GoogleAdsError.
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
extraFailedLines_(batchResult, failures, lines, fieldPathIndex) {
|
|
408
|
+
batchResult.result = false;
|
|
409
|
+
batchResult.failedLines = [];
|
|
410
|
+
batchResult.groupedFailed = {};
|
|
411
|
+
const errors = new Set();
|
|
412
|
+
failures.forEach((failure) => {
|
|
413
|
+
failure.errors.forEach(({message, location}) => {
|
|
414
|
+
errors.add(message);
|
|
415
|
+
if (location && location.field_path_elements[fieldPathIndex]) {
|
|
416
|
+
const {index} = location.field_path_elements[fieldPathIndex];
|
|
417
|
+
if (typeof index === 'undefined') {
|
|
418
|
+
this.logger.warn(`Unknown field path index: ${fieldPathIndex}`,
|
|
419
|
+
location.field_path_elements);
|
|
420
|
+
} else {
|
|
421
|
+
const groupedFailed = batchResult.groupedFailed[message] || [];
|
|
422
|
+
const failedLine = lines[index];
|
|
423
|
+
batchResult.failedLines.push(failedLine);
|
|
424
|
+
groupedFailed.push(failedLine);
|
|
425
|
+
if (groupedFailed.length === 1) {
|
|
426
|
+
batchResult.groupedFailed[message] = groupedFailed;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
batchResult.errors = Array.from(errors);
|
|
433
|
+
}
|
|
434
|
+
|
|
228
435
|
/**
|
|
229
436
|
* Uploads click conversions to google ads account.
|
|
230
437
|
* It requires an array of click conversions and customer id.
|
|
@@ -232,30 +439,38 @@ class GoogleAds {
|
|
|
232
439
|
* @param {Array<ClickConversionConfig>} clickConversions ClickConversions
|
|
233
440
|
* @param {string} customerId
|
|
234
441
|
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
235
|
-
* @return {!Promise
|
|
442
|
+
* @return {!Promise<!UploadClickConversionsResponse>}
|
|
236
443
|
*/
|
|
237
|
-
|
|
444
|
+
uploadClickConversions(clickConversions, customerId, loginCustomerId) {
|
|
238
445
|
this.logger.debug('Upload click conversions for customerId:', customerId);
|
|
239
446
|
const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
|
|
240
447
|
const request = new UploadClickConversionsRequest({
|
|
241
448
|
conversions: clickConversions,
|
|
242
449
|
customer_id: customerId,
|
|
243
|
-
validate_only: this.
|
|
450
|
+
validate_only: this.debugMode, // when true makes no changes
|
|
244
451
|
partial_failure: true, // Will still create the non-failed entities
|
|
245
452
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
453
|
+
return customer.conversionUploads.uploadClickConversions(request);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Returns the id of Conversion Custom Variable with the given tag.
|
|
458
|
+
* @param {string} tag Custom Variable tag.
|
|
459
|
+
* @param {string} customerId
|
|
460
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
461
|
+
* @return {Promise<number|undefined>} Returns undefined if can't find tag.
|
|
462
|
+
*/
|
|
463
|
+
async getConversionCustomVariableId(tag, customerId, loginCustomerId) {
|
|
464
|
+
const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
|
|
465
|
+
const customVariables = await customer.query(`
|
|
466
|
+
SELECT conversion_custom_variable.id,
|
|
467
|
+
conversion_custom_variable.tag
|
|
468
|
+
FROM conversion_custom_variable
|
|
469
|
+
WHERE conversion_custom_variable.tag = "${tag}" LIMIT 1
|
|
470
|
+
`);
|
|
471
|
+
if (customVariables.length > 0) {
|
|
472
|
+
return customVariables[0].conversion_custom_variable.id;
|
|
257
473
|
}
|
|
258
|
-
return !failed;
|
|
259
474
|
}
|
|
260
475
|
|
|
261
476
|
/**
|
|
@@ -271,29 +486,36 @@ class GoogleAds {
|
|
|
271
486
|
* @param {!Array<string>} lines Data for single request. It should be
|
|
272
487
|
* guaranteed that it doesn't exceed quota limitation.
|
|
273
488
|
* @param {string} batchId The tag for log.
|
|
274
|
-
* @return {!Promise<
|
|
489
|
+
* @return {!Promise<BatchResult>}
|
|
275
490
|
*/
|
|
276
491
|
return async (lines, batchId) => {
|
|
277
492
|
/** @type {Array<CustomerMatchRecord>} */
|
|
278
493
|
const userIds = lines.map((line) => JSON.parse(line));
|
|
494
|
+
/** @const {BatchResult} */ const batchResult = {
|
|
495
|
+
result: true,
|
|
496
|
+
numberOfLines: lines.length,
|
|
497
|
+
};
|
|
279
498
|
try {
|
|
280
|
-
|
|
499
|
+
const response = await this.uploadUserDataToUserList(userIds,
|
|
281
500
|
customerMatchConfig);
|
|
501
|
+
this.logger.debug(`Customer Match upload batch[${batchId}]`, response);
|
|
502
|
+
return batchResult;
|
|
282
503
|
} catch (error) {
|
|
283
504
|
this.logger.error(
|
|
284
|
-
`Error in
|
|
285
|
-
|
|
505
|
+
`Error in Customer Match upload batch[${batchId}]`, error);
|
|
506
|
+
this.updateBatchResultWithError_(batchResult, error, lines, 2);
|
|
507
|
+
return batchResult;
|
|
286
508
|
}
|
|
287
509
|
}
|
|
288
510
|
}
|
|
289
511
|
|
|
290
512
|
/**
|
|
291
513
|
* Uploads a user data to a user list (aka customer match).
|
|
292
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
293
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
294
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
295
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
296
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
514
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataService
|
|
515
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
516
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
517
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
518
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
297
519
|
* Please note: The UserDataService has a limit of 10 UserDataOperations
|
|
298
520
|
* and 100 user IDs per request
|
|
299
521
|
* @see https://developers.google.com/google-ads/api/docs/migration/user-data-service#rate_limits
|
|
@@ -302,7 +524,7 @@ class GoogleAds {
|
|
|
302
524
|
* customer_id, login_customer_id, list_id, list_type which can be one of the following
|
|
303
525
|
* hashed_email, hashed_phone_number, mobile_id, third_party_user_id or address_info and
|
|
304
526
|
* operation which can be either 'create' or 'remove'
|
|
305
|
-
* @return {!Promise<
|
|
527
|
+
* @return {!Promise<UploadUserDataResponse>}
|
|
306
528
|
*/
|
|
307
529
|
async uploadUserDataToUserList(customerMatchRecords, customerMatchConfig) {
|
|
308
530
|
const customerId = customerMatchConfig.customer_id.replace(/-/g, '');
|
|
@@ -323,16 +545,15 @@ class GoogleAds {
|
|
|
323
545
|
customer_match_user_list_metadata: metadata,
|
|
324
546
|
});
|
|
325
547
|
const response = await customer.userData.uploadUserData(request);
|
|
326
|
-
|
|
327
|
-
return true;
|
|
548
|
+
return response;
|
|
328
549
|
}
|
|
329
550
|
|
|
330
551
|
/**
|
|
331
552
|
* Builds a list of UserDataOperations.
|
|
332
553
|
* Since v6 you can set a user_attribute
|
|
333
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
334
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
335
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
554
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
555
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
556
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
336
557
|
* @param {string} operationType either 'create' or 'remove'
|
|
337
558
|
* @param {Array<CustomerMatchRecord>} customerMatchRecords userIds
|
|
338
559
|
* @param {string} userListType One of the following hashed_email, hashed_phone_number,
|
|
@@ -351,7 +572,7 @@ class GoogleAds {
|
|
|
351
572
|
|
|
352
573
|
/**
|
|
353
574
|
* Creates CustomerMatchUserListMetadata.
|
|
354
|
-
* @see https://developers.google.com/google-ads/api/reference/rpc/
|
|
575
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
355
576
|
* @param {string} customerId part of the ResourceName to be mutated
|
|
356
577
|
* @param {string} userListId part of the ResourceName to be mutated
|
|
357
578
|
* @return {!CustomerMatchUserListMetadata}
|
|
@@ -383,6 +604,43 @@ class GoogleAds {
|
|
|
383
604
|
|
|
384
605
|
}
|
|
385
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Returns a conversion object based the given config and line data.
|
|
609
|
+
* @param {string} line A JSON string of a conversion data.
|
|
610
|
+
* @param {ClickConversionConfig} config Default click conversion params
|
|
611
|
+
* @param {string} customerId
|
|
612
|
+
* @return {object} A conversion
|
|
613
|
+
*/
|
|
614
|
+
const buildClickConversionFromLine = (line, config, customerId) => {
|
|
615
|
+
const {customVariables, user_identifier_source} = config;
|
|
616
|
+
const record = JSON.parse(line);
|
|
617
|
+
const conversion = lodash.merge(lodash.pick(config, PICKED_PROPERTIES),
|
|
618
|
+
lodash.pick(record, PICKED_PROPERTIES));
|
|
619
|
+
if (customVariables) {
|
|
620
|
+
const tags = Object.keys(customVariables);
|
|
621
|
+
conversion.custom_variables = tags.map((tag) => {
|
|
622
|
+
return {
|
|
623
|
+
conversion_custom_variable:
|
|
624
|
+
`customers/${customerId}/conversionCustomVariables/${customVariables[tag]}`,
|
|
625
|
+
value: record[tag],
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const user_identifiers = [];
|
|
630
|
+
IDENTIFIERS.forEach((identifier) => {
|
|
631
|
+
if (record[identifier]) {
|
|
632
|
+
user_identifiers.push({
|
|
633
|
+
user_identifier_source,
|
|
634
|
+
[identifier]: record[identifier],
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
if (user_identifiers.length > 0) {
|
|
639
|
+
conversion.user_identifiers = user_identifiers;
|
|
640
|
+
}
|
|
641
|
+
return conversion;
|
|
642
|
+
}
|
|
643
|
+
|
|
386
644
|
module.exports = {
|
|
387
645
|
ClickConversionConfig,
|
|
388
646
|
CustomerMatchRecord,
|
|
@@ -390,4 +648,5 @@ module.exports = {
|
|
|
390
648
|
GoogleAds,
|
|
391
649
|
ReportQueryConfig,
|
|
392
650
|
GoogleAdsField,
|
|
651
|
+
buildClickConversionFromLine,
|
|
393
652
|
};
|
package/src/apis/index.js
CHANGED
|
@@ -110,3 +110,13 @@ exports.adsdatahub = require('./ads_data_hub.js');
|
|
|
110
110
|
* }}
|
|
111
111
|
*/
|
|
112
112
|
exports.measurementprotocolga4 = require('./measurement_protocol_ga4.js');
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* APIs integration class for YouTube.
|
|
116
|
+
* @const {{
|
|
117
|
+
* YouTube:!YouTube,
|
|
118
|
+
* ListChannelsConfig: !ListChannelsConfig,
|
|
119
|
+
* ListVideosConfig: !ListVideosConfig,
|
|
120
|
+
* }}
|
|
121
|
+
*/
|
|
122
|
+
exports.youtube = require('./youtube.js');
|
|
@@ -69,18 +69,14 @@ class MeasurementProtocol {
|
|
|
69
69
|
* @return {!Promise<BatchResult>}
|
|
70
70
|
*/
|
|
71
71
|
return async (lines, batchId) => {
|
|
72
|
-
const payload =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
})
|
|
81
|
-
.join('&');
|
|
82
|
-
})
|
|
83
|
-
.join('\n');
|
|
72
|
+
const payload = lines.map((line) => {
|
|
73
|
+
const record = JSON.parse(line);
|
|
74
|
+
const hit = Object.assign({}, config, record);
|
|
75
|
+
return Object.keys(hit).map(
|
|
76
|
+
(key) => `${key}=${encodeURIComponent(hit[key])}`)
|
|
77
|
+
.join('&');
|
|
78
|
+
})
|
|
79
|
+
.join('\n');
|
|
84
80
|
// In debug mode, the path is fixed to '/debug/collect'.
|
|
85
81
|
const path = (this.debugMode) ? '/debug/collect' : '/batch';
|
|
86
82
|
const requestOptions = {
|
|
@@ -112,24 +108,48 @@ class MeasurementProtocol {
|
|
|
112
108
|
if (!this.debugMode) {
|
|
113
109
|
batchResult.result = true;
|
|
114
110
|
} else {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
response.data.hitParsingResult.forEach((result, index) => {
|
|
118
|
-
if (!result.valid) {
|
|
119
|
-
failedHits.push(lines[index]);
|
|
120
|
-
result.parserMessage.forEach(({description}) => {
|
|
121
|
-
failedReasons.add(description);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
batchResult.result = failedHits.length === 0;
|
|
126
|
-
batchResult.failedLines = failedHits;
|
|
127
|
-
batchResult.errors = Array.from(failedReasons);
|
|
111
|
+
this.extraFailedLines_(batchResult, response.data.hitParsingResult,
|
|
112
|
+
lines);
|
|
128
113
|
}
|
|
129
114
|
return batchResult;
|
|
130
115
|
};
|
|
131
116
|
};
|
|
132
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Extras failed lines based on the hitParsingResult, see:
|
|
120
|
+
* https://developers.google.com/analytics/devguides/collection/protocol/v1/validating-hits
|
|
121
|
+
*
|
|
122
|
+
* Note, only in 'debug' mode, Google Analytics will return this part of data.
|
|
123
|
+
*
|
|
124
|
+
* @param {!BatchResult} batchResult
|
|
125
|
+
* @param {!Array<!Object>} hitParsingResults
|
|
126
|
+
* @param {!Array<string>} lines The original input data.
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
extraFailedLines_(batchResult, hitParsingResults, lines) {
|
|
130
|
+
batchResult.failedLines = [];
|
|
131
|
+
batchResult.groupedFailed = {};
|
|
132
|
+
const errors = new Set();
|
|
133
|
+
hitParsingResults.forEach((result, index) => {
|
|
134
|
+
if (!result.valid) {
|
|
135
|
+
const failedLine = lines[index];
|
|
136
|
+
batchResult.failedLines.push(failedLine);
|
|
137
|
+
result.parserMessage.forEach(({description: error, messageType}) => {
|
|
138
|
+
this.logger.info(`[${messageType}]: ${error} for ${failedLine}`);
|
|
139
|
+
if (messageType === 'ERROR') {
|
|
140
|
+
errors.add(error);
|
|
141
|
+
const groupedFailed = batchResult.groupedFailed[error] || [];
|
|
142
|
+
groupedFailed.push(failedLine);
|
|
143
|
+
if (groupedFailed.length === 1) {
|
|
144
|
+
batchResult.groupedFailed[error] = groupedFailed;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
batchResult.result = batchResult.failedLines.length === 0;
|
|
151
|
+
batchResult.errors = Array.from(errors);
|
|
152
|
+
}
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
module.exports = {MeasurementProtocol};
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
22
|
const {request} = require('gaxios');
|
|
23
|
+
const lodash = require('lodash');
|
|
23
24
|
const {
|
|
24
25
|
getLogger,
|
|
25
26
|
SendSingleBatch,
|
|
@@ -93,7 +94,11 @@ class MeasurementProtocolGA4 {
|
|
|
93
94
|
*/
|
|
94
95
|
return async (lines, batchId) => {
|
|
95
96
|
const line = lines[0]; // Each request contains one record only.
|
|
96
|
-
|
|
97
|
+
if (lines.length > 1) {
|
|
98
|
+
this.logger.warn(
|
|
99
|
+
"Only one line data expected. Will only send the first line.");
|
|
100
|
+
}
|
|
101
|
+
const hit = lodash.merge({}, config.requestBody, JSON.parse(line));
|
|
97
102
|
|
|
98
103
|
const requestOptions = {
|
|
99
104
|
method: 'POST',
|
|
@@ -135,6 +140,7 @@ class MeasurementProtocolGA4 {
|
|
|
135
140
|
batchResult.failedLines = lines;
|
|
136
141
|
batchResult.errors = response.data.validationMessages.map(
|
|
137
142
|
({description}) => description);
|
|
143
|
+
batchResult.groupedFailed = {[batchResult.errors.join()]: [lines[0]]};
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
146
|
return batchResult;
|