Skip to content

Commit

Permalink
feat: error clusters (#236)
Browse files Browse the repository at this point in the history
  • Loading branch information
ASaiAnudeep authored Aug 18, 2024
1 parent b5ade22 commit 4098eb6
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 3 deletions.
15 changes: 15 additions & 0 deletions src/beats/beats.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ class BeatsApi {
getBaseUrl() {
return process.env.TEST_BEATS_URL || "https://app.testbeats.com";
}

/**
*
* @param {string} run_id
* @param {number} limit
* @returns {import('./beats.types').IErrorClustersResponse}
*/
getErrorClusters(run_id, limit = 3) {
return request.get({
url: `${this.getBaseUrl()}/api/core/v1/test-runs/${run_id}/error-clusters?limit=${limit}`,
headers: {
'x-api-key': this.config.api_key
}
});
}
}

module.exports = { BeatsApi }
29 changes: 29 additions & 0 deletions src/beats/beats.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Beats {
this.#updateTitleLink();
await this.#attachFailureSummary();
await this.#attachSmartAnalysis();
await this.#attachErrorClusters();
}

#setCIInfo() {
Expand Down Expand Up @@ -189,6 +190,34 @@ class Beats {
logger.warn(`🙈 ${text} not generated in given time`);
}

async #attachErrorClusters() {
if (!this.test_run_id) {
return;
}
if (!this.config.targets) {
return;
}
if (this.result.status !== 'FAIL') {
return;
}
if (this.config.show_error_clusters === false) {
return;
}
try {
logger.info('🧮 Fetching Error Clusters...');
const res = await this.api.getErrorClusters(this.test_run_id, 3);
this.config.extensions.push({
name: 'error-clusters',
hook: HOOK.AFTER_SUMMARY,
inputs: {
data: res.values
}
});
} catch (error) {
logger.error(`❌ Unable to attach error clusters: ${error.message}`, error);
}
}

}

module.exports = { Beats }
15 changes: 15 additions & 0 deletions src/beats/beats.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,18 @@ export type IBeatExecutionMetric = {
test_run_id: string
org_id: string
}

export type IPaginatedAPIResponse<T> = {
page: number
limit: number
total: number
values: T[]
}

export type IErrorClustersResponse = {} & IPaginatedAPIResponse<IErrorCluster>;

export type IErrorCluster = {
test_failure_id: string
failure: string
count: number
}
16 changes: 16 additions & 0 deletions src/extensions/base.extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ class BaseExtension {
}
}

/**
* @param {string|number} text
*/
bold(text) {
switch (this.target.name) {
case 'teams':
return `**${text}**`;
case 'slack':
return `*${text}*`;
case 'chat':
return `<b>${text}</b>`;
default:
break;
}
}

}

module.exports = { BaseExtension }
46 changes: 46 additions & 0 deletions src/extensions/error-clusters.extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { BaseExtension } = require('./base.extension');
const { STATUS, HOOK } = require("../helpers/constants");

class ErrorClustersExtension extends BaseExtension {

constructor(target, extension, result, payload, root_payload) {
super(target, extension, result, payload, root_payload);
this.#setDefaultOptions();
this.#setDefaultInputs();
this.updateExtensionInputs();
}

run() {
this.#setText();
this.attach();
}

#setDefaultOptions() {
this.default_options.hook = HOOK.AFTER_SUMMARY,
this.default_options.condition = STATUS.PASS_OR_FAIL;
}

#setDefaultInputs() {
this.default_inputs.title = 'Top Errors';
this.default_inputs.title_link = '';
}

#setText() {
const data = this.extension.inputs.data;
if (!data || !data.length) {
return;
}

const clusters = data;

this.extension.inputs.title = `Top ${clusters.length} Errors`;

const texts = [];
for (const cluster of clusters) {
texts.push(`${this.bold(`(${cluster.count})`)} - ${cluster.failure}`);
}
this.text = this.mergeTexts(texts);
}
}

module.exports = { ErrorClustersExtension }
3 changes: 3 additions & 0 deletions src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const { CustomExtension } = require('./custom.extension');
const { EXTENSION } = require('../helpers/constants');
const { checkCondition } = require('../helpers/helper');
const logger = require('../utils/logger');
const { ErrorClustersExtension } = require('./error-clusters.extension');

