Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add influx target #116

Merged
merged 1 commit into from
Feb 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "test-results-reporter",
"version": "1.0.15",
"description": "Publish test results to Microsoft Teams, Google Chat and Slack",
"version": "1.0.16",
"description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
"main": "src/index.js",
"types": "./src/index.d.ts",
"bin": {
Expand All @@ -26,6 +26,8 @@
"microsoft teams",
"teams",
"slack",
"influx",
"influxdb",
"junit",
"mocha",
"cucumber",
Expand All @@ -45,7 +47,8 @@
"dependencies": {
"async-retry": "^1.3.3",
"dotenv": "^14.3.2",
"performance-results-parser": "0.0.4",
"influxdb-v1": "^1.0.4",
"performance-results-parser": "0.0.5",
"phin-retry": "^1.0.3",
"pretty-ms": "^7.0.1",
"rosters": "0.0.1",
Expand Down
17 changes: 1 addition & 16 deletions src/extensions/percy-analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,32 +68,24 @@ async function setBuildByLastRun(extension) {
*/
function getLastFinishedBuild(extension) {
const { inputs } = extension;

// Added a default value for retries in case the input is missing or is not a number
const retries = inputs.retries || 3;
const minTimeout = 5000;

return retry(async () => {
let response;
try {
response = await getLastBuild(inputs);
} catch (error) {
// Used the Error class to throw errors instead of using template literals,
// which provides more information about the error,
// including a stack trace, and allows us to distinguish between different types of errors.
throw new Error(`Error occurred while fetching the last build: ${error}`);
}
// Added Checks to make sure that the response data is valid and is as expected.
if (!response.data || !response.data[0] || !response.data[0].attributes) {
throw new Error(`Invalid response data: ${JSON.stringify(response)}`);
}
const state = response.data[0].attributes.state;
// Included percy's "failed" build status for this fix: #113
if (state !== "finished" && state !== "failed") {
throw new Error(`build is still '${state}'`);
}
return response;
}, { retries, minTimeout });
}, { retries: inputs.retries, minTimeout });
}

/**
Expand Down Expand Up @@ -164,20 +156,13 @@ function getAnalysisSummary(outputs, bold_start = '**', bold_end = '**') {
if (approved) {
results.push(`${bold_start}✔ AP - ${approved}${bold_end}`);
} else {
// Implemented a defensive check to handle scenarios where the variable "approved" may be null,
// by setting it to zero to avoid displaying null values.
results.push(`✔ AP - ${approved || 0}`);
}
if (un_reviewed) {
results.push(`${bold_start}🔎 UR - ${un_reviewed}${bold_end}`);
} else {
// As a solution for issue #113, we have implemented a check for Percy's build status,
// if it is "failed", we set the value of "un_reviewed" to 0,
// this way we handle the scenario where "un_reviewed" would be null and display a default value of zero instead.
results.push(`🔎 UR - ${un_reviewed || 0}`);
}
// Implemented a defensive check for when "removed_snapshots" is null or undefined,
// to avoid displaying null or undefined values and provide a default value of zero instead.
if (removed_snapshots && removed_snapshots.length) {
results.push(`${bold_start}🗑 RM - ${removed_snapshots.length}${bold_end}`);
} else {
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const TARGET = Object.freeze({
TEAMS: 'teams',
CHAT: 'chat',
CUSTOM: 'custom',
DELAY: 'delay'
DELAY: 'delay',
INFLUX: 'influx',
});

const EXTENSION = Object.freeze({
Expand Down
12 changes: 11 additions & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ export interface TeamsInputs extends TargetInputs {

export interface ChatInputs extends TargetInputs { }

export interface InfluxDBTargetInputs {
url: string;
db: string;
username?: string;
password?: string;
measurement_perf_run?: string;
measurement_perf_transaction?: string;
tags?: object;
}

export interface CustomTargetFunctionContext {
target: Target;
result: TestResult;
Expand All @@ -181,7 +191,7 @@ export interface CustomTargetInputs {
export interface Target {
name: TargetName;
condition: Condition;
inputs: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs;
inputs: SlackInputs | TeamsInputs | ChatInputs | CustomTargetInputs | InfluxDBTargetInputs;
extensions?: Extension[];
}

Expand Down
3 changes: 3 additions & 0 deletions src/targets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const slack = require('./slack');
const chat = require('./chat');
const custom = require('./custom');
const delay = require('./delay');
const influx = require('./influx');
const { TARGET } = require('../helpers/constants');
const { checkCondition } = require('../helpers/helper');

Expand All @@ -18,6 +19,8 @@ function getTargetRunner(target) {
return custom;
case TARGET.DELAY:
return delay;
case TARGET.INFLUX:
return influx;
default:
return require(target.name);
}
Expand Down
122 changes: 122 additions & 0 deletions src/targets/influx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const influx_v1 = require('influxdb-v1');
const Metric = require('performance-results-parser/src/models/Metric');
const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
const Transaction = require('performance-results-parser/src/models/Transaction');


const { STATUS } = require('../helpers/constants');

/**
*
* @param {object} param0
* @param {PerformanceTestResult | TestResult} param0.result
*/
async function run({ result, target }) {
target.inputs = Object.assign({}, default_inputs, target.inputs);
const metrics = getMetrics({ result, target });
await influx_v1.write(
{
url: target.inputs.url,
db: target.inputs.db,
username: target.inputs.username,
password: target.inputs.password,
},
metrics
);
}

function getMetrics({ result, target }) {
const influx_metrics = [];
if (result instanceof PerformanceTestResult) {
const tags = Object.assign({}, target.inputs.tags);
tags.Name = result.name;
tags.Status = result.status;

const fields = {};
fields.status = result.status === 'PASS' ? 0 : 1;
fields.transactions = result.transactions.length;
fields.transactions_passed = result.transactions.filter(_transaction => _transaction.status === "PASS").length;
fields.transactions_failed = result.transactions.filter(_transaction => _transaction.status === "FAIL").length;

for (const metric of result.metrics) {
setPerfMetrics(metric, fields);
}

influx_metrics.push({
measurement: target.inputs.measurement_perf_run,
tags,
fields
});

for (const transaction of result.transactions) {
influx_metrics.push(getTransactionInfluxMetric(transaction, target));
}

}
return influx_metrics;
}

/**
*
* @param {Metric} metric
*/
function setPerfMetrics(metric, fields) {
let name = metric.name;
name = name.toLowerCase();
name = name.replace(' ', '_');
if (metric.type === "COUNTER" || metric.type === "RATE") {
fields[`${name}_sum`] = metric.sum;
fields[`${name}_rate`] = metric.rate;
} else if (metric.type === "TREND") {
fields[`${name}_avg`] = metric.avg;
fields[`${name}_med`] = metric.med;
fields[`${name}_max`] = metric.max;
fields[`${name}_min`] = metric.min;
fields[`${name}_p90`] = metric.p90;
fields[`${name}_p95`] = metric.p95;
fields[`${name}_p99`] = metric.p99;
}
}

/**
*
* @param {Transaction} transaction
*/
function getTransactionInfluxMetric(transaction, target) {
const tags = Object.assign({}, target.inputs.tags);
tags.Name = transaction.name;
tags.Status = transaction.status;

const fields = {};
fields.status = transaction.status === 'PASS' ? 0 : 1;

for (const metric of transaction.metrics) {
setPerfMetrics(metric, fields);
}

return {
measurement: target.inputs.measurement_perf_transaction,
tags,
fields
}
}


const default_inputs = {
url: '',
db: '',
username: '',
password: '',
measurement_perf_run: 'PerfRun',
measurement_perf_transaction: 'PerfTransaction',
tags: {}
}

const default_options = {
condition: STATUS.PASS_OR_FAIL
}

module.exports = {
run,
default_options
}
3 changes: 2 additions & 1 deletion test/mocks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ require('./rp.mock');
require('./slack.mock');
require('./teams.mock');
require('./chat.mock');
require('./percy.mock');
require('./percy.mock');
require('./influx.mock');
39 changes: 39 additions & 0 deletions test/mocks/influx.mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const { addInteractionHandler } = require('pactum').handler;

addInteractionHandler('save perf results', () => {
return {
request: {
method: 'POST',
path: '/write',
headers: {
"authorization": "Basic dXNlcjpwYXNz"
},
queryParams: {
"db": "TestResults"
},
body: "PerfRun,Name=TOTAL,Status=PASS status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=38.87,data_received_sum=0,data_received_rate=5166.44\nPerfTransaction,Name=S01_T01_Application_Launch,Status=PASS status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=0,data_sent_rate=5.36,data_received_sum=0,data_received_rate=2662.79\nPerfTransaction,Name=S01_T02_Application_Login,Status=PASS status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=12.94,data_received_sum=0,data_received_rate=2754.9"
},
response: {
status: 200
}
}
});

addInteractionHandler('save perf results with custom tags', () => {
return {
request: {
method: 'POST',
path: '/write',
headers: {
"authorization": "Basic dXNlcjpwYXNz"
},
queryParams: {
"db": "TestResults"
},
body: "PerfRun,Team=QA,App=PactumJS,Name=TOTAL,Status=PASS status=0,transactions=2,transactions_passed=2,transactions_failed=0,samples_sum=39,samples_rate=0.55535,duration_avg=4660,duration_med=3318,duration_max=15513,duration_min=1135,duration_p90=11354,duration_p95=11446,duration_p99=15513,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=38.87,data_received_sum=0,data_received_rate=5166.44\nPerfTransaction,Team=QA,App=PactumJS,Name=S01_T01_Application_Launch,Status=PASS status=0,samples_sum=10,samples_rate=0.14422,duration_avg=3086,duration_med=2832,duration_max=3797,duration_min=2119,duration_p90=3795,duration_p95=3795,duration_p99=3797,errors_sum=0,errors_rate=0.001,data_sent_sum=0,data_sent_rate=5.36,data_received_sum=0,data_received_rate=2662.79\nPerfTransaction,Team=QA,App=PactumJS,Name=S01_T02_Application_Login,Status=PASS status=0,samples_sum=9,samples_rate=0.1461,duration_avg=4355,duration_med=3273,duration_max=10786,duration_min=3042,duration_p90=4416,duration_p95=10786,duration_p99=10786,errors_sum=0,errors_rate=0,data_sent_sum=0,data_sent_rate=12.94,data_received_sum=0,data_received_rate=2754.9"
},
response: {
status: 200
}
}
});
Loading