@google-cloud/nodejs-common 0.9.5-alpha → 1.0.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 +69 -60
- package/bin/install_functions.sh +15 -20
- package/package.json +17 -16
- package/src/apis/doubleclick_search.js +1 -1
- package/src/apis/google_ads.js +114 -7
- package/src/apis/measurement_protocol_ga4.js +2 -1
- package/src/apis/youtube.js +168 -3
- package/src/components/utils.js +34 -19
package/README.md
CHANGED
|
@@ -1,69 +1,78 @@
|
|
|
1
1
|
# NodeJS Common Library
|
|
2
2
|
|
|
3
|
-
<!--* freshness: { owner: 'lushu' reviewed: '2021-
|
|
4
|
-
|
|
5
|
-
A NodeJs common library for other projects, e.g. [GMP and Google Ads
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
1.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
1.
|
|
12
|
-
Connector]:
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
1.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
3
|
+
<!--* freshness: { owner: 'lushu' reviewed: '2021-12-02' } *-->
|
|
4
|
+
|
|
5
|
+
A NodeJs common library for other projects, e.g. [GMP and Google Ads Connector]
|
|
6
|
+
and [Data Tasks Coordinator]. This library includes:
|
|
7
|
+
|
|
8
|
+
1. Authentication wrapper based on google auth library to support OAuth, JWT and
|
|
9
|
+
ADC authentication;
|
|
10
|
+
|
|
11
|
+
1. Wrapper for some Google APIs for integration, mainly
|
|
12
|
+
for [GMP and Google Ads Connector]:
|
|
13
|
+
|
|
14
|
+
* Google Analytics data import
|
|
15
|
+
* Google Analytics measurement protocol
|
|
16
|
+
* Campaign Manager offline conversion upload
|
|
17
|
+
* Search Ads 360 conversions upload
|
|
18
|
+
* Google Ads click conversions upload
|
|
19
|
+
* Google Ads customer match upload
|
|
20
|
+
* Google Ads conversions scheduled uploads based on Google Sheets
|
|
21
|
+
* Measurement Protocol Google Analytics 4
|
|
22
|
+
|
|
23
|
+
1. Wrapper for some Google APIs for reporting, mainly
|
|
24
|
+
for [Data Tasks Coordinator]:
|
|
25
|
+
|
|
26
|
+
* Google Ads reporting
|
|
27
|
+
* Campaign Manager reporting
|
|
28
|
+
* Search Ads 360 reporting
|
|
29
|
+
* Display and Video 360 reporting
|
|
30
|
+
* YouTube Data API
|
|
31
|
+
* Ads Data Hub querying
|
|
32
|
+
|
|
33
|
+
1. Utilities wrapper class for Google Cloud Products:
|
|
34
|
+
|
|
35
|
+
* **Firestore Access Object**: Firestore has two modes which are excluded to
|
|
36
|
+
each other and can't be changed once selected in a Cloud Project[[2]].
|
|
37
|
+
This class, with its two successors offer a unified interface to operate
|
|
38
|
+
data objects on either Firestore or Datastore.
|
|
39
|
+
|
|
40
|
+
* **AutoMl Tables API**: Offers a unified entry to use this API based on
|
|
41
|
+
Google Cloud client library combined with REST requests to service
|
|
42
|
+
directly due to some functionalities missed in the client library.
|
|
43
|
+
|
|
44
|
+
* **Vertex AI API**: Offers a unified entry to use this API based on Google
|
|
45
|
+
Cloud client library.
|
|
46
|
+
|
|
47
|
+
* **Pub/Sub Utilities**: Offers utilities functions to create topics and
|
|
48
|
+
subscriptions for Pub/Sub, as well as the convenient way to publish a
|
|
49
|
+
message.
|
|
50
|
+
|
|
51
|
+
* **Storage Utilities**: Offers functions to manipulate the files on Cloud
|
|
52
|
+
Storage. The main functions are:
|
|
53
|
+
|
|
54
|
+
* Reading a given length (or slightly less) content without breaking a
|
|
55
|
+
line;
|
|
56
|
+
* Splitting a file into multiple files with the given length (or
|
|
57
|
+
slightly less) without breaking a line;
|
|
58
|
+
* Merging files into one file.
|
|
59
|
+
|
|
60
|
+
* **Cloud Scheduler Adapter**: A wrapper to pause and resume Cloud Scheduler
|
|
61
|
+
jobs.
|
|
62
|
+
|
|
63
|
+
* **Cloud Functions Adapter**: Cloud Functions have different parameters in
|
|
64
|
+
different environments, e.g. Node6 vs Node8[[1]]. This utility file offers
|
|
65
|
+
an adapter to wrap a Node8 Cloud Functions into Node6 and Node8 compatible
|
|
66
|
+
functions.
|
|
67
|
+
|
|
63
68
|
1. A share library for [Bash] to facilitate installation tasks.
|
|
64
69
|
|
|
65
70
|
[GMP and Google Ads Connector]:https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/gmp-googleads-connector
|
|
71
|
+
|
|
66
72
|
[Data Tasks Coordinator]:https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/data-tasks-coordinator
|
|
73
|
+
|
|
67
74
|
[1]:https://cloud.google.com/functions/docs/writing/background#functions-writing-background-hello-pubsub-node8-10
|
|
75
|
+
|
|
68
76
|
[2]:https://cloud.google.com/datastore/docs/concepts/overview#comparison_with_traditional_databases
|
|
77
|
+
|
|
69
78
|
[Bash]:https://www.gnu.org/software/bash/
|
package/bin/install_functions.sh
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# limitations under the License.
|
|
16
16
|
|
|
17
17
|
# Cloud Functions Runtime Environment.
|
|
18
|
-
CF_RUNTIME="${CF_RUNTIME:=
|
|
18
|
+
CF_RUNTIME="${CF_RUNTIME:=nodejs14}"
|
|
19
19
|
|
|
20
20
|
# Counter for steps.
|
|
21
21
|
STEP=0
|
|
@@ -743,8 +743,8 @@ select_dataset_location() {
|
|
|
743
743
|
"ASIA_PACIFIC"
|
|
744
744
|
)
|
|
745
745
|
local MULTI_REGIONAL=(
|
|
746
|
-
"Data centers within member states of the European Union (
|
|
747
|
-
"Data centers in the United States (
|
|
746
|
+
"Data centers within member states of the European Union (eu)"
|
|
747
|
+
"Data centers in the United States (us)"
|
|
748
748
|
)
|
|
749
749
|
local AMERICAS=(
|
|
750
750
|
"${NORTH_AMERICA[@]}"
|
|
@@ -1199,21 +1199,17 @@ create_or_update_sink() {
|
|
|
1199
1199
|
local existingFilter
|
|
1200
1200
|
existingFilter=$(gcloud logging sinks list --filter="name:${sinkName}" \
|
|
1201
1201
|
--format="value(filter)")
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
|
|
1207
|
-
else
|
|
1208
|
-
action="update"
|
|
1209
|
-
printf '%s\n' " Logging Export [${sinkName}] exists with a different \
|
|
1210
|
-
filter. Updating..."
|
|
1211
|
-
fi
|
|
1212
|
-
gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
|
|
1213
|
-
--log-filter="${logFilter}"
|
|
1202
|
+
local action
|
|
1203
|
+
if [[ -z "${existingFilter}" ]]; then
|
|
1204
|
+
action="create"
|
|
1205
|
+
printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
|
|
1214
1206
|
else
|
|
1215
|
-
|
|
1207
|
+
action="update"
|
|
1208
|
+
printf '%s\n' " Logging Export [${sinkName}] exists with a different \
|
|
1209
|
+
filter. Updating..."
|
|
1216
1210
|
fi
|
|
1211
|
+
gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
|
|
1212
|
+
--log-filter="${logFilter}"
|
|
1217
1213
|
if [[ $? -gt 0 ]];then
|
|
1218
1214
|
printf '%s\n' "Failed to create or update Logs router sink."
|
|
1219
1215
|
return 1
|
|
@@ -1833,9 +1829,8 @@ get_cloud_functions_service_account() {
|
|
|
1833
1829
|
# None.
|
|
1834
1830
|
#######################################
|
|
1835
1831
|
check_firestore_existence() {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
while [[ ${firestore_status} =~ .*NOT_FOUND.* ]]; do
|
|
1832
|
+
gcloud firestore indexes fields list >/dev/null 2>&1
|
|
1833
|
+
while [[ $? -gt 0 ]]; do
|
|
1839
1834
|
cat <<EOF
|
|
1840
1835
|
Cannot find Firestore or Datastore in current project. Please visit \
|
|
1841
1836
|
https://console.cloud.google.com/firestore?project=${GCP_PROJECT} to create a \
|
|
@@ -1846,7 +1841,7 @@ EOF
|
|
|
1846
1841
|
local any
|
|
1847
1842
|
read -n1 -s any
|
|
1848
1843
|
printf '\n'
|
|
1849
|
-
|
|
1844
|
+
gcloud firestore indexes fields list >/dev/null 2>&1
|
|
1850
1845
|
done
|
|
1851
1846
|
}
|
|
1852
1847
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@google-cloud/nodejs-common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A NodeJs common library for solutions based on Cloud Functions",
|
|
5
5
|
"author": "Google Inc.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,21 +16,22 @@
|
|
|
16
16
|
},
|
|
17
17
|
"homepage": "https://github.com/GoogleCloudPlatform/cloud-for-marketing/blob/master/marketing-analytics/activation/common-libs/nodejs-common/README.md",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@google-cloud/aiplatform": "^1.
|
|
20
|
-
"@google-cloud/automl": "^2.
|
|
21
|
-
"@google-cloud/bigquery": "^5.
|
|
22
|
-
"@google-cloud/datastore": "^6.
|
|
23
|
-
"@google-cloud/firestore": "^4.
|
|
24
|
-
"@google-cloud/logging-winston": "^4.
|
|
25
|
-
"@google-cloud/pubsub": "^2.
|
|
26
|
-
"@google-cloud/storage": "^5.
|
|
27
|
-
"gaxios": "^4.3.
|
|
28
|
-
"google-ads-api": "^
|
|
29
|
-
"google-ads-node": "^
|
|
30
|
-
"google-auth-library": "^7.
|
|
31
|
-
"googleapis": "^
|
|
32
|
-
"soap": "^0.
|
|
33
|
-
"winston": "^3.3.3"
|
|
19
|
+
"@google-cloud/aiplatform": "^1.13.0",
|
|
20
|
+
"@google-cloud/automl": "^2.5.1",
|
|
21
|
+
"@google-cloud/bigquery": "^5.9.2",
|
|
22
|
+
"@google-cloud/datastore": "^6.6.2",
|
|
23
|
+
"@google-cloud/firestore": "^4.15.1",
|
|
24
|
+
"@google-cloud/logging-winston": "^4.1.1",
|
|
25
|
+
"@google-cloud/pubsub": "^2.18.2",
|
|
26
|
+
"@google-cloud/storage": "^5.16.0",
|
|
27
|
+
"gaxios": "^4.3.2",
|
|
28
|
+
"google-ads-api": "^9.0.0",
|
|
29
|
+
"google-ads-node": "^7.0.0",
|
|
30
|
+
"google-auth-library": "^7.10.2",
|
|
31
|
+
"googleapis": "^91.0.0",
|
|
32
|
+
"soap": "^0.43.0",
|
|
33
|
+
"winston": "^3.3.3",
|
|
34
|
+
"lodash": "^4.17.21"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"jasmine": "^3.5.0"
|
|
@@ -267,7 +267,7 @@ class DoubleClickSearch {
|
|
|
267
267
|
batchResult.failedLines = [];
|
|
268
268
|
batchResult.groupedFailed = {};
|
|
269
269
|
const errors = new Set();
|
|
270
|
-
const messageReg = /.*Details: \[(.*) index=\d
|
|
270
|
+
const messageReg = /.*Details: \[(.*) index=\d+ conversionId=.*/;
|
|
271
271
|
const indexReg = /.*index=(\d*) .*/;
|
|
272
272
|
errorMessages.forEach((message) => {
|
|
273
273
|
const errorMessage = messageReg.exec(message);
|
package/src/apis/google_ads.js
CHANGED
|
@@ -41,25 +41,77 @@ const {
|
|
|
41
41
|
},
|
|
42
42
|
} = googleAdsLib;
|
|
43
43
|
const {GoogleAdsApi} = require('google-ads-api');
|
|
44
|
+
const lodash = require('lodash');
|
|
45
|
+
|
|
44
46
|
const AuthClient = require('./auth_client.js');
|
|
45
47
|
const {getLogger, BatchResult,} = require('../components/utils.js');
|
|
46
48
|
|
|
47
49
|
/** @type {!ReadonlyArray<string>} */
|
|
48
50
|
const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
|
|
49
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
|
+
|
|
50
83
|
/**
|
|
51
84
|
* Configuration for uploading click conversions for Google Ads, includes:
|
|
52
85
|
* gclid, conversion_action, conversion_date_time, conversion_value,
|
|
53
|
-
* currency_code, order_id, external_attribution_data
|
|
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
|
+
*
|
|
54
102
|
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
55
103
|
* @typedef {{
|
|
104
|
+
* external_attribution_data: (GoogleAdsApi.ExternalAttributionData|undefined),
|
|
105
|
+
* cart_data: (object|undefined),
|
|
56
106
|
* gclid: string,
|
|
57
107
|
* conversion_action: string,
|
|
58
108
|
* conversion_date_time: string,
|
|
59
109
|
* conversion_value: number,
|
|
60
110
|
* currency_code:(string|undefined),
|
|
61
111
|
* order_id: (string|undefined),
|
|
62
|
-
*
|
|
112
|
+
* user_identifier_source:(UserIdentifierSource|undefined),
|
|
113
|
+
* custom_variable_tags:(!Array<string>|undefined),
|
|
114
|
+
* customVariables:(!object<string,string>|undefined),
|
|
63
115
|
* }}
|
|
64
116
|
*/
|
|
65
117
|
let ClickConversionConfig;
|
|
@@ -219,10 +271,8 @@ class GoogleAds {
|
|
|
219
271
|
*/
|
|
220
272
|
return async (lines, batchId) => {
|
|
221
273
|
/** @type {!Array<ClickConversionConfig>} */
|
|
222
|
-
const conversions = lines.map(
|
|
223
|
-
|
|
224
|
-
return Object.assign({}, adsConfig, record);
|
|
225
|
-
});
|
|
274
|
+
const conversions = lines.map(
|
|
275
|
+
(line) => buildClickConversionFromLine(line, adsConfig, customerId));
|
|
226
276
|
/** @const {BatchResult} */
|
|
227
277
|
const batchResult = {
|
|
228
278
|
result: true,
|
|
@@ -346,7 +396,6 @@ class GoogleAds {
|
|
|
346
396
|
* failure.
|
|
347
397
|
* groupedFailed - a hashmap of failed the lines. The key is the reason, the
|
|
348
398
|
* value is the array of failed lines due to this reason.
|
|
349
|
-
* TODO: Confirm how to surface and handle groupedFailed.
|
|
350
399
|
* @param {!BatchResult} batchResult
|
|
351
400
|
* @param {!Array<!GoogleAdsFailure>} failures
|
|
352
401
|
* @param {!Array<string>} lines The original input data.
|
|
@@ -404,6 +453,26 @@ class GoogleAds {
|
|
|
404
453
|
return customer.conversionUploads.uploadClickConversions(request);
|
|
405
454
|
}
|
|
406
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;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
407
476
|
/**
|
|
408
477
|
* Returns the function to send out a request to Google Ads API with
|
|
409
478
|
* user ids for Customer Match upload
|
|
@@ -535,6 +604,43 @@ class GoogleAds {
|
|
|
535
604
|
|
|
536
605
|
}
|
|
537
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
|
+
|
|
538
644
|
module.exports = {
|
|
539
645
|
ClickConversionConfig,
|
|
540
646
|
CustomerMatchRecord,
|
|
@@ -542,4 +648,5 @@ module.exports = {
|
|
|
542
648
|
GoogleAds,
|
|
543
649
|
ReportQueryConfig,
|
|
544
650
|
GoogleAdsField,
|
|
651
|
+
buildClickConversionFromLine,
|
|
545
652
|
};
|
|
@@ -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,
|
|
@@ -97,7 +98,7 @@ class MeasurementProtocolGA4 {
|
|
|
97
98
|
this.logger.warn(
|
|
98
99
|
"Only one line data expected. Will only send the first line.");
|
|
99
100
|
}
|
|
100
|
-
const hit =
|
|
101
|
+
const hit = lodash.merge({}, config.requestBody, JSON.parse(line));
|
|
101
102
|
|
|
102
103
|
const requestOptions = {
|
|
103
104
|
method: 'POST',
|
package/src/apis/youtube.js
CHANGED
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
21
21
|
const {google} = require('googleapis');
|
|
22
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
Schema$Channel,
|
|
24
|
+
Schema$Video,
|
|
25
|
+
Schema$CommentThread,
|
|
26
|
+
Schema$Playlist,
|
|
27
|
+
Schema$Search,
|
|
28
|
+
} = google.youtube;
|
|
23
29
|
const AuthClient = require('./auth_client.js');
|
|
24
30
|
const {getLogger} = require('../components/utils.js');
|
|
25
31
|
|
|
@@ -83,6 +89,62 @@ let ListVideosConfig;
|
|
|
83
89
|
*/
|
|
84
90
|
let ListCommentThreadsConfig;
|
|
85
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Configuration for listing youtube play list.
|
|
94
|
+
* @see https://developers.google.com/youtube/v3/docs/Playlists/list
|
|
95
|
+
* @typedef {{
|
|
96
|
+
* part: Array<string>,
|
|
97
|
+
* channelId: (string|undefined),
|
|
98
|
+
* id: (string|undefined),
|
|
99
|
+
* mine: (boolean|undefined),
|
|
100
|
+
* hl: (string|undefined),
|
|
101
|
+
* maxResults: (unsigned integer|undefined),
|
|
102
|
+
* onBehalfOfContentOwner: (string|undefined),
|
|
103
|
+
* onBehalfOfContentOwnerChannel: (string|undefined),
|
|
104
|
+
* pageToken: (string|undefined)
|
|
105
|
+
* }}
|
|
106
|
+
*/
|
|
107
|
+
let ListPlaylistConfig;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Configuration for listing youtube search results.
|
|
111
|
+
* @see https://developers.google.com/youtube/v3/docs/search/list
|
|
112
|
+
* @typedef {{
|
|
113
|
+
* part: Array<string>,
|
|
114
|
+
* forContentOwner: (boolean|undefined),
|
|
115
|
+
* forDeveloper: (boolean|undefined),
|
|
116
|
+
* forMine: (boolean|undefined),
|
|
117
|
+
* relatedToVideoId: (string|undefined),
|
|
118
|
+
* channelId: (string|undefined),
|
|
119
|
+
* channelType: (string|undefined),
|
|
120
|
+
* eventType: (string|undefined),
|
|
121
|
+
* location: (string|undefined),
|
|
122
|
+
* locationRadius: (string|undefined),
|
|
123
|
+
* maxResults: (unsigned integer|undefined),
|
|
124
|
+
* onBehalfOfContentOwner: (string|undefined),
|
|
125
|
+
* order: (string|undefined),
|
|
126
|
+
* pageToken: (string|undefined),
|
|
127
|
+
* publishedAfter: (datetime|undefined),
|
|
128
|
+
* publishedBefore: (datetime|undefined),
|
|
129
|
+
* q: (string|undefined),
|
|
130
|
+
* regionCode: (string|undefined),
|
|
131
|
+
* relevanceLanguage: (string|undefined),
|
|
132
|
+
* safeSearch: (string|undefined),
|
|
133
|
+
* topicId: (string|undefined),
|
|
134
|
+
* type: (string|undefined),
|
|
135
|
+
* videoCaption: (string|undefined),
|
|
136
|
+
* videoCategoryId: (string|undefined),
|
|
137
|
+
* videoDefinition: (string|undefined),
|
|
138
|
+
* videoDimension: (string|undefined),
|
|
139
|
+
* videoDuration: (string|undefined),
|
|
140
|
+
* videoEmbeddable: (string|undefined),
|
|
141
|
+
* videoLicense: (string|undefined),
|
|
142
|
+
* videoSyndicated: (string|undefined),
|
|
143
|
+
* videoType: (string|undefined)
|
|
144
|
+
* }}
|
|
145
|
+
*/
|
|
146
|
+
let ListSearchConfig;
|
|
147
|
+
|
|
86
148
|
/**
|
|
87
149
|
* Youtube API v3 stub.
|
|
88
150
|
* See: https://developers.google.com/youtube/v3/docs
|
|
@@ -90,10 +152,21 @@ let ListCommentThreadsConfig;
|
|
|
90
152
|
* https://developers.google.com/youtube/v3/docs/channels/list
|
|
91
153
|
* Video list type definition, see:
|
|
92
154
|
* https://developers.google.com/youtube/v3/docs/videos/list
|
|
155
|
+
* CommentThread list type definition, see:
|
|
156
|
+
* https://developers.google.com/youtube/v3/docs/commentThreads/list
|
|
157
|
+
* Playlist list type definition, see:
|
|
158
|
+
* https://developers.google.com/youtube/v3/docs/playlists/list
|
|
159
|
+
* Search list type definition, see:
|
|
160
|
+
* https://developers.google.com/youtube/v3/docs/search/list
|
|
93
161
|
*/
|
|
94
162
|
class YouTube {
|
|
95
|
-
|
|
96
|
-
|
|
163
|
+
/**
|
|
164
|
+
* @constructor
|
|
165
|
+
* @param {!Object<string,string>=} env The environment object to hold env
|
|
166
|
+
* variables.
|
|
167
|
+
*/
|
|
168
|
+
constructor(env = process.env) {
|
|
169
|
+
const authClient = new AuthClient(API_SCOPES, env);
|
|
97
170
|
this.auth = authClient.getDefaultAuth();
|
|
98
171
|
/** @const {!google.youtube} */
|
|
99
172
|
this.instance = google.youtube({
|
|
@@ -179,6 +252,96 @@ class YouTube {
|
|
|
179
252
|
throw new Error(errorMsg);
|
|
180
253
|
}
|
|
181
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Returns a collection of playlists that match the API request parameters.
|
|
258
|
+
* @see https://developers.google.com/youtube/v3/docs/playlists/list
|
|
259
|
+
* @param {!ListPlaylistConfig} config List playlist configuration.
|
|
260
|
+
* @param {number} resultLimit The limit of the number of results.
|
|
261
|
+
* @param {string} pageToken Token to identify a specific page in the result.
|
|
262
|
+
* @return {!Promise<Array<Schema$Playlist>>}
|
|
263
|
+
*/
|
|
264
|
+
async listPlaylists(config, resultLimit = 1000, pageToken = null) {
|
|
265
|
+
if (resultLimit <= 0) return [];
|
|
266
|
+
|
|
267
|
+
const playlistsRequest = Object.assign({
|
|
268
|
+
auth: this.auth,
|
|
269
|
+
}, config, {
|
|
270
|
+
pageToken
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (Array.isArray(playlistsRequest.part)) {
|
|
274
|
+
playlistsRequest.part = playlistsRequest.part.join(',');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const response = await this.instance.playlists.list(
|
|
279
|
+
playlistsRequest);
|
|
280
|
+
this.logger.debug('Response: ', response.data);
|
|
281
|
+
if (response.data.nextPageToken) {
|
|
282
|
+
this.logger.debug(
|
|
283
|
+
'Call youtube playlist:list API agian with Token: ',
|
|
284
|
+
response.data.nextPageToken);
|
|
285
|
+
const nextResponse = await this.listPlaylists(
|
|
286
|
+
config,
|
|
287
|
+
resultLimit - response.data.items.length,
|
|
288
|
+
response.data.nextPageToken);
|
|
289
|
+
return response.data.items.concat(nextResponse);
|
|
290
|
+
}
|
|
291
|
+
return response.data.items;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const errorMsg =
|
|
294
|
+
`Fail to list playlists: ${JSON.stringify(playlistsRequest)}`;
|
|
295
|
+
this.logger.error('YouTube list playlists failed.', error.message);
|
|
296
|
+
this.logger.debug('Errors in response:', error);
|
|
297
|
+
throw new Error(errorMsg);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Returns a collection of search results that match the query parameters
|
|
303
|
+
* specified in the API request.
|
|
304
|
+
* @see https://developers.google.com/youtube/v3/docs/search/list
|
|
305
|
+
* @param {!ListSearchConfig} config List search result configuration.
|
|
306
|
+
* @param {number} resultLimit The limit of the number of results.
|
|
307
|
+
* @param {string} pageToken Token to identify a specific page in the result.
|
|
308
|
+
* @return {!Promise<Array<Schema$Search>>}
|
|
309
|
+
*/
|
|
310
|
+
async listSearchResults(config, resultLimit = 1000, pageToken = null) {
|
|
311
|
+
if (resultLimit <= 0) return [];
|
|
312
|
+
|
|
313
|
+
const searchRequest = Object.assign({
|
|
314
|
+
auth: this.auth,
|
|
315
|
+
}, config, {
|
|
316
|
+
pageToken
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (Array.isArray(searchRequest.part)) {
|
|
320
|
+
searchRequest.part = searchRequest.part.join(',');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const response = await this.instance.search.list(searchRequest);
|
|
325
|
+
this.logger.debug('Response: ', response.data);
|
|
326
|
+
if (response.data.nextPageToken) {
|
|
327
|
+
this.logger.debug(
|
|
328
|
+
'Call youtube search:list API agian with Token: ',
|
|
329
|
+
response.data.nextPageToken);
|
|
330
|
+
const nextResponse = await this.listSearchResults(
|
|
331
|
+
config,
|
|
332
|
+
resultLimit - response.data.items.length,
|
|
333
|
+
response.data.nextPageToken);
|
|
334
|
+
return response.data.items.concat(nextResponse);
|
|
335
|
+
}
|
|
336
|
+
return response.data.items;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const errorMsg =
|
|
339
|
+
`Fail to list search results: ${JSON.stringify(searchRequest)}`;
|
|
340
|
+
this.logger.error('YouTube list search results failed.', error.message);
|
|
341
|
+
this.logger.debug('Errors in response:', error);
|
|
342
|
+
throw new Error(errorMsg);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
182
345
|
}
|
|
183
346
|
|
|
184
347
|
module.exports = {
|
|
@@ -186,6 +349,8 @@ module.exports = {
|
|
|
186
349
|
ListChannelsConfig,
|
|
187
350
|
ListVideosConfig,
|
|
188
351
|
ListCommentThreadsConfig,
|
|
352
|
+
ListPlaylistConfig,
|
|
353
|
+
ListSearchConfig,
|
|
189
354
|
API_VERSION,
|
|
190
355
|
API_SCOPES,
|
|
191
356
|
};
|
package/src/components/utils.js
CHANGED
|
@@ -26,8 +26,11 @@ const {CloudPlatformApis} = require('../apis/cloud_platform_apis.js');
|
|
|
26
26
|
/**
|
|
27
27
|
* The result of a batch of data sent to target API. The batch here means the
|
|
28
28
|
* data that will be sent out in one single request.
|
|
29
|
+
* Some APIs allows partial failure: it will take those correct data and
|
|
30
|
+
* response with reasons for those failed ones. 'groupedFailed' uses error
|
|
31
|
+
* message as the key, and tthe array of related failed lines(records) as value.
|
|
29
32
|
* Some APIs upload whole file. In this case, there will be not 'numberOfLines'
|
|
30
|
-
* or 'failedLines'.
|
|
33
|
+
* or 'failedLines', etc.
|
|
31
34
|
* @typedef {{
|
|
32
35
|
* result: boolean,
|
|
33
36
|
* numberOfLines: (number|undefined),
|
|
@@ -177,6 +180,27 @@ const splitArray = (records, splitSize) => {
|
|
|
177
180
|
return results;
|
|
178
181
|
};
|
|
179
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Merges an object of 'groupedFailed into the object 'mergedResult'
|
|
185
|
+
* @param {!BatchResult} mergedResult
|
|
186
|
+
* @param {!BatchResult} groupedFailed
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
const mergeGroupedFailed_ = (mergedResult, groupedFailed) => {
|
|
190
|
+
if (groupedFailed) {
|
|
191
|
+
const mergedKeys = Object.keys(mergedResult.groupedFailed);
|
|
192
|
+
mergedKeys.forEach((key) => {
|
|
193
|
+
mergedResult.groupedFailed[key] =
|
|
194
|
+
mergedResult.groupedFailed[key].concat(groupedFailed[key]);
|
|
195
|
+
});
|
|
196
|
+
Object.keys(groupedFailed)
|
|
197
|
+
.filter((key) => mergedKeys.indexOf(key) < 0)
|
|
198
|
+
.forEach((key) => {
|
|
199
|
+
mergedResult.groupedFailed[key] = groupedFailed[key];
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
180
204
|
/**
|
|
181
205
|
* Merges an array of API results (BatchResult) in to a single one.
|
|
182
206
|
*
|
|
@@ -198,8 +222,8 @@ const mergeBatchResults = (batchResults, batchPrefix) => {
|
|
|
198
222
|
const {
|
|
199
223
|
result,
|
|
200
224
|
numberOfLines,
|
|
201
|
-
failedLines,
|
|
202
|
-
errors,
|
|
225
|
+
failedLines = [],
|
|
226
|
+
errors = [],
|
|
203
227
|
groupedFailed,
|
|
204
228
|
} = batchResult;
|
|
205
229
|
if (logger.isDebugEnabled()) {
|
|
@@ -213,22 +237,13 @@ const mergeBatchResults = (batchResults, batchPrefix) => {
|
|
|
213
237
|
}
|
|
214
238
|
mergedResult.result = mergedResult.result && result;
|
|
215
239
|
mergedResult.numberOfLines += numberOfLines;
|
|
216
|
-
mergedResult.failedLines = mergedResult.failedLines.concat(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
mergedResult.groupedFailed[key].concat(groupedFailed[key]);
|
|
224
|
-
});
|
|
225
|
-
Object.keys(groupedFailed)
|
|
226
|
-
.filter((key) => mergedKeys.indexOf(key) < 0)
|
|
227
|
-
.forEach((key) => {
|
|
228
|
-
mergedResult.groupedFailed[key] = groupedFailed[key];
|
|
229
|
-
})
|
|
230
|
-
}
|
|
231
|
-
|
|
240
|
+
mergedResult.failedLines = mergedResult.failedLines.concat(failedLines);
|
|
241
|
+
errors.forEach((error) => {
|
|
242
|
+
if (mergedResult.errors.indexOf(error) === -1) {
|
|
243
|
+
mergedResult.errors.push(error);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
mergeGroupedFailed_(mergedResult, groupedFailed);
|
|
232
247
|
});
|
|
233
248
|
return mergedResult;
|
|
234
249
|
};
|