@google-cloud/nodejs-common 2.0.16-alpha → 2.1.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/README.md +4 -3
- package/bin/google_ads.sh +1 -1
- package/package.json +1 -1
- package/src/apis/base/ads_api_common.js +116 -0
- package/src/apis/display_video.js +2 -2
- package/src/apis/google_ads_api.js +108 -48
- package/src/apis/search_ads.js +94 -32
- package/src/components/utils.js +35 -1
package/README.md
CHANGED
|
@@ -18,10 +18,11 @@ and [Data Tasks Coordinator]. This library includes:
|
|
|
18
18
|
- Google Ads click conversions upload
|
|
19
19
|
- Google Ads customer match upload
|
|
20
20
|
- Google Ads enhanced conversions upload
|
|
21
|
+
- Google Ads offline userdata job data upload
|
|
21
22
|
- Google Ads conversions scheduled uploads based on Google Sheets
|
|
22
23
|
- Measurement Protocol Google Analytics 4
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
2. Wrapper for some Google APIs for reporting, mainly
|
|
25
26
|
for [Data Tasks Coordinator]:
|
|
26
27
|
|
|
27
28
|
- Google Ads reporting
|
|
@@ -31,7 +32,7 @@ and [Data Tasks Coordinator]. This library includes:
|
|
|
31
32
|
- YouTube Data API
|
|
32
33
|
- Ads Data Hub querying
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
3. Utilities wrapper class for Google Cloud Products:
|
|
35
36
|
|
|
36
37
|
- **Firestore Access Object**: Firestore has two modes[[comparison]] which
|
|
37
38
|
have different API. This class, with its two successors, offer a unified
|
|
@@ -66,7 +67,7 @@ and [Data Tasks Coordinator]. This library includes:
|
|
|
66
67
|
an adapter to wrap a Node8 Cloud Functions into Node6 and Node8 compatible
|
|
67
68
|
functions.~~ (This has been removed since v1.9.0)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
4. A share library for [Bash] to facilitate installation tasks.
|
|
70
71
|
|
|
71
72
|
[gmp and google ads connector]: https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/gmp-googleads-connector
|
|
72
73
|
[data tasks coordinator]: https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/data-tasks-coordinator
|
package/bin/google_ads.sh
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Copyright 2024 Google Inc.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this fileAccessObject 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
|
+
* @fileoverview Common functions for Google Ads and Search Ads API classes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { Transform } = require('stream');
|
|
21
|
+
const {
|
|
22
|
+
extractObject,
|
|
23
|
+
changeObjectNamingFromLowerCamelToSnake,
|
|
24
|
+
getLogger,
|
|
25
|
+
} = require('../../components/utils.js');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns a integer format CID by removing dashes and spaces.
|
|
29
|
+
* @param {string} cid
|
|
30
|
+
* @return {string}
|
|
31
|
+
*/
|
|
32
|
+
function getCleanCid(cid) {
|
|
33
|
+
return cid.toString().trim().replace(/-/g, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const START_TAG = '"results":';
|
|
37
|
+
const FIELD_MASK_TAG = '"fieldMask"';
|
|
38
|
+
const END_TAG = '"requestId"';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generates a function that can convert a given JSON object to a JSON string
|
|
42
|
+
* with only specified fields(fieldMask), in specified naming convention.
|
|
43
|
+
* @param {string} fieldMask The 'fieldMask' string from response.
|
|
44
|
+
* @param {boolean=} snakeCase Whether or not output JSON in snake naming.
|
|
45
|
+
*/
|
|
46
|
+
function generateProcessFn(fieldMask, snakeCase = false) {
|
|
47
|
+
const extractor = extractObject(fieldMask.split(','));
|
|
48
|
+
return (originalObject) => {
|
|
49
|
+
const extracted = extractor(originalObject);
|
|
50
|
+
const generatedObject = snakeCase
|
|
51
|
+
? changeObjectNamingFromLowerCamelToSnake(extracted) : extracted;
|
|
52
|
+
return JSON.stringify(generatedObject);
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A stream.Transform that can extract properties and convert naming of the
|
|
58
|
+
* reponse of Google/Search Ads report from REST interface.
|
|
59
|
+
*/
|
|
60
|
+
class RestSearchStreamTransform extends Transform {
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @constructor
|
|
64
|
+
* @param {boolean=} snakeCase Whether or not output JSON in snake naming.
|
|
65
|
+
*/
|
|
66
|
+
constructor(snakeCase = false) {
|
|
67
|
+
super({ objectMode: true });
|
|
68
|
+
this.snakeCase = snakeCase;
|
|
69
|
+
this.chunks = [Buffer.from('')];
|
|
70
|
+
this.processFn; // The function to process a row of the report.
|
|
71
|
+
this.logger = getLogger('ADS.STREAM.T');
|
|
72
|
+
this.stopwatch = Date.now();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_transform(chunk, encoding, callback) {
|
|
76
|
+
const latest = Buffer.concat([this.chunks[this.chunks.length - 1], chunk]);
|
|
77
|
+
const endIndex = latest.indexOf(END_TAG);
|
|
78
|
+
if (endIndex > -1) {
|
|
79
|
+
this.chunks.push(chunk);
|
|
80
|
+
const rawString = Buffer.concat(this.chunks).toString();
|
|
81
|
+
const startIndex = rawString.indexOf(START_TAG) + START_TAG.length;
|
|
82
|
+
const maskIndex = rawString.lastIndexOf(FIELD_MASK_TAG);
|
|
83
|
+
if (!this.processFn) {
|
|
84
|
+
const fieldMask = rawString
|
|
85
|
+
.substring(maskIndex + FIELD_MASK_TAG.length, rawString.indexOf(END_TAG))
|
|
86
|
+
.split('"')[1];
|
|
87
|
+
this.logger.debug(`Got fieldMask: ${fieldMask}`);
|
|
88
|
+
this.processFn = generateProcessFn(fieldMask, this.snakeCase);
|
|
89
|
+
}
|
|
90
|
+
const resultsWithTailing = rawString.substring(startIndex, maskIndex);
|
|
91
|
+
const results = resultsWithTailing.substring(
|
|
92
|
+
0, resultsWithTailing.lastIndexOf(','));
|
|
93
|
+
const rows = JSON.parse(results);
|
|
94
|
+
const data = rows.map(this.processFn).join('\n') + '\n';
|
|
95
|
+
// Clear cached chunks.
|
|
96
|
+
this.chunks = [latest.subarray(latest.indexOf(END_TAG) + END_TAG.length)];
|
|
97
|
+
|
|
98
|
+
this.logger.debug(`Got ${rows.length} rows. Process time:`,
|
|
99
|
+
Date.now() - this.stopwatch);
|
|
100
|
+
this.stopwatch = Date.now();
|
|
101
|
+
callback(null, data);
|
|
102
|
+
} else {
|
|
103
|
+
if (chunk.length < END_TAG.length) {// Update latest chunk for short chunk
|
|
104
|
+
this.chunks[this.chunks.length - 1] = latest;
|
|
105
|
+
} else {
|
|
106
|
+
this.chunks.push(chunk);
|
|
107
|
+
}
|
|
108
|
+
callback();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
getCleanCid,
|
|
115
|
+
RestSearchStreamTransform,
|
|
116
|
+
};
|
|
@@ -34,8 +34,8 @@ const API_SCOPES = Object.freeze([
|
|
|
34
34
|
const API_VERSION = 'v3';
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Display and Video 360 API
|
|
38
|
-
* @see https://developers.google.com/display-video/api/reference/rest/
|
|
37
|
+
* Display and Video 360 API v3 stub.
|
|
38
|
+
* @see https://developers.google.com/display-video/api/reference/rest/v3
|
|
39
39
|
* This is not the same to Reports Display & Video 360 API which is from Google
|
|
40
40
|
* Bid Manager API.
|
|
41
41
|
* @see https://developers.google.com/bid-manager/reference/rest
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
const { Transform } = require('stream');
|
|
24
24
|
const lodash = require('lodash');
|
|
25
|
-
|
|
25
|
+
const { request: gaxiosRequest } = require('gaxios');
|
|
26
26
|
const {
|
|
27
27
|
ConversionAdjustmentUploadServiceClient,
|
|
28
28
|
ConversionUploadServiceClient,
|
|
@@ -33,6 +33,8 @@ const {
|
|
|
33
33
|
UserListServiceClient,
|
|
34
34
|
protos: { google: { ads: { googleads } } },
|
|
35
35
|
} = require('google-ads-nodejs-client');
|
|
36
|
+
|
|
37
|
+
const API_VERSION = Object.keys(googleads)[0];
|
|
36
38
|
const {
|
|
37
39
|
common: {
|
|
38
40
|
Consent,
|
|
@@ -88,19 +90,23 @@ const {
|
|
|
88
90
|
UserListTypeEnum: { UserListType },
|
|
89
91
|
CustomerMatchUploadKeyTypeEnum: { CustomerMatchUploadKeyType },
|
|
90
92
|
},
|
|
91
|
-
} = googleads
|
|
93
|
+
} = googleads[API_VERSION];
|
|
92
94
|
|
|
93
95
|
const AuthClient = require('./auth_client.js');
|
|
94
96
|
const {
|
|
95
97
|
getLogger,
|
|
96
98
|
BatchResult,
|
|
97
99
|
extractObject,
|
|
100
|
+
changeNamingFromSnakeToLowerCamel,
|
|
98
101
|
changeObjectNamingFromSnakeToLowerCamel,
|
|
102
|
+
changeObjectNamingFromLowerCamelToSnake,
|
|
99
103
|
} = require('../components/utils.js');
|
|
104
|
+
const { getCleanCid, RestSearchStreamTransform }
|
|
105
|
+
= require('./base/ads_api_common.js');
|
|
100
106
|
|
|
101
107
|
/** @type {!ReadonlyArray<string>} */
|
|
102
108
|
const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords']);
|
|
103
|
-
|
|
109
|
+
const API_ENDPOINT = 'https://googleads.googleapis.com';
|
|
104
110
|
/**
|
|
105
111
|
* List of properties that will be taken from the data file as elements of a
|
|
106
112
|
* conversion or a conversion adjustment.
|
|
@@ -255,6 +261,9 @@ let ConversionConfig;
|
|
|
255
261
|
* If audience listId is not present, 'listName' and 'uploadKeyType' need to
|
|
256
262
|
* be there so they can be used to create a customer match user list.
|
|
257
263
|
* operation must be one of 'create' or 'remove'.
|
|
264
|
+
* `customerMatchUserListMetadata` offers a request level metadata, including
|
|
265
|
+
* 'consent'. While the top level 'consent', if presents, it will serve as the
|
|
266
|
+
* fallback value for each UserIdentifier.
|
|
258
267
|
* Should not include `userIdentifierSource` based on:
|
|
259
268
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
260
269
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
@@ -265,6 +274,7 @@ let ConversionConfig;
|
|
|
265
274
|
* listId: (string|undefined),
|
|
266
275
|
* listName: (string|undefined),
|
|
267
276
|
* uploadKeyType: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
|
|
277
|
+
* customerMatchUserListMetadata: (undefined|CustomerMatchUserListMetadata),
|
|
268
278
|
* operation: ('create'|'remove'),
|
|
269
279
|
* consent: (!Consent),
|
|
270
280
|
* }}
|
|
@@ -282,6 +292,9 @@ let CustomerMatchConfig;
|
|
|
282
292
|
* create a customer match user list.
|
|
283
293
|
* For job type 'CUSTOMER_MATCH_WITH_ATTRIBUTES', 'user_attribute' can be used
|
|
284
294
|
* to store shared additional user attributes.
|
|
295
|
+
* For job type 'CUSTOMER_MATCH_*', `customerMatchUserListMetadata` offers a job
|
|
296
|
+
* level metadata, including 'consent'. While the top level 'consent',
|
|
297
|
+
* if presents, it will serve as the fallback value for each UserIdentifier.
|
|
285
298
|
* For job type 'STORE_SALES_UPLOAD_FIRST_PARTY', `storeSalesMetadata` is
|
|
286
299
|
* required to offer StoreSalesMetadata. Besides that, for the store sales data,
|
|
287
300
|
* common data (e.g. `currencyCode`, `conversionAction`) in
|
|
@@ -296,6 +309,7 @@ let CustomerMatchConfig;
|
|
|
296
309
|
* operation: ('create'|'remove'),
|
|
297
310
|
* type: !OfflineUserDataJobType,
|
|
298
311
|
* storeSalesMetadata: (undefined|StoreSalesMetadata),
|
|
312
|
+
* customerMatchUserListMetadata: (undefined|CustomerMatchUserListMetadata),
|
|
299
313
|
* transactionAttribute: (undefined|TransactionAttribute),
|
|
300
314
|
* userAttribute: (undefined|UserAttribute),
|
|
301
315
|
* userIdentifierSource: (!UserIdentifierSource|undefined),
|
|
@@ -354,7 +368,7 @@ class GoogleAdsApi {
|
|
|
354
368
|
async getReport(customerId, loginCustomerId, query) {
|
|
355
369
|
const request = new SearchGoogleAdsRequest({
|
|
356
370
|
query,
|
|
357
|
-
customerId:
|
|
371
|
+
customerId: getCleanCid(customerId),
|
|
358
372
|
});
|
|
359
373
|
return this.getReport_(request, loginCustomerId, true);
|
|
360
374
|
}
|
|
@@ -380,7 +394,7 @@ class GoogleAdsApi {
|
|
|
380
394
|
const request = new SearchGoogleAdsRequest(
|
|
381
395
|
Object.assign({
|
|
382
396
|
query,
|
|
383
|
-
customerId:
|
|
397
|
+
customerId: getCleanCid(customerId),
|
|
384
398
|
pageSize: 10000,
|
|
385
399
|
}, options)
|
|
386
400
|
);
|
|
@@ -411,7 +425,6 @@ class GoogleAdsApi {
|
|
|
411
425
|
return result;
|
|
412
426
|
}
|
|
413
427
|
|
|
414
|
-
//TODO: Test a big report see how the event 'data' is triggered
|
|
415
428
|
/**
|
|
416
429
|
* Gets stream report of a given Customer account. The stream will send
|
|
417
430
|
* `SearchGoogleAdsResponse` objects with the event 'data'.
|
|
@@ -424,7 +437,7 @@ class GoogleAdsApi {
|
|
|
424
437
|
const client = await this.getGoogleAdsServiceClient_();
|
|
425
438
|
const request = new SearchGoogleAdsRequest({
|
|
426
439
|
query,
|
|
427
|
-
customerId:
|
|
440
|
+
customerId: getCleanCid(customerId),
|
|
428
441
|
});
|
|
429
442
|
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
430
443
|
const response = await client.searchStream(request, callOptions);
|
|
@@ -438,23 +451,76 @@ class GoogleAdsApi {
|
|
|
438
451
|
* `results` in JSON format strings with the delimit of a line breaker.
|
|
439
452
|
* @param {string} customerId
|
|
440
453
|
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
441
|
-
* @param {string} query
|
|
442
|
-
* @
|
|
454
|
+
* @param {string} query A Google Ads Query string.
|
|
455
|
+
* @param {boolean} snakeCase Output JSON objects in snake_case.
|
|
456
|
+
* @return {!Promise<stream>}
|
|
443
457
|
*/
|
|
444
|
-
async cleanedStreamReport(customerId, loginCustomerId, query
|
|
458
|
+
async cleanedStreamReport(customerId, loginCustomerId, query,
|
|
459
|
+
snakeCase = false) {
|
|
445
460
|
const cleanReportStream = new Transform({
|
|
446
461
|
writableObjectMode: true,
|
|
447
462
|
transform(chunk, encoding, callback) {
|
|
448
463
|
const { fieldMask: { paths } } = chunk;
|
|
449
|
-
const
|
|
464
|
+
const camelPaths = paths.map((path) => {
|
|
465
|
+
return path.split('.').map(changeNamingFromSnakeToLowerCamel).join('.');
|
|
466
|
+
});
|
|
467
|
+
const extractor = extractObject(camelPaths);
|
|
468
|
+
const results = snakeCase
|
|
469
|
+
? chunk.results.map(extractor).map(changeObjectNamingFromLowerCamelToSnake)
|
|
470
|
+
: chunk.results.map(extractor);
|
|
450
471
|
// Add a line break after each chunk to keep files in proper format.
|
|
451
|
-
const data =
|
|
452
|
-
+ '\n';
|
|
472
|
+
const data = results.map(JSON.stringify).join('\n') + '\n';
|
|
453
473
|
callback(null, data);
|
|
454
474
|
}
|
|
455
475
|
});
|
|
456
476
|
const stream = await this.streamReport(customerId, loginCustomerId, query);
|
|
457
|
-
return stream.
|
|
477
|
+
return stream.on('error', (error) => cleanReportStream.emit('error', error))
|
|
478
|
+
.pipe(cleanReportStream);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Gets the report stream through REST interface.
|
|
483
|
+
* @param {string} customerId
|
|
484
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
485
|
+
* @param {string} query A Google Ads Query string.
|
|
486
|
+
* @return {!Promise<stream>}
|
|
487
|
+
*/
|
|
488
|
+
async restStreamReport(customerId, loginCustomerId, query) {
|
|
489
|
+
await this.authClient.prepareCredentials();
|
|
490
|
+
const headers = Object.assign(
|
|
491
|
+
await this.authClient.getDefaultAuth().getRequestHeaders(),
|
|
492
|
+
this.getGoogleAdsHeaders_(loginCustomerId)
|
|
493
|
+
);
|
|
494
|
+
const options = {
|
|
495
|
+
baseURL: `${API_ENDPOINT}/${API_VERSION}/`,
|
|
496
|
+
url: `customers/${getCleanCid(customerId)}/googleAds:searchStream`,
|
|
497
|
+
headers,
|
|
498
|
+
data: { query },
|
|
499
|
+
method: 'POST',
|
|
500
|
+
responseType: 'stream',
|
|
501
|
+
};
|
|
502
|
+
const response = await gaxiosRequest(options);
|
|
503
|
+
return response.data;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Gets the report stream through REST interface.
|
|
508
|
+
* Based on the `fieldMask` in the response to filter out
|
|
509
|
+
* selected fields of the report and returns an array of JSON format strings
|
|
510
|
+
* with the delimit of a line breaker.
|
|
511
|
+
* @param {string} customerId
|
|
512
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
513
|
+
* @param {string} query A Google Ads Query string.
|
|
514
|
+
* @param {boolean} snakeCase Output JSON objects in snake_case.
|
|
515
|
+
* @return {!Promise<stream>}
|
|
516
|
+
*/
|
|
517
|
+
async cleanedRestStreamReport(customerId, loginCustomerId, query,
|
|
518
|
+
snakeCase = false) {
|
|
519
|
+
const transform = new RestSearchStreamTransform(snakeCase);
|
|
520
|
+
const stream =
|
|
521
|
+
await this.restStreamReport(customerId, loginCustomerId, query);
|
|
522
|
+
return stream.on('error', (error) => transform.emit('error', error))
|
|
523
|
+
.pipe(transform);
|
|
458
524
|
}
|
|
459
525
|
|
|
460
526
|
/**
|
|
@@ -462,20 +528,19 @@ class GoogleAdsApi {
|
|
|
462
528
|
* https://developers.google.com/google-ads/api/docs/concepts/field-service
|
|
463
529
|
* Note, it looks like this function doesn't check the CID, just using
|
|
464
530
|
* developer token and OAuth.
|
|
465
|
-
* @param {string|number} loginCustomerId Login customer account ID.
|
|
466
531
|
* @param {Array<string>} adFields Array of Ad fields.
|
|
467
532
|
* @param {Array<string>} metadata Select fields, default values are:
|
|
468
533
|
* name, data_type, is_repeated, type_url.
|
|
469
534
|
* @return {!Promise<!Array<GoogleAdsField>>}
|
|
470
535
|
*/
|
|
471
|
-
async
|
|
472
|
-
'name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
536
|
+
async searchReportField(adFields,
|
|
537
|
+
metadata = ['name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
473
538
|
const client = await this.getGoogleAdsFieldServiceClient_();
|
|
474
539
|
const selectClause = metadata.join(',');
|
|
475
540
|
const fields = adFields.join('","');
|
|
476
541
|
const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
|
|
477
542
|
const request = new SearchGoogleAdsFieldsRequest({ query });
|
|
478
|
-
const callOptions = this.getCallOptions_(
|
|
543
|
+
const callOptions = this.getCallOptions_();
|
|
479
544
|
const [results] = await client.searchGoogleAdsFields(request, callOptions);
|
|
480
545
|
return results;
|
|
481
546
|
}
|
|
@@ -559,8 +624,8 @@ class GoogleAdsApi {
|
|
|
559
624
|
functionName, propertyForDebug) {
|
|
560
625
|
/** @type {!ConversionConfig} */
|
|
561
626
|
const adsConfig = this.getCamelConfig_(conversionConfig);
|
|
562
|
-
adsConfig.customerId =
|
|
563
|
-
adsConfig.loginCustomerId =
|
|
627
|
+
adsConfig.customerId = getCleanCid(customerId);
|
|
628
|
+
adsConfig.loginCustomerId = getCleanCid(loginCustomerId);
|
|
564
629
|
/**
|
|
565
630
|
* Sends a batch of hits to Google Ads API.
|
|
566
631
|
* @param {!Array<string>} lines Data for single request. It should be
|
|
@@ -875,7 +940,7 @@ class GoogleAdsApi {
|
|
|
875
940
|
SELECT user_list.id, user_list.resource_name
|
|
876
941
|
FROM user_list
|
|
877
942
|
WHERE user_list.name = '${listName}'
|
|
878
|
-
AND customer.id = ${
|
|
943
|
+
AND customer.id = ${getCleanCid(customerId)}
|
|
879
944
|
AND user_list.type = CRM_BASED
|
|
880
945
|
AND user_list.membership_status = OPEN
|
|
881
946
|
AND user_list.crm_based_user_list.upload_key_type = ${uploadKeyType}
|
|
@@ -905,7 +970,7 @@ class GoogleAdsApi {
|
|
|
905
970
|
crmBasedUserList: { uploadKeyType },
|
|
906
971
|
});
|
|
907
972
|
const request = new MutateUserListsRequest({
|
|
908
|
-
customerId:
|
|
973
|
+
customerId: getCleanCid(customerId),
|
|
909
974
|
operations: [{ create: userList }],
|
|
910
975
|
validateOnly: this.debugMode, // when true makes no changes
|
|
911
976
|
partialFailure: false, // Simplify error handling in creating userlist
|
|
@@ -1011,9 +1076,9 @@ class GoogleAdsApi {
|
|
|
1011
1076
|
const operations = userDataList.map(
|
|
1012
1077
|
(userData) => new UserDataOperation({ [operation]: new UserData(userData) })
|
|
1013
1078
|
);
|
|
1014
|
-
const metadata = this.buildCustomerMatchUserListMetadata_(
|
|
1079
|
+
const metadata = this.buildCustomerMatchUserListMetadata_(config);
|
|
1015
1080
|
const request = new UploadUserDataRequest({
|
|
1016
|
-
customerId:
|
|
1081
|
+
customerId: getCleanCid(customerId),
|
|
1017
1082
|
operations,
|
|
1018
1083
|
customerMatchUserListMetadata: metadata,
|
|
1019
1084
|
});
|
|
@@ -1025,16 +1090,20 @@ class GoogleAdsApi {
|
|
|
1025
1090
|
/**
|
|
1026
1091
|
* Creates CustomerMatchUserListMetadata.
|
|
1027
1092
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
1028
|
-
* @param {
|
|
1029
|
-
*
|
|
1093
|
+
* @param {{
|
|
1094
|
+
* customerId: string,
|
|
1095
|
+
* listId: string,
|
|
1096
|
+
* customerMatchUserListMetadata: undefined|!CustomerMatchUserListMetadata
|
|
1097
|
+
* }} config Configuration for CustomerMatchUserListMetadata
|
|
1030
1098
|
* @return {!CustomerMatchUserListMetadata}
|
|
1031
1099
|
* @private
|
|
1032
1100
|
*/
|
|
1033
|
-
buildCustomerMatchUserListMetadata_(
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1101
|
+
buildCustomerMatchUserListMetadata_(config) {
|
|
1102
|
+
const { customerId, listId, customerMatchUserListMetadata } = config;
|
|
1103
|
+
const resourceName = `customers/${customerId}/userLists/${listId}`;
|
|
1104
|
+
return new CustomerMatchUserListMetadata(Object.assign({
|
|
1036
1105
|
userList: resourceName,
|
|
1037
|
-
});
|
|
1106
|
+
}, customerMatchUserListMetadata));
|
|
1038
1107
|
}
|
|
1039
1108
|
|
|
1040
1109
|
/**
|
|
@@ -1080,8 +1149,7 @@ class GoogleAdsApi {
|
|
|
1080
1149
|
const jobData = { type };
|
|
1081
1150
|
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJobs?hl=en#CustomerMatchUserListMetadata
|
|
1082
1151
|
if (type.startsWith('CUSTOMER_MATCH')) {
|
|
1083
|
-
const metadata = this.buildCustomerMatchUserListMetadata_(
|
|
1084
|
-
listId);
|
|
1152
|
+
const metadata = this.buildCustomerMatchUserListMetadata_(config);
|
|
1085
1153
|
jobData.customerMatchUserListMetadata = metadata;
|
|
1086
1154
|
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJob?hl=en#StoreSalesMetadata
|
|
1087
1155
|
} else if (type.startsWith('STORE_SALES')) {
|
|
@@ -1095,7 +1163,7 @@ class GoogleAdsApi {
|
|
|
1095
1163
|
}
|
|
1096
1164
|
const job = new OfflineUserDataJob(jobData);
|
|
1097
1165
|
const request = new CreateOfflineUserDataJobRequest({
|
|
1098
|
-
customerId:
|
|
1166
|
+
customerId: getCleanCid(customerId),
|
|
1099
1167
|
job,
|
|
1100
1168
|
validateOnly: this.debugMode, // when true makes no changes
|
|
1101
1169
|
enableMatchRateRangePreview: true,
|
|
@@ -1248,16 +1316,6 @@ class GoogleAdsApi {
|
|
|
1248
1316
|
}
|
|
1249
1317
|
}
|
|
1250
1318
|
|
|
1251
|
-
/**
|
|
1252
|
-
* Returns a integer format CID by removing dashes.
|
|
1253
|
-
* @param {string} cid
|
|
1254
|
-
* @return {string}
|
|
1255
|
-
* @private
|
|
1256
|
-
*/
|
|
1257
|
-
getCleanCid_(cid) {
|
|
1258
|
-
return cid.toString().replace(/-/g, '');
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
1319
|
/**
|
|
1262
1320
|
* Historically, we used a 3rd party library that adopted snake naming
|
|
1263
1321
|
* convention as the protobuf files and API documents. However, the
|
|
@@ -1279,10 +1337,11 @@ class GoogleAdsApi {
|
|
|
1279
1337
|
* @private
|
|
1280
1338
|
*/
|
|
1281
1339
|
getGoogleAdsHeaders_(loginCustomerId) {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1340
|
+
const headers = { 'developer-token': this.developerToken };
|
|
1341
|
+
if (loginCustomerId) {
|
|
1342
|
+
headers['login-customer-id'] = getCleanCid(loginCustomerId);
|
|
1343
|
+
}
|
|
1344
|
+
return headers;
|
|
1286
1345
|
}
|
|
1287
1346
|
|
|
1288
1347
|
/**
|
|
@@ -1295,7 +1354,7 @@ class GoogleAdsApi {
|
|
|
1295
1354
|
getCallOptions_(loginCustomerId) {
|
|
1296
1355
|
return {
|
|
1297
1356
|
otherArgs: {
|
|
1298
|
-
headers: this.getGoogleAdsHeaders_(
|
|
1357
|
+
headers: this.getGoogleAdsHeaders_(loginCustomerId),
|
|
1299
1358
|
},
|
|
1300
1359
|
};
|
|
1301
1360
|
}
|
|
@@ -1536,6 +1595,7 @@ const debugGoogleAdsFailure = (failures, request) => {
|
|
|
1536
1595
|
}
|
|
1537
1596
|
|
|
1538
1597
|
module.exports = {
|
|
1598
|
+
GoogleAdsField,
|
|
1539
1599
|
ConversionConfig,
|
|
1540
1600
|
CustomerMatchRecord,
|
|
1541
1601
|
CustomerMatchConfig,
|
package/src/apis/search_ads.js
CHANGED
|
@@ -18,13 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
21
|
+
const { request: gaxiosRequest } = require('gaxios');
|
|
21
22
|
const { google } = require('googleapis');
|
|
22
23
|
const AuthClient = require('./auth_client.js');
|
|
23
24
|
const { getLogger } = require('../components/utils.js');
|
|
25
|
+
const { getCleanCid, RestSearchStreamTransform }
|
|
26
|
+
= require('./base/ads_api_common.js');
|
|
24
27
|
|
|
25
28
|
const API_SCOPES = Object.freeze([
|
|
26
29
|
'https://www.googleapis.com/auth/doubleclicksearch',
|
|
27
30
|
]);
|
|
31
|
+
const API_ENDPOINT = 'https://searchads360.googleapis.com';
|
|
28
32
|
const API_VERSION = 'v0';
|
|
29
33
|
|
|
30
34
|
/**
|
|
@@ -56,13 +60,15 @@ class SearchAds {
|
|
|
56
60
|
* @private
|
|
57
61
|
*/
|
|
58
62
|
async getApiClient_(loginCustomerId) {
|
|
59
|
-
this.logger.debug(`Initialized
|
|
60
|
-
|
|
63
|
+
this.logger.debug(`Initialized SA reporting for ${loginCustomerId}`);
|
|
64
|
+
const options = {
|
|
61
65
|
version: API_VERSION,
|
|
62
66
|
auth: await this.getAuth_(),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
};
|
|
68
|
+
if (loginCustomerId) {
|
|
69
|
+
options.headers = { 'login-customer-id': getCleanCid(loginCustomerId) };
|
|
70
|
+
}
|
|
71
|
+
return google.searchads360(options);
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -77,61 +83,117 @@ class SearchAds {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* is too large to be handled in this way, a possible solution is to parse
|
|
85
|
-
* the string directly to get the content of `results`.
|
|
86
|
-
* @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/customers.searchAds360/searchStream
|
|
86
|
+
* Gets a report synchronously from a given Customer account.
|
|
87
|
+
* If there is a `nextPageToken` in the response, it means the report is not
|
|
88
|
+
* finished and there are more pages.
|
|
89
|
+
* @see https://developers.google.com/search-ads/reporting/api/reference/rpc/google.ads.searchads360.v0.services#searchads360service
|
|
87
90
|
* @param {string} customerId
|
|
88
91
|
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
89
92
|
* @param {string} query
|
|
90
|
-
* @
|
|
93
|
+
* @param {object=} options Options for `SearchSearchAds360Request`.
|
|
94
|
+
* @see https://developers.google.com/search-ads/reporting/api/reference/rpc/google.ads.searchads360.v0.services#searchsearchads360request
|
|
95
|
+
* @return {!SearchAds360Field}
|
|
96
|
+
* @see https://developers.google.com/search-ads/reporting/api/reference/rpc/google.ads.searchads360.v0.services#searchsearchads360response
|
|
91
97
|
*/
|
|
92
|
-
async
|
|
98
|
+
async getPaginatedReport(customerId, loginCustomerId, query, options = {}) {
|
|
93
99
|
const searchads = await this.getApiClient_(loginCustomerId);
|
|
100
|
+
const requestBody = Object.assign({
|
|
101
|
+
query,
|
|
102
|
+
pageSize: 10000,
|
|
103
|
+
}, options);
|
|
94
104
|
const response = await searchads.customers.searchAds360.search({
|
|
95
|
-
customerId,
|
|
96
|
-
requestBody
|
|
97
|
-
}
|
|
105
|
+
customerId: getCleanCid(customerId),
|
|
106
|
+
requestBody,
|
|
107
|
+
});
|
|
98
108
|
return response.data;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
/**
|
|
102
|
-
* Gets a report
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
112
|
+
* Gets a report stream from a Search Ads 360 reporting API.
|
|
113
|
+
* The streamed content is not NDJSON format, but an array of JSON objects
|
|
114
|
+
* with each element has a property `results`.
|
|
115
|
+
* `data` support `batchSize` to set how many rows in one result element.
|
|
106
116
|
* @param {string} customerId
|
|
107
117
|
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
108
118
|
* @param {string} query
|
|
109
|
-
* @return {!
|
|
110
|
-
* @see https://developers.google.com/search-ads/reporting/api/reference/rest/
|
|
119
|
+
* @return {!Promise<stream>}
|
|
120
|
+
* @see https://developers.google.com/search-ads/reporting/api/reference/rest/search
|
|
111
121
|
*/
|
|
112
|
-
async
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
async restStreamReport(customerId, loginCustomerId, query) {
|
|
123
|
+
const auth = await this.getAuth_();
|
|
124
|
+
const headers = Object.assign(
|
|
125
|
+
await auth.getRequestHeaders(), {
|
|
126
|
+
'login-customer-id': getCleanCid(loginCustomerId),
|
|
117
127
|
});
|
|
118
|
-
|
|
128
|
+
const options = {
|
|
129
|
+
baseURL: `${API_ENDPOINT}/${API_VERSION}/`,
|
|
130
|
+
url: `customers/${getCleanCid(customerId)}/searchAds360:searchStream`,
|
|
131
|
+
headers,
|
|
132
|
+
data: { query },
|
|
133
|
+
method: 'POST',
|
|
134
|
+
responseType: 'stream',
|
|
135
|
+
};
|
|
136
|
+
const response = await gaxiosRequest(options);
|
|
137
|
+
return response.data;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets the report stream through REST interface.
|
|
142
|
+
* Based on the `fieldMask` in the response to filter out
|
|
143
|
+
* selected fields of the report and returns an array of JSON format strings
|
|
144
|
+
* with the delimit of a line breaker.
|
|
145
|
+
* @param {string} customerId
|
|
146
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
147
|
+
* @param {string} query A Google Ads Query string.
|
|
148
|
+
* @param {boolean=} snakeCase Output JSON objects in snake_case.
|
|
149
|
+
* @return {!Promise<stream>}
|
|
150
|
+
*/
|
|
151
|
+
async cleanedRestStreamReport(customerId, loginCustomerId, query,
|
|
152
|
+
snakeCase = false) {
|
|
153
|
+
const transform = new RestSearchStreamTransform(snakeCase);
|
|
154
|
+
const stream =
|
|
155
|
+
await this.restStreamReport(customerId, loginCustomerId, query);
|
|
156
|
+
return stream.on('error', (error) => transform.emit('error', error))
|
|
157
|
+
.pipe(transform);
|
|
119
158
|
}
|
|
120
159
|
|
|
121
160
|
/**
|
|
122
161
|
* Returns the requested field or resource (artifact) used by SearchAds360Service.
|
|
123
162
|
* This service doesn't require `login-customer-id` HTTP header.
|
|
124
163
|
* @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields/get
|
|
125
|
-
* @param {string}
|
|
164
|
+
* @param {string} fieldName
|
|
126
165
|
* @return {!SearchAds360Field}
|
|
127
166
|
* @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields#SearchAds360Field
|
|
128
167
|
*/
|
|
129
|
-
async getReportField(
|
|
168
|
+
async getReportField(fieldName) {
|
|
130
169
|
const searchads = await this.getApiClient_();
|
|
131
|
-
const
|
|
170
|
+
const resourceName = `searchAds360Fields/${fieldName}`;
|
|
171
|
+
const response =
|
|
172
|
+
await searchads.searchAds360Fields.get({ resourceName });
|
|
132
173
|
return response.data;
|
|
133
174
|
}
|
|
134
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Returns resources information from Search Ads API.
|
|
178
|
+
* @see: https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields
|
|
179
|
+
* Note, it looks like this function doesn't check the CID, just using OAuth.
|
|
180
|
+
* @param {Array<string>} adFields Array of Ad fields.
|
|
181
|
+
* @param {Array<string>} metadata Select fields, default values are:
|
|
182
|
+
* name, data_type, is_repeated, type_url.
|
|
183
|
+
* @return {!Promise<!Array<GoogleAdsField>>}
|
|
184
|
+
* @see GoogleAdsApi.searchReportField
|
|
185
|
+
*/
|
|
186
|
+
async searchReportField(adFields,
|
|
187
|
+
metadata = ['name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
188
|
+
const searchads = await this.getApiClient_();
|
|
189
|
+
const selectClause = metadata.join(',');
|
|
190
|
+
const fields = adFields.join('","');
|
|
191
|
+
const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
|
|
192
|
+
const response =
|
|
193
|
+
await searchads.searchAds360Fields.search({ query, pageSize: 10000 });
|
|
194
|
+
return response.data.results;
|
|
195
|
+
}
|
|
196
|
+
|
|
135
197
|
/**
|
|
136
198
|
* Returns all the custom columns associated with the customer in full detail.
|
|
137
199
|
* @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/customers.customColumns/list
|
package/src/components/utils.js
CHANGED
|
@@ -489,7 +489,7 @@ const extractObject = (paths) => {
|
|
|
489
489
|
paths.forEach((path) => {
|
|
490
490
|
const [value, owner, property] = path.split('.')
|
|
491
491
|
.reduce(transcribe, [sourceObject, output, undefined]);
|
|
492
|
-
if (value) {
|
|
492
|
+
if (typeof value !== 'undefined') {
|
|
493
493
|
owner[property] = value;
|
|
494
494
|
}
|
|
495
495
|
});
|
|
@@ -539,6 +539,16 @@ const changeNamingFromSnakeToLowerCamel = (name) => {
|
|
|
539
539
|
(initial) => initial.substring(1).toUpperCase());
|
|
540
540
|
};
|
|
541
541
|
|
|
542
|
+
/**
|
|
543
|
+
* For more details, see:
|
|
544
|
+
* https://developers.google.com/google-ads/api/docs/rest/design/json-mappings
|
|
545
|
+
* @param {string} name Identifiers.
|
|
546
|
+
* @return {string}
|
|
547
|
+
*/
|
|
548
|
+
const changeNamingFromLowerCamelToSnake = (name) => {
|
|
549
|
+
return name.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
|
550
|
+
};
|
|
551
|
+
|
|
542
552
|
/**
|
|
543
553
|
* Converts a JSON object which has snake naming convention to lower camel
|
|
544
554
|
* naming.
|
|
@@ -561,6 +571,28 @@ const changeObjectNamingFromSnakeToLowerCamel = (obj) => {
|
|
|
561
571
|
}
|
|
562
572
|
}
|
|
563
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Converts a JSON object which has lower camel naming convention to snake
|
|
576
|
+
* naming.
|
|
577
|
+
*
|
|
578
|
+
* @param {object} obj
|
|
579
|
+
* @return {object}
|
|
580
|
+
*/
|
|
581
|
+
const changeObjectNamingFromLowerCamelToSnake = (obj) => {
|
|
582
|
+
if (Array.isArray(obj)) {
|
|
583
|
+
return obj.map(changeObjectNamingFromLowerCamelToSnake);
|
|
584
|
+
} else if (typeof obj === 'object') {
|
|
585
|
+
const newObj = {};
|
|
586
|
+
Object.keys(obj).forEach((key) => {
|
|
587
|
+
newObj[changeNamingFromLowerCamelToSnake(key)] =
|
|
588
|
+
changeObjectNamingFromLowerCamelToSnake(obj[key]);
|
|
589
|
+
});
|
|
590
|
+
return newObj;
|
|
591
|
+
} else {
|
|
592
|
+
return obj;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
564
596
|
/**
|
|
565
597
|
* Returns the response data for a HTTP request. It will retry the specific
|
|
566
598
|
* times if there was errors happened.
|
|
@@ -606,6 +638,8 @@ module.exports = {
|
|
|
606
638
|
getObjectByPath,
|
|
607
639
|
changeNamingFromSnakeToUpperCamel,
|
|
608
640
|
changeNamingFromSnakeToLowerCamel,
|
|
641
|
+
changeNamingFromLowerCamelToSnake,
|
|
609
642
|
changeObjectNamingFromSnakeToLowerCamel,
|
|
643
|
+
changeObjectNamingFromLowerCamelToSnake,
|
|
610
644
|
requestWithRetry,
|
|
611
645
|
};
|