@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.
- package/bin/install_functions.sh +4 -0
- package/package.json +3 -2
- package/src/apis/auth_client.js +15 -0
- package/src/apis/dfa_reporting.js +67 -19
- package/src/apis/doubleclick_search.js +161 -89
- package/src/apis/google_ads.js +198 -46
- package/src/apis/index.js +10 -0
- package/src/apis/measurement_protocol.js +45 -25
- package/src/apis/measurement_protocol_ga4.js +7 -1
- package/src/apis/spreadsheets.js +13 -13
- package/src/apis/youtube.js +356 -0
- package/src/components/utils.js +47 -12
package/bin/install_functions.sh
CHANGED
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@google-cloud/nodejs-common",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5-beta2",
|
|
4
4
|
"description": "A NodeJs common library for solutions based on Cloud Functions",
|
|
5
5
|
"author": "Google Inc.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"google-auth-library": "^7.1.0",
|
|
31
31
|
"googleapis": "^74.2.0",
|
|
32
32
|
"soap": "^0.38.0",
|
|
33
|
-
"winston": "^3.3.3"
|
|
33
|
+
"winston": "^3.3.3",
|
|
34
|
+
"lodash": "^4.17.21"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"jasmine": "^3.5.0"
|
package/src/apis/auth_client.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
83
|
-
|
|
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<
|
|
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
|
-
|
|
164
|
-
|
|
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
|
|
180
|
+
return batchResult;
|
|
175
181
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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.
|
|
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.
|
|
287
|
+
* @see https://developers.google.com/doubleclick-advertisers/rest/v3.5/reports/get
|
|
240
288
|
*
|
|
241
289
|
* @param {{
|
|
242
290
|
* accountId:(string|undefined),
|
|
@@ -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 {
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
153
|
+
availabilities.forEach((availability) => {
|
|
144
154
|
availability.availabilityTimestamp = availabilityTimestamp;
|
|
145
|
-
}
|
|
155
|
+
});
|
|
146
156
|
this.logger.debug('Sending out availabilities', availabilities);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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<
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
}
|