@google-cloud/nodejs-common 0.9.3-beta → 0.9.4-beta3
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 +1 -1
- package/package.json +1 -1
- package/src/apis/analytics.js +16 -9
- package/src/apis/index.js +0 -10
- package/src/apis/measurement_protocol.js +45 -24
- package/src/apis/measurement_protocol_ga4.js +25 -7
- package/src/apis/spreadsheets.js +69 -88
- package/src/components/utils.js +110 -50
- package/src/apis/youtube.js +0 -145
package/bin/install_functions.sh
CHANGED
package/package.json
CHANGED
package/src/apis/analytics.js
CHANGED
|
@@ -23,7 +23,7 @@ const stream = require('stream');
|
|
|
23
23
|
const {google} = require('googleapis');
|
|
24
24
|
const {Schema$Upload} = google.analytics;
|
|
25
25
|
const AuthClient = require('./auth_client.js');
|
|
26
|
-
const {wait, getLogger} = require('../components/utils.js');
|
|
26
|
+
const {wait, getLogger, BatchResult} = require('../components/utils.js');
|
|
27
27
|
|
|
28
28
|
const API_SCOPES = Object.freeze([
|
|
29
29
|
'https://www.googleapis.com/auth/analytics',
|
|
@@ -81,8 +81,7 @@ class Analytics {
|
|
|
81
81
|
* @param {string|!stream.Readable} data A string or a stream to be uploaded.
|
|
82
82
|
* @param {!DataImportConfig} config GA data import configuration.
|
|
83
83
|
* @param {string=} batchId A tag for log.
|
|
84
|
-
* @return {!
|
|
85
|
-
* succeeded.
|
|
84
|
+
* @return {!BatchResult}
|
|
86
85
|
*/
|
|
87
86
|
async uploadData(data, config, batchId = 'unnamed') {
|
|
88
87
|
const uploadConfig = Object.assign(
|
|
@@ -100,29 +99,37 @@ class Analytics {
|
|
|
100
99
|
this.logger.debug('Response: ', response);
|
|
101
100
|
const job = /** @type {Schema$Upload} */ response.data;
|
|
102
101
|
const uploadId = (/** @type {Schema$Upload} */job).id;
|
|
103
|
-
|
|
102
|
+
this.logger.info(
|
|
103
|
+
`Task [${batchId}] creates GA Data import job: ${uploadId}`);
|
|
104
104
|
const jobConfig = Object.assign({uploadId}, config);
|
|
105
105
|
const result = await Promise.race([
|
|
106
106
|
this.checkJobStatus(jobConfig),
|
|
107
107
|
wait(8 * 60 * 1000, job), // wait up to 8 minutes here
|
|
108
108
|
]);
|
|
109
|
+
/** @type {BatchResult} */ const batchResult = {};
|
|
109
110
|
switch ((/** @type {Schema$Upload} */ result).status) {
|
|
110
111
|
case 'FAILED':
|
|
111
112
|
this.logger.error('GA Data Import failed', result);
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
batchResult.result = false;
|
|
114
|
+
batchResult.errors = result.errors
|
|
115
|
+
|| [`Unknown reason. ID: ${uploadId}`];
|
|
116
|
+
break;
|
|
114
117
|
case 'COMPLETED':
|
|
115
118
|
this.logger.info(`GA Data Import job[${uploadId}] completed.`);
|
|
116
119
|
this.logger.debug('Response: ', result);
|
|
117
|
-
|
|
120
|
+
batchResult.result = true;
|
|
121
|
+
break;
|
|
118
122
|
case 'PENDING':
|
|
119
123
|
this.logger.info('GA Data Import pending.', result);
|
|
120
124
|
this.logger.info('Still will return true here.');
|
|
121
|
-
|
|
125
|
+
batchResult.result = true;
|
|
126
|
+
break;
|
|
122
127
|
default:
|
|
123
128
|
this.logger.error('Unknown results of GA Data Import: ', result);
|
|
124
|
-
|
|
129
|
+
batchResult.result = false;
|
|
130
|
+
batchResult.errors = [`Unknown status. ID: ${uploadId}`];
|
|
125
131
|
}
|
|
132
|
+
return batchResult;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
/**
|
package/src/apis/index.js
CHANGED
|
@@ -110,13 +110,3 @@ 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');
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
22
|
const {request} = require('gaxios');
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
getLogger,
|
|
25
|
+
SendSingleBatch,
|
|
26
|
+
BatchResult,
|
|
27
|
+
} = require('../components/utils.js');
|
|
24
28
|
/** Base URL for Google Analytics service. */
|
|
25
29
|
const BASE_URL = 'https://www.google-analytics.com';
|
|
26
30
|
|
|
@@ -62,9 +66,9 @@ class MeasurementProtocol {
|
|
|
62
66
|
* @param {!Array<string>} lines Data for single request. It should be
|
|
63
67
|
* guaranteed that it doesn't exceed quota limitation.
|
|
64
68
|
* @param {string} batchId The tag for log.
|
|
65
|
-
* @return {!Promise<
|
|
69
|
+
* @return {!Promise<BatchResult>}
|
|
66
70
|
*/
|
|
67
|
-
return (lines, batchId) => {
|
|
71
|
+
return async (lines, batchId) => {
|
|
68
72
|
const payload =
|
|
69
73
|
lines
|
|
70
74
|
.map((line) => {
|
|
@@ -85,30 +89,47 @@ class MeasurementProtocol {
|
|
|
85
89
|
body: payload,
|
|
86
90
|
headers: {'User-Agent': 'Tentacles/MeasurementProtocol-v1'}
|
|
87
91
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.logger.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
92
|
+
const response = await request(requestOptions);
|
|
93
|
+
/** @type {BatchResult} */ const batchResult = {
|
|
94
|
+
numberOfLines: lines.length,
|
|
95
|
+
};
|
|
96
|
+
if (response.status < 200 || response.status >= 300) {
|
|
97
|
+
const errorMessages = [
|
|
98
|
+
`Measurement Protocol [${batchId}] didn't succeed.`,
|
|
99
|
+
`Get response code: ${response.status}`,
|
|
100
|
+
`response: ${response.data}`,
|
|
101
|
+
];
|
|
102
|
+
this.logger.error(errorMessages.join('\n'));
|
|
103
|
+
batchResult.errors = errorMessages;
|
|
104
|
+
batchResult.result = false;
|
|
105
|
+
return batchResult;
|
|
106
|
+
}
|
|
107
|
+
this.logger.debug(`Configuration:`, config);
|
|
108
|
+
this.logger.debug(`Input Data: `, lines);
|
|
109
|
+
this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
|
|
110
|
+
this.logger.debug(response.data);
|
|
111
|
+
// There is not enough information from the non-debug mode.
|
|
112
|
+
if (!this.debugMode) {
|
|
113
|
+
batchResult.result = true;
|
|
114
|
+
} 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);
|
|
128
|
+
}
|
|
129
|
+
return batchResult;
|
|
106
130
|
};
|
|
107
131
|
};
|
|
108
132
|
|
|
109
|
-
static getInstance() {
|
|
110
|
-
return new MeasurementProtocol(process.env['DEBUG'] === 'true');
|
|
111
|
-
}
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
module.exports = {MeasurementProtocol};
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
'use strict';
|
|
21
21
|
|
|
22
22
|
const {request} = require('gaxios');
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
getLogger,
|
|
25
|
+
SendSingleBatch,
|
|
26
|
+
BatchResult,
|
|
27
|
+
} = require('../components/utils.js');
|
|
24
28
|
/** Base URL for Google Analytics service. */
|
|
25
29
|
const BASE_URL = 'https://www.google-analytics.com';
|
|
26
30
|
|
|
@@ -85,7 +89,7 @@ class MeasurementProtocolGA4 {
|
|
|
85
89
|
* @param {!Array<string>} lines Data for single request. It should be
|
|
86
90
|
* guaranteed that it doesn't exceed quota limitation.
|
|
87
91
|
* @param {string} batchId The tag for log.
|
|
88
|
-
* @return {!
|
|
92
|
+
* @return {!BatchResult}
|
|
89
93
|
*/
|
|
90
94
|
return async (lines, batchId) => {
|
|
91
95
|
const line = lines[0]; // Each request contains one record only.
|
|
@@ -104,22 +108,36 @@ class MeasurementProtocolGA4 {
|
|
|
104
108
|
headers: {'User-Agent': 'Tentacles/MeasurementProtocol-GA4'}
|
|
105
109
|
};
|
|
106
110
|
const response = await request(requestOptions);
|
|
111
|
+
/** @type {BatchResult} */ const batchResult = {
|
|
112
|
+
numberOfLines: lines.length,
|
|
113
|
+
};
|
|
107
114
|
if (response.status < 200 || response.status >= 300) {
|
|
108
115
|
const errorMessages = [
|
|
109
116
|
`Measurement Protocol GA4 [${batchId}] didn't succeed.`,
|
|
110
117
|
`Get response code: ${response.status}`,
|
|
111
118
|
`response: ${response.data}`,
|
|
112
119
|
];
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
this.logger.error(errorMessages.join('\n'));
|
|
121
|
+
batchResult.errors = errorMessages;
|
|
122
|
+
batchResult.result = false;
|
|
123
|
+
return batchResult;
|
|
115
124
|
}
|
|
116
125
|
this.logger.debug('Configuration:', config);
|
|
117
126
|
this.logger.debug('Input Data: ', lines);
|
|
118
127
|
this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
|
|
119
|
-
this.logger.debug(
|
|
128
|
+
this.logger.debug(response.data);
|
|
120
129
|
// There is not enough information from the non-debug mode.
|
|
121
|
-
if (!this.debugMode)
|
|
122
|
-
|
|
130
|
+
if (!this.debugMode) {
|
|
131
|
+
batchResult.result = true;
|
|
132
|
+
} else {
|
|
133
|
+
batchResult.result = response.data.validationMessages.length === 0;
|
|
134
|
+
if (!batchResult.result) {
|
|
135
|
+
batchResult.failedLines = lines;
|
|
136
|
+
batchResult.errors = response.data.validationMessages.map(
|
|
137
|
+
({description}) => description);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return batchResult;
|
|
123
141
|
};
|
|
124
142
|
};
|
|
125
143
|
|
package/src/apis/spreadsheets.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const {google} = require('googleapis');
|
|
22
22
|
const {Params$Resource$Spreadsheets$Get} = google.sheets;
|
|
23
23
|
const AuthClient = require('./auth_client.js');
|
|
24
|
-
const {getLogger} = require('../components/utils.js');
|
|
24
|
+
const {getLogger, BatchResult} = require('../components/utils.js');
|
|
25
25
|
|
|
26
26
|
const API_SCOPES = Object.freeze([
|
|
27
27
|
'https://www.googleapis.com/auth/spreadsheets',
|
|
@@ -92,16 +92,15 @@ class Spreadsheets {
|
|
|
92
92
|
* @param {string} sheetName Name of the Sheet.
|
|
93
93
|
* @return {!Promise<number>} Sheet Id.
|
|
94
94
|
*/
|
|
95
|
-
getSheetId(sheetName) {
|
|
95
|
+
async getSheetId(sheetName) {
|
|
96
96
|
const request = /** @type{Params$Resource$Spreadsheets$Get} */ {
|
|
97
97
|
spreadsheetId: this.spreadsheetId,
|
|
98
98
|
ranges: sheetName,
|
|
99
99
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
100
|
+
const response = await this.instance.spreadsheets.get(request);
|
|
101
|
+
const sheet = response.data.sheets[0];
|
|
102
|
+
this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
|
|
103
|
+
return sheet.properties.sheetId;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
/**
|
|
@@ -111,21 +110,20 @@ class Spreadsheets {
|
|
|
111
110
|
* @param {string} sheetName Name of the Sheet.
|
|
112
111
|
* @return {!Promise<boolean>} Whether the operation succeeded.
|
|
113
112
|
*/
|
|
114
|
-
clearSheet(sheetName) {
|
|
113
|
+
async clearSheet(sheetName) {
|
|
115
114
|
const request = {
|
|
116
115
|
spreadsheetId: this.spreadsheetId,
|
|
117
116
|
range: sheetName,
|
|
118
117
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
118
|
+
try {
|
|
119
|
+
const response = await this.instance.spreadsheets.values.clear(request);
|
|
120
|
+
const data = response.data;
|
|
121
|
+
this.logger.debug(`Clear sheet[${sheetName}}]: `, data);
|
|
122
|
+
return true;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.logger.error(error);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
/**
|
|
@@ -147,26 +145,13 @@ class Spreadsheets {
|
|
|
147
145
|
* @private
|
|
148
146
|
*/
|
|
149
147
|
getChangeDimensionRequest_(sheetId, dimension, current, target) {
|
|
150
|
-
if (current === target)
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
148
|
+
if (current === target) return;
|
|
153
149
|
if (current < target) { // Appends dimension.
|
|
154
|
-
return {
|
|
155
|
-
appendDimension: {
|
|
156
|
-
sheetId: sheetId,
|
|
157
|
-
dimension: dimension,
|
|
158
|
-
length: target - current,
|
|
159
|
-
}
|
|
160
|
-
};
|
|
150
|
+
return {appendDimension: {sheetId, dimension, length: target - current}};
|
|
161
151
|
} else { // Deletes dimension.
|
|
162
152
|
return {
|
|
163
153
|
deleteDimension: {
|
|
164
|
-
range: {
|
|
165
|
-
sheetId: sheetId,
|
|
166
|
-
dimension: dimension,
|
|
167
|
-
startIndex: target,
|
|
168
|
-
endIndex: current,
|
|
169
|
-
}
|
|
154
|
+
range: {sheetId, dimension, startIndex: target, endIndex: current},
|
|
170
155
|
}
|
|
171
156
|
};
|
|
172
157
|
}
|
|
@@ -179,49 +164,44 @@ class Spreadsheets {
|
|
|
179
164
|
* @param {number} targetColumns Loaded data columns number.
|
|
180
165
|
* @return {!Promise<boolean>} Whether the operation succeeded.
|
|
181
166
|
*/
|
|
182
|
-
reshape(sheetName, targetRows, targetColumns) {
|
|
167
|
+
async reshape(sheetName, targetRows, targetColumns) {
|
|
183
168
|
const request = /** @type{Params$Resource$Spreadsheets$Get} */ {
|
|
184
169
|
spreadsheetId: this.spreadsheetId,
|
|
185
170
|
ranges: sheetName,
|
|
186
171
|
};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
})
|
|
221
|
-
.catch((error) => {
|
|
222
|
-
console.error(error);
|
|
223
|
-
return false;
|
|
224
|
-
});
|
|
172
|
+
try {
|
|
173
|
+
const response = await this.instance.spreadsheets.get(request);
|
|
174
|
+
const sheet = response.data.sheets[0];
|
|
175
|
+
const sheetId = sheet.properties.sheetId;
|
|
176
|
+
const rowCount = sheet.properties.gridProperties.rowCount;
|
|
177
|
+
const columnCount = sheet.properties.gridProperties.columnCount;
|
|
178
|
+
this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
|
|
179
|
+
const requests = {
|
|
180
|
+
spreadsheetId: this.spreadsheetId,
|
|
181
|
+
resource: {requests: []},
|
|
182
|
+
};
|
|
183
|
+
if (rowCount !== targetRows) {
|
|
184
|
+
requests.resource.requests.push(this.getChangeDimensionRequest_(
|
|
185
|
+
sheetId, 'ROWS', rowCount, targetRows));
|
|
186
|
+
}
|
|
187
|
+
if (columnCount !== targetColumns) {
|
|
188
|
+
requests.resource.requests.push(this.getChangeDimensionRequest_(
|
|
189
|
+
sheetId, 'COLUMNS', columnCount, targetColumns));
|
|
190
|
+
}
|
|
191
|
+
this.logger.debug(`Reshape Sheet from [${rowCount}, ${
|
|
192
|
+
columnCount}] to [${targetRows}, ${targetColumns}]`,
|
|
193
|
+
JSON.stringify(requests.resource.requests));
|
|
194
|
+
if (requests.resource.requests.length > 0) {
|
|
195
|
+
const {data} = await this.instance.spreadsheets.batchUpdate(requests);
|
|
196
|
+
this.logger.debug(`Reshape Sheet [${sheetName}]: `, data);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
this.logger.debug('No need to reshape.');
|
|
200
|
+
return true;
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(error);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
225
205
|
}
|
|
226
206
|
|
|
227
207
|
/**
|
|
@@ -230,25 +210,26 @@ class Spreadsheets {
|
|
|
230
210
|
* @param {!ParseDataRequest} config A ParseDataRequest object template. The
|
|
231
211
|
* data will be put in before it is send out through Sheets batchUpdate.
|
|
232
212
|
* @param {string=} batchId The tag for log.
|
|
233
|
-
* @return {!
|
|
234
|
-
* succeeded.
|
|
213
|
+
* @return {!BatchResult}
|
|
235
214
|
*/
|
|
236
|
-
loadData(data, config, batchId = 'unnamed') {
|
|
237
|
-
const pasteData = Object.assign({}, config, {data
|
|
215
|
+
async loadData(data, config, batchId = 'unnamed') {
|
|
216
|
+
const pasteData = Object.assign({}, config, {data});
|
|
238
217
|
const request = {
|
|
239
218
|
spreadsheetId: this.spreadsheetId,
|
|
240
|
-
resource: {requests: [{pasteData
|
|
219
|
+
resource: {requests: [{pasteData}]},
|
|
241
220
|
};
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
221
|
+
/** @type {BatchResult} */ const batchResult = {};
|
|
222
|
+
try {
|
|
223
|
+
const response = await this.instance.spreadsheets.batchUpdate(request);
|
|
224
|
+
const data = response.data;
|
|
225
|
+
this.logger.debug(`Batch[${batchId}] uploaded: `, data);
|
|
226
|
+
batchResult.result = true;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.logger.error(error);
|
|
229
|
+
batchResult.result = false;
|
|
230
|
+
batchResult.errors = [error.toString()];
|
|
231
|
+
}
|
|
232
|
+
return batchResult;
|
|
252
233
|
}
|
|
253
234
|
}
|
|
254
235
|
|
package/src/components/utils.js
CHANGED
|
@@ -23,12 +23,27 @@ const {inspect} = require('util');
|
|
|
23
23
|
const {LoggingWinston} = require('@google-cloud/logging-winston');
|
|
24
24
|
const {CloudPlatformApis} = require('../apis/cloud_platform_apis.js');
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The result of a batch of data sent to target API. The batch here means the
|
|
28
|
+
* data that will be sent out in one single request.
|
|
29
|
+
* Some APIs upload whole file. In this case, there will be not 'numberOfLines'
|
|
30
|
+
* or 'failedLines'.
|
|
31
|
+
* @typedef {{
|
|
32
|
+
* result: boolean,
|
|
33
|
+
* numberOfLines: (number|undefined),
|
|
34
|
+
* failedLines: (Array<string>|undefined),
|
|
35
|
+
* errors: (Array<string>|undefined),
|
|
36
|
+
* output: (Array<string>|undefined),
|
|
37
|
+
* }}
|
|
38
|
+
*/
|
|
39
|
+
let BatchResult;
|
|
40
|
+
|
|
26
41
|
/**
|
|
27
42
|
* Function which sends a batch of data. Takes two parameters:
|
|
28
43
|
* {!Array<string>} Data for single request. It should be guaranteed that it
|
|
29
44
|
* doesn't exceed quota limitation.
|
|
30
45
|
* {string} The tag for log.
|
|
31
|
-
* @typedef {function(!Array<string>,string): !
|
|
46
|
+
* @typedef {function(!Array<string>,string): !BatchResult}
|
|
32
47
|
*/
|
|
33
48
|
let SendSingleBatch;
|
|
34
49
|
|
|
@@ -162,6 +177,44 @@ const splitArray = (records, splitSize) => {
|
|
|
162
177
|
return results;
|
|
163
178
|
};
|
|
164
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Merges an array of API results (BatchResult) in to a single one.
|
|
182
|
+
*
|
|
183
|
+
* @param {!Array<!BatchResult>} batchResults
|
|
184
|
+
* @param {string} batchPrefix Prefix tag for this batch, used for logging.
|
|
185
|
+
* @return {!BatchResult}
|
|
186
|
+
*/
|
|
187
|
+
const mergeBatchResults = (batchResults, batchPrefix) => {
|
|
188
|
+
const logger = getLogger('SPEED_CTL');
|
|
189
|
+
/** @const {!BatchResult} */ const mergedResult = {
|
|
190
|
+
result: true,
|
|
191
|
+
numberOfLines: 0,
|
|
192
|
+
failedLines: [],
|
|
193
|
+
errors: [],
|
|
194
|
+
output: []
|
|
195
|
+
};
|
|
196
|
+
batchResults.forEach((batchResult, index) => {
|
|
197
|
+
const batchId = `${batchPrefix}-${index}`;
|
|
198
|
+
const {result, numberOfLines, failedLines, errors, output} = batchResult;
|
|
199
|
+
if (logger.isDebugEnabled()) {
|
|
200
|
+
logger.debug(
|
|
201
|
+
` Task [${batchId}] has ${numberOfLines} lines: ${result
|
|
202
|
+
? 'succeeded' : 'failed'}.`);
|
|
203
|
+
if (!result) {
|
|
204
|
+
logger.debug(` Errors: ${errors.join('\n')}`);
|
|
205
|
+
logger.debug(` Failed lines: ${failedLines.join('\n')}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
mergedResult.result = mergedResult.result && result;
|
|
209
|
+
mergedResult.numberOfLines += numberOfLines;
|
|
210
|
+
mergedResult.failedLines = mergedResult.failedLines.concat(
|
|
211
|
+
failedLines || []);
|
|
212
|
+
mergedResult.errors = mergedResult.errors.concat(errors || []);
|
|
213
|
+
mergedResult.output = mergedResult.output.concat(output || []);
|
|
214
|
+
});
|
|
215
|
+
return mergedResult;
|
|
216
|
+
};
|
|
217
|
+
|
|
165
218
|
/**
|
|
166
219
|
* Sends a round of data in multiple batches (requests). Number of records in
|
|
167
220
|
* every batch is defined by 'recordSize'. All these requests will be send out
|
|
@@ -175,45 +228,28 @@ const splitArray = (records, splitSize) => {
|
|
|
175
228
|
* single request.
|
|
176
229
|
* @param {number} qps Queries per second.
|
|
177
230
|
* @param {string} roundId Round ID for log.
|
|
178
|
-
* @return {!Promise
|
|
231
|
+
* @return {!Promise<!Array<!BatchResult>>}
|
|
179
232
|
* @private
|
|
180
233
|
*/
|
|
181
|
-
const sendSingleRound = (sendingFn, sliced, recordSize, qps, roundId) => {
|
|
234
|
+
const sendSingleRound = async (sendingFn, sliced, recordSize, qps, roundId) => {
|
|
182
235
|
const logger = getLogger('SPEED_CTL');
|
|
183
236
|
const batchArray = splitArray(sliced, recordSize);
|
|
184
237
|
const securedDefer = Math.ceil(batchArray.length / qps * 1000);
|
|
185
|
-
logger.debug(
|
|
186
|
-
batchArray.length} requests/batches
|
|
187
|
-
const deferPromise =
|
|
238
|
+
logger.debug(
|
|
239
|
+
`Task round[${roundId}] has ${batchArray.length} requests/batches:`);
|
|
240
|
+
const deferPromise = async () => {
|
|
241
|
+
await wait(securedDefer);
|
|
188
242
|
logger.debug(`Task round[${roundId}] is secured for ${securedDefer} ms.`);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return batchResult;
|
|
198
|
-
}));
|
|
199
|
-
});
|
|
200
|
-
return Promise.all(batchPromises).then((results) => {
|
|
201
|
-
const roundResult = !results.includes(false);
|
|
202
|
-
logger.debug(
|
|
203
|
-
`Task round[${roundId}] ${roundResult ? 'succeeded' : 'failed'}.`);
|
|
204
|
-
if (!roundResult) {
|
|
205
|
-
console.log(
|
|
206
|
-
`Task round[${roundId}] has batch(es) failed batches:`, results);
|
|
207
|
-
}
|
|
208
|
-
return roundResult;
|
|
209
|
-
});
|
|
243
|
+
};
|
|
244
|
+
const batchPromises = batchArray.map(
|
|
245
|
+
(batch, index) => sendingFn(batch, `${roundId}-${index}`));
|
|
246
|
+
const [batchResults] = await Promise.all([
|
|
247
|
+
Promise.all(batchPromises),
|
|
248
|
+
deferPromise(),
|
|
249
|
+
]);
|
|
250
|
+
return batchResults;
|
|
210
251
|
};
|
|
211
|
-
|
|
212
|
-
// Ideally return every error line's result.
|
|
213
|
-
// pseudo code:
|
|
214
|
-
// p.then((previousResult)=>{
|
|
215
|
-
// return thisPromise().then((newValue) => value_merge_newValue_with_previousResult);
|
|
216
|
-
// }
|
|
252
|
+
|
|
217
253
|
/**
|
|
218
254
|
* Some APIs will have the limitations for:
|
|
219
255
|
* 1. The number of conversions for each request;
|
|
@@ -225,11 +261,18 @@ const sendSingleRound = (sendingFn, sliced, recordSize, qps, roundId) => {
|
|
|
225
261
|
* @param {number=} numberOfThreads The number of requests will be fired
|
|
226
262
|
* simultaneously.
|
|
227
263
|
* @param {number=} qps Queries per second.
|
|
264
|
+
* @param {function(!Array<!BatchResult>, string):!BatchResult} mergeFn
|
|
265
|
+
* The function to merge the array of result of each request.
|
|
266
|
+
* @param {number} timeout The time (seconds) that this function can run. This
|
|
267
|
+
* is used to evaluate whether the given data could be processed with the
|
|
268
|
+
* given 'qps' and 'recordSize' in time.
|
|
228
269
|
* @return {function(!SendSingleBatch,(string|!Array<string>),string=):
|
|
229
|
-
* !
|
|
270
|
+
* !BatchResult} Speed and content managed sending function.
|
|
230
271
|
*/
|
|
231
|
-
const apiSpeedControl = (recordSize = 1, numberOfThreads = 1, qps = 1
|
|
272
|
+
const apiSpeedControl = (recordSize = 1, numberOfThreads = 1, qps = 1,
|
|
273
|
+
mergeFn = mergeBatchResults, timeout = 540) => {
|
|
232
274
|
const roundSize = recordSize * numberOfThreads;
|
|
275
|
+
const maxRecords = qps * recordSize * timeout;
|
|
233
276
|
/**
|
|
234
277
|
* Returns a sending function with speed and content managed.
|
|
235
278
|
* @param {!SendSingleBatch} sendingFn Function to send out a
|
|
@@ -239,27 +282,42 @@ const apiSpeedControl = (recordSize = 1, numberOfThreads = 1, qps = 1) => {
|
|
|
239
282
|
* time, the element in Array<string> is expected to be in the format of
|
|
240
283
|
* JSON object.
|
|
241
284
|
* @param {string=} taskId Task ID for log.
|
|
242
|
-
* @return {!
|
|
285
|
+
* @return {!BatchResult}
|
|
243
286
|
*/
|
|
244
|
-
return (sendingFn, data, taskId = 'unnamed') => {
|
|
287
|
+
return async (sendingFn, data, taskId = 'unnamed') => {
|
|
288
|
+
const logger = getLogger('SPEED_CTL');
|
|
245
289
|
const records = Array.isArray(data) ?
|
|
246
290
|
data :
|
|
247
291
|
data.split('\n').filter((line) => line.trim() !== '');
|
|
292
|
+
if (maxRecords < data.length) {
|
|
293
|
+
const error = `Predicted timeout: ${records.length} records with config: `
|
|
294
|
+
+ `${recordSize} records/request and ${qps} QPS in ${timeout} sec.`;
|
|
295
|
+
logger.error(error);
|
|
296
|
+
return {
|
|
297
|
+
result: false,
|
|
298
|
+
errors: [error],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
248
301
|
const roundArray = splitArray(records, roundSize);
|
|
249
|
-
|
|
250
|
-
roundArray.length} rounds.`);
|
|
251
|
-
|
|
252
|
-
|
|
302
|
+
logger.debug(
|
|
303
|
+
`Task [${taskId}] has ${records.length} records in ${roundArray.length} rounds.`);
|
|
304
|
+
const reduceFn = async (previous, roundData, index) => {
|
|
305
|
+
const results = await previous;
|
|
253
306
|
const roundId = `${taskId}-${index}`;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
307
|
+
/** @const {!Array<!BatchResult>} */ const roundResult =
|
|
308
|
+
await sendSingleRound(sendingFn, roundData, recordSize, qps, roundId);
|
|
309
|
+
/** @const {!BatchResult} */
|
|
310
|
+
const mergedRoundResult = mergeFn(roundResult, roundId);
|
|
311
|
+
return results.concat(mergedRoundResult);
|
|
312
|
+
};
|
|
313
|
+
/** @const {!Array<!BatchResult>} */
|
|
314
|
+
const taskResult = await roundArray.reduce(reduceFn, []);
|
|
315
|
+
/** @const {!BatchResult} */
|
|
316
|
+
const mergedTaskResult = mergeBatchResults(taskResult, taskId);
|
|
317
|
+
logger.debug(
|
|
318
|
+
`Task [${taskId}]: ${mergedTaskResult.result ? 'succeeded' : 'failed'}.`
|
|
319
|
+
);
|
|
320
|
+
return mergedTaskResult;
|
|
263
321
|
};
|
|
264
322
|
};
|
|
265
323
|
|
|
@@ -463,7 +521,9 @@ const changeNamingFromSnakeToLowerCamel = (name) => {
|
|
|
463
521
|
module.exports = {
|
|
464
522
|
getLogger,
|
|
465
523
|
wait,
|
|
524
|
+
BatchResult,
|
|
466
525
|
SendSingleBatch,
|
|
526
|
+
sendSingleRound,
|
|
467
527
|
apiSpeedControl,
|
|
468
528
|
splitArray,
|
|
469
529
|
getProperValue,
|
package/src/apis/youtube.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
// Copyright 2021 Google Inc.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @fileoverview Youtube API Client Library.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
'use strict';
|
|
20
|
-
|
|
21
|
-
const {google} = require('googleapis');
|
|
22
|
-
const {Schema$Channel, Schema$Video} = google.youtube;
|
|
23
|
-
const AuthClient = require('./auth_client.js');
|
|
24
|
-
const {getLogger} = require('../components/utils.js');
|
|
25
|
-
|
|
26
|
-
const API_SCOPES = Object.freeze([
|
|
27
|
-
'https://www.googleapis.com/auth/youtube.readonly'
|
|
28
|
-
]);
|
|
29
|
-
const API_VERSION = 'v3';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Configuration for listing youtube channels.
|
|
33
|
-
* @see https://developers.google.com/youtube/v3/docs/channels/list
|
|
34
|
-
* @typedef {{
|
|
35
|
-
* part: Array<string>,
|
|
36
|
-
* categoryId: (string|undefined),
|
|
37
|
-
* forUsername: (string|undefined),
|
|
38
|
-
* id: (string|undefined),
|
|
39
|
-
* managedByMe: (boolean|undefined),
|
|
40
|
-
* mine: (boolean|undefined),
|
|
41
|
-
* hl: (string|undefined),
|
|
42
|
-
* onBehalfOfContentOwner: (string|undefined),
|
|
43
|
-
* pageToken: (string|undefined),
|
|
44
|
-
* }}
|
|
45
|
-
*/
|
|
46
|
-
let ListChannelsConfig;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Configuration for listing youtube videos.
|
|
50
|
-
* @see https://developers.google.com/youtube/v3/docs/videos/list
|
|
51
|
-
* @typedef {{
|
|
52
|
-
* part: Array<string>,
|
|
53
|
-
* id: (string|undefined),
|
|
54
|
-
* chart: (string|undefined),
|
|
55
|
-
* hl: (string|undefined),
|
|
56
|
-
* maxHeight: (unsigned integer|undefined),
|
|
57
|
-
* maxResults: (unsigned integer|undefined),
|
|
58
|
-
* maxWidth: (unsigned integer|undefined),
|
|
59
|
-
* onBehalfOfContentOwner: (string|undefined),
|
|
60
|
-
* pageToken: (string|undefined),
|
|
61
|
-
* regionCode: (string|undefined),
|
|
62
|
-
* }}
|
|
63
|
-
*/
|
|
64
|
-
let ListVideosConfig;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Youtube API v3 stub.
|
|
68
|
-
* See: https://developers.google.com/youtube/v3/docs
|
|
69
|
-
* Channel list type definition, see:
|
|
70
|
-
* https://developers.google.com/youtube/v3/docs/channels/list
|
|
71
|
-
* Video list type definition, see:
|
|
72
|
-
* https://developers.google.com/youtube/v3/docs/videos/list
|
|
73
|
-
*/
|
|
74
|
-
class YouTube {
|
|
75
|
-
constructor() {
|
|
76
|
-
const authClient = new AuthClient(API_SCOPES);
|
|
77
|
-
this.auth = authClient.getDefaultAuth();
|
|
78
|
-
/** @const {!google.youtube} */
|
|
79
|
-
this.instance = google.youtube({
|
|
80
|
-
version: API_VERSION,
|
|
81
|
-
});
|
|
82
|
-
/**
|
|
83
|
-
* Logger object from 'log4js' package where this type is not exported.
|
|
84
|
-
*/
|
|
85
|
-
this.logger = getLogger('API.YT');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Returns a collection of zero or more channel resources that match the
|
|
90
|
-
* request criteria.
|
|
91
|
-
* @see https://developers.google.com/youtube/v3/docs/channels/list
|
|
92
|
-
* @param {!ListChannelsConfig} config List channels configuration.
|
|
93
|
-
* @return {!Promise<Array<Schema$Channel>>}
|
|
94
|
-
*/
|
|
95
|
-
async listChannels(config) {
|
|
96
|
-
const channelListRequest = Object.assign({
|
|
97
|
-
auth: this.auth,
|
|
98
|
-
}, config);
|
|
99
|
-
channelListRequest.part = channelListRequest.part.join(',')
|
|
100
|
-
try {
|
|
101
|
-
const response = await this.instance.channels.list(channelListRequest);
|
|
102
|
-
this.logger.debug('Response: ', response);
|
|
103
|
-
return response.data.items;
|
|
104
|
-
} catch (error) {
|
|
105
|
-
const errorMsg =
|
|
106
|
-
`Fail to list channels: ${JSON.stringify(channelListRequest)}`;
|
|
107
|
-
this.logger.error('YouTube list channels failed.', error.message);
|
|
108
|
-
this.logger.debug('Errors in response:', error);
|
|
109
|
-
throw new Error(errorMsg);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Returns a list of videos that match the API request parameters.
|
|
115
|
-
* @see https://developers.google.com/youtube/v3/docs/videos/list
|
|
116
|
-
* @param {!ListVideosConfig} config List videos configuration.
|
|
117
|
-
* @return {!Promise<Array<Schema$Video>>}
|
|
118
|
-
*/
|
|
119
|
-
async listVideos(config) {
|
|
120
|
-
const videoListRequest = Object.assign({
|
|
121
|
-
auth: this.auth,
|
|
122
|
-
}, config);
|
|
123
|
-
videoListRequest.part = videoListRequest.part.join(',')
|
|
124
|
-
try {
|
|
125
|
-
const response = await this.instance.videos.list(videoListRequest);
|
|
126
|
-
this.logger.debug('Response: ', response);
|
|
127
|
-
return response.data.items;
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const errorMsg =
|
|
130
|
-
`Fail to list videos: ${JSON.stringify(videoListRequest)}`;
|
|
131
|
-
this.logger.error('YouTube list videos failed.', error.message);
|
|
132
|
-
this.logger.debug('Errors in response:', error);
|
|
133
|
-
throw new Error(errorMsg);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
module.exports = {
|
|
140
|
-
YouTube,
|
|
141
|
-
ListChannelsConfig,
|
|
142
|
-
ListVideosConfig,
|
|
143
|
-
API_VERSION,
|
|
144
|
-
API_SCOPES,
|
|
145
|
-
};
|