@google-cloud/nodejs-common 1.1.1-beta → 1.3.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.
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Copyright 2022 Google Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # Const the folder name of Apps Script.
18
+ DEFAULT_APPS_SCRIPT_FOLDER="apps_script"
19
+
20
+ #######################################
21
+ # Clasp login.
22
+ # Globals:
23
+ # None
24
+ # Arguments:
25
+ # None
26
+ #######################################
27
+ clasp_login() {
28
+ while :; do
29
+ local claspLogin=$(clasp login --status)
30
+ if [[ "${claspLogin}" != "You are not logged in." ]]; then
31
+ printf '%s' "${claspLogin} Would you like to continue with it? [Y/n]"
32
+ local logout
33
+ read -r logout
34
+ logout=${logout:-"Y"}
35
+ if [[ ${logout} == "Y" || ${logout} == "y" ]]; then
36
+ break
37
+ else
38
+ clasp logout
39
+ fi
40
+ fi
41
+ clasp login --no-localhost
42
+ done
43
+ }
44
+
45
+ #######################################
46
+ # Initialize a AppsScript project. Usually it involves following steps:
47
+ # 1. Create a AppsScript project within a new Google Sheet.
48
+ # 2. Prompt to update the Google Cloud Project number of the AppsScript project
49
+ # to enable external APIs for this AppsScript project.
50
+ # 3. Prompt to grant the access of Cloud Functions' default service account to
51
+ # this Google Sheet, so the Cloud Functions can query this Sheet later.
52
+ # 4. Initialize the Sheet based on requests.
53
+ # Globals:
54
+ # None
55
+ # Arguments:
56
+ # The Google Sheet name.
57
+ # The folder for Apps Script code, default value ${DEFAULT_APPS_SCRIPT_FOLDER}
58
+ #######################################
59
+ clasp_initialize() {
60
+ ((STEP += 1))
61
+ printf '%s\n' "Step ${STEP}: Starting to create Google Sheets..."
62
+ local sheetName="${1}"
63
+ local apps_script_src="${2-"${DEFAULT_APPS_SCRIPT_FOLDER}"}"
64
+ clasp_login
65
+ while :; do
66
+ local claspStatus=$(
67
+ clasp status -P "${apps_script_src}" >/dev/null 2>&1
68
+ echo $?
69
+ )
70
+ if [[ $claspStatus -gt 0 ]]; then
71
+ clasp create --type sheets --title "${sheetName}" --rootDir "${apps_script_src}"
72
+ local createResult=$?
73
+ if [[ $createResult -gt 0 ]]; then
74
+ printf '%s' "Press any key to continue after you enable the Google \
75
+ Apps Script API: https://script.google.com/home/usersettings..."
76
+ local any
77
+ read -n1 -s any
78
+ printf '\n\n'
79
+ continue
80
+ fi
81
+ break
82
+ else
83
+ printf '%s' "AppsScript project exists. Would you like to continue with \
84
+ it? [Y/n]"
85
+ local useCurrent
86
+ read -r useCurrent
87
+ useCurrent=${useCurrent:-"Y"}
88
+ if [[ ${useCurrent} = "Y" || ${useCurrent} = "y" ]]; then
89
+ break
90
+ else
91
+ printf '%s' "Would you like to delete current AppsScript and create a \
92
+ new one? [N/y]"
93
+ local deleteCurrent
94
+ read -r deleteCurrent
95
+ deleteCurrent=${deleteCurrent:-"N"}
96
+ if [[ ${deleteCurrent} = "Y" || ${deleteCurrent} = "y" ]]; then
97
+ rm "${apps_script_src}/.clasp.json"
98
+ continue
99
+ fi
100
+ fi
101
+ fi
102
+ done
103
+ }
104
+
105
+ #######################################
106
+ # Copy GCP project configuration file to AppsScript codes as a constant named
107
+ # `GCP_CONFIG`.
108
+ # Globals:
109
+ # None
110
+ # Arguments:
111
+ # The folder for Apps Script code, default value ${DEFAULT_APPS_SCRIPT_FOLDER}
112
+ #######################################
113
+ generate_config_js_for_apps_script() {
114
+ local apps_script_src="${1-"${DEFAULT_APPS_SCRIPT_FOLDER}"}"
115
+ local generated_file="${apps_script_src}/.generated_config.js"
116
+ if [[ -f "${CONFIG_FILE}" ]]; then
117
+ echo '// Copyright 2022 Google Inc.
118
+ //
119
+ // Licensed under the Apache License, Version 2.0 (the "License");
120
+ // you may not use this file except in compliance with the License.
121
+ // You may obtain a copy of the License at
122
+ //
123
+ // http://www.apache.org/licenses/LICENSE-2.0
124
+ //
125
+ // Unless required by applicable law or agreed to in writing, software
126
+ // distributed under the License is distributed on an "AS IS" BASIS,
127
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
128
+ // See the License for the specific language governing permissions and
129
+ // limitations under the License.
130
+
131
+ /** @fileoverview Auto-generated configuration file for Apps Script. */
132
+ '>"${generated_file}"
133
+ echo -n "const GCP_CONFIG = " >>"${generated_file}"
134
+ cat "${CONFIG_FILE}" >>"${generated_file}"
135
+ else
136
+ printf '%s\n' "Couldn't find ${CONFIG_FILE}."
137
+ fi
138
+ }
139
+
140
+ #######################################
141
+ # Clasp pushes AppsScript codes.
142
+ # Globals:
143
+ # None
144
+ # Arguments:
145
+ # The folder for Apps Script code, default value ${DEFAULT_APPS_SCRIPT_FOLDER}
146
+ #######################################
147
+ clasp_push_codes() {
148
+ ((STEP += 1))
149
+ printf '%s\n' "Step ${STEP}: Starting to push codes to the Google Sheets..."
150
+ local apps_script_src="${1-"${DEFAULT_APPS_SCRIPT_FOLDER}"}"
151
+ clasp status -P "${apps_script_src}" >>/dev/null
152
+ local project_status=$?
153
+ if [[ ${project_status} -gt 0 ]]; then
154
+ return ${project_status}
155
+ else
156
+ generate_config_js_for_apps_script "${apps_script_src}"
157
+ clasp push --force -P "${apps_script_src}"
158
+ fi
159
+ }
160
+
161
+ #######################################
162
+ # Ask user to update the GCP number of this AppsScript.
163
+ # Globals:
164
+ # GCP_PROJECT
165
+ # Arguments:
166
+ # The folder for Apps Script code, default value ${DEFAULT_APPS_SCRIPT_FOLDER}
167
+ #######################################
168
+ clasp_update_project_number() {
169
+ ((STEP += 1))
170
+ local projectNumber=$(get_project_number)
171
+ local apps_script_src="${1-"${DEFAULT_APPS_SCRIPT_FOLDER}"}"
172
+ printf '%s\n' "Step ${STEP}: Update Google Cloud Platform (GCP) Project for \
173
+ Apps Script."
174
+ printf '%s' " "
175
+ clasp open -P "${apps_script_src}"
176
+ printf '%s\n' " On the open tab of Apps Script, use 'Project \
177
+ Settings' to set the Google Cloud Platform (GCP) Project as: ${projectNumber}"
178
+ printf '%s' "Press any key to continue after you update the GCP number..."
179
+ local any
180
+ read -n1 -s any
181
+ printf '\n'
182
+ }
183
+
184
+ #######################################
185
+ # Ask user to grant the access to CF's default service account.
186
+ # Note: the target GCP needs to have OAuth consent screen.
187
+ # Globals:
188
+ # SHEET_URL
189
+ # Arguments:
190
+ # The folder for Apps Script code, default value ${DEFAULT_APPS_SCRIPT_FOLDER}
191
+ #######################################
192
+ grant_access_to_service_account() {
193
+ ((STEP += 1))
194
+ local apps_script_src="${1-"${DEFAULT_APPS_SCRIPT_FOLDER}"}"
195
+ local defaultServiceAccount=$(get_cloud_functions_service_account \
196
+ "${PROJECT_NAMESPACE}_main")
197
+ local parentId=$(get_value_from_json_file "${apps_script_src}"/.clasp.json \
198
+ parentId|cut -d\" -f2)
199
+ printf '%s\n' "Step ${STEP}: Share the Google Sheet with ${SOLUTION_NAME}."
200
+
201
+ printf '%s\n' " Open Google Sheet: \
202
+ https://drive.google.com/open?id=${parentId}"
203
+ printf '%s\n' " Click 'Share' and grant the Viewer access to: \
204
+ ${defaultServiceAccount}"
205
+ printf '%s' "Press any key to continue after you grant the access..."
206
+ local any
207
+ read -n1 -s any
208
+ printf '\n'
209
+ }
@@ -384,6 +384,15 @@ solution installed [${GCP_PROJECT}]: "
384
384
  local input result
385
385
  read -r input
386
386
  input=${input:-"${GCP_PROJECT}"}
387
+ printf '%s' "Checking billing status for [${input}]..."
388
+ result=$(gcloud beta billing projects describe "${input}" \
389
+ --format="csv[no-heading](billingEnabled)")
390
+ if [[ "${result}" != "True" && "${result}" != "true" ]]; then
391
+ printf '%s\n' " there is no billing account."
392
+ return 1
393
+ else
394
+ printf '%s\n' "succeeded."
395
+ fi
387
396
  result=$(gcloud config set project "${input}" --user-output-enabled=false \
388
397
  2>&1)
389
398
  if [[ -z ${result} ]]; then
@@ -1043,13 +1052,24 @@ refresh the list:"
1043
1052
  # Globals:
1044
1053
  # GCP_PROJECT
1045
1054
  # Arguments:
1046
- # Bucket name var, default value 'GCS_BUCKET'
1055
+ # Bucket name var with optional usage, e.g. 'GCS_BUCKET_REPORT:reports'.
1056
+ # The default value is 'GCS_BUCKET'.
1047
1057
  # Location var name, default value 'REGION'
1048
1058
  # If the second location var is unset, use this var as default value
1049
1059
  #######################################
1050
1060
  confirm_located_bucket() {
1051
- local gcsName defaultValue defaultBucketName locationName location
1052
- gcsName="${1:-"GCS_BUCKET"}"
1061
+ local gcsName usage defaultValue defaultBucketName locationName location
1062
+ if [[ -z "${1}" ]]; then
1063
+ gcsName="GCS_BUCKET"
1064
+ elif [[ "${1}" == *":"* ]]; then
1065
+ gcsName=$(echo "${1}" | cut -d\: -f1)
1066
+ usage=$(echo "${1}" | cut -d\: -f2)
1067
+ if [[ -n "${usage}" ]]; then
1068
+ usage=" for ${usage}"
1069
+ fi
1070
+ else
1071
+ gcsName="${1}"
1072
+ fi
1053
1073
  locationName="${2:-"REGION"}"
1054
1074
  defaultValue="${!gcsName}"
1055
1075
  defaultBucketName=$(get_default_bucket_name "${GCP_PROJECT}")
@@ -1064,7 +1084,8 @@ confirm_located_bucket() {
1064
1084
 
1065
1085
  (( STEP += 1 ))
1066
1086
  if [[ -z "${location}" ]]; then
1067
- printf '%s\n' "Step ${STEP}: Checking or creating a Cloud Storage Bucket..."
1087
+ printf '%s\n' "Step ${STEP}: Checking or creating a Cloud Storage \
1088
+ Bucket${usage}..."
1068
1089
  if [[ "${locationName}" == "REGION" ]]; then
1069
1090
  select_functions_location ${locationName}
1070
1091
  else
@@ -1072,8 +1093,8 @@ confirm_located_bucket() {
1072
1093
  fi
1073
1094
  location="${!locationName}"
1074
1095
  else
1075
- printf '%s\n' "Step ${STEP}: Checking or creating a Cloud Storage Bucket in \
1076
- location [${location}] ..."
1096
+ printf '%s\n' "Step ${STEP}: Checking or creating a Cloud Storage \
1097
+ Bucket${usage} in location [${location}] ..."
1077
1098
  fi
1078
1099
  declare -g "${locationName}=${location}"
1079
1100
  while :; do
@@ -1474,20 +1495,18 @@ EOF
1474
1495
  "client_id=${client_id}" "scope=${scope}")
1475
1496
  local auth_url
1476
1497
  auth_url="${OAUTH_BASE_URL}auth?${parameters}"
1477
- printf '%s\n' "3. Open the link in browser and finish authentication: \
1478
- ${auth_url}"
1498
+ printf '%s\n' "3. Open the link in your browser and finish authentication. \
1499
+ Do not close the redirected page: ${auth_url}"
1479
1500
  cat <<EOF
1480
1501
  Note:
1502
+ The succeeded OAuth flow will land the browser on an error page - \
1503
+ "This site can't be reached". This is expected behavior. Copy the whole URL and continue.
1481
1504
  If the OAuth client is not for a native application, there will be an \
1482
1505
  "Error 400: redirect_uri_mismatch" shown up on the page. In this case, press \
1483
1506
  "Enter" to start again with a native application OAuth client ID.
1484
- If there is no local web server serving at ${REDIRECT_URI}, the \
1485
- succeeded OAuth flow will land the browser on an error page ("This site can't \
1486
- be reached"). This is an expected behavior. Copy the whole URL and continue.
1487
1507
 
1488
1508
  EOF
1489
- printf '%s' "4. Copy the authorization code or complete url from browser \
1490
- and paste here: "
1509
+ printf '%s' "4. Copy the complete URL from your browser and paste here: "
1491
1510
  read -r auth_code
1492
1511
  if [[ -z ${auth_code} ]]; then
1493
1512
  printf '%s\n\n' "No authorization code. Starting from beginning again..."
@@ -1679,7 +1698,9 @@ customized_install() {
1679
1698
  local tasks=("$@")
1680
1699
  local task
1681
1700
  for task in "${tasks[@]}"; do
1682
- ${task}
1701
+ local cmd
1702
+ eval "cmd=(${task})"
1703
+ "${cmd[@]}"
1683
1704
  quit_if_failed $?
1684
1705
  done
1685
1706
  }
@@ -1992,5 +2013,6 @@ join_string_array() {
1992
2013
  _SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1993
2014
  source "${_SELF}/google_ads.sh"
1994
2015
  source "${_SELF}/bigquery.sh"
2016
+ source "${_SELF}/apps_scripts.sh"
1995
2017
 
1996
2018
  printf '%s\n' "Common Bash Library is loaded."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "1.1.1-beta",
3
+ "version": "1.3.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",
@@ -18,24 +18,23 @@
18
18
  "dependencies": {
19
19
  "@google-cloud/aiplatform": "^1.19.0",
20
20
  "@google-cloud/automl": "^2.5.2",
21
- "@google-cloud/bigquery": "^5.12.0",
21
+ "@google-cloud/bigquery": "^6.0.0",
22
22
  "@google-cloud/datastore": "^6.6.2",
23
23
  "@google-cloud/firestore": "^5.0.2",
24
- "@google-cloud/logging-winston": "^4.2.2",
25
- "@google-cloud/pubsub": "^2.19.0",
26
- "@google-cloud/storage": "^5.18.3",
27
- "@google-cloud/scheduler": "^2.3.0",
28
- "gaxios": "^4.3.2",
29
- "google-ads-api": "^10.0.1",
30
- "google-ads-node":"^8.0.1",
31
- "google-auth-library": "^7.14.1",
24
+ "@google-cloud/logging-winston": "^5.1.0",
25
+ "@google-cloud/pubsub": "^3.0.1",
26
+ "@google-cloud/storage": "^6.0.1",
27
+ "@google-cloud/scheduler": "^3.0.0",
28
+ "gaxios": "^5.0.0",
29
+ "google-ads-api": "^11.0.0",
30
+ "google-ads-node": "^9.0.0",
31
+ "google-auth-library": "^8.0.2",
32
32
  "googleapis": "^100.0.0",
33
- "soap": "^0.43.0",
34
33
  "winston": "^3.7.2",
35
34
  "lodash": "^4.17.21"
36
35
  },
37
36
  "devDependencies": {
38
- "jasmine": "^4.0.2"
37
+ "jasmine": "^4.1.0"
39
38
  },
40
39
  "scripts": {
41
40
  "test": "node node_modules/jasmine/bin/jasmine"
@@ -27,9 +27,15 @@ const {
27
27
  },
28
28
  resources: {
29
29
  GoogleAdsField,
30
+ OfflineUserDataJob,
30
31
  },
31
32
  services: {
33
+ CreateOfflineUserDataJobRequest,
34
+ AddOfflineUserDataJobOperationsRequest,
35
+ RunOfflineUserDataJobRequest,
36
+ UploadCallConversionsRequest,
32
37
  UploadClickConversionsRequest,
38
+ UploadCallConversionsResponse,
33
39
  UploadClickConversionsResponse,
34
40
  UploadConversionAdjustmentsRequest,
35
41
  UploadConversionAdjustmentsResponse,
@@ -41,6 +47,10 @@ const {
41
47
  errors: {
42
48
  GoogleAdsFailure,
43
49
  },
50
+ enums: {
51
+ OfflineUserDataJobTypeEnum: { OfflineUserDataJobType },
52
+ OfflineUserDataJobStatusEnum: { OfflineUserDataJobStatus },
53
+ },
44
54
  } = googleAdsLib;
45
55
  const {GoogleAdsApi} = require('google-ads-api');
46
56
  const lodash = require('lodash');
@@ -55,6 +65,7 @@ const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
55
65
  * List of properties that will be taken from the data file as elements of a
56
66
  * conversion or a conversion adjustment.
57
67
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
68
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/CallConversion
58
69
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/ConversionAdjustment
59
70
  * @type {Array<string>}
60
71
  */
@@ -63,6 +74,8 @@ const PICKED_PROPERTIES = [
63
74
  'cart_data',
64
75
  'user_identifiers',
65
76
  'gclid',
77
+ 'caller_id',
78
+ 'call_start_date_time',
66
79
  'conversion_action',
67
80
  'conversion_date_time',
68
81
  'conversion_value',
@@ -95,10 +108,11 @@ const IDENTIFIERS = [
95
108
  const MAX_IDENTIFIERS_PER_USER = 20;
96
109
 
97
110
  /**
98
- * Configuration for uploading click conversions or conversion adjustments for
99
- * Google Ads, includes:
111
+ * Configuration for uploading click conversions, call converions or conversion
112
+ * adjustments for Google Ads, includes:
100
113
  * gclid, conversion_action, conversion_date_time, conversion_value,
101
114
  * currency_code, order_id, external_attribution_data,
115
+ * caller_id, call_start_date_time,
102
116
  * adjustment_type, adjustment_date_time, user_agent, gclid_date_time_pair, etc.
103
117
  * @see PICKED_PROPERTIES
104
118
  *
@@ -119,7 +133,9 @@ const MAX_IDENTIFIERS_PER_USER = 20;
119
133
  * @typedef {{
120
134
  * external_attribution_data: (GoogleAdsApi.ExternalAttributionData|undefined),
121
135
  * cart_data: (object|undefined),
122
- * gclid: string,
136
+ * gclid: (string|undefined),
137
+ * caller_id: (string|undefined),
138
+ * call_start_date_time: (string|undefined),
123
139
  * conversion_action: string,
124
140
  * conversion_date_time: string,
125
141
  * conversion_value: number,
@@ -138,7 +154,7 @@ let ConversionConfig;
138
154
  /**
139
155
  * Configuration for uploading customer match to Google Ads, includes:
140
156
  * customer_id, login_customer_id, list_id and operation.
141
- * operation must be one of the two: 'create' or 'remove';
157
+ * operation must be one of the two: 'create' or 'remove'.
142
158
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
143
159
  * @typedef {{
144
160
  * customer_id: string,
@@ -149,6 +165,24 @@ let ConversionConfig;
149
165
  */
150
166
  let CustomerMatchConfig;
151
167
 
168
+ /**
169
+ * Configuration for offline user data job, includes:
170
+ * customer_id, login_customer_id, list_id, operation and type.
171
+ * 'operation' should be one of the two: 'create' or 'remove',
172
+ * 'type' is OfflineUserDataJobType, it can be 'CUSTOMER_MATCH_USER_LIST' or
173
+ * 'STORE_SALES_UPLOAD_FIRST_PARTY'.
174
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/OfflineUserDataJob
175
+ * @typedef {{
176
+ * customer_id: string,
177
+ * login_customer_id: string,
178
+ * list_id: (undefined|string),
179
+ * operation: 'create'|'remove',
180
+ * type: !OfflineUserDataJobType,
181
+ * storeSalesMetadata: (undefined|object),
182
+ * }}
183
+ */
184
+ let OfflineUserDataJobConfig;
185
+
152
186
  /**
153
187
  * Configuration for uploading customer match data for Google Ads.
154
188
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
@@ -215,6 +249,19 @@ class GoogleAds {
215
249
  this.logger.debug(`Init ${this.constructor.name} with Debug Mode.`);
216
250
  }
217
251
 
252
+ /**
253
+ * Gets a report synchronously from a given Customer account.
254
+ * The enum fields are present as index number.
255
+ * @param {string} customerId
256
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
257
+ * @param {!ReportQueryConfig} reportQueryConfig
258
+ * @return {!ReadableStream}
259
+ */
260
+ async getReport(customerId, loginCustomerId, reportQueryConfig) {
261
+ const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
262
+ return customer.report(reportQueryConfig);
263
+ }
264
+
218
265
  /**
219
266
  * Gets report as generator of a given Customer account.
220
267
  * @param {string} customerId
@@ -261,6 +308,19 @@ class GoogleAds {
261
308
  .searchGoogleAdsFields(request);
262
309
  return results;
263
310
  }
311
+ /**
312
+ * Returns the function to send out a request to Google Ads API with a batch
313
+ * of call conversions.
314
+ * @param {string} customerId
315
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
316
+ * @param {!ConversionConfig} adsConfig Default call conversion params
317
+ * @return {!SendSingleBatch} Function which can send a batch of hits to
318
+ * Google Ads API.
319
+ */
320
+ getUploadCallConversionFn(customerId, loginCustomerId, adsConfig) {
321
+ return this.getUploadConversionFnBase_(customerId, loginCustomerId,
322
+ adsConfig, 'uploadCallConversions', 'caller_id');
323
+ }
264
324
 
265
325
  /**
266
326
  * Returns the function to send out a request to Google Ads API with a batch
@@ -271,45 +331,9 @@ class GoogleAds {
271
331
  * @return {!SendSingleBatch} Function which can send a batch of hits to
272
332
  * Google Ads API.
273
333
  */
274
- getUploadConversionFn(customerId, loginCustomerId, adsConfig) {
275
- /**
276
- * Sends a batch of hits to Google Ads API.
277
- * @param {!Array<string>} lines Data for single request. It should be
278
- * guaranteed that it doesn't exceed quota limitation.
279
- * @param {string} batchId The tag for log.
280
- * @return {!BatchResult}
281
- */
282
- return async (lines, batchId) => {
283
- /** @type {!Array<ConversionConfig>} */
284
- const conversions = lines.map(
285
- (line) => buildClickConversionFromLine(line, adsConfig, customerId));
286
- /** @const {BatchResult} */
287
- const batchResult = {
288
- result: true,
289
- numberOfLines: lines.length,
290
- };
291
- try {
292
- const response = await this.uploadClickConversions(conversions,
293
- customerId, loginCustomerId);
294
- const {results, partial_failure_error: failed} = response;
295
- if (this.logger.isDebugEnabled()) {
296
- const gclids = results.map((conversion) => conversion.gclid);
297
- this.logger.debug('Uploaded gclids:', gclids);
298
- }
299
- if (failed) {
300
- this.logger.info('partial_failure_error:', failed.message);
301
- const failures = failed.details.map(
302
- ({value}) => GoogleAdsFailure.decode(value));
303
- this.extraFailedLines_(batchResult, failures, lines, 0);
304
- }
305
- return batchResult;
306
- } catch (error) {
307
- this.logger.error(
308
- `Error in upload conversions batch: ${batchId}`, error);
309
- this.updateBatchResultWithError_(batchResult, error, lines, 0);
310
- return batchResult;
311
- }
312
- }
334
+ getUploadClickConversionFn(customerId, loginCustomerId, adsConfig) {
335
+ return this.getUploadConversionFnBase_(customerId, loginCustomerId,
336
+ adsConfig, 'uploadClickConversions', 'gclid');
313
337
  }
314
338
 
315
339
  /**
@@ -323,6 +347,26 @@ class GoogleAds {
323
347
  * Google Ads API.
324
348
  */
325
349
  getUploadConversionAdjustmentFn(customerId, loginCustomerId, adsConfig) {
350
+ return this.getUploadConversionFnBase_(customerId, loginCustomerId,
351
+ adsConfig, 'uploadConversionAdjustments', 'order_id');
352
+ }
353
+
354
+ /**
355
+ * Returns the function to send call conversions, click conversions or
356
+ * conversion adjustment (enhanced conversions).
357
+ * @param {string} customerId
358
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
359
+ * @param {!ConversionConfig} adsConfig Default click conversion params
360
+ * @param {string} functionName The name of sending converions function, could
361
+ * be `uploadClickConversions`, `uploadCallConversions` or
362
+ * `uploadConversionAdjustments`.
363
+ * @param {string} propertyForDebug The name of property for debug info.
364
+ * @return {!SendSingleBatch} Function which can send a batch of hits to
365
+ * Google Ads API.
366
+ * @private
367
+ */
368
+ getUploadConversionFnBase_(customerId, loginCustomerId, adsConfig,
369
+ functionName, propertyForDebug) {
326
370
  /**
327
371
  * Sends a batch of hits to Google Ads API.
328
372
  * @param {!Array<string>} lines Data for single request. It should be
@@ -333,30 +377,30 @@ class GoogleAds {
333
377
  return async (lines, batchId) => {
334
378
  /** @type {!Array<ConversionConfig>} */
335
379
  const conversions = lines.map(
336
- (line) => buildClickConversionFromLine(line, adsConfig, customerId));
380
+ (line) => buildClickConversionFromLine(line, adsConfig, customerId));
337
381
  /** @const {BatchResult} */
338
382
  const batchResult = {
339
383
  result: true,
340
384
  numberOfLines: lines.length,
341
385
  };
342
386
  try {
343
- const response = await this.uploadConversionAdjustments(conversions,
344
- customerId, loginCustomerId);
345
- const {results, partial_failure_error: failed} = response;
387
+ const response = await this[functionName](conversions, customerId,
388
+ loginCustomerId);
389
+ const { results, partial_failure_error: failed } = response;
346
390
  if (this.logger.isDebugEnabled()) {
347
- const orderId = results.map((conversion) => conversion.order_id);
348
- this.logger.debug('Uploaded order_id:', orderId);
391
+ const id = results.map((conversion) => conversion[propertyForDebug]);
392
+ this.logger.debug(`Uploaded ${propertyForDebug}:`, id);
349
393
  }
350
394
  if (failed) {
351
395
  this.logger.info('partial_failure_error:', failed.message);
352
396
  const failures = failed.details.map(
353
- ({value}) => GoogleAdsFailure.decode(value));
397
+ ({ value }) => GoogleAdsFailure.decode(value));
354
398
  this.extraFailedLines_(batchResult, failures, lines, 0);
355
399
  }
356
400
  return batchResult;
357
401
  } catch (error) {
358
402
  this.logger.error(
359
- `Error in upload conversion adjustments batch: ${batchId}`, error);
403
+ `Error in ${functionName} batch: ${batchId}`, error);
360
404
  this.updateBatchResultWithError_(batchResult, error, lines, 0);
361
405
  return batchResult;
362
406
  }
@@ -493,6 +537,27 @@ class GoogleAds {
493
537
  batchResult.errors = Array.from(errors);
494
538
  }
495
539
 
540
+ /**
541
+ * Uploads call conversions to google ads account.
542
+ * It requires an array of call conversions and customer id.
543
+ * In DEBUG mode, this function will only validate the conversions.
544
+ * @param {Array<ConversionConfig>} callConversions Call Conversions
545
+ * @param {string} customerId
546
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
547
+ * @return {!Promise<!UploadCallConversionsResponse>}
548
+ */
549
+ uploadCallConversions(callConversions, customerId, loginCustomerId) {
550
+ this.logger.debug('Upload call conversions for customerId:', customerId);
551
+ const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
552
+ const request = new UploadCallConversionsRequest({
553
+ conversions: callConversions,
554
+ customer_id: customerId,
555
+ validate_only: this.debugMode, // when true makes no changes
556
+ partial_failure: true, // Will still create the non-failed entities
557
+ });
558
+ return customer.conversionUploads.uploadCallConversions(request);
559
+ }
560
+
496
561
  /**
497
562
  * Uploads click conversions to google ads account.
498
563
  * It requires an array of click conversions and customer id.
@@ -613,9 +678,9 @@ class GoogleAds {
613
678
  * @return {!Promise<UploadUserDataResponse>}
614
679
  */
615
680
  async uploadUserDataToUserList(customerMatchRecords, customerMatchConfig) {
616
- const customerId = customerMatchConfig.customer_id.replace(/-/g, '');
617
- const loginCustomerId = customerMatchConfig.login_customer_id.replace(/-/g,
618
- '');
681
+ const customerId = this.getCleanCid_(customerMatchConfig.customer_id);
682
+ const loginCustomerId = this.getCleanCid_(
683
+ customerMatchConfig.login_customer_id);
619
684
  const userListId = customerMatchConfig.list_id;
620
685
  const operation = customerMatchConfig.operation;
621
686
 
@@ -688,6 +753,181 @@ class GoogleAds {
688
753
  });
689
754
  }
690
755
 
756
+ /**
757
+ * Returns a integer format CID by removing dashes.
758
+ * @param {string} cid
759
+ * @return {string}
760
+ * @private
761
+ */
762
+ getCleanCid_(cid) {
763
+ return cid.replace(/-/g, '');
764
+ }
765
+
766
+ /**
767
+ * Get OfflineUserDataJob status.
768
+ * @param {OfflineUserDataJobConfig} config Offline user data job config.
769
+ * @param {string} resourceName
770
+ * @return {!OfflineUserDataJobStatus} Job status
771
+ */
772
+ async getOfflineUserDataJob(config, resourceName) {
773
+ const loginCustomerId = this.getCleanCid_(config.login_customer_id);
774
+ const customerId = this.getCleanCid_(config.customer_id);
775
+ const reportConfig = {
776
+ entity: 'offline_user_data_job',
777
+ attributes: [
778
+ 'offline_user_data_job.id',
779
+ 'offline_user_data_job.status',
780
+ 'offline_user_data_job.type',
781
+ 'offline_user_data_job.customer_match_user_list_metadata.user_list',
782
+ 'offline_user_data_job.failure_reason',
783
+ ],
784
+ constraints: {
785
+ 'offline_user_data_job.resource_name': resourceName,
786
+ },
787
+ };
788
+ const jobs = await this.getReport(customerId, loginCustomerId, reportConfig);
789
+ if (jobs.length === 0) {
790
+ throw new Error(`Can't find the OfflineUserDataJob: ${resourceName}`);
791
+ }
792
+ return OfflineUserDataJobStatus[jobs[0].offline_user_data_job.status];
793
+ }
794
+
795
+ //resource_name: 'customers/8368692804/offlineUserDataJobs/23130531867'
796
+ //'customers/8368692804/offlineUserDataJobs/23232922761'
797
+ /**
798
+ * Creates a OfflineUserDataJob and returns resource name.
799
+ * @param {OfflineUserDataJobConfig} config Offline user data job config.
800
+ * @return {string} The resouce name of the creaed job.
801
+ */
802
+ async createOfflineUserDataJob(config) {
803
+ const loginCustomerId = this.getCleanCid_(config.login_customer_id);
804
+ const customerId = this.getCleanCid_(config.customer_id);
805
+ const { list_id: userListId, type } = config;
806
+ this.logger.debug('Creating OfflineUserDataJob for CID:', customerId);
807
+ const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
808
+ // if()CUSTOMER_MATCH_USER_LIST
809
+ const job = OfflineUserDataJob.create({
810
+ type,
811
+ });
812
+ // https://developers.google.com/google-ads/api/rest/reference/rest/latest/customers.offlineUserDataJobs?hl=en#CustomerMatchUserListMetadata
813
+ if (type.startsWith('CUSTOMER_MATCH')) {
814
+ const metadata = this.buildCustomerMatchUserListMetadata_(customerId,
815
+ userListId);
816
+ job.customer_match_user_list_metadata = metadata;
817
+ // https://developers.google.com/google-ads/api/rest/reference/rest/latest/customers.offlineUserDataJobs?hl=en#StoreSalesMetadata
818
+ } else if (type.startsWith('STORE_SALES')) {
819
+ // If there is StoreSalesMetadata in the config
820
+ if (config.storeSalesMetadata) {
821
+ job.store_sales_list_metadata = config.storeSalesMetadata;
822
+ }
823
+ } else {
824
+ throw new Error(`UNSUPPORTED OfflineUserDataJobType: ${type}.`);
825
+ }
826
+ const request = CreateOfflineUserDataJobRequest.create({
827
+ customer_id: customerId,
828
+ job,
829
+ validate_only: this.debugMode, // when true makes no changes
830
+ enable_match_rate_range_preview: true,
831
+ });
832
+ const { resource_name: resourceName } =
833
+ await customer.offlineUserDataJobs.createOfflineUserDataJob(request);
834
+ this.logger.info('Created OfflineUserDataJob:', resourceName);
835
+ return resourceName;
836
+ }
837
+
838
+ /**
839
+ * Adds user data in to the OfflineUserDataJob.
840
+ * @param {OfflineUserDataJobConfig} config Offline user data job config.
841
+ * @param {string} jobResourceName
842
+ * @param {!Array<CustomerMatchRecord>} customerMatchRecords user Ids
843
+ * @return {!Promise<AddOfflineUserDataJobOperationsResponse>}
844
+ */
845
+ async addOperationsToOfflineUserDataJob(config, jobResourceName, records) {
846
+ const start = new Date().getTime();
847
+ const loginCustomerId = this.getCleanCid_(config.login_customer_id);
848
+ const customerId = this.getCleanCid_(config.customer_id);
849
+ const operation = config.operation;
850
+ const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
851
+ const operationsList = this.buildOperationsList_(operation, records);
852
+ const request = AddOfflineUserDataJobOperationsRequest.create({
853
+ resource_name: jobResourceName,
854
+ operations: operationsList,
855
+ validate_only: false,//this.debugMode,
856
+ enable_partial_failure: true,
857
+ enable_warnings: true,
858
+ });
859
+ const response = await customer.
860
+ offlineUserDataJobs.addOfflineUserDataJobOperations(request);
861
+ this.logger.debug(`Added ${records.length} records in (ms):`,
862
+ new Date().getTime() - start);
863
+ return response;
864
+ }
865
+
866
+ /**
867
+ * Starts the OfflineUserDataJob.
868
+ * @param {OfflineUserDataJobConfig} config Offline user data job config.
869
+ * @param {string} jobResourceName
870
+ * @returns
871
+ */
872
+ async runOfflineUserDataJob(config, jobResourceName) {
873
+ const loginCustomerId = this.getCleanCid_(config.login_customer_id);
874
+ const customerId = this.getCleanCid_(config.customer_id);
875
+ const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
876
+ const request = RunOfflineUserDataJobRequest.create({
877
+ resource_name: jobResourceName,
878
+ validate_only: false,//this.debugMode,
879
+ });
880
+ const rawResponse = await customer.
881
+ offlineUserDataJobs.runOfflineUserDataJob(request);
882
+ const response = lodash.pick(rawResponse, ['name', 'done', 'error']);
883
+ this.logger.debug('runOfflineUserDataJob response: ', response);
884
+ return response;
885
+ }
886
+
887
+ /**
888
+ * Returns the function to send out a request to Google Ads API with
889
+ * user data as operations in OfflineUserDataJob.
890
+ * @param {!OfflineUserDataJobConfig} config
891
+ * @param {string} jobResourceName
892
+ * @return {!SendSingleBatch} Function which can send a batch of hits to
893
+ * Google Ads API.
894
+ */
895
+ getAddOperationsToOfflineUserDataJobFn(config, jobResourceName) {
896
+ /**
897
+ * Sends a batch of hits to Google Ads API.
898
+ * @param {!Array<string>} lines Data for single request. It should be
899
+ * guaranteed that it doesn't exceed quota limitation.
900
+ * @param {string} batchId The tag for log.
901
+ * @return {!Promise<BatchResult>}
902
+ */
903
+ return async (lines, batchId) => {
904
+ /** @type {Array<CustomerMatchRecord>} */
905
+ const records = lines.map((line) => JSON.parse(line));
906
+ /** @const {BatchResult} */ const batchResult = {
907
+ result: true,
908
+ numberOfLines: lines.length,
909
+ };
910
+ try {
911
+ const response = await this.addOperationsToOfflineUserDataJob(config,
912
+ jobResourceName, records);
913
+ this.logger.debug(`Add operation to job batch[${batchId}]`, response);
914
+ const { results, partial_failure_error: failed } = response;
915
+ if (failed) {
916
+ this.logger.info('partial_failure_error:', failed.message);
917
+ const failures = failed.details.map(
918
+ ({ value }) => GoogleAdsFailure.decode(value));
919
+ this.extraFailedLines_(batchResult, failures, lines, 0);
920
+ }
921
+ return batchResult;
922
+ } catch (error) {
923
+ this.logger.error(
924
+ `Error in OfflineUserDataJob add operations batch[${batchId}]`, error);
925
+ this.updateBatchResultWithError_(batchResult, error, lines, 2);
926
+ return batchResult;
927
+ }
928
+ }
929
+ }
930
+
691
931
  /**
692
932
  * Returns an instance of GoogleAdsApi.Customer on google-ads-api.
693
933
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
@@ -748,6 +988,8 @@ module.exports = {
748
988
  ConversionConfig,
749
989
  CustomerMatchRecord,
750
990
  CustomerMatchConfig,
991
+ OfflineUserDataJobType,
992
+ OfflineUserDataJobConfig,
751
993
  GoogleAds,
752
994
  ReportQueryConfig,
753
995
  GoogleAdsField,