@google-cloud/nodejs-common 2.0.16-beta → 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 +94 -45
- package/src/apis/search_ads.js +94 -32
- package/src/components/utils.js +1 -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,7 +90,7 @@ 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 {
|
|
@@ -99,10 +101,12 @@ const {
|
|
|
99
101
|
changeObjectNamingFromSnakeToLowerCamel,
|
|
100
102
|
changeObjectNamingFromLowerCamelToSnake,
|
|
101
103
|
} = require('../components/utils.js');
|
|
104
|
+
const { getCleanCid, RestSearchStreamTransform }
|
|
105
|
+
= require('./base/ads_api_common.js');
|
|
102
106
|
|
|
103
107
|
/** @type {!ReadonlyArray<string>} */
|
|
104
108
|
const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords']);
|
|
105
|
-
|
|
109
|
+
const API_ENDPOINT = 'https://googleads.googleapis.com';
|
|
106
110
|
/**
|
|
107
111
|
* List of properties that will be taken from the data file as elements of a
|
|
108
112
|
* conversion or a conversion adjustment.
|
|
@@ -257,6 +261,9 @@ let ConversionConfig;
|
|
|
257
261
|
* If audience listId is not present, 'listName' and 'uploadKeyType' need to
|
|
258
262
|
* be there so they can be used to create a customer match user list.
|
|
259
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.
|
|
260
267
|
* Should not include `userIdentifierSource` based on:
|
|
261
268
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
262
269
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
@@ -267,6 +274,7 @@ let ConversionConfig;
|
|
|
267
274
|
* listId: (string|undefined),
|
|
268
275
|
* listName: (string|undefined),
|
|
269
276
|
* uploadKeyType: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
|
|
277
|
+
* customerMatchUserListMetadata: (undefined|CustomerMatchUserListMetadata),
|
|
270
278
|
* operation: ('create'|'remove'),
|
|
271
279
|
* consent: (!Consent),
|
|
272
280
|
* }}
|
|
@@ -284,6 +292,9 @@ let CustomerMatchConfig;
|
|
|
284
292
|
* create a customer match user list.
|
|
285
293
|
* For job type 'CUSTOMER_MATCH_WITH_ATTRIBUTES', 'user_attribute' can be used
|
|
286
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.
|
|
287
298
|
* For job type 'STORE_SALES_UPLOAD_FIRST_PARTY', `storeSalesMetadata` is
|
|
288
299
|
* required to offer StoreSalesMetadata. Besides that, for the store sales data,
|
|
289
300
|
* common data (e.g. `currencyCode`, `conversionAction`) in
|
|
@@ -298,6 +309,7 @@ let CustomerMatchConfig;
|
|
|
298
309
|
* operation: ('create'|'remove'),
|
|
299
310
|
* type: !OfflineUserDataJobType,
|
|
300
311
|
* storeSalesMetadata: (undefined|StoreSalesMetadata),
|
|
312
|
+
* customerMatchUserListMetadata: (undefined|CustomerMatchUserListMetadata),
|
|
301
313
|
* transactionAttribute: (undefined|TransactionAttribute),
|
|
302
314
|
* userAttribute: (undefined|UserAttribute),
|
|
303
315
|
* userIdentifierSource: (!UserIdentifierSource|undefined),
|
|
@@ -356,7 +368,7 @@ class GoogleAdsApi {
|
|
|
356
368
|
async getReport(customerId, loginCustomerId, query) {
|
|
357
369
|
const request = new SearchGoogleAdsRequest({
|
|
358
370
|
query,
|
|
359
|
-
customerId:
|
|
371
|
+
customerId: getCleanCid(customerId),
|
|
360
372
|
});
|
|
361
373
|
return this.getReport_(request, loginCustomerId, true);
|
|
362
374
|
}
|
|
@@ -382,7 +394,7 @@ class GoogleAdsApi {
|
|
|
382
394
|
const request = new SearchGoogleAdsRequest(
|
|
383
395
|
Object.assign({
|
|
384
396
|
query,
|
|
385
|
-
customerId:
|
|
397
|
+
customerId: getCleanCid(customerId),
|
|
386
398
|
pageSize: 10000,
|
|
387
399
|
}, options)
|
|
388
400
|
);
|
|
@@ -413,7 +425,6 @@ class GoogleAdsApi {
|
|
|
413
425
|
return result;
|
|
414
426
|
}
|
|
415
427
|
|
|
416
|
-
//TODO: Test a big report see how the event 'data' is triggered
|
|
417
428
|
/**
|
|
418
429
|
* Gets stream report of a given Customer account. The stream will send
|
|
419
430
|
* `SearchGoogleAdsResponse` objects with the event 'data'.
|
|
@@ -426,7 +437,7 @@ class GoogleAdsApi {
|
|
|
426
437
|
const client = await this.getGoogleAdsServiceClient_();
|
|
427
438
|
const request = new SearchGoogleAdsRequest({
|
|
428
439
|
query,
|
|
429
|
-
customerId:
|
|
440
|
+
customerId: getCleanCid(customerId),
|
|
430
441
|
});
|
|
431
442
|
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
432
443
|
const response = await client.searchStream(request, callOptions);
|
|
@@ -441,11 +452,11 @@ class GoogleAdsApi {
|
|
|
441
452
|
* @param {string} customerId
|
|
442
453
|
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
443
454
|
* @param {string} query A Google Ads Query string.
|
|
444
|
-
* @param {boolean}
|
|
445
|
-
* @return {!Promise<
|
|
455
|
+
* @param {boolean} snakeCase Output JSON objects in snake_case.
|
|
456
|
+
* @return {!Promise<stream>}
|
|
446
457
|
*/
|
|
447
458
|
async cleanedStreamReport(customerId, loginCustomerId, query,
|
|
448
|
-
|
|
459
|
+
snakeCase = false) {
|
|
449
460
|
const cleanReportStream = new Transform({
|
|
450
461
|
writableObjectMode: true,
|
|
451
462
|
transform(chunk, encoding, callback) {
|
|
@@ -454,7 +465,7 @@ class GoogleAdsApi {
|
|
|
454
465
|
return path.split('.').map(changeNamingFromSnakeToLowerCamel).join('.');
|
|
455
466
|
});
|
|
456
467
|
const extractor = extractObject(camelPaths);
|
|
457
|
-
const results =
|
|
468
|
+
const results = snakeCase
|
|
458
469
|
? chunk.results.map(extractor).map(changeObjectNamingFromLowerCamelToSnake)
|
|
459
470
|
: chunk.results.map(extractor);
|
|
460
471
|
// Add a line break after each chunk to keep files in proper format.
|
|
@@ -467,25 +478,69 @@ class GoogleAdsApi {
|
|
|
467
478
|
.pipe(cleanReportStream);
|
|
468
479
|
}
|
|
469
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);
|
|
524
|
+
}
|
|
525
|
+
|
|
470
526
|
/**
|
|
471
527
|
* Returns resources information from Google Ads API. see:
|
|
472
528
|
* https://developers.google.com/google-ads/api/docs/concepts/field-service
|
|
473
529
|
* Note, it looks like this function doesn't check the CID, just using
|
|
474
530
|
* developer token and OAuth.
|
|
475
|
-
* @param {string|number} loginCustomerId Login customer account ID.
|
|
476
531
|
* @param {Array<string>} adFields Array of Ad fields.
|
|
477
532
|
* @param {Array<string>} metadata Select fields, default values are:
|
|
478
533
|
* name, data_type, is_repeated, type_url.
|
|
479
534
|
* @return {!Promise<!Array<GoogleAdsField>>}
|
|
480
535
|
*/
|
|
481
|
-
async
|
|
482
|
-
'name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
536
|
+
async searchReportField(adFields,
|
|
537
|
+
metadata = ['name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
483
538
|
const client = await this.getGoogleAdsFieldServiceClient_();
|
|
484
539
|
const selectClause = metadata.join(',');
|
|
485
540
|
const fields = adFields.join('","');
|
|
486
541
|
const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
|
|
487
542
|
const request = new SearchGoogleAdsFieldsRequest({ query });
|
|
488
|
-
const callOptions = this.getCallOptions_(
|
|
543
|
+
const callOptions = this.getCallOptions_();
|
|
489
544
|
const [results] = await client.searchGoogleAdsFields(request, callOptions);
|
|
490
545
|
return results;
|
|
491
546
|
}
|
|
@@ -569,8 +624,8 @@ class GoogleAdsApi {
|
|
|
569
624
|
functionName, propertyForDebug) {
|
|
570
625
|
/** @type {!ConversionConfig} */
|
|
571
626
|
const adsConfig = this.getCamelConfig_(conversionConfig);
|
|
572
|
-
adsConfig.customerId =
|
|
573
|
-
adsConfig.loginCustomerId =
|
|
627
|
+
adsConfig.customerId = getCleanCid(customerId);
|
|
628
|
+
adsConfig.loginCustomerId = getCleanCid(loginCustomerId);
|
|
574
629
|
/**
|
|
575
630
|
* Sends a batch of hits to Google Ads API.
|
|
576
631
|
* @param {!Array<string>} lines Data for single request. It should be
|
|
@@ -885,7 +940,7 @@ class GoogleAdsApi {
|
|
|
885
940
|
SELECT user_list.id, user_list.resource_name
|
|
886
941
|
FROM user_list
|
|
887
942
|
WHERE user_list.name = '${listName}'
|
|
888
|
-
AND customer.id = ${
|
|
943
|
+
AND customer.id = ${getCleanCid(customerId)}
|
|
889
944
|
AND user_list.type = CRM_BASED
|
|
890
945
|
AND user_list.membership_status = OPEN
|
|
891
946
|
AND user_list.crm_based_user_list.upload_key_type = ${uploadKeyType}
|
|
@@ -915,7 +970,7 @@ class GoogleAdsApi {
|
|
|
915
970
|
crmBasedUserList: { uploadKeyType },
|
|
916
971
|
});
|
|
917
972
|
const request = new MutateUserListsRequest({
|
|
918
|
-
customerId:
|
|
973
|
+
customerId: getCleanCid(customerId),
|
|
919
974
|
operations: [{ create: userList }],
|
|
920
975
|
validateOnly: this.debugMode, // when true makes no changes
|
|
921
976
|
partialFailure: false, // Simplify error handling in creating userlist
|
|
@@ -1021,9 +1076,9 @@ class GoogleAdsApi {
|
|
|
1021
1076
|
const operations = userDataList.map(
|
|
1022
1077
|
(userData) => new UserDataOperation({ [operation]: new UserData(userData) })
|
|
1023
1078
|
);
|
|
1024
|
-
const metadata = this.buildCustomerMatchUserListMetadata_(
|
|
1079
|
+
const metadata = this.buildCustomerMatchUserListMetadata_(config);
|
|
1025
1080
|
const request = new UploadUserDataRequest({
|
|
1026
|
-
customerId:
|
|
1081
|
+
customerId: getCleanCid(customerId),
|
|
1027
1082
|
operations,
|
|
1028
1083
|
customerMatchUserListMetadata: metadata,
|
|
1029
1084
|
});
|
|
@@ -1035,16 +1090,20 @@ class GoogleAdsApi {
|
|
|
1035
1090
|
/**
|
|
1036
1091
|
* Creates CustomerMatchUserListMetadata.
|
|
1037
1092
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
1038
|
-
* @param {
|
|
1039
|
-
*
|
|
1093
|
+
* @param {{
|
|
1094
|
+
* customerId: string,
|
|
1095
|
+
* listId: string,
|
|
1096
|
+
* customerMatchUserListMetadata: undefined|!CustomerMatchUserListMetadata
|
|
1097
|
+
* }} config Configuration for CustomerMatchUserListMetadata
|
|
1040
1098
|
* @return {!CustomerMatchUserListMetadata}
|
|
1041
1099
|
* @private
|
|
1042
1100
|
*/
|
|
1043
|
-
buildCustomerMatchUserListMetadata_(
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1101
|
+
buildCustomerMatchUserListMetadata_(config) {
|
|
1102
|
+
const { customerId, listId, customerMatchUserListMetadata } = config;
|
|
1103
|
+
const resourceName = `customers/${customerId}/userLists/${listId}`;
|
|
1104
|
+
return new CustomerMatchUserListMetadata(Object.assign({
|
|
1046
1105
|
userList: resourceName,
|
|
1047
|
-
});
|
|
1106
|
+
}, customerMatchUserListMetadata));
|
|
1048
1107
|
}
|
|
1049
1108
|
|
|
1050
1109
|
/**
|
|
@@ -1090,8 +1149,7 @@ class GoogleAdsApi {
|
|
|
1090
1149
|
const jobData = { type };
|
|
1091
1150
|
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJobs?hl=en#CustomerMatchUserListMetadata
|
|
1092
1151
|
if (type.startsWith('CUSTOMER_MATCH')) {
|
|
1093
|
-
const metadata = this.buildCustomerMatchUserListMetadata_(
|
|
1094
|
-
listId);
|
|
1152
|
+
const metadata = this.buildCustomerMatchUserListMetadata_(config);
|
|
1095
1153
|
jobData.customerMatchUserListMetadata = metadata;
|
|
1096
1154
|
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJob?hl=en#StoreSalesMetadata
|
|
1097
1155
|
} else if (type.startsWith('STORE_SALES')) {
|
|
@@ -1105,7 +1163,7 @@ class GoogleAdsApi {
|
|
|
1105
1163
|
}
|
|
1106
1164
|
const job = new OfflineUserDataJob(jobData);
|
|
1107
1165
|
const request = new CreateOfflineUserDataJobRequest({
|
|
1108
|
-
customerId:
|
|
1166
|
+
customerId: getCleanCid(customerId),
|
|
1109
1167
|
job,
|
|
1110
1168
|
validateOnly: this.debugMode, // when true makes no changes
|
|
1111
1169
|
enableMatchRateRangePreview: true,
|
|
@@ -1258,16 +1316,6 @@ class GoogleAdsApi {
|
|
|
1258
1316
|
}
|
|
1259
1317
|
}
|
|
1260
1318
|
|
|
1261
|
-
/**
|
|
1262
|
-
* Returns a integer format CID by removing dashes.
|
|
1263
|
-
* @param {string} cid
|
|
1264
|
-
* @return {string}
|
|
1265
|
-
* @private
|
|
1266
|
-
*/
|
|
1267
|
-
getCleanCid_(cid) {
|
|
1268
|
-
return cid.toString().replace(/-/g, '');
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
1319
|
/**
|
|
1272
1320
|
* Historically, we used a 3rd party library that adopted snake naming
|
|
1273
1321
|
* convention as the protobuf files and API documents. However, the
|
|
@@ -1289,10 +1337,11 @@ class GoogleAdsApi {
|
|
|
1289
1337
|
* @private
|
|
1290
1338
|
*/
|
|
1291
1339
|
getGoogleAdsHeaders_(loginCustomerId) {
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1340
|
+
const headers = { 'developer-token': this.developerToken };
|
|
1341
|
+
if (loginCustomerId) {
|
|
1342
|
+
headers['login-customer-id'] = getCleanCid(loginCustomerId);
|
|
1343
|
+
}
|
|
1344
|
+
return headers;
|
|
1296
1345
|
}
|
|
1297
1346
|
|
|
1298
1347
|
/**
|
|
@@ -1305,7 +1354,7 @@ class GoogleAdsApi {
|
|
|
1305
1354
|
getCallOptions_(loginCustomerId) {
|
|
1306
1355
|
return {
|
|
1307
1356
|
otherArgs: {
|
|
1308
|
-
headers: this.getGoogleAdsHeaders_(
|
|
1357
|
+
headers: this.getGoogleAdsHeaders_(loginCustomerId),
|
|
1309
1358
|
},
|
|
1310
1359
|
};
|
|
1311
1360
|
}
|
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
|
});
|