async function run(options) {
const { target, result, hook } = options;
Expand Down Expand Up @@ -60,6 +61,8 @@ function getExtensionRunner(extension, options) {
return new AIFailureSummaryExtension(options.target, extension, options.result, options.payload, options.root_payload);
case EXTENSION.SMART_ANALYSIS:
return new SmartAnalysisExtension(options.target, extension, options.result, options.payload, options.root_payload);
case EXTENSION.ERROR_CLUSTERS:
return new ErrorClustersExtension(options.target, extension, options.result, options.payload, options.root_payload);
default:
return require(extension.name);
}
Expand Down
1 change: 1 addition & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const TARGET = Object.freeze({
const EXTENSION = Object.freeze({
AI_FAILURE_SUMMARY: 'ai-failure-summary',
SMART_ANALYSIS: 'smart-analysis',
ERROR_CLUSTERS: 'error-clusters',
HYPERLINKS: 'hyperlinks',
MENTIONS: 'mentions',
REPORT_PORTAL_ANALYSIS: 'report-portal-analysis',
Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export interface PublishReport {
run?: string;
show_failure_summary?: boolean;
show_smart_analysis?: boolean;
show_error_clusters?: boolean;
targets?: Target[];
extensions?: Extension[];
results?: ParseOptions[] | PerformanceParseOptions[] | CustomResultOptions[];
Expand Down
46 changes: 43 additions & 3 deletions test/beats.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ describe('TestBeats', () => {
it('should send results with failures to beats', async () => {
const id1 = mock.addInteraction('post test results to beats');
const id2 = mock.addInteraction('get test results from beats');
const id3 = mock.addInteraction('post test-summary with beats to teams with ai failure summary');
const id3 = mock.addInteraction('get empty error clusters from beats');
const id4 = mock.addInteraction('post test-summary with beats to teams with ai failure summary');
await publish({
config: {
api_key: 'api-key',
Expand All @@ -74,13 +75,15 @@ describe('TestBeats', () => {
assert.equal(mock.getInteraction(id1).exercised, true);
assert.equal(mock.getInteraction(id2).exercised, true);
assert.equal(mock.getInteraction(id3).exercised, true);
assert.equal(mock.getInteraction(id4).exercised, true);
});

it('should send results with attachments to beats', async () => {
const id1 = mock.addInteraction('post test results to beats');
const id2 = mock.addInteraction('get test results from beats');
const id3 = mock.addInteraction('upload attachments');
const id4 = mock.addInteraction('post test-summary to teams with strict as false');
const id4 = mock.addInteraction('get empty error clusters from beats');
const id5 = mock.addInteraction('post test-summary to teams with strict as false');
await publish({
config: {
api_key: 'api-key',
Expand Down Expand Up @@ -108,6 +111,7 @@ describe('TestBeats', () => {
assert.equal(mock.getInteraction(id2).exercised, true);
assert.equal(mock.getInteraction(id3).exercised, true);
assert.equal(mock.getInteraction(id4).exercised, true);
assert.equal(mock.getInteraction(id5).exercised, true);
});

it('should send results to beats without targets', async () => {
Expand All @@ -133,7 +137,8 @@ describe('TestBeats', () => {
it('should send results with smart analysis to beats', async () => {
const id1 = mock.addInteraction('post test results to beats');
const id2 = mock.addInteraction('get test results with smart analysis from beats');
const id3 = mock.addInteraction('post test-summary with beats to teams with ai failure summary and smart analysis');
const id3 = mock.addInteraction('get empty error clusters from beats');
const id4 = mock.addInteraction('post test-summary with beats to teams with ai failure summary and smart analysis');
await publish({
config: {
api_key: 'api-key',
Expand All @@ -160,6 +165,41 @@ describe('TestBeats', () => {
assert.equal(mock.getInteraction(id1).exercised, true);
assert.equal(mock.getInteraction(id2).exercised, true);
assert.equal(mock.getInteraction(id3).exercised, true);
assert.equal(mock.getInteraction(id4).exercised, true);
});

it('should send results with error clusters to beats', async () => {
const id1 = mock.addInteraction('post test results to beats');
const id2 = mock.addInteraction('get test results from beats');
const id3 = mock.addInteraction('get error clusters from beats');
const id4 = mock.addInteraction('post test-summary with beats to teams with error clusters');
await publish({
config: {
api_key: 'api-key',
project: 'project-name',
run: 'build-name',
targets: [
{
name: 'teams',
inputs: {
url: 'http://localhost:9393/message'
}
}
],
results: [
{
type: 'testng',
files: [
'test/data/testng/single-suite-failures.xml'
]
}
]
}
});
assert.equal(mock.getInteraction(id1).exercised, true);
assert.equal(mock.getInteraction(id2).exercised, true);
assert.equal(mock.getInteraction(id3).exercised, true);
assert.equal(mock.getInteraction(id4).exercised, true);
});

});
49 changes: 49 additions & 0 deletions test/mocks/beats.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,55 @@ addInteractionHandler('get test results with smart analysis from beats', () => {
}
});

addInteractionHandler('get error clusters from beats', () => {
return {
strict: false,
request: {
method: 'GET',
path: '/api/core/v1/test-runs/test-run-id/error-clusters',
queryParams: {
"limit": 3
}
},
response: {
status: 200,
body: {
values: [
{
test_failure_id: 'test-failure-id',
failure: 'failure two',
count: 2
},
{
test_failure_id: 'test-failure-id',
failure: 'failure one',
count: 1
}
]
}
}
}
});

addInteractionHandler('get empty error clusters from beats', () => {
return {
strict: false,
request: {
method: 'GET',
path: '/api/core/v1/test-runs/test-run-id/error-clusters',
queryParams: {
"limit": 3
}
},
response: {
status: 200,
body: {
values: []
}
}
}
});

addInteractionHandler('upload attachments', () => {
return {
strict: false,
Expand Down
Loading

0 comments on commit 4098eb6

Please sign in to comment.