@google-cloud/nodejs-common 0.9.4-beta3 → 0.9.9-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,69 +1,78 @@
1
1
  # NodeJS Common Library
2
2
 
3
- <!--* freshness: { owner: 'lushu' reviewed: '2021-06-01' } *-->
4
-
5
- A NodeJs common library for other projects, e.g. [GMP and Google Ads
6
- Connector] and [Data Tasks Coordinator]. This library includes:
7
-
8
- 1. Authentication wrapper based on google auth library to support OAuth, JWT
9
- and ADC authentication;
10
-
11
- 1. Wrapper for some Google APIs for integration, mainly for [GMP and Google Ads
12
- 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 for [Data Tasks Coordinator]:
24
-
25
- * Google Ads reporting
26
- * Campaign Manager reporting
27
- * Search Ads 360 reporting
28
- * Display and Video 360 reporting
29
- * Ads Data Hub querying
30
-
31
- 1. Utilities wrapper class for Google Cloud Products:
32
-
33
- * **Firestore Access Object**: Firestore has two modes which are excluded
34
- to each other and can't be changed once selected in a Cloud
35
- Project[[2]]. This class, with its two successors offer a unified
36
- interface to operate data objects on either Firestore or Datastore.
37
-
38
- * **AutoMl Tables API**: Offers a unified entry to use this API based on
39
- Google Cloud client library combined with REST requests to service
40
- directly due to some functionalities missed in the client library.
41
-
42
- * **Pub/Sub Utilities**: Offers utilities functions to create topics and
43
- subscriptions for Pub/Sub, as well as the convenient way to publish a
44
- message.
45
-
46
- * **Storage Utilities**: Offers functions to manipulate the files on Cloud
47
- Storage. The main functions are:
48
-
49
- * Reading a given length (or slightly less) content without breaking a
50
- line;
51
- * Splitting a file into multiple files with the given length (or
52
- slightly less) without breaking a line;
53
- * Merging files into one file.
54
-
55
- * **Cloud Scheduler Adapter**: A wrapper to pause and resume Cloud
56
- Scheduler jobs.
57
-
58
- * **Cloud Functions Adapter**: Cloud Functions have different parameters
59
- in different environments, e.g. Node6 vs Node8[[1]]. This utility file
60
- offers an adapter to wrap a Node8 Cloud Functions into Node6 and Node8
61
- compatible functions.
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/
@@ -108,6 +108,7 @@ declare EXTERNAL_APIS=(
108
108
  "sheets.googleapis.com"
109
109
  "googleads.googleapis.com"
110
110
  "adsdatahub.googleapis.com"
111
+ "youtube.googleapis.com"
111
112
  )
112
113
 
113
114
  # Some APIs only support OAuth which needs an process to generate refresh token.
@@ -115,6 +116,7 @@ declare EXTERNAL_APIS=(
115
116
  declare EXTERNAL_APIS_OAUTH_ONLY=(
116
117
  "googleads.googleapis.com"
117
118
  "doubleclickbidmanager.googleapis.com"
119
+ "youtube.googleapis.com"
118
120
  )
119
121
 
120
122
  # API scopes for OAuth authentication.
@@ -133,6 +135,8 @@ EXTERNAL_API_SCOPES=(
133
135
  ["sheets.googleapis.com"]="https://www.googleapis.com/auth/spreadsheets"
134
136
  ["googleads.googleapis.com"]="https://www.googleapis.com/auth/adwords"
135
137
  ["adsdatahub.googleapis.com"]="https://www.googleapis.com/auth/adsdatahub"
138
+ ["youtube.googleapis.com"]=\
139
+ "https://www.googleapis.com/auth/youtube.force-ssl"
136
140
  )
137
141
 
138
142
  # Enabled APIs' OAuth scopes.
@@ -739,8 +743,8 @@ select_dataset_location() {
739
743
  "ASIA_PACIFIC"
740
744
  )
741
745
  local MULTI_REGIONAL=(
742
- "Data centers within member states of the European Union (EU)"
743
- "Data centers in the United States (US)"
746
+ "Data centers within member states of the European Union (eu)"
747
+ "Data centers in the United States (us)"
744
748
  )
745
749
  local AMERICAS=(
746
750
  "${NORTH_AMERICA[@]}"
@@ -1195,21 +1199,17 @@ create_or_update_sink() {
1195
1199
  local existingFilter
1196
1200
  existingFilter=$(gcloud logging sinks list --filter="name:${sinkName}" \
1197
1201
  --format="value(filter)")
1198
- if [[ "${existingFilter}" != "${logFilter}" ]]; then
1199
- local action
1200
- if [[ -z "${existingFilter}" ]]; then
1201
- action="create"
1202
- printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
1203
- else
1204
- action="update"
1205
- printf '%s\n' " Logging Export [${sinkName}] exists with a different \
1206
- filter. Updating..."
1207
- fi
1208
- gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
1209
- --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..."
1210
1206
  else
1211
- printf '%s\n' " Logging Export [${sinkName}] exists. Continue..."
1207
+ action="update"
1208
+ printf '%s\n' " Logging Export [${sinkName}] exists with a different \
1209
+ filter. Updating..."
1212
1210
  fi
1211
+ gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
1212
+ --log-filter="${logFilter}"
1213
1213
  if [[ $? -gt 0 ]];then
1214
1214
  printf '%s\n' "Failed to create or update Logs router sink."
1215
1215
  return 1
@@ -1829,9 +1829,8 @@ get_cloud_functions_service_account() {
1829
1829
  # None.
1830
1830
  #######################################
1831
1831
  check_firestore_existence() {
1832
- local firestore_status
1833
- firestore_status=$(gcloud firestore operations list 2>&1)
1834
- while [[ ${firestore_status} =~ .*NOT_FOUND.* ]]; do
1832
+ gcloud firestore indexes fields list >/dev/null 2>&1
1833
+ while [[ $? -gt 0 ]]; do
1835
1834
  cat <<EOF
1836
1835
  Cannot find Firestore or Datastore in current project. Please visit \
1837
1836
  https://console.cloud.google.com/firestore?project=${GCP_PROJECT} to create a \
@@ -1842,7 +1841,7 @@ EOF
1842
1841
  local any
1843
1842
  read -n1 -s any
1844
1843
  printf '\n'
1845
- firestore_status=$(gcloud firestore operations list 2>&1)
1844
+ gcloud firestore indexes fields list >/dev/null 2>&1
1846
1845
  done
1847
1846
  }
1848
1847
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "0.9.4-beta3",
3
+ "version": "0.9.9-alpha",
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.10.1",
20
- "@google-cloud/automl": "^2.4.2",
21
- "@google-cloud/bigquery": "^5.6.0",
22
- "@google-cloud/datastore": "^6.4.0",
23
- "@google-cloud/firestore": "^4.12.2",
24
- "@google-cloud/logging-winston": "^4.0.5",
25
- "@google-cloud/pubsub": "^2.12.0",
26
- "@google-cloud/storage": "^5.8.5",
27
- "gaxios": "^4.3.0",
28
- "google-ads-api": "^8.1.0",
29
- "google-ads-node": "^6.1.3",
30
- "google-auth-library": "^7.1.0",
31
- "googleapis": "^74.2.0",
32
- "soap": "^0.38.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"
@@ -110,6 +110,7 @@ class AuthClient {
110
110
  * @return {!OAuth2Client}
111
111
  */
112
112
  getOAuth2Client(keyFile = this.oauthTokenFile) {
113
+ this.ensureFileExisting_(keyFile, 'OAUTH token');
113
114
  const key = JSON.parse(fs.readFileSync(keyFile).toString());
114
115
  console.log(`Get OAuth token with client Id: ${key.client_id}`);
115
116
  const oAuth2Client = new OAuth2Client(key.client_id, key.client_secret);
@@ -123,6 +124,7 @@ class AuthClient {
123
124
  * @return {!JWT}
124
125
  */
125
126
  getServiceAccount(keyFile = this.serviceAccountKeyFile) {
127
+ this.ensureFileExisting_(keyFile, 'Service Account key');
126
128
  console.log(`Get Service Account's key file: ${keyFile}`);
127
129
  return new JWT({keyFile, scopes: this.scopes,});
128
130
  }
@@ -141,6 +143,7 @@ class AuthClient {
141
143
  * }}
142
144
  */
143
145
  getOAuth2Token(keyFile = this.oauthTokenFile) {
146
+ this.ensureFileExisting_(keyFile, 'OAuth token');
144
147
  const key = JSON.parse(fs.readFileSync(keyFile).toString());
145
148
  return {
146
149
  clientId: key.client_id,
@@ -148,6 +151,18 @@ class AuthClient {
148
151
  refreshToken: key.token.refresh_token,
149
152
  };
150
153
  }
154
+
155
+ /**
156
+ * Some APIs only support one authorization type, so we have to designate a
157
+ * specific auth method. If the related key/token file is not available, the
158
+ * auth will fail. The function throws errors with more meaningful message.
159
+ * @param {string} file Target file path
160
+ * @param {string} type Key type name, 'OAuth token' or 'Service Account key'
161
+ * @private
162
+ */
163
+ ensureFileExisting_(file, type) {
164
+ if (!file) throw new Error(`Required ${type} file doesn't exist.`);
165
+ }
151
166
  }
152
167
 
153
168
  /**
@@ -26,6 +26,7 @@ const {
26
26
  getLogger,
27
27
  getFilterFunction,
28
28
  SendSingleBatch,
29
+ BatchResult,
29
30
  } = require('../components/utils.js');
30
31
 
31
32
  const API_SCOPES = Object.freeze([
@@ -64,7 +65,7 @@ let InsertConversionsConfig;
64
65
  /**
65
66
  * List of properties that will be take from the data file as elements of a
66
67
  * conversion.
67
- * See https://developers.google.com/doubleclick-advertisers/v3.3/conversions
68
+ * See https://developers.google.com/doubleclick-advertisers/rest/v3.5/Conversion
68
69
  * @type {Array<string>}
69
70
  */
70
71
  const PICKED_PROPERTIES = [
@@ -79,8 +80,14 @@ const PICKED_PROPERTIES = [
79
80
  * see https://developers.google.com/doubleclick-advertisers/service_accounts
80
81
  */
81
82
  class DfaReporting {
82
- constructor() {
83
- const authClient = new AuthClient(API_SCOPES);
83
+
84
+ /**
85
+ * @constructor
86
+ * @param {!Object<string,string>=} env The environment object to hold env
87
+ * variables.
88
+ */
89
+ constructor(env = process.env) {
90
+ const authClient = new AuthClient(API_SCOPES, env);
84
91
  this.auth = authClient.getDefaultAuth();
85
92
  /** @const {!google.dfareporting} */
86
93
  this.instance = google.dfareporting({
@@ -126,7 +133,7 @@ class DfaReporting {
126
133
  * @param {!Array<string>} lines Data for single request. It should be
127
134
  * guaranteed that it doesn't exceed quota limitation.
128
135
  * @param {string} batchId The tag for log.
129
- * @return {!Promise<boolean>}
136
+ * @return {!Promise<BatchResult>}
130
137
  */
131
138
  return async (lines, batchId) => {
132
139
  /** @type {function} Gets the conversion elements from the data object. */
@@ -153,6 +160,11 @@ class DfaReporting {
153
160
  if (config.idType === 'encryptedUserId') {
154
161
  requestBody.encryptionInfo = config.encryptionInfo;
155
162
  }
163
+ /** @const {BatchResult} */
164
+ const batchResult = {
165
+ result: true,
166
+ numberOfLines: lines.length,
167
+ };
156
168
  try {
157
169
  const response = await this.instance.conversions.batchinsert({
158
170
  profileId: config.profileId,
@@ -160,25 +172,61 @@ class DfaReporting {
160
172
  });
161
173
  const failed = response.data.hasFailures;
162
174
  if (failed) {
163
- console.error(`Dfareporting[${batchId}] has failures.`);
164
- const errorMessages = response.data.status
165
- .filter((record) => typeof record.errors !== 'undefined')
166
- .map((record) => {
167
- this.logger.debug(record);
168
- return record.errors.map((error) => error.message).join(',');
169
- });
170
- console.error(errorMessages.join('\n'));
175
+ this.logger.warn(`CM [${batchId}] has failures.`);
176
+ this.extraFailedLines_(batchResult, response.data.status, lines);
171
177
  }
172
178
  this.logger.debug('Configuration: ', config);
173
179
  this.logger.debug('Response: ', response);
174
- return !failed;
180
+ return batchResult;
175
181
  } catch (error) {
176
- console.error(`Dfareporting[${batchId}] failed.`, error);
177
- return false;
182
+ this.logger.error(`CM[${batchId}] failed.`, error);
183
+ batchResult.result = false;
184
+ batchResult.errors = [error.message || error.toString()];
185
+ return batchResult;
178
186
  }
179
187
  };
180
188
  };
181
189
 
190
+ /**
191
+ * Campaign Manager API returns an array of ConversionStatus for the status of
192
+ * uploaded conversions. If there are errors related to the conversion, then
193
+ * an array of 'ConversionError' named 'errors' will be available in the
194
+ * ConversionStatus object. This function extras failed lines and error
195
+ * messages based on the 'errors'.
196
+ * For 'ConversionStatus', see:
197
+ * https://developers.google.com/doubleclick-advertisers/rest/v3.5/ConversionStatus
198
+ * For 'ConversionError', see:
199
+ * https://developers.google.com/doubleclick-advertisers/rest/v3.5/ConversionStatus#ConversionError
200
+ * @param {!BatchResult} batchResult
201
+ * @param {!Array<!Schema$ConversionStatus>} statuses
202
+ * @param {!Array<string>} lines The original input data.
203
+ * @private
204
+ */
205
+ extraFailedLines_(batchResult, statuses, lines) {
206
+ batchResult.result = false;
207
+ batchResult.failedLines = [];
208
+ batchResult.groupedFailed = {};
209
+ const errors = new Set();
210
+ statuses.forEach((conversionStatus, index) => {
211
+ if (conversionStatus.errors) {
212
+ const failedLine = lines[index];
213
+ batchResult.failedLines.push(failedLine);
214
+ conversionStatus.errors.forEach(({message}) => {
215
+ // error messages have detailed IDs. Need to generalize them.
216
+ const generalMessage = message.replace(/.*error: /, '');
217
+ errors.add(generalMessage);
218
+ const groupedFailed = batchResult.groupedFailed[generalMessage]
219
+ || [];
220
+ groupedFailed.push(failedLine);
221
+ if (groupedFailed.length === 1) {
222
+ batchResult.groupedFailed[generalMessage] = groupedFailed;
223
+ }
224
+ });
225
+ }
226
+ batchResult.errors = Array.from(errors);
227
+ });
228
+ }
229
+
182
230
  /**
183
231
  * Lists all UserProfiles.
184
232
  * @return {!Promise<!Array<string>>}
@@ -203,8 +251,8 @@ class DfaReporting {
203
251
  * @return {!Promise<string>} Profile Id.
204
252
  * @private
205
253
  */
206
- getProfileForOperation_(config) {
207
- if (config.profileId) return Promise.resolve(config.profileId);
254
+ async getProfileForOperation_(config) {
255
+ if (config.profileId) return config.profileId;
208
256
  if (config.accountId) return this.getProfileId(config.accountId);
209
257
  throw new Error('There is no profileId or accountId in the configuration.');
210
258
  }
@@ -213,7 +261,7 @@ class DfaReporting {
213
261
  * Runs a report and return the file Id. As an asynchronized process, the
214
262
  * returned file Id will be a placeholder until the status changes to
215
263
  * 'REPORT_AVAILABLE' in the response of `getFile`.
216
- * @see https://developers.google.com/doubleclick-advertisers/v3.3/reports/run
264
+ * @see https://developers.google.com/doubleclick-advertisers/rest/v3.5/reports/run
217
265
  *
218
266
  * @param {{
219
267
  * accountId:(string|undefined),
@@ -236,7 +284,7 @@ class DfaReporting {
236
284
  * Returns file url from a report. If the report status is 'REPORT_AVAILABLE',
237
285
  * then return the apiUrl from the response; if the status is 'PROCESSING',
238
286
  * returns undefined; otherwise throws an error.
239
- * @see https://developers.google.com/doubleclick-advertisers/v3.3/reports/files/get
287
+ * @see https://developers.google.com/doubleclick-advertisers/rest/v3.5/reports/get
240
288
  *
241
289
  * @param {{
242
290
  * accountId:(string|undefined),