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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
5
5
  "main": "src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -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/key?id=${run_id}`,
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 }
@@ -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
- logger.info('✅ Results published successfully!');
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
- this.results.push(trp.parse(result_options));
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 }
@@ -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 = 'Smart Analysis';
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
- this.text = smart_analysis.join(' | ');
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
  }
@@ -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[];
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }