@google-cloud/nodejs-common 0.9.4-beta2 → 0.9.5-beta2

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.
@@ -30,14 +30,19 @@ const {
30
30
  },
31
31
  services: {
32
32
  UploadClickConversionsRequest,
33
+ UploadClickConversionsResponse,
33
34
  UploadUserDataRequest,
35
+ UploadUserDataResponse,
34
36
  UserDataOperation,
35
37
  SearchGoogleAdsFieldsRequest,
36
38
  },
39
+ errors: {
40
+ GoogleAdsFailure,
41
+ },
37
42
  } = googleAdsLib;
38
43
  const {GoogleAdsApi} = require('google-ads-api');
39
44
  const AuthClient = require('./auth_client.js');
40
- const {getLogger} = require('../components/utils.js');
45
+ const {getLogger, BatchResult,} = require('../components/utils.js');
41
46
 
42
47
  /** @type {!ReadonlyArray<string>} */
43
48
  const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
@@ -46,7 +51,7 @@ const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
46
51
  * Configuration for uploading click conversions for Google Ads, includes:
47
52
  * gclid, conversion_action, conversion_date_time, conversion_value,
48
53
  * currency_code, order_id, external_attribution_data
49
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/ClickConversion
54
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
50
55
  * @typedef {{
51
56
  * gclid: string,
52
57
  * conversion_action: string,
@@ -65,7 +70,7 @@ let ClickConversionConfig;
65
70
  * list_type must be one of the following: hashed_email,
66
71
  * hashed_phone_number, mobile_id, third_party_user_id or address_info;
67
72
  * operation must be one of the two: 'create' or 'remove';
68
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserDataOperation
73
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
69
74
  * @typedef {{
70
75
  * customer_id: string,
71
76
  * login_customer_id: string,
@@ -80,7 +85,7 @@ let CustomerMatchConfig;
80
85
  /**
81
86
  * Configuration for uploading customer match data for Google Ads, includes one of:
82
87
  * hashed_email, hashed_phone_number, mobile_id, third_party_user_id or address_info
83
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserIdentifier
88
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
84
89
  * @typedef {{
85
90
  * hashed_email: string,
86
91
  * }|{
@@ -123,17 +128,21 @@ let CustomerMatchRecord;
123
128
  let ReportQueryConfig;
124
129
 
125
130
  /**
126
- * Google Ads API v6.1 stub.
131
+ * Google Ads API class based on Opteo's Nodejs library.
127
132
  * see https://opteo.com/dev/google-ads-api/#features
128
133
  */
129
134
  class GoogleAds {
130
135
  /**
131
136
  * Note: Rate limits is set by the access level of Developer token.
132
137
  * @param {string} developerToken Developer token to access the API.
138
+ * @param {boolean=} debugMode This is used to set ONLY validate conversions
139
+ * but not real uploading.
140
+ * @param {!Object<string,string>=} env The environment object to hold env
141
+ * variables.
133
142
  */
134
- constructor(developerToken) {
135
- this.debug = process.env['DEBUG'] === 'true';
136
- const oauthClient = new AuthClient(API_SCOPES).getOAuth2Token();
143
+ constructor(developerToken, debugMode = false, env = process.env) {
144
+ this.debugMode = debugMode;
145
+ const oauthClient = new AuthClient(API_SCOPES, env).getOAuth2Token();
137
146
  /** @const {GoogleAdsApi} */ this.apiClient = new GoogleAdsApi({
138
147
  client_id: oauthClient.clientId,
139
148
  client_secret: oauthClient.clientSecret,
@@ -206,7 +215,7 @@ class GoogleAds {
206
215
  * @param {!Array<string>} lines Data for single request. It should be
207
216
  * guaranteed that it doesn't exceed quota limitation.
208
217
  * @param {string} batchId The tag for log.
209
- * @return {!Promise<boolean>}
218
+ * @return {!BatchResult}
210
219
  */
211
220
  return async (lines, batchId) => {
212
221
  /** @type {!Array<ClickConversionConfig>} */
@@ -214,17 +223,166 @@ class GoogleAds {
214
223
  const record = JSON.parse(line);
215
224
  return Object.assign({}, adsConfig, record);
216
225
  });
226
+ /** @const {BatchResult} */
227
+ const batchResult = {
228
+ result: true,
229
+ numberOfLines: lines.length,
230
+ };
217
231
  try {
218
- return await this.uploadClickConversions(conversions, customerId,
219
- loginCustomerId);
232
+ const response = await this.uploadClickConversions(conversions,
233
+ customerId, loginCustomerId);
234
+ const {results, partial_failure_error: failed} = response;
235
+ if (this.logger.isDebugEnabled()) {
236
+ const gclids = results.map((conversion) => conversion.gclid);
237
+ this.logger.debug('Uploaded gclids:', gclids);
238
+ }
239
+ if (failed) {
240
+ this.logger.info('partial_failure_error:', failed.message);
241
+ const failures = failed.details.map(
242
+ ({value}) => GoogleAdsFailure.decode(value));
243
+ this.extraFailedLines_(batchResult, failures, lines, 0);
244
+ }
245
+ return batchResult;
220
246
  } catch (error) {
221
- this.logger.info(
222
- `Error in getUploadConFn in batchId: ${batchId}`, error);
223
- return false;
247
+ this.logger.error(
248
+ `Error in upload conversions batch: ${batchId}`, error);
249
+ this.updateBatchResultWithError_(batchResult, error, lines, 0);
250
+ return batchResult;
224
251
  }
225
252
  }
226
253
  }
227
254
 
255
+ /**
256
+ * Updates the BatchResult based on errors.
257
+ *
258
+ * There are 2 types of errors here:
259
+ * 1. Normal JavaScript Error object. It happens when the whole process fails
260
+ * (not partial failure), so there is no detailed failed lines.
261
+ * 2. GoogleAdsFailure. It is a Google Ads' own error object which has an
262
+ * array of GoogleAdsError (property name 'errors'). GoogleAdsError contains
263
+ * the detailed failed data if it is a line-error. For example, a wrong
264
+ * encoded user identifier is a line-error, while a wrong user list id is not.
265
+ * GoogleAdsFailure: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsFailure
266
+ * GoogleAdsError: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsError
267
+ *
268
+ * For Customer Match data uploading, there is not partial failure, so the
269
+ * result can be either succeeded or a thrown error. The thrown error will be
270
+ * used to build the returned result here.
271
+ * For Conversions uploading (partial failure enabled), if there is an error
272
+ * fails the whole process, the error will also be thrown and handled here.
273
+ * Otherwise, the errors will be wrapped in the response as the property named
274
+ * 'partial_failure_error' which contains an array of GoogleAdsFailure. This
275
+ * kind of failure doesn't fail the process, while line-errors can be
276
+ * extracted from it.
277
+ * For more information, see the function `extraFailedLines_`.
278
+ *
279
+ * An example of 'GoogleAdsFailure' is:
280
+ * GoogleAdsFailure {
281
+ * errors: [
282
+ * GoogleAdsError {
283
+ * error_code: ErrorCode { offline_user_data_job_error: 25 },
284
+ * message: 'The SHA256 encoded value is malformed.',
285
+ * location: ErrorLocation {
286
+ * field_path_elements: [
287
+ * FieldPathElement { field_name: 'operations', index: 0 },
288
+ * FieldPathElement { field_name: 'create' },
289
+ * FieldPathElement { field_name: 'user_identifiers', index: 0 },
290
+ * FieldPathElement { field_name: 'hashed_email' }
291
+ * ]
292
+ * }
293
+ * }
294
+ * ],
295
+ * request_id: 'xxxxxxxxxxxxxxx'
296
+ * }
297
+ *
298
+ * @param {!BatchResult} batchResult
299
+ * @param {(!GoogleAdsFailure|!Error)} error
300
+ * @param {!Array<string>} lines The original input data.
301
+ * @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
302
+ * 'field_path_elements'. This is used to get the original line related to
303
+ * this GoogleAdsError.
304
+ * @private
305
+ */
306
+ updateBatchResultWithError_(batchResult, error, lines, fieldPathIndex) {
307
+ batchResult.result = false;
308
+ if (error.errors) { //GoogleAdsFailure
309
+ this.extraFailedLines_(batchResult, [error], lines, fieldPathIndex);
310
+ } else {
311
+ batchResult.errors = [error.message || error.toString()];
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Extras failed lines based on the GoogleAdsFailures.
317
+ *
318
+ * Different errors have different 'fieldPathIndex' which is the index of
319
+ * failed lines in original input data (an array of a string).
320
+ *
321
+ * For conversions, the ErrorLocation is like:
322
+ * ErrorLocation {
323
+ * field_path_elements: [
324
+ * FieldPathElement { field_name: 'operations', index: 0 },
325
+ * FieldPathElement { field_name: 'create' }
326
+ * ]
327
+ * }
328
+ * So the index is 0, index of 'operations'.
329
+ *
330
+ * For customer match upload, the ErrorLocation is like:
331
+ * ErrorLocation {
332
+ * field_path_elements: [
333
+ * FieldPathElement { field_name: 'operations', index: 0 },
334
+ * FieldPathElement { field_name: 'create' },
335
+ * FieldPathElement { field_name: 'user_identifiers', index: 0 },
336
+ * FieldPathElement { field_name: 'hashed_email' }
337
+ * ]
338
+ * }
339
+ * The index should be 2, index of 'user_identifiers'.
340
+ *
341
+ * With this we can get errors and failed lines. The function will set
342
+ * following for the given BatchResult object:
343
+ * result - false
344
+ * errors - de-duplicated error reasons
345
+ * failedLines - failed lines, an array of string. Without the reason of
346
+ * failure.
347
+ * groupedFailed - a hashmap of failed the lines. The key is the reason, the
348
+ * value is the array of failed lines due to this reason.
349
+ * TODO: Confirm how to surface and handle groupedFailed.
350
+ * @param {!BatchResult} batchResult
351
+ * @param {!Array<!GoogleAdsFailure>} failures
352
+ * @param {!Array<string>} lines The original input data.
353
+ * @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
354
+ * 'field_path_elements'. This is used to get the original line related to
355
+ * this GoogleAdsError.
356
+ * @private
357
+ */
358
+ extraFailedLines_(batchResult, failures, lines, fieldPathIndex) {
359
+ batchResult.result = false;
360
+ batchResult.failedLines = [];
361
+ batchResult.groupedFailed = {};
362
+ const errors = new Set();
363
+ failures.forEach((failure) => {
364
+ failure.errors.forEach(({message, location}) => {
365
+ errors.add(message);
366
+ if (location && location.field_path_elements[fieldPathIndex]) {
367
+ const {index} = location.field_path_elements[fieldPathIndex];
368
+ if (typeof index === 'undefined') {
369
+ this.logger.warn(`Unknown field path index: ${fieldPathIndex}`,
370
+ location.field_path_elements);
371
+ } else {
372
+ const groupedFailed = batchResult.groupedFailed[message] || [];
373
+ const failedLine = lines[index];
374
+ batchResult.failedLines.push(failedLine);
375
+ groupedFailed.push(failedLine);
376
+ if (groupedFailed.length === 1) {
377
+ batchResult.groupedFailed[message] = groupedFailed;
378
+ }
379
+ }
380
+ }
381
+ });
382
+ });
383
+ batchResult.errors = Array.from(errors);
384
+ }
385
+
228
386
  /**
229
387
  * Uploads click conversions to google ads account.
230
388
  * It requires an array of click conversions and customer id.
@@ -232,30 +390,18 @@ class GoogleAds {
232
390
  * @param {Array<ClickConversionConfig>} clickConversions ClickConversions
233
391
  * @param {string} customerId
234
392
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
235
- * @return {!Promise<boolean>}
393
+ * @return {!Promise<!UploadClickConversionsResponse>}
236
394
  */
237
- async uploadClickConversions(clickConversions, customerId, loginCustomerId) {
395
+ uploadClickConversions(clickConversions, customerId, loginCustomerId) {
238
396
  this.logger.debug('Upload click conversions for customerId:', customerId);
239
397
  const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
240
398
  const request = new UploadClickConversionsRequest({
241
399
  conversions: clickConversions,
242
400
  customer_id: customerId,
243
- validate_only: this.debug, // when true makes no changes
401
+ validate_only: this.debugMode, // when true makes no changes
244
402
  partial_failure: true, // Will still create the non-failed entities
245
403
  });
246
- const result = await customer.conversionUploads.uploadClickConversions(
247
- request);
248
- const {results, partial_failure_error: failed} = result;
249
- const response = results.map((conversion) => conversion.gclid);
250
- this.logger.debug('Uploaded gclids:', response);
251
- // const failed = result.partial_failure_error;
252
- // Note: the response is different from previous version. current 'message'
253
- // only contains partial failed conversions. The other field 'details'
254
- // contains more information with the type of Array<Buffer>.
255
- if (failed) {
256
- this.logger.info('Errors:', failed.message);
257
- }
258
- return !failed;
404
+ return customer.conversionUploads.uploadClickConversions(request);
259
405
  }
260
406
 
261
407
  /**
@@ -271,29 +417,36 @@ class GoogleAds {
271
417
  * @param {!Array<string>} lines Data for single request. It should be
272
418
  * guaranteed that it doesn't exceed quota limitation.
273
419
  * @param {string} batchId The tag for log.
274
- * @return {!Promise<boolean>}
420
+ * @return {!Promise<BatchResult>}
275
421
  */
276
422
  return async (lines, batchId) => {
277
423
  /** @type {Array<CustomerMatchRecord>} */
278
424
  const userIds = lines.map((line) => JSON.parse(line));
425
+ /** @const {BatchResult} */ const batchResult = {
426
+ result: true,
427
+ numberOfLines: lines.length,
428
+ };
279
429
  try {
280
- return await this.uploadUserDataToUserList(userIds,
430
+ const response = await this.uploadUserDataToUserList(userIds,
281
431
  customerMatchConfig);
432
+ this.logger.debug(`Customer Match upload batch[${batchId}]`, response);
433
+ return batchResult;
282
434
  } catch (error) {
283
435
  this.logger.error(
284
- `Error in getUploadCustomerMatchFn in batchId: ${batchId}`, error);
285
- return false;
436
+ `Error in Customer Match upload batch[${batchId}]`, error);
437
+ this.updateBatchResultWithError_(batchResult, error, lines, 2);
438
+ return batchResult;
286
439
  }
287
440
  }
288
441
  }
289
442
 
290
443
  /**
291
444
  * Uploads a user data to a user list (aka customer match).
292
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserDataService
293
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserDataOperation
294
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserData
295
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserIdentifier
296
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/CustomerMatchUserListMetadata
445
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataService
446
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
447
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
448
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
449
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
297
450
  * Please note: The UserDataService has a limit of 10 UserDataOperations
298
451
  * and 100 user IDs per request
299
452
  * @see https://developers.google.com/google-ads/api/docs/migration/user-data-service#rate_limits
@@ -302,7 +455,7 @@ class GoogleAds {
302
455
  * customer_id, login_customer_id, list_id, list_type which can be one of the following
303
456
  * hashed_email, hashed_phone_number, mobile_id, third_party_user_id or address_info and
304
457
  * operation which can be either 'create' or 'remove'
305
- * @return {!Promise<boolean>}
458
+ * @return {!Promise<UploadUserDataResponse>}
306
459
  */
307
460
  async uploadUserDataToUserList(customerMatchRecords, customerMatchConfig) {
308
461
  const customerId = customerMatchConfig.customer_id.replace(/-/g, '');
@@ -323,16 +476,15 @@ class GoogleAds {
323
476
  customer_match_user_list_metadata: metadata,
324
477
  });
325
478
  const response = await customer.userData.uploadUserData(request);
326
- this.logger.debug('Uploaded CM users:', response);
327
- return true;
479
+ return response;
328
480
  }
329
481
 
330
482
  /**
331
483
  * Builds a list of UserDataOperations.
332
484
  * Since v6 you can set a user_attribute
333
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserData
334
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserIdentifier
335
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/UserDataOperation
485
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
486
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
487
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
336
488
  * @param {string} operationType either 'create' or 'remove'
337
489
  * @param {Array<CustomerMatchRecord>} customerMatchRecords userIds
338
490
  * @param {string} userListType One of the following hashed_email, hashed_phone_number,
@@ -351,7 +503,7 @@ class GoogleAds {
351
503
 
352
504
  /**
353
505
  * Creates CustomerMatchUserListMetadata.
354
- * @see https://developers.google.com/google-ads/api/reference/rpc/v7/CustomerMatchUserListMetadata
506
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
355
507
  * @param {string} customerId part of the ResourceName to be mutated
356
508
  * @param {string} userListId part of the ResourceName to be mutated
357
509
  * @return {!CustomerMatchUserListMetadata}
package/src/apis/index.js CHANGED
@@ -110,3 +110,13 @@ exports.adsdatahub = require('./ads_data_hub.js');
110
110
  * }}
111
111
  */
112
112
  exports.measurementprotocolga4 = require('./measurement_protocol_ga4.js');
113
+
114
+ /**
115
+ * APIs integration class for YouTube.
116
+ * @const {{
117
+ * YouTube:!YouTube,
118
+ * ListChannelsConfig: !ListChannelsConfig,
119
+ * ListVideosConfig: !ListVideosConfig,
120
+ * }}
121
+ */
122
+ exports.youtube = require('./youtube.js');
@@ -69,18 +69,14 @@ class MeasurementProtocol {
69
69
  * @return {!Promise<BatchResult>}
70
70
  */
71
71
  return async (lines, batchId) => {
72
- const payload =
73
- lines
74
- .map((line) => {
75
- const record = JSON.parse(line);
76
- const hit = Object.assign({}, config, record);
77
- return Object.keys(hit)
78
- .map((key) => {
79
- return `${key}=${encodeURIComponent(hit[key])}`;
80
- })
81
- .join('&');
82
- })
83
- .join('\n');
72
+ const payload = lines.map((line) => {
73
+ const record = JSON.parse(line);
74
+ const hit = Object.assign({}, config, record);
75
+ return Object.keys(hit).map(
76
+ (key) => `${key}=${encodeURIComponent(hit[key])}`)
77
+ .join('&');
78
+ })
79
+ .join('\n');
84
80
  // In debug mode, the path is fixed to '/debug/collect'.
85
81
  const path = (this.debugMode) ? '/debug/collect' : '/batch';
86
82
  const requestOptions = {
@@ -112,24 +108,48 @@ class MeasurementProtocol {
112
108
  if (!this.debugMode) {
113
109
  batchResult.result = true;
114
110
  } else {
115
- const failedHits = [];
116
- const failedReasons = new Set();
117
- response.data.hitParsingResult.forEach((result, index) => {
118
- if (!result.valid) {
119
- failedHits.push(lines[index]);
120
- result.parserMessage.forEach(({description}) => {
121
- failedReasons.add(description);
122
- });
123
- }
124
- });
125
- batchResult.result = failedHits.length === 0;
126
- batchResult.failedLines = failedHits;
127
- batchResult.errors = Array.from(failedReasons);
111
+ this.extraFailedLines_(batchResult, response.data.hitParsingResult,
112
+ lines);
128
113
  }
129
114
  return batchResult;
130
115
  };
131
116
  };
132
117
 
118
+ /**
119
+ * Extras failed lines based on the hitParsingResult, see:
120
+ * https://developers.google.com/analytics/devguides/collection/protocol/v1/validating-hits
121
+ *
122
+ * Note, only in 'debug' mode, Google Analytics will return this part of data.
123
+ *
124
+ * @param {!BatchResult} batchResult
125
+ * @param {!Array<!Object>} hitParsingResults
126
+ * @param {!Array<string>} lines The original input data.
127
+ * @private
128
+ */
129
+ extraFailedLines_(batchResult, hitParsingResults, lines) {
130
+ batchResult.failedLines = [];
131
+ batchResult.groupedFailed = {};
132
+ const errors = new Set();
133
+ hitParsingResults.forEach((result, index) => {
134
+ if (!result.valid) {
135
+ const failedLine = lines[index];
136
+ batchResult.failedLines.push(failedLine);
137
+ result.parserMessage.forEach(({description: error, messageType}) => {
138
+ this.logger.info(`[${messageType}]: ${error} for ${failedLine}`);
139
+ if (messageType === 'ERROR') {
140
+ errors.add(error);
141
+ const groupedFailed = batchResult.groupedFailed[error] || [];
142
+ groupedFailed.push(failedLine);
143
+ if (groupedFailed.length === 1) {
144
+ batchResult.groupedFailed[error] = groupedFailed;
145
+ }
146
+ }
147
+ });
148
+ }
149
+ });
150
+ batchResult.result = batchResult.failedLines.length === 0;
151
+ batchResult.errors = Array.from(errors);
152
+ }
133
153
  }
134
154
 
135
155
  module.exports = {MeasurementProtocol};
@@ -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,
@@ -93,7 +94,11 @@ class MeasurementProtocolGA4 {
93
94
  */
94
95
  return async (lines, batchId) => {
95
96
  const line = lines[0]; // Each request contains one record only.
96
- const hit = Object.assign({}, config.requestBody, JSON.parse(line));
97
+ if (lines.length > 1) {
98
+ this.logger.warn(
99
+ "Only one line data expected. Will only send the first line.");
100
+ }
101
+ const hit = lodash.merge({}, config.requestBody, JSON.parse(line));
97
102
 
98
103
  const requestOptions = {
99
104
  method: 'POST',
@@ -135,6 +140,7 @@ class MeasurementProtocolGA4 {
135
140
  batchResult.failedLines = lines;
136
141
  batchResult.errors = response.data.validationMessages.map(
137
142
  ({description}) => description);
143
+ batchResult.groupedFailed = {[batchResult.errors.join()]: [lines[0]]};
138
144
  }
139
145
  }
140
146
  return batchResult;
@@ -67,11 +67,13 @@ class Spreadsheets {
67
67
  /**
68
68
  * Init Spreadsheets API client.
69
69
  * @param {string} spreadsheetId
70
+ * @param {!Object<string,string>=} env The environment object to hold env
71
+ * variables.
70
72
  */
71
- constructor(spreadsheetId) {
73
+ constructor(spreadsheetId, env = process.env) {
72
74
  /** @const {string} */
73
75
  this.spreadsheetId = spreadsheetId;
74
- const authClient = new AuthClient(API_SCOPES);
76
+ const authClient = new AuthClient(API_SCOPES, env);
75
77
  const auth = authClient.getDefaultAuth();
76
78
  /** @const {!!google.sheets} */
77
79
  this.instance = google.sheets({
@@ -108,7 +110,6 @@ class Spreadsheets {
108
110
  * see:
109
111
  * https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/clear
110
112
  * @param {string} sheetName Name of the Sheet.
111
- * @return {!Promise<boolean>} Whether the operation succeeded.
112
113
  */
113
114
  async clearSheet(sheetName) {
114
115
  const request = {
@@ -119,10 +120,9 @@ class Spreadsheets {
119
120
  const response = await this.instance.spreadsheets.values.clear(request);
120
121
  const data = response.data;
121
122
  this.logger.debug(`Clear sheet[${sheetName}}]: `, data);
122
- return true;
123
123
  } catch (error) {
124
- this.logger.error(error);
125
- return false;
124
+ this.logger.error(error.toString());
125
+ throw error;
126
126
  }
127
127
  }
128
128
 
@@ -162,7 +162,6 @@ class Spreadsheets {
162
162
  * @param {string} sheetName Name of the Sheet.
163
163
  * @param {number} targetRows Loaded data rows number.
164
164
  * @param {number} targetColumns Loaded data columns number.
165
- * @return {!Promise<boolean>} Whether the operation succeeded.
166
165
  */
167
166
  async reshape(sheetName, targetRows, targetColumns) {
168
167
  const request = /** @type{Params$Resource$Spreadsheets$Get} */ {
@@ -194,13 +193,12 @@ class Spreadsheets {
194
193
  if (requests.resource.requests.length > 0) {
195
194
  const {data} = await this.instance.spreadsheets.batchUpdate(requests);
196
195
  this.logger.debug(`Reshape Sheet [${sheetName}]: `, data);
197
- return true;
196
+ } else {
197
+ this.logger.debug('No need to reshape.');
198
198
  }
199
- this.logger.debug('No need to reshape.');
200
- return true;
201
199
  } catch (error) {
202
- console.error(error);
203
- return false;
200
+ this.logger.error(error.toString());
201
+ throw error;
204
202
  }
205
203
  }
206
204
 
@@ -218,7 +216,9 @@ class Spreadsheets {
218
216
  spreadsheetId: this.spreadsheetId,
219
217
  resource: {requests: [{pasteData}]},
220
218
  };
221
- /** @type {BatchResult} */ const batchResult = {};
219
+ /** @type {BatchResult} */ const batchResult = {
220
+ numberOfLines: data.trim().split('\n').length,
221
+ };
222
222
  try {
223
223
  const response = await this.instance.spreadsheets.batchUpdate(request);
224
224
  const data = response.data;