@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.
@@ -22,7 +22,11 @@
22
22
  const {google} = require('googleapis');
23
23
  const {request} = require('gaxios');
24
24
  const AuthClient = require('./auth_client.js');
25
- const {getLogger, SendSingleBatch} = require('../components/utils.js');
25
+ const {
26
+ getLogger,
27
+ SendSingleBatch,
28
+ BatchResult,
29
+ } = require('../components/utils.js');
26
30
 
27
31
  const API_SCOPES = Object.freeze([
28
32
  'https://www.googleapis.com/auth/doubleclicksearch',
@@ -115,8 +119,14 @@ let ReportRequest;
115
119
  * https://support.google.com/adsihc/answer/6346075?hl=en
116
120
  */
117
121
  class DoubleClickSearch {
118
- constructor() {
119
- const authClient = new AuthClient(API_SCOPES);
122
+
123
+ /**
124
+ * @constructor
125
+ * @param {!Object<string,string>=} env The environment object to hold env
126
+ * variables.
127
+ */
128
+ constructor(env = process.env) {
129
+ const authClient = new AuthClient(API_SCOPES, env);
120
130
  this.auth = authClient.getDefaultAuth();
121
131
  /** @const {!google.doubleclicksearch} */
122
132
  this.instance = google.doubleclicksearch({
@@ -138,22 +148,21 @@ class DoubleClickSearch {
138
148
  * availabilities array.
139
149
  * @return {!Promise<boolean>} Update result.
140
150
  */
141
- updateAvailability(availabilities) {
151
+ async updateAvailability(availabilities) {
142
152
  const availabilityTimestamp = Date.now();
143
- for (const availability of availabilities) {
153
+ availabilities.forEach((availability) => {
144
154
  availability.availabilityTimestamp = availabilityTimestamp;
145
- }
155
+ });
146
156
  this.logger.debug('Sending out availabilities', availabilities);
147
- return this.instance.conversion
148
- .updateAvailability({requestBody: {availabilities}})
149
- .then((response) => {
150
- this.logger.debug('Get response: ', response);
151
- return response.status === 200;
152
- })
153
- .catch((error) => {
154
- console.error(error);
155
- return false;
156
- });
157
+ try {
158
+ const response = await this.instance.conversion.updateAvailability(
159
+ {requestBody: {availabilities}});
160
+ this.logger.debug('Get response: ', response);
161
+ return response.status === 200;
162
+ } catch (e) {
163
+ this.logger.error(e);
164
+ return false;
165
+ }
157
166
  }
158
167
 
159
168
  /**
@@ -170,16 +179,16 @@ class DoubleClickSearch {
170
179
  * @param {!Array<string>} lines Data for single request. It should be
171
180
  * guaranteed that it doesn't exceed quota limitation.
172
181
  * @param {string} batchId The tag for log.
173
- * @return {!Promise<boolean>}
182
+ * @return {!Promise<BatchResult>}
174
183
  */
175
- return (lines, batchId) => {
184
+ return async (lines, batchId) => {
176
185
  const conversionTimestamp = new Date().getTime();
177
186
  const conversions = lines.map((line, index) => {
178
187
  const record = JSON.parse(line);
179
188
  return Object.assign(
180
189
  {
181
190
  // Default value, can be overwritten by the exported data.
182
- conversionTimestamp: conversionTimestamp,
191
+ conversionTimestamp,
183
192
  // Default conversion Id should be unique in a single request.
184
193
  // See error code 0x0000011F at here:
185
194
  // https://developers.google.com/search-ads/v2/troubleshooting#conversion-upload-errors
@@ -188,33 +197,100 @@ class DoubleClickSearch {
188
197
  config, record);
189
198
  });
190
199
  this.logger.debug('Configuration: ', config);
191
- return this.instance.conversion
192
- .insert({requestBody: {conversion: conversions}})
193
- .then((response) => {
194
- this.logger.debug('Response: ', response);
195
- console.log(`SA[${batchId}] Insert ${
196
- response.data.conversion.length} conversions`);
197
- return true;
198
- })
199
- .catch((error) => {
200
- if (error.code === 400 && error.errors) {//requestValidation error
201
- const messages = error.errors.map((singleError) => {
202
- return singleError.message.replace(
203
- 'The request was not valid. Details: ', '');
204
- });
205
- console.log(`SA[${batchId}] partially failed.\n`,
206
- messages.join(',\n'));
207
- return false;
208
- }
209
- console.error(
210
- `SA insert conversions [${batchId}] failed.`, error.message);
211
- this.logger.debug(
212
- 'Errors in response:', error.response.data.error);
213
- return false;
214
- });
200
+ /** @const {BatchResult} */
201
+ const batchResult = {
202
+ result: true,
203
+ numberOfLines: lines.length,
204
+ };
205
+ try {
206
+ const response = await this.instance.conversion.insert(
207
+ {requestBody: {conversion: conversions}}
208
+ );
209
+ this.logger.debug('Response: ', response);
210
+ const insertedConversions = response.data.conversion.length;
211
+ if (lines.length !== insertedConversions) {
212
+ const errorMessage =
213
+ `Conversions input/inserted: ${lines.length}/${insertedConversions}`;
214
+ this.logger.warn(errorMessage);
215
+ batchResult.result = false;
216
+ batchResult.numberOfLines = insertedConversions;
217
+ batchResult.errors = [errorMessage];
218
+ }
219
+ this.logger.info(
220
+ `SA[${batchId}] Insert ${insertedConversions} conversions.`);
221
+ return batchResult;
222
+ } catch (error) {
223
+ this.updateBatchResultWithError_(batchResult, error, lines);
224
+ return batchResult;
225
+ }
215
226
  };
216
227
  }
217
228
 
229
+ /**
230
+ * Updates the BatchResult based on errors.
231
+ * There are 3 types of errors here:
232
+ * The first two errors are from 'Standard Error Responses';
233
+ * The last one is normal Javascript Error object.
234
+ *
235
+ * For more details of 'Standard Error Responses', see:
236
+ * https://developers.google.com/search-ads/v2/standard-error-responses
237
+ *
238
+ * Error 1. error code is not 400, e.g. 403 for no access to SA360 API. This
239
+ * error fail the whole process and no need to extract detailed lines.
240
+ * Error 2. error code 400 and 'errors' has one or more lines. Each line might
241
+ * have different failure reason. Failed lines can be extracted to give
242
+ * users more information.
243
+ * Note, some failure reason may fail every line, e.g. wrong
244
+ * 'segmentationName' in the config (Error code '0x0000010E', see:
245
+ * https://developers.google.com/search-ads/v2/troubleshooting#conversion-upload-errors )
246
+ * Error 3. normal JavaScript Error object. No property 'errors'.
247
+ *
248
+ * @param {!BatchResult} batchResult
249
+ * @param {(!GoogleAdsFailure|!Error)} error
250
+ * @param {!Array<string>} lines The original input data.
251
+ * @private
252
+ */
253
+ updateBatchResultWithError_(batchResult, error, lines) {
254
+ batchResult.result = false;
255
+ // Error 3.
256
+ if (!error.errors) {
257
+ batchResult.errors = [error.message || error.toString()];
258
+ return;
259
+ }
260
+ const errorMessages = error.errors.map(({message}) => message);
261
+ // Error 1.
262
+ if (error.code !== 400) {
263
+ batchResult.errors = errorMessages;
264
+ return;
265
+ }
266
+ // Error 2.
267
+ batchResult.failedLines = [];
268
+ batchResult.groupedFailed = {};
269
+ const errors = new Set();
270
+ const messageReg = /.*Details: \[(.*) index=\d+ conversionId=.*/;
271
+ const indexReg = /.*index=(\d*) .*/;
272
+ errorMessages.forEach((message) => {
273
+ const errorMessage = messageReg.exec(message);
274
+ if (errorMessage) {
275
+ const index = indexReg.exec(message);
276
+ const failedLine = lines[index[1]];
277
+ batchResult.failedLines.push(failedLine);
278
+ // error messages have detailed IDs. Need to generalize them.
279
+ const generalMessage =
280
+ errorMessage[1].replace(/ \'[^\']*\'/, '');
281
+ errors.add(generalMessage);
282
+ const groupedFailed = batchResult.groupedFailed[generalMessage] || [];
283
+ groupedFailed.push(failedLine);
284
+ if (groupedFailed.length === 1) {
285
+ batchResult.groupedFailed[generalMessage] = groupedFailed;
286
+ }
287
+ } else {
288
+ errors.add(message);
289
+ }
290
+ });
291
+ batchResult.errors = Array.from(errors);
292
+ }
293
+
218
294
  /**
219
295
  * There are three steps to get asynchronous reports in SA360:
220
296
  * 1. Call Reports.request() to specify the type of data for the report.
@@ -226,15 +302,14 @@ class DoubleClickSearch {
226
302
  * @param {!ReportRequest} requestBody
227
303
  * @return {!Promise<string>}
228
304
  */
229
- requestReports(requestBody) {
230
- return this.instance.reports.request({requestBody})
231
- .then(({status, data}) => {
232
- if (status >= 200 && status < 300) return data.id;
233
- const errorMsg =
234
- `Fail to request reports: ${JSON.stringify(requestBody)}`;
235
- console.error(errorMsg, data);
236
- throw new Error(errorMsg);
237
- });
305
+ async requestReports(requestBody) {
306
+ const {status, data} = await this.instance.reports.request({requestBody});
307
+ if (status >= 200 && status < 300) {
308
+ return data.id;
309
+ }
310
+ const errorMsg = `Fail to request reports: ${JSON.stringify(requestBody)}`;
311
+ this.logger.error(errorMsg, data);
312
+ throw new Error(errorMsg);
238
313
  }
239
314
 
240
315
  /**
@@ -245,24 +320,23 @@ class DoubleClickSearch {
245
320
  * byteCount:string,
246
321
  * }>>}
247
322
  */
248
- getReportUrls(reportId) {
249
- return this.instance.reports.get({reportId})
250
- .then(({status, data}) => {
251
- switch (status) {
252
- case 200:
253
- console.log(
254
- `Report[${reportId}] has ${data.rowCount} rows and ${data.files.length} files.`);
255
- return data.files;
256
- case 202:
257
- console.log(`Report[${reportId}] is not ready.`);
258
- break;
259
- default:
260
- const errorMsg =
261
- `Error in get reports: ${reportId} with status code: ${status}`;
262
- console.error(errorMsg, data);
263
- throw new Error(errorMsg);
264
- }
265
- });
323
+ async getReportUrls(reportId) {
324
+ const {status, data} = await this.instance.reports.get({reportId});
325
+ switch (status) {
326
+ case 200:
327
+ const {rowCount, files} = data;
328
+ this.logger.info(
329
+ `Report[${reportId}] has ${rowCount} rows and ${files.length} files.`);
330
+ return files;
331
+ case 202:
332
+ this.logger.info(`Report[${reportId}] is not ready.`);
333
+ break;
334
+ default:
335
+ const errorMsg =
336
+ `Error in get reports: ${reportId} with status code: ${status}`;
337
+ this.logger.error(errorMsg, data);
338
+ throw new Error(errorMsg);
339
+ }
266
340
  }
267
341
 
268
342
  /**
@@ -271,15 +345,14 @@ class DoubleClickSearch {
271
345
  * @param {number} reportFragment The index (based 0) of report files.
272
346
  * @return {!Promise<string>}
273
347
  */
274
- getReportFile(reportId, reportFragment) {
275
- return this.instance.reports.getFile({reportId, reportFragment})
276
- .then((response) => {
277
- if (response.status === 200) return response.data;
278
- const errorMsg =
279
- `Error in get file from reports: ${reportFragment}@${reportId}`;
280
- console.error(errorMsg, response);
281
- throw new Error(errorMsg);
282
- });
348
+ async getReportFile(reportId, reportFragment) {
349
+ const response = await this.instance.reports.getFile(
350
+ {reportId, reportFragment});
351
+ if (response.status === 200) return response.data;
352
+ const errorMsg =
353
+ `Error in get file from reports: ${reportFragment}@${reportId}`;
354
+ this.logger.error(errorMsg, response);
355
+ throw new Error(errorMsg);
283
356
  }
284
357
 
285
358
  /**
@@ -289,16 +362,15 @@ class DoubleClickSearch {
289
362
  * @param {string} url
290
363
  * @return {!Promise<ReadableStream>}
291
364
  */
292
- getReportFileStream(url) {
293
- return this.auth.getRequestHeaders()
294
- .then((headers) => {
295
- return request({
296
- method: 'GET',
297
- headers,
298
- url,
299
- responseType: 'stream',
300
- });
301
- }).then((response) => response.data);
365
+ async getReportFileStream(url) {
366
+ const headers = await this.auth.getRequestHeaders();
367
+ const response = await request({
368
+ method: 'GET',
369
+ headers,
370
+ url,
371
+ responseType: 'stream',
372
+ });
373
+ return response.data;
302
374
  }
303
375
 
304
376
  }