testbeats 2.0.8 → 2.0.9
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/package.json +1 -1
- package/src/beats/beats.api.js +16 -1
- package/src/beats/beats.js +29 -0
- package/src/beats/beats.types.d.ts +15 -0
- package/src/commands/publish.command.js +31 -2
- package/src/extensions/base.extension.js +16 -0
- package/src/extensions/error-clusters.extension.js +46 -0
- package/src/extensions/index.js +3 -0
- package/src/extensions/smart-analysis.extension.js +22 -2
- package/src/helpers/constants.js +1 -0
- package/src/index.d.ts +1 -0
- package/src/targets/chat.js +24 -0
- package/src/targets/index.js +9 -1
- package/src/targets/slack.js +38 -0
- package/src/targets/teams.js +33 -0
package/package.json
CHANGED
package/src/beats/beats.api.js
CHANGED
|
@@ -25,7 +25,7 @@ class BeatsApi {
|
|
|
25
25
|
*/
|
|
26
26
|
getTestRun(run_id) {
|
|
27
27
|
return request.get({
|
|
28
|
-
url: `${this.getBaseUrl()}/api/core/v1/test-runs
|
|
28
|
+
url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}`,
|
|
29
29
|
headers: {
|
|
30
30
|
'x-api-key': this.config.api_key
|
|
31
31
|
}
|
|
@@ -46,6 +46,21 @@ class BeatsApi {
|
|
|
46
46
|
getBaseUrl() {
|
|
47
47
|
return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
*
|
|
52
|
+
* @param {string} run_id
|
|
53
|
+
* @param {number} limit
|
|
54
|
+
* @returns {import('./beats.types').IErrorClustersResponse}
|
|
55
|
+
*/
|
|
56
|
+
getErrorClusters(run_id, limit = 3) {
|
|
57
|
+
return request.get({
|
|
58
|
+
url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}/error-clusters?limit=${limit}`,
|
|
59
|
+
headers: {
|
|
60
|
+
'x-api-key': this.config.api_key
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
module.exports = { BeatsApi }
|
package/src/beats/beats.js
CHANGED
|
@@ -33,6 +33,7 @@ class Beats {
|
|
|
33
33
|
this.#updateTitleLink();
|
|
34
34
|
await this.#attachFailureSummary();
|
|
35
35
|
await this.#attachSmartAnalysis();
|
|
36
|
+
await this.#attachErrorClusters();
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
#setCIInfo() {
|
|
@@ -189,6 +190,34 @@ class Beats {
|
|
|
189
190
|
logger.warn(`🙈 ${text} not generated in given time`);
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
async #attachErrorClusters() {
|
|
194
|
+
if (!this.test_run_id) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!this.config.targets) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.result.status !== 'FAIL') {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (this.config.show_error_clusters === false) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
logger.info('🧮 Fetching Error Clusters...');
|
|
208
|
+
const res = await this.api.getErrorClusters(this.test_run_id, 3);
|
|
209
|
+
this.config.extensions.push({
|
|
210
|
+
name: 'error-clusters',
|
|
211
|
+
hook: HOOK.AFTER_SUMMARY,
|
|
212
|
+
inputs: {
|
|
213
|
+
data: res.values
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
logger.error(`❌ Unable to attach error clusters: ${error.message}`, error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
192
221
|
}
|
|
193
222
|
|
|
194
223
|
module.exports = { Beats }
|
|
@@ -17,3 +17,18 @@ export type IBeatExecutionMetric = {
|
|
|
17
17
|
test_run_id: string
|
|
18
18
|
org_id: string
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export type IPaginatedAPIResponse<T> = {
|
|
22
|
+
page: number
|
|
23
|
+
limit: number
|
|
24
|
+
total: number
|
|
25
|
+
values: T[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type IErrorClustersResponse = {} & IPaginatedAPIResponse<IErrorCluster>;
|
|
29
|
+
|
|
30
|
+
export type IErrorCluster = {
|
|
31
|
+
test_failure_id: string
|
|
32
|
+
failure: string
|
|
33
|
+
count: number
|
|
34
|
+
}
|
|
@@ -18,6 +18,7 @@ class PublishCommand {
|
|
|
18
18
|
*/
|
|
19
19
|
constructor(opts) {
|
|
20
20
|
this.opts = opts;
|
|
21
|
+
this.errors = [];
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async publish() {
|
|
@@ -31,7 +32,7 @@ class PublishCommand {
|
|
|
31
32
|
this.#validateConfig();
|
|
32
33
|
this.#processResults();
|
|
33
34
|
await this.#publishResults();
|
|
34
|
-
|
|
35
|
+
await this.#publishErrors();
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
#validateEnvDetails() {
|
|
@@ -184,13 +185,24 @@ class PublishCommand {
|
|
|
184
185
|
} else if (result_options.type === 'jmeter') {
|
|
185
186
|
this.results.push(prp.parse(result_options));
|
|
186
187
|
} else {
|
|
187
|
-
|
|
188
|
+
const { result, errors } = trp.parseV2(result_options);
|
|
189
|
+
if (result) {
|
|
190
|
+
this.results.push(result);
|
|
191
|
+
}
|
|
192
|
+
if (errors) {
|
|
193
|
+
this.errors = this.errors.concat(errors);
|
|
194
|
+
}
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
async #publishResults() {
|
|
201
|
+
if (!this.results.length) {
|
|
202
|
+
logger.warn('⚠️ No results to publish');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
194
206
|
for (const config of this.configs) {
|
|
195
207
|
for (let i = 0; i < this.results.length; i++) {
|
|
196
208
|
const result = this.results[i];
|
|
@@ -207,6 +219,23 @@ class PublishCommand {
|
|
|
207
219
|
}
|
|
208
220
|
}
|
|
209
221
|
}
|
|
222
|
+
logger.info('✅ Results published successfully!');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async #publishErrors() {
|
|
226
|
+
if (!this.errors.length) {
|
|
227
|
+
logger.debug('⚠️ No errors to publish');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
logger.info('🛑 Publishing errors...');
|
|
231
|
+
for (const config of this.configs) {
|
|
232
|
+
if (config.targets) {
|
|
233
|
+
for (const target of config.targets) {
|
|
234
|
+
await target_manager.handleErrors({ target, errors: this.errors });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw new Error(this.errors.join('\n'));
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
}
|
|
@@ -86,6 +86,22 @@ class BaseExtension {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* @param {string|number} text
|
|
91
|
+
*/
|
|
92
|
+
bold(text) {
|
|
93
|
+
switch (this.target.name) {
|
|
94
|
+
case 'teams':
|
|
95
|
+
return `**${text}**`;
|
|
96
|
+
case 'slack':
|
|
97
|
+
return `*${text}*`;
|
|
98
|
+
case 'chat':
|
|
99
|
+
return `<b>${text}</b>`;
|
|
100
|
+
default:
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
module.exports = { BaseExtension }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { BaseExtension } = require('./base.extension');
|
|
2
|
+
const { STATUS, HOOK } = require("../helpers/constants");
|
|
3
|
+
|
|
4
|
+
class ErrorClustersExtension extends BaseExtension {
|
|
5
|
+
|
|
6
|
+
constructor(target, extension, result, payload, root_payload) {
|
|
7
|
+
super(target, extension, result, payload, root_payload);
|
|
8
|
+
this.#setDefaultOptions();
|
|
9
|
+
this.#setDefaultInputs();
|
|
10
|
+
this.updateExtensionInputs();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
run() {
|
|
14
|
+
this.#setText();
|
|
15
|
+
this.attach();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#setDefaultOptions() {
|
|
19
|
+
this.default_options.hook = HOOK.AFTER_SUMMARY,
|
|
20
|
+
this.default_options.condition = STATUS.PASS_OR_FAIL;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#setDefaultInputs() {
|
|
24
|
+
this.default_inputs.title = 'Top Errors';
|
|
25
|
+
this.default_inputs.title_link = '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#setText() {
|
|
29
|
+
const data = this.extension.inputs.data;
|
|
30
|
+
if (!data || !data.length) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const clusters = data;
|
|
35
|
+
|
|
36
|
+
this.extension.inputs.title = `Top ${clusters.length} Errors`;
|
|
37
|
+
|
|
38
|
+
const texts = [];
|
|
39
|
+
for (const cluster of clusters) {
|
|
40
|
+
texts.push(`${this.bold(`(${cluster.count})`)} - ${cluster.failure}`);
|
|
41
|
+
}
|
|
42
|
+
this.text = this.mergeTexts(texts);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { ErrorClustersExtension }
|
package/src/extensions/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const { CustomExtension } = require('./custom.extension');
|
|
|
12
12
|
const { EXTENSION } = require('../helpers/constants');
|
|
13
13
|
const { checkCondition } = require('../helpers/helper');
|
|
14
14
|
const logger = require('../utils/logger');
|
|
15
|
+
const { ErrorClustersExtension } = require('./error-clusters.extension');
|
|
15
16
|
|
|
16
17
|
async function run(options) {
|
|
17
18
|
const { target, result, hook } = options;
|
|
@@ -60,6 +61,8 @@ function getExtensionRunner(extension, options) {
|
|
|
60
61
|
return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
|
|
61
62
|
case EXTENSION.SMART_ANALYSIS:
|
|
62
63
|
return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
|
|
64
|
+
case EXTENSION.ERROR_CLUSTERS:
|
|
65
|
+
return new ErrorClustersExtension(options.target, extension, options.result, options.payload, options.root_payload);
|
|
63
66
|
default:
|
|
64
67
|
return require(extension.name);
|
|
65
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { BaseExtension } = require('./base.extension');
|
|
2
2
|
const { STATUS, HOOK } = require("../helpers/constants");
|
|
3
|
+
const logger = require('../utils/logger');
|
|
3
4
|
|
|
4
5
|
class SmartAnalysisExtension extends BaseExtension {
|
|
5
6
|
|
|
@@ -21,7 +22,7 @@ class SmartAnalysisExtension extends BaseExtension {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
#setDefaultInputs() {
|
|
24
|
-
this.default_inputs.title = '
|
|
25
|
+
this.default_inputs.title = '';
|
|
25
26
|
this.default_inputs.title_link = '';
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -37,6 +38,11 @@ class SmartAnalysisExtension extends BaseExtension {
|
|
|
37
38
|
*/
|
|
38
39
|
const execution_metrics = data.execution_metrics[0];
|
|
39
40
|
|
|
41
|
+
if (!execution_metrics) {
|
|
42
|
+
logger.warn('⚠️ No execution metrics found. Skipping.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
const smart_analysis = [];
|
|
41
47
|
if (execution_metrics.newly_failed) {
|
|
42
48
|
smart_analysis.push(`⭕ Newly Failed: ${execution_metrics.newly_failed}`);
|
|
@@ -54,7 +60,21 @@ class SmartAnalysisExtension extends BaseExtension {
|
|
|
54
60
|
smart_analysis.push(`🟢 Recovered: ${execution_metrics.recovered}`);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
const texts = [];
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const item of smart_analysis) {
|
|
66
|
+
rows.push(item);
|
|
67
|
+
if (rows.length === 3) {
|
|
68
|
+
texts.push(rows.join(' | '));
|
|
69
|
+
rows.length = 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (rows.length > 0) {
|
|
74
|
+
texts.push(rows.join(' | '));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.text = this.mergeTexts(texts);
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
}
|
package/src/helpers/constants.js
CHANGED
|
@@ -22,6 +22,7 @@ const TARGET = Object.freeze({
|
|
|
22
22
|
const EXTENSION = Object.freeze({
|
|
23
23
|
AI_FAILURE_SUMMARY: 'ai-failure-summary',
|
|
24
24
|
SMART_ANALYSIS: 'smart-analysis',
|
|
25
|
+
ERROR_CLUSTERS: 'error-clusters',
|
|
25
26
|
HYPERLINKS: 'hyperlinks',
|
|
26
27
|
MENTIONS: 'mentions',
|
|
27
28
|
REPORT_PORTAL_ANALYSIS: 'report-portal-analysis',
|
package/src/index.d.ts
CHANGED
|
@@ -230,6 +230,7 @@ export interface PublishReport {
|
|
|
230
230
|
run?: string;
|
|
231
231
|
show_failure_summary?: boolean;
|
|
232
232
|
show_smart_analysis?: boolean;
|
|
233
|
+
show_error_clusters?: boolean;
|
|
233
234
|
targets?: Target[];
|
|
234
235
|
extensions?: Extension[];
|
|
235
236
|
results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
|
package/src/targets/chat.js
CHANGED
|
@@ -238,7 +238,31 @@ const default_inputs = {
|
|
|
238
238
|
]
|
|
239
239
|
};
|
|
240
240
|
|
|
241
|
+
async function handleErrors({ target, errors }) {
|
|
242
|
+
let title = 'Error: Reporting Test Results';
|
|
243
|
+
title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
|
|
244
|
+
|
|
245
|
+
const root_payload = getRootPayload();
|
|
246
|
+
const payload = root_payload.cards[0];
|
|
247
|
+
|
|
248
|
+
payload.sections.push({
|
|
249
|
+
"widgets": [
|
|
250
|
+
{
|
|
251
|
+
"textParagraph": {
|
|
252
|
+
text: `<b>${title}</b><br><br><b>Errors</b>: <br>${errors.join('<br>')}`
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return request.post({
|
|
259
|
+
url: target.inputs.url,
|
|
260
|
+
body: root_payload
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
241
264
|
module.exports = {
|
|
242
265
|
run,
|
|
266
|
+
handleErrors,
|
|
243
267
|
default_options
|
|
244
268
|
}
|
package/src/targets/index.js
CHANGED
|
@@ -34,6 +34,14 @@ async function run(target, result) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
async function handleErrors({ target, errors }) {
|
|
38
|
+
const target_runner = getTargetRunner(target);
|
|
39
|
+
if (target_runner.handleErrors) {
|
|
40
|
+
await target_runner.handleErrors({ target, errors });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
37
44
|
module.exports = {
|
|
38
|
-
run
|
|
45
|
+
run,
|
|
46
|
+
handleErrors
|
|
39
47
|
}
|
package/src/targets/slack.js
CHANGED
|
@@ -264,7 +264,45 @@ const default_inputs = {
|
|
|
264
264
|
]
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
async function handleErrors({ target, errors }) {
|
|
268
|
+
let title = 'Error: Reporting Test Results';
|
|
269
|
+
title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
|
|
270
|
+
|
|
271
|
+
const blocks = [];
|
|
272
|
+
|
|
273
|
+
blocks.push({
|
|
274
|
+
"type": "section",
|
|
275
|
+
"text": {
|
|
276
|
+
"type": "mrkdwn",
|
|
277
|
+
"text": title
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
blocks.push({
|
|
281
|
+
"type": "section",
|
|
282
|
+
"text": {
|
|
283
|
+
"type": "mrkdwn",
|
|
284
|
+
"text": errors.join('\n\n')
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const payload = {
|
|
289
|
+
"attachments": [
|
|
290
|
+
{
|
|
291
|
+
"color": COLORS.DANGER,
|
|
292
|
+
"blocks": blocks,
|
|
293
|
+
"fallback": title,
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return request.post({
|
|
299
|
+
url: target.inputs.url,
|
|
300
|
+
body: payload
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
267
304
|
module.exports = {
|
|
268
305
|
run,
|
|
306
|
+
handleErrors,
|
|
269
307
|
default_options
|
|
270
308
|
}
|
package/src/targets/teams.js
CHANGED
|
@@ -300,7 +300,40 @@ const default_inputs = {
|
|
|
300
300
|
]
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
async function handleErrors({ target, errors }) {
|
|
304
|
+
let title = 'Error: Reporting Test Results';
|
|
305
|
+
title = target.inputs.title ? title + ' - ' + target.inputs.title : title;
|
|
306
|
+
|
|
307
|
+
const root_payload = getRootPayload();
|
|
308
|
+
const payload = getMainPayload(target);
|
|
309
|
+
|
|
310
|
+
payload.body.push({
|
|
311
|
+
"type": "TextBlock",
|
|
312
|
+
"text": title,
|
|
313
|
+
"size": "medium",
|
|
314
|
+
"weight": "bolder",
|
|
315
|
+
"wrap": true
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
payload.body.push({
|
|
319
|
+
"type": "TextBlock",
|
|
320
|
+
"text": errors.join('\n'),
|
|
321
|
+
"size": "medium",
|
|
322
|
+
"weight": "bolder",
|
|
323
|
+
"wrap": true
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
setRootPayload(root_payload, payload);
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
return request.post({
|
|
330
|
+
url: target.inputs.url,
|
|
331
|
+
body: root_payload
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
303
335
|
module.exports = {
|
|
304
336
|
run,
|
|
337
|
+
handleErrors,
|
|
305
338
|
default_options
|
|
306
339
|
}
|