From 09b42a7a56614c4110428b6fc6c3221ba4c1c23f Mon Sep 17 00:00:00 2001 From: Vijay Choudhary Date: Mon, 20 Oct 2025 12:11:51 +0530 Subject: [PATCH 01/20] Adding ui alert table in the database, along with an entity to store the different ui alerts in the database --- db/init.sql | 11 ++++++ .../V6__create_performance_ui_alert_table.sql | 10 ++++++ src/database/entity/uiAlert.ts | 34 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 db/migrations/V6__create_performance_ui_alert_table.sql create mode 100644 src/database/entity/uiAlert.ts diff --git a/db/init.sql b/db/init.sql index f90faab..29e6246 100644 --- a/db/init.sql +++ b/db/init.sql @@ -100,4 +100,15 @@ CREATE TABLE IF NOT EXISTS performance.ui_test_result ( component_load_time integer, salesforce_load_time integer, overall_load_time integer +); + +CREATE TABLE IF NOT EXISTS performance.ui_alert ( + id serial PRIMARY KEY, + create_date_time timestamp without time zone DEFAULT now() NOT NULL, + update_date_time timestamp without time zone DEFAULT now() NOT NULL, + ui_test_result_id integer, + alert_type text, + test_suite_name text, + individual_test_name text, + component_load_time_degraded integer ); \ No newline at end of file diff --git a/db/migrations/V6__create_performance_ui_alert_table.sql b/db/migrations/V6__create_performance_ui_alert_table.sql new file mode 100644 index 0000000..89786bd --- /dev/null +++ b/db/migrations/V6__create_performance_ui_alert_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS performance.ui_alert ( + id serial PRIMARY KEY, + create_date_time timestamp without time zone DEFAULT now() NOT NULL, + update_date_time timestamp without time zone DEFAULT now() NOT NULL, + ui_test_result_id integer, + alert_type text, + test_suite_name text, + individual_test_name text, + component_load_time_degraded integer +); \ No newline at end of file diff --git a/src/database/entity/uiAlert.ts b/src/database/entity/uiAlert.ts new file mode 100644 index 0000000..a1823af --- /dev/null +++ b/src/database/entity/uiAlert.ts @@ -0,0 +1,34 @@ +/** @ignore */ +/* + * Copyright (c) 2025 Certinia Inc. All rights reserved. + */ +import { Entity, Column } from 'typeorm'; +import { PerformanceBaseEntity } from './base'; +import { + DEFAULT_NUMERIC_VALUE, + DEFAULT_STRING_VALUE, +} from '../../shared/constants'; + +@Entity({ name: 'ui_alert' }) +export class UiAlert extends PerformanceBaseEntity { + [key: string]: number | string | Date | boolean | undefined; + + @Column('integer', { nullable: true, name: 'ui_test_result_id' }) + public uiTestResultId = DEFAULT_NUMERIC_VALUE; + + @Column('text', { nullable: true, name: 'test_suite_name' }) + public testSuiteName = DEFAULT_STRING_VALUE; + + @Column('text', { nullable: true, name: 'individual_test_name' }) + public individualTestName = DEFAULT_STRING_VALUE; + + @Column('text', { nullable: true, name: 'alert_type' }) + public alertType = DEFAULT_STRING_VALUE; + + @Column('integer', { nullable: true, name: 'component_load_time_degraded' }) + public componentLoadTimeDegraded = DEFAULT_NUMERIC_VALUE; + + public constructor() { + super(); + } +} From 7a616a0ec71093e545aa861d1cfa0a2a8f2b6a54 Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Mon, 20 Oct 2025 12:29:14 +0530 Subject: [PATCH 02/20] added getAverageLimitValuesFromDB to get the avg from db for uiTestResult --- src/database/uiAlertInfo.ts | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/database/uiAlertInfo.ts diff --git a/src/database/uiAlertInfo.ts b/src/database/uiAlertInfo.ts new file mode 100644 index 0000000..e22df53 --- /dev/null +++ b/src/database/uiAlertInfo.ts @@ -0,0 +1,72 @@ +/** @ignore */ +/** + * Copyright (c) 2025 Certinia, Inc. All rights reserved. + */ +import { getConnection } from './connection'; + +export async function getAverageLimitValuesFromDB( + suiteAndTestNamePairs: { testSuiteName: string; individualTestName: string }[] +) { + const connection = await getConnection(); + + const suiteAndTestNameConditions = suiteAndTestNamePairs + .map(pair => `('${pair.testSuiteName}', '${pair.individualTestName}')`) + .join(', '); + + const query = ` + WITH ranked AS ( + SELECT + test_suite_name, + individual_test_name, + component_load_time, + create_date_time, + ROW_NUMBER() OVER ( + PARTITION BY test_suite_name, individual_test_name + ORDER BY create_date_time DESC + ) AS rn + FROM performance.ui_test_result + WHERE (create_date_time >= CURRENT_TIMESTAMP - INTERVAL '30 DAYS') + AND (test_suite_name, individual_test_name) IN (${suiteAndTestNameConditions}) + ) + SELECT + test_suite_name, + individual_test_name, + ROUND(AVG(CASE WHEN rn BETWEEN 1 AND 5 THEN component_load_time END)::numeric, 0) AS avg_first_5, + ROUND(AVG(CASE WHEN rn BETWEEN 6 AND 15 THEN component_load_time END)::numeric, 0) AS avg_next_10 + FROM ranked + GROUP BY test_suite_name, individual_test_name + HAVING COUNT(*) >= 15 + `; + + const resultsMap: { + [key: string]: { + avg_first_5: number; + avg_next_10: number; + }; + } = {}; + + try { + const result = await connection.query(query); + + // Populate the results map + result.forEach( + (row: { + test_suite_name: string; + individual_test_name: string; + avg_first_5: number; + avg_next_10: number; + }) => { + const key = `${row.test_suite_name}_${row.individual_test_name}`; + resultsMap[key] = { + avg_first_5: row.avg_first_5 ?? 0, + avg_next_10: row.avg_next_10 ?? 0, + }; + } + ); + + return resultsMap; + } catch (error) { + console.error('Error in fetching the average values: ', error); + return {}; + } +} From 1763bc65af009f8f0759acb989b282d026e97ed9 Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Mon, 20 Oct 2025 14:28:38 +0530 Subject: [PATCH 03/20] added tests for getAverageLimitValuesFromDB --- test/database/uiAlertInfo.test.ts | 166 ++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 test/database/uiAlertInfo.test.ts diff --git a/test/database/uiAlertInfo.test.ts b/test/database/uiAlertInfo.test.ts new file mode 100644 index 0000000..72ece8a --- /dev/null +++ b/test/database/uiAlertInfo.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025 Certinia, inc. All rights reserved. + */ + +import { getAverageLimitValuesFromDB } from '../../src/database/uiAlertInfo'; +import * as db from '../../src/database/connection'; +import sinon from 'sinon'; +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; + +chai.use(sinonChai); + +describe('src/database/alertInfo', () => { + let mockQuery: sinon.SinonStub; + let mockDataSource: any; + + beforeEach(() => { + mockQuery = sinon.stub(); + mockDataSource = { query: mockQuery }; + sinon.stub(db, 'getConnection').resolves(mockDataSource); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('getAverageLimitValuesFromDB', () => { + it('should return average limit values for valid data', async () => { + // Given + const flowActionPairs = [ + { + testSuiteName: 'testSuiteName1', + individualTestName: 'individualTestName1', + }, + { + testSuiteName: 'testSuiteName2', + individualTestName: 'individualTestName2', + }, + ]; + + const mockResults = [ + { + test_suite_name: 'testSuiteName1', + individual_test_name: 'individualTestName1', + avg_first_5: 2000, + avg_next_10: 1500, + }, + { + test_suite_name: 'testSuiteName2', + individual_test_name: 'individualTestName2', + avg_first_5: 2000, + avg_next_10: 1500, + }, + ]; + + mockQuery.resolves(mockResults); + + // When + const results = await getAverageLimitValuesFromDB(flowActionPairs); + + // Then + expect(mockQuery.calledOnce).to.be.true; + expect(mockQuery.args[0][0]).to.include('SELECT'); + expect(mockQuery.args[0][0]).to.include( + "(test_suite_name, individual_test_name) IN (('testSuiteName1', 'individualTestName1'), ('testSuiteName2', 'individualTestName2'))" + ); + + expect(results).to.deep.equal({ + testSuiteName1_individualTestName1: { + avg_first_5: 2000, + avg_next_10: 1500, + }, + testSuiteName2_individualTestName2: { + avg_first_5: 2000, + avg_next_10: 1500, + }, + }); + }); + + it('should return an empty object when no results are found', async () => { + // Given + const flowActionPairs = [ + { + testSuiteName: 'testSuiteName1', + individualTestName: 'individualTestName1', + }, + ]; + + mockQuery.resolves([]); + + // When + const results = await getAverageLimitValuesFromDB(flowActionPairs); + + // Then + expect(mockQuery.calledOnce).to.be.true; + expect(results).to.deep.equal({}); + }); + + it('should handle missing fields and default them to zero', async () => { + // Given + const flowActionPairs = [ + { + testSuiteName: 'testSuiteName1', + individualTestName: 'individualTestName1', + }, + ]; + + const mockResults = [ + { + test_suite_name: 'testSuiteName1', + individual_test_name: 'individualTestName1', + avg_first_5: null, + avg_next_10: undefined, + }, + ]; + + mockQuery.resolves(mockResults); + + // When + const results = await getAverageLimitValuesFromDB(flowActionPairs); + + // Then + expect(results).to.deep.equal({ + testSuiteName1_individualTestName1: { + avg_first_5: 0, + avg_next_10: 0, + }, + }); + }); + + it('should handle an empty flowActionPairs array and return an empty object', async () => { + // Given + const flowActionPairs: { + testSuiteName: string; + individualTestName: string; + }[] = []; + + // Simulate no results (empty array) + mockQuery.resolves([]); + + // When + const results = await getAverageLimitValuesFromDB(flowActionPairs); + + // Then + expect(results).to.deep.equal({}); + }); + + it('should handle errors and return an empty object', async () => { + // Given + const flowActionPairs = [ + { + testSuiteName: 'testSuiteName1', + individualTestName: 'individualTestName1', + }, + ]; + + mockQuery.rejects(new Error('Database error')); + + // When + const results = await getAverageLimitValuesFromDB(flowActionPairs); + + // Then + expect(results).to.deep.equal({}); + }); + }); +}); From 3b92d381df1c88eb0b83e1ba7708642f526bfe76 Mon Sep 17 00:00:00 2001 From: Astha Date: Thu, 23 Oct 2025 11:22:49 +0530 Subject: [PATCH 04/20] added logic for generating alerts based on threshold --- src/services/result/uiAlert.ts | 102 +++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/services/result/uiAlert.ts diff --git a/src/services/result/uiAlert.ts b/src/services/result/uiAlert.ts new file mode 100644 index 0000000..3bfad3c --- /dev/null +++ b/src/services/result/uiAlert.ts @@ -0,0 +1,102 @@ +/** @ignore */ +/* + * Copyright (c) 2025 Certinia Inc. All rights reserved. + */ +import { + shouldStoreUiAlerts, + getNormalComponentLoadThreshold, + getCriticalComponentLoadThreshold, +} from '../../shared/env'; +import { getAverageLimitValuesFromDB } from '../../database/uiAlertInfo'; +import { UiAlert } from '../../database/entity/uiAlert'; +import { UiTestResultDTO } from '../../database/uiTestResult'; + +export async function generateValidAlerts( + testResultOutput: UiTestResultDTO[] +): Promise { + const needToStoreAlert = testResultOutput.filter(result => { + const testLevel = result.alertInfo?.storeAlerts; + return testLevel || (testLevel !== false && shouldStoreUiAlerts()); + }); + + if (needToStoreAlert.length === 0) { + return []; + } + + try { + // Extract flow-action pairs + const suiteAndTestNamePairs = needToStoreAlert.map(result => ({ + testSuiteName: result.testSuiteName, + individualTestName: result.individualTestName, + })); + + // Fetch average values + const preFetchedAverages = await getAverageLimitValuesFromDB( + suiteAndTestNamePairs + ); + + // Generate alerts + const alerts = await Promise.all( + needToStoreAlert.map(async item => + addAlertByComparingAvg(item, preFetchedAverages) + ) + ); + + // Filter valid alerts (non-null and degraded) + return alerts.filter((alert): alert is UiAlert => { + return alert.componentLoadTimeDegraded > 0; + }); + } catch (err) { + console.error('Error generating alerts:', err); + return []; + } +} + +async function addAlertByComparingAvg( + output: UiTestResultDTO, + preFetchedAverages: { + [key: string]: { + avg_first_5: number; + avg_next_10: number; + }; + } +): Promise { + const alert: UiAlert = new UiAlert(); + alert.testSuiteName = output.testSuiteName; + alert.individualTestName = output.individualTestName; + + // Construct the key for the current individualTestName and testSuiteName + const key = `${output.testSuiteName}_${output.individualTestName}`; + + const averageResults = preFetchedAverages[key]; + + if (!averageResults) { + return alert; + } + + //storing alerts if there is a degradation + const normalComponentLoadThreshold: number = output.alertInfo + ?.uiAlertThresholds + ? output.alertInfo.uiAlertThresholds.componentLoadTimeThresholdNormal + : Number(getNormalComponentLoadThreshold()); + const criticalComponentLoadThreshold: number = output.alertInfo + ?.uiAlertThresholds + ? output.alertInfo.uiAlertThresholds.componentLoadTimeThresholdCritical + : Number(getCriticalComponentLoadThreshold()); + const componentLoadThresholdDegraded = Math.abs( + averageResults.avg_first_5 - averageResults.avg_next_10 + ); + + if ( + componentLoadThresholdDegraded >= normalComponentLoadThreshold && + componentLoadThresholdDegraded < criticalComponentLoadThreshold + ) { + alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; + alert.alertType = 'normal'; + } else if (componentLoadThresholdDegraded >= criticalComponentLoadThreshold) { + alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; + alert.alertType = 'critical'; + } + + return alert; +} From 6b9cc18746084eb2a9a1490af699a91940a97699 Mon Sep 17 00:00:00 2001 From: Astha Date: Thu, 23 Oct 2025 11:52:55 +0530 Subject: [PATCH 05/20] added constants for alert types --- src/services/result/uiAlert.ts | 5 +++-- src/shared/constants.ts | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/services/result/uiAlert.ts b/src/services/result/uiAlert.ts index 3bfad3c..d888111 100644 --- a/src/services/result/uiAlert.ts +++ b/src/services/result/uiAlert.ts @@ -10,6 +10,7 @@ import { import { getAverageLimitValuesFromDB } from '../../database/uiAlertInfo'; import { UiAlert } from '../../database/entity/uiAlert'; import { UiTestResultDTO } from '../../database/uiTestResult'; +import { NORMAL, CRITICAL } from '../../shared/constants'; export async function generateValidAlerts( testResultOutput: UiTestResultDTO[] @@ -92,10 +93,10 @@ async function addAlertByComparingAvg( componentLoadThresholdDegraded < criticalComponentLoadThreshold ) { alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; - alert.alertType = 'normal'; + alert.alertType = NORMAL; } else if (componentLoadThresholdDegraded >= criticalComponentLoadThreshold) { alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; - alert.alertType = 'critical'; + alert.alertType = CRITICAL; } return alert; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 62b8aec..0126501 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -59,3 +59,8 @@ export const CSV_EID = ''; ******************************************************************************/ export const ERROR_OPENING_BROWSER = 'No test will be executed, a problem was found during browser opening and login'; +/***************************************************************************** + * UI ALERT TYPES + ******************************************************************************/ +export const NORMAL = 'normal'; +export const CRITICAL = 'critical'; From 95e5e9797a94fdf51c1fcde367c3eb89a712bd1f Mon Sep 17 00:00:00 2001 From: Mohit Date: Thu, 23 Oct 2025 13:42:55 +0530 Subject: [PATCH 06/20] Provided a way to give custom thresholds from test and env for ui alerts --- src/database/uiTestResult.ts | 22 ++++++++++++++++++++++ src/index.ts | 2 ++ src/shared/env.ts | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/src/database/uiTestResult.ts b/src/database/uiTestResult.ts index ae3bbb3..331f8d6 100644 --- a/src/database/uiTestResult.ts +++ b/src/database/uiTestResult.ts @@ -7,6 +7,27 @@ import { saveRecords } from './saveRecords'; import { getConnection } from './connection'; import { MoreThanOrEqual } from 'typeorm'; +/** + * Describes the Thresholds for different limits + * Thresholds that needs to be defined using this class: componentLoadTimeThresholdNormal, componentLoadTimeThresholdCritical + */ +export class UiAlertThresholds { + componentLoadTimeThresholdNormal: number; + componentLoadTimeThresholdCritical: number; +} + +export class UiAlertInfo { + /** + * Describes whether alerts need to be stored or not at the test level + */ + public storeAlerts: boolean; + + /** + * Describes the custom thresholds at test level. If you define these then thresholds will be read from here instead of the JSON + */ + public uiAlertThresholds: UiAlertThresholds; +} + /** * Data Transfer Object for UI Test Results */ @@ -16,6 +37,7 @@ export interface UiTestResultDTO { componentLoadTime?: number; salesforceLoadTime?: number; overallLoadTime: number; + alertInfo?: UiAlertInfo; } /** diff --git a/src/index.ts b/src/index.ts index 61519d4..d7cee92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -109,6 +109,8 @@ export { loadUiTestResults, UiTestResultDTO, UiTestResultFilterOptions, + UiAlertInfo, + UiAlertThresholds, } from './database/uiTestResult'; export namespace Constants { diff --git a/src/shared/env.ts b/src/shared/env.ts index 77fac87..a2824a7 100644 --- a/src/shared/env.ts +++ b/src/shared/env.ts @@ -28,6 +28,10 @@ export function shouldStoreAlerts() { return process.env.STORE_ALERTS === 'true'; } +export function shouldStoreUiAlerts() { + return process.env.STORE_UI_ALERTS === 'true'; +} + export function getPuppeteerLaunchOptions( headless?: boolean ): PuppeteerNodeLaunchOptions { @@ -162,3 +166,11 @@ export function getRangeCollection(): RangeCollection { export function getSourceRef() { return process.env.SOURCE_REF || ''; } + +export function getNormalComponentLoadThreshold() { + return process.env.NORMAL_COMPONENT_LOAD_THRESHOLD || '1000'; +} + +export function getCriticalComponentLoadThreshold() { + return process.env.CRITICAL_COMPONENT_LOAD_THRESHOLD || '10000'; +} \ No newline at end of file From 1a3a85c80fff7708c7dd83f9803a29c9596201bd Mon Sep 17 00:00:00 2001 From: Mohit Date: Thu, 23 Oct 2025 15:51:55 +0530 Subject: [PATCH 07/20] Modified comments --- src/database/uiTestResult.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/uiTestResult.ts b/src/database/uiTestResult.ts index 331f8d6..b00a3d3 100644 --- a/src/database/uiTestResult.ts +++ b/src/database/uiTestResult.ts @@ -9,7 +9,7 @@ import { MoreThanOrEqual } from 'typeorm'; /** * Describes the Thresholds for different limits - * Thresholds that needs to be defined using this class: componentLoadTimeThresholdNormal, componentLoadTimeThresholdCritical + * Use this class to specify thresholds such as: componentLoadTimeThresholdNormal and componentLoadTimeThresholdCritical. */ export class UiAlertThresholds { componentLoadTimeThresholdNormal: number; @@ -23,7 +23,7 @@ export class UiAlertInfo { public storeAlerts: boolean; /** - * Describes the custom thresholds at test level. If you define these then thresholds will be read from here instead of the JSON + * Defines custom thresholds at the test level. When specified, these values take precedence over defaults and those set in the environment file. */ public uiAlertThresholds: UiAlertThresholds; } From d4d897d09180d324fb6effc40e94cc76d1019c8d Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Thu, 23 Oct 2025 17:04:16 +0530 Subject: [PATCH 08/20] review change --- test/database/uiAlertInfo.test.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/database/uiAlertInfo.test.ts b/test/database/uiAlertInfo.test.ts index 72ece8a..ab235f6 100644 --- a/test/database/uiAlertInfo.test.ts +++ b/test/database/uiAlertInfo.test.ts @@ -27,7 +27,7 @@ describe('src/database/alertInfo', () => { describe('getAverageLimitValuesFromDB', () => { it('should return average limit values for valid data', async () => { // Given - const flowActionPairs = [ + const suiteAndTestNamePairs = [ { testSuiteName: 'testSuiteName1', individualTestName: 'individualTestName1', @@ -56,7 +56,7 @@ describe('src/database/alertInfo', () => { mockQuery.resolves(mockResults); // When - const results = await getAverageLimitValuesFromDB(flowActionPairs); + const results = await getAverageLimitValuesFromDB(suiteAndTestNamePairs); // Then expect(mockQuery.calledOnce).to.be.true; @@ -79,7 +79,7 @@ describe('src/database/alertInfo', () => { it('should return an empty object when no results are found', async () => { // Given - const flowActionPairs = [ + const suiteAndTestNamePairs = [ { testSuiteName: 'testSuiteName1', individualTestName: 'individualTestName1', @@ -89,7 +89,7 @@ describe('src/database/alertInfo', () => { mockQuery.resolves([]); // When - const results = await getAverageLimitValuesFromDB(flowActionPairs); + const results = await getAverageLimitValuesFromDB(suiteAndTestNamePairs); // Then expect(mockQuery.calledOnce).to.be.true; @@ -98,7 +98,7 @@ describe('src/database/alertInfo', () => { it('should handle missing fields and default them to zero', async () => { // Given - const flowActionPairs = [ + const suiteAndTestNamePairs = [ { testSuiteName: 'testSuiteName1', individualTestName: 'individualTestName1', @@ -117,7 +117,7 @@ describe('src/database/alertInfo', () => { mockQuery.resolves(mockResults); // When - const results = await getAverageLimitValuesFromDB(flowActionPairs); + const results = await getAverageLimitValuesFromDB(suiteAndTestNamePairs); // Then expect(results).to.deep.equal({ @@ -128,9 +128,9 @@ describe('src/database/alertInfo', () => { }); }); - it('should handle an empty flowActionPairs array and return an empty object', async () => { + it('should handle an empty suiteAndTestNamePairs array and return an empty object', async () => { // Given - const flowActionPairs: { + const suiteAndTestNamePairs: { testSuiteName: string; individualTestName: string; }[] = []; @@ -139,7 +139,7 @@ describe('src/database/alertInfo', () => { mockQuery.resolves([]); // When - const results = await getAverageLimitValuesFromDB(flowActionPairs); + const results = await getAverageLimitValuesFromDB(suiteAndTestNamePairs); // Then expect(results).to.deep.equal({}); @@ -147,7 +147,7 @@ describe('src/database/alertInfo', () => { it('should handle errors and return an empty object', async () => { // Given - const flowActionPairs = [ + const suiteAndTestNamePairs = [ { testSuiteName: 'testSuiteName1', individualTestName: 'individualTestName1', @@ -157,7 +157,7 @@ describe('src/database/alertInfo', () => { mockQuery.rejects(new Error('Database error')); // When - const results = await getAverageLimitValuesFromDB(flowActionPairs); + const results = await getAverageLimitValuesFromDB(suiteAndTestNamePairs); // Then expect(results).to.deep.equal({}); From 99e969f76a70e1e1c9718b7de44a854f197a09d0 Mon Sep 17 00:00:00 2001 From: Astha Date: Thu, 23 Oct 2025 17:07:26 +0530 Subject: [PATCH 09/20] added tests --- test/services/uiAlert.test.ts | 248 ++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 test/services/uiAlert.test.ts diff --git a/test/services/uiAlert.test.ts b/test/services/uiAlert.test.ts new file mode 100644 index 0000000..567be26 --- /dev/null +++ b/test/services/uiAlert.test.ts @@ -0,0 +1,248 @@ +import * as sinon from 'sinon'; +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; + +import { generateValidAlerts } from '../../src/services/result/uiAlert'; +import * as env from '../../src/shared/env'; +import * as uiAlertInfo from '../../src/database/uiAlertInfo'; + +import { UiTestResultDTO } from '../../src/database/uiTestResult'; +import { NORMAL, CRITICAL } from '../../src/shared/constants'; + +chai.use(sinonChai); + +const MOCK_TEST_DTO_BASE: UiTestResultDTO = { + testSuiteName: 'ComponentLoadSuite', + individualTestName: 'ComponentXLoadTime', + overallLoadTime: 1000, + componentLoadTime: 500, + salesforceLoadTime: 500, + alertInfo: undefined, +} as UiTestResultDTO; + +describe('generateValidAlerts', () => { + let sandbox: sinon.SinonSandbox; + let storeAlertsStub: sinon.SinonStub; + let getAveragesStub: sinon.SinonStub; + let normalThresholdStub: sinon.SinonStub; + let criticalThresholdStub: sinon.SinonStub; + + before(() => { + sandbox = sinon.createSandbox(); + + storeAlertsStub = sandbox.stub(env, 'shouldStoreUiAlerts'); + normalThresholdStub = sandbox.stub(env, 'getNormalComponentLoadThreshold'); + criticalThresholdStub = sandbox.stub( + env, + 'getCriticalComponentLoadThreshold' + ); + + getAveragesStub = sandbox.stub(uiAlertInfo, 'getAverageLimitValuesFromDB'); + }); + + beforeEach(() => { + sandbox.resetHistory(); + + storeAlertsStub.returns(true); + normalThresholdStub.returns('20'); + criticalThresholdStub.returns('50'); + getAveragesStub.resolves({}); + }); + + after(() => { + sandbox.restore(); + }); + + describe('alert eligibility filtering', () => { + it('should return empty array if global alerts are disabled and no override exists', async () => { + // Given + storeAlertsStub.returns(false); + + // When + const result = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(result).to.be.an('array').that.is.empty; + expect(getAveragesStub).to.not.have.been.called; + }); + + it('should enable processing if a test explicitly enables alerts (override global disable)', async () => { + // Given + storeAlertsStub.returns(false); + const overriddenTest = { + ...MOCK_TEST_DTO_BASE, + alertInfo: { storeAlerts: true }, + } as UiTestResultDTO; + + getAveragesStub.resolves({}); + + // When + await generateValidAlerts([overriddenTest]); + + // Then + expect(getAveragesStub).to.have.been.calledOnce; + }); + + it('should skip processing if a test explicitly disables alerts (override global enable)', async () => { + // Given + storeAlertsStub.returns(true); + const disabledTest = { + ...MOCK_TEST_DTO_BASE, + alertInfo: { storeAlerts: false }, + } as UiTestResultDTO; + + // When + const results = await generateValidAlerts([disabledTest]); + + // Then + expect(results).to.be.an('array').that.is.empty; + expect(getAveragesStub).to.not.have.been.called; + }); + }); + + describe('alert generation logic', () => { + const avgFirst5 = 200; + + it('should generate a CRITICAL alert when degradation is greater than critical threshold (50)', async () => { + //Given + // Degradation is 100ms: |200 - 100| = 100ms. 100 >= 50 (Critical). + const avgNext10 = 100; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_first_5: avgFirst5, + avg_next_10: avgNext10, + }, + }; + getAveragesStub.resolves(mockAverages); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.have.lengthOf(1); + expect(results[0].alertType).to.equal(CRITICAL); + expect(results[0].componentLoadTimeDegraded).to.equal(100); + }); + + it('should generate a NORMAL alert when degradation is between normal (20) and critical (50)', async () => { + //Given + // Degradation is 35ms: |200 - 165| = 35ms. 20 <= 35 < 50 (Normal). + const avgNext10 = 165; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_first_5: avgFirst5, + avg_next_10: avgNext10, + }, + }; + getAveragesStub.resolves(mockAverages); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.have.lengthOf(1); + expect(results[0].alertType).to.equal(NORMAL); + expect(results[0].componentLoadTimeDegraded).to.equal(35); + }); + + it('should return NO alert when degradation is below the normal threshold (20)', async () => { + // Given + // Degradation is 15ms: |200 - 185| = 15ms. 15 < 20. + const avgNext10 = 185; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_first_5: avgFirst5, + avg_next_10: avgNext10, + }, + }; + getAveragesStub.resolves(mockAverages); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.be.an('array').that.is.empty; + }); + + it('should return NO alert when historical average data is not found', async () => { + // Given + getAveragesStub.resolves({}); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.be.an('array').that.is.empty; + }); + + it('should return NO alert if degradation is zero', async () => { + //Given + // Degradation is 0ms: |200 - 200| = 0ms. + const avgNext10 = 200; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_first_5: avgFirst5, + avg_next_10: avgNext10, + }, + }; + getAveragesStub.resolves(mockAverages); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.be.an('array').that.is.empty; + }); + }); + + it('should use test-level thresholds if provided, overriding global environment settings', async () => { + // Given + const testLevelNormal = 10; + const testLevelCritical = 30; + + // Degradation is 25ms: |200 - 175| = 25ms. 10 <= 25 < 30 --> NORMAL + const avgNext10 = 175; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_first_5: 200, + avg_next_10: avgNext10, + }, + }; + getAveragesStub.resolves(mockAverages); + + const testWithOverride = { + ...MOCK_TEST_DTO_BASE, + alertInfo: { + uiAlertThresholds: { + componentLoadTimeThresholdNormal: testLevelNormal, + componentLoadTimeThresholdCritical: testLevelCritical, + }, + }, + } as UiTestResultDTO; + + // When + const results = await generateValidAlerts([testWithOverride]); + + // Then + expect(results).to.have.lengthOf(1); + expect(results[0].alertType).to.equal(NORMAL); + expect(results[0].componentLoadTimeDegraded).to.equal(25); + expect(normalThresholdStub).to.not.have.been.called; + }); + + it('should handle errors during average fetching and return an empty array, logging the error', async () => { + // Given + const errorStub = sandbox.stub(console, 'error'); // Stub console.error to prevent test runner failure + getAveragesStub.rejects(new Error('Database connection failed')); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.be.an('array').that.is.empty; + expect(getAveragesStub).to.have.been.calledOnce; + expect(errorStub).to.have.been.calledWith( + sinon.match('Error generating alerts') + ); + }); +}); From aac1bfaf9bc3ff5b4f2b14d41eddd5fe39f4558e Mon Sep 17 00:00:00 2001 From: Astha Date: Thu, 23 Oct 2025 17:09:42 +0530 Subject: [PATCH 10/20] updated test --- test/services/uiAlert.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/services/uiAlert.test.ts b/test/services/uiAlert.test.ts index 567be26..b2d380d 100644 --- a/test/services/uiAlert.test.ts +++ b/test/services/uiAlert.test.ts @@ -105,7 +105,6 @@ describe('generateValidAlerts', () => { it('should generate a CRITICAL alert when degradation is greater than critical threshold (50)', async () => { //Given - // Degradation is 100ms: |200 - 100| = 100ms. 100 >= 50 (Critical). const avgNext10 = 100; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { @@ -126,7 +125,6 @@ describe('generateValidAlerts', () => { it('should generate a NORMAL alert when degradation is between normal (20) and critical (50)', async () => { //Given - // Degradation is 35ms: |200 - 165| = 35ms. 20 <= 35 < 50 (Normal). const avgNext10 = 165; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { @@ -147,7 +145,6 @@ describe('generateValidAlerts', () => { it('should return NO alert when degradation is below the normal threshold (20)', async () => { // Given - // Degradation is 15ms: |200 - 185| = 15ms. 15 < 20. const avgNext10 = 185; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { @@ -177,7 +174,6 @@ describe('generateValidAlerts', () => { it('should return NO alert if degradation is zero', async () => { //Given - // Degradation is 0ms: |200 - 200| = 0ms. const avgNext10 = 200; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { @@ -200,7 +196,6 @@ describe('generateValidAlerts', () => { const testLevelNormal = 10; const testLevelCritical = 30; - // Degradation is 25ms: |200 - 175| = 25ms. 10 <= 25 < 30 --> NORMAL const avgNext10 = 175; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { @@ -232,7 +227,7 @@ describe('generateValidAlerts', () => { it('should handle errors during average fetching and return an empty array, logging the error', async () => { // Given - const errorStub = sandbox.stub(console, 'error'); // Stub console.error to prevent test runner failure + const errorStub = sandbox.stub(console, 'error'); getAveragesStub.rejects(new Error('Database connection failed')); // When From d5ad6530d82c035fd28b734ffe1274deee9851c9 Mon Sep 17 00:00:00 2001 From: Mohit Date: Thu, 23 Oct 2025 19:30:41 +0530 Subject: [PATCH 11/20] Added test for ui alert thresholds --- test/shared/env.test.ts | 83 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/test/shared/env.test.ts b/test/shared/env.test.ts index b4c16bb..a7afd96 100644 --- a/test/shared/env.test.ts +++ b/test/shared/env.test.ts @@ -600,4 +600,87 @@ describe('shared/env/index', () => { expect(customRanges.soql_ranges).to.be.an('array'); }); }); + + describe('shouldStoreUiAlerts', () => { + it('returns true when STORE_UI_ALERTS is true', () => { + // Given + process.env.STORE_UI_ALERTS = 'true'; + + // When + const shouldStoreUiAlerts = env.shouldStoreUiAlerts(); + + // Then + expect(shouldStoreUiAlerts).to.be.true; + }); + + it('returns false when STORE_UI_ALERTS is false', () => { + // Given + process.env.STORE_UI_ALERTS = 'false'; + + // When + const shouldStoreUiAlerts = env.shouldStoreUiAlerts(); + + // Then + expect(shouldStoreUiAlerts).to.be.false; + }); + + it('returns false when STORE_UI_ALERTS is undefined', () => { + // Given + process.env.STORE_UI_ALERTS = undefined; + + // When + const shouldStoreUiAlerts = env.shouldStoreUiAlerts(); + + // Then + expect(shouldStoreUiAlerts).to.be.false; + }); + }); + + describe('getNormalComponentLoadThreshold', () => { + it('returns NORMAL_COMPONENT_LOAD_THRESHOLD given value', () => { + // Given + process.env.NORMAL_COMPONENT_LOAD_THRESHOLD = '10'; + + // When + const normalComponentLoadThreshold = env.getNormalComponentLoadThreshold(); + + // Then + expect(normalComponentLoadThreshold).to.eql('10'); + }); + + it('return default value when NORMAL_COMPONENT_LOAD_THRESHOLD is not set', () => { + // Given + delete process.env.NORMAL_COMPONENT_LOAD_THRESHOLD; + + // When + const normalComponentLoadThreshold = env.getNormalComponentLoadThreshold(); + + // Then + expect(normalComponentLoadThreshold).to.eql('1000'); + }); + }); + + describe('getCriticalComponentLoadThreshold', () => { + it('returns CRITICAL_COMPONENT_LOAD_THRESHOLD given value', () => { + // Given + process.env.CRITICAL_COMPONENT_LOAD_THRESHOLD = '10'; + + // When + const criticalComponentLoadThreshold = env.getCriticalComponentLoadThreshold(); + + // Then + expect(criticalComponentLoadThreshold).to.eql('10'); + }); + + it('return default value when CRITICAL_COMPONENT_LOAD_THRESHOLD is not set', () => { + // Given + delete process.env.CRITICAL_COMPONENT_LOAD_THRESHOLD; + + // When + const criticalComponentLoadThreshold = env.getCriticalComponentLoadThreshold(); + + // Then + expect(criticalComponentLoadThreshold).to.eql('10000'); + }); + }); }); From 2a533f1411d72358ec2bcb2ed89e2e01a3d6efe8 Mon Sep 17 00:00:00 2001 From: Vijay Choudhary Date: Fri, 24 Oct 2025 11:55:24 +0530 Subject: [PATCH 12/20] Store alerts into database --- src/database/connection.ts | 2 ++ src/database/uiAlertInfo.ts | 21 +++++++++++++++++++++ src/database/uiTestResult.ts | 6 ++++++ 3 files changed, 29 insertions(+) diff --git a/src/database/connection.ts b/src/database/connection.ts index aee6403..76c71b7 100644 --- a/src/database/connection.ts +++ b/src/database/connection.ts @@ -12,6 +12,7 @@ import { ExecutionInfo } from './entity/execution'; import { Alert } from './entity/alert'; import { TestInfo } from './entity/testInfo'; import { UiTestResult } from './entity/uiTestResult'; +import { UiAlert } from './entity/uiAlert'; export const DB_ENTITIES = [ TestResult, @@ -21,6 +22,7 @@ export const DB_ENTITIES = [ Alert, TestInfo, UiTestResult, + UiAlert, ]; let connection: DataSource | null = null; diff --git a/src/database/uiAlertInfo.ts b/src/database/uiAlertInfo.ts index e22df53..6522308 100644 --- a/src/database/uiAlertInfo.ts +++ b/src/database/uiAlertInfo.ts @@ -3,6 +3,27 @@ * Copyright (c) 2025 Certinia, Inc. All rights reserved. */ import { getConnection } from './connection'; +import { UiAlert } from './entity/uiAlert'; +import { UiTestResult } from './entity/uiTestResult'; + +export async function saveAlerts( + testResultsDB: UiTestResult[], + alerts: UiAlert[] +) { + alerts.forEach(alert => { + const match = testResultsDB.find( + result => + result.testSuiteName === alert.testSuiteName && + result.individualTestName === alert.individualTestName + ); + if (match) { + alert.uiTestResultId = match.id; + } + }); + + const connection = await getConnection(); + return connection.manager.save(alerts); +} export async function getAverageLimitValuesFromDB( suiteAndTestNamePairs: { testSuiteName: string; individualTestName: string }[] diff --git a/src/database/uiTestResult.ts b/src/database/uiTestResult.ts index 331f8d6..8574546 100644 --- a/src/database/uiTestResult.ts +++ b/src/database/uiTestResult.ts @@ -6,6 +6,8 @@ import { UiTestResult } from './entity/uiTestResult'; import { saveRecords } from './saveRecords'; import { getConnection } from './connection'; import { MoreThanOrEqual } from 'typeorm'; +import { generateValidAlerts } from '../services/result/uiAlert'; +import { saveAlerts } from './uiAlertInfo'; /** * Describes the Thresholds for different limits @@ -81,6 +83,10 @@ export async function saveUiTestResult( ): Promise { const entities = testStepResults.map(dtoToEntity); const savedEntities = await saveRecords(entities); + const alerts = await generateValidAlerts(testStepResults); + if (alerts.length) { + await saveAlerts(savedEntities, alerts); + } return savedEntities.map(entityToDto); } From fcb35183eee3d1d250e7fb2cced9f9476dae37ec Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Fri, 24 Oct 2025 12:41:28 +0530 Subject: [PATCH 13/20] updated alert.md for ui alerts --- docs/user/alerts.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/user/alerts.md b/docs/user/alerts.md index 8441293..a2da45b 100644 --- a/docs/user/alerts.md +++ b/docs/user/alerts.md @@ -110,3 +110,48 @@ await TransactionProcess.executeTestStep(..., alertInfo); ```txt Note: If the test level threshold is misconfigured below the average, you get an alert with a value of 0. Recommend filtering out zero alerts when querying for new records. ``` +# UI Alerts + +UI Alerts can be stored in the database to monitor performance degradation over time. Each record’s degradation value represents the difference between the average of the first 5 ui test results and the average of the subsequent 10 ui test results recorded within the past 10 days. If there have not been at least 15 results in the last 30 days, an ui alert is not stored. + +## Usage + +### Environment Variables + +To enable UI alerts globally, set the `STORE_UI_ALERTS` variable to `true` in the environment file + +To override the default normal component load time threshold (1000 ms), set the `NORMAL_COMPONENT_LOAD_THRESHOLD` variable to any desired value in the environment file + +To override the default critical component load time threshold (10000 ms), set the `CRITICAL_COMPONENT_LOAD_THRESHOLD` variable to any desired value in the environment file + +#### Example Scenario + +* The average component load time for the first 5 results was 1500 ms, while the next 10 results averaged 1000 ms. +* You will get an normal alert, saying it has degraded by 500 above the average. + +### Test Level Thresholds + +Alternatively, thresholds can be configured at the test level. If the difference between the average of the first 5 and the subsequent 10 UI test results exceeds the defined custom threshold, an alert will be stored. The degradation value remains the difference between the average of the first 5 results and the average of the next 10 results recorded. The threshold simply determines whether an alert is triggered. + +```ts +// Replace global alert behaviour with exact thresholds +const customThresholds: UiAlertThresholds = new UiAlertThresholds(); +customThresholds.componentLoadTimeThresholdNormal = 50; +customThresholds.componentLoadTimeThresholdCritical = 100; + +// Enable alerting for this test if not already active +const alertInfo: UiAlertInfo = new UiAlertInfo(); +alertInfo.storeAlerts = true; +alertInfo.uiAlertThresholds = customThresholds; + +const testResult: UiTestResultDTO = { + ..., + alertInfo, + }; + +await saveUiTestResult([testResult]); +``` +#### Example Scenario + +* Test-level thresholds for normal and critical component load times are set to 50 ms and 100 ms, respectively. The average load time for the first 5 results was 1500 ms, while the subsequent 10 results averaged 1000 ms. +* You will get an critical alert, saying it has degraded by 500 above the average. From 1cbb3f0c5156d84eec0cd718018d5a98fa295aa5 Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Fri, 24 Oct 2025 12:58:27 +0530 Subject: [PATCH 14/20] review chnage --- docs/user/alerts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/alerts.md b/docs/user/alerts.md index a2da45b..1772d79 100644 --- a/docs/user/alerts.md +++ b/docs/user/alerts.md @@ -112,7 +112,7 @@ Note: If the test level threshold is misconfigured below the average, you get an ``` # UI Alerts -UI Alerts can be stored in the database to monitor performance degradation over time. Each record’s degradation value represents the difference between the average of the first 5 ui test results and the average of the subsequent 10 ui test results recorded within the past 10 days. If there have not been at least 15 results in the last 30 days, an ui alert is not stored. +UI Alerts can be stored in the database to monitor performance degradation over time. Each record’s degradation value represents the difference between the average of the first 5 ui test results and the average of the subsequent 10 ui test results recorded within the past 30 days. If there have not been at least 15 results in the last 30 days, an ui alert is not stored. ## Usage @@ -131,7 +131,7 @@ To override the default critical component load time threshold (10000 ms), set t ### Test Level Thresholds -Alternatively, thresholds can be configured at the test level. If the difference between the average of the first 5 and the subsequent 10 UI test results exceeds the defined custom threshold, an alert will be stored. The degradation value remains the difference between the average of the first 5 results and the average of the next 10 results recorded. The threshold simply determines whether an alert is triggered. +Alternatively, thresholds can be configured at the test level. If the difference between the average of the first 5 and the subsequent 10 ui test results exceeds the defined custom threshold, an alert will be stored. The degradation value remains the difference between the average of the first 5 results and the average of the next 10 results recorded. The threshold simply determines whether an alert is triggered. ```ts // Replace global alert behaviour with exact thresholds From 3400367b46618b35232a77a64b69d35a8697928b Mon Sep 17 00:00:00 2001 From: Astha Date: Fri, 24 Oct 2025 13:00:45 +0530 Subject: [PATCH 15/20] updated comment --- src/services/result/uiAlert.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/result/uiAlert.ts b/src/services/result/uiAlert.ts index d888111..8dc001a 100644 --- a/src/services/result/uiAlert.ts +++ b/src/services/result/uiAlert.ts @@ -75,7 +75,7 @@ async function addAlertByComparingAvg( return alert; } - //storing alerts if there is a degradation + // Storing alerts if there is a degradation const normalComponentLoadThreshold: number = output.alertInfo ?.uiAlertThresholds ? output.alertInfo.uiAlertThresholds.componentLoadTimeThresholdNormal From 6cdffcc6b847a27a2ea29919959dcfa67cde4ae5 Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Fri, 24 Oct 2025 13:03:35 +0530 Subject: [PATCH 16/20] updated changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17dcb2d..c05ed47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Benchmarker - Changelog +## 6.0.0 + +### Breaking Changes + +- Database schema updated: Use `db/migrations/V6__create_performance_ui_alert_table.sql` script to create new table to store alerts for performance UI test results. + ## 5.0.0 ### Breaking Changes From feeb62a2e96ad6d97328ca4b96dbc4a96c1deb95 Mon Sep 17 00:00:00 2001 From: Vijay Choudhary Date: Fri, 24 Oct 2025 13:16:19 +0530 Subject: [PATCH 17/20] adding test --- test/database/uiAlertInfo.test.ts | 37 ++++++++++++++++++++-- test/database/uiTestResult.test.ts | 51 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/test/database/uiAlertInfo.test.ts b/test/database/uiAlertInfo.test.ts index 72ece8a..644379c 100644 --- a/test/database/uiAlertInfo.test.ts +++ b/test/database/uiAlertInfo.test.ts @@ -2,22 +2,29 @@ * Copyright (c) 2025 Certinia, inc. All rights reserved. */ -import { getAverageLimitValuesFromDB } from '../../src/database/uiAlertInfo'; +import { + getAverageLimitValuesFromDB, + saveAlerts, +} from '../../src/database/uiAlertInfo'; import * as db from '../../src/database/connection'; import sinon from 'sinon'; import chai, { expect } from 'chai'; import sinonChai from 'sinon-chai'; +import { DataSource } from 'typeorm'; +import { UiAlert } from '../../src/database/entity/uiAlert'; +import { UiTestResult } from '../../src/database/entity/uiTestResult'; chai.use(sinonChai); describe('src/database/alertInfo', () => { let mockQuery: sinon.SinonStub; + let connectionStub: sinon.SinonStub; let mockDataSource: any; beforeEach(() => { mockQuery = sinon.stub(); mockDataSource = { query: mockQuery }; - sinon.stub(db, 'getConnection').resolves(mockDataSource); + connectionStub = sinon.stub(db, 'getConnection').resolves(mockDataSource); }); afterEach(() => { @@ -163,4 +170,30 @@ describe('src/database/alertInfo', () => { expect(results).to.deep.equal({}); }); }); + + describe('saveAlerts', () => { + it('should save alert', async () => { + // Given + const saveStub: sinon.SinonStub = sinon.stub().resolvesArg(0); + connectionStub.resolves({ + manager: { save: saveStub }, + } as unknown as DataSource); + + const results = [new UiAlert()]; + const savedEntity = new UiTestResult(); + savedEntity.id = 1; + savedEntity.testSuiteName = 'suite'; + savedEntity.individualTestName = 'test'; + savedEntity.componentLoadTime = 10; + savedEntity.salesforceLoadTime = 20; + savedEntity.overallLoadTime = 30; + + // When + const savedRecords = await saveAlerts([savedEntity], results); + + // Then + expect(saveStub).to.be.calledOnce; + expect(savedRecords).to.eql(results); + }); + }); }); diff --git a/test/database/uiTestResult.test.ts b/test/database/uiTestResult.test.ts index 392ea94..3406de7 100644 --- a/test/database/uiTestResult.test.ts +++ b/test/database/uiTestResult.test.ts @@ -15,16 +15,26 @@ import { } from '../../src/database/uiTestResult'; import { DataSource } from 'typeorm'; import { UiTestResult } from '../../src/database/entity/uiTestResult'; +import * as uiAlert from '../../src/services/result/uiAlert'; +import { UiAlert } from '../../src/database/entity/uiAlert'; +import * as alertInfo from '../../src/database/uiAlertInfo'; chai.use(sinonChai); describe('src/database/uiTestResult', () => { let connectionStub: SinonStub; let saveRecordsStub: SinonStub; + let generateValidAlertsStub: SinonStub; + let alertInfoStub: SinonStub; beforeEach(() => { connectionStub = sinon.stub(db, 'getConnection'); saveRecordsStub = sinon.stub(saveModule, 'saveRecords'); + generateValidAlertsStub = sinon.stub(uiAlert, 'generateValidAlerts'); + alertInfoStub = sinon.stub(alertInfo, 'saveAlerts'); + + alertInfoStub.resolvesArg(0); + alertInfoStub.resolvesArg(1); }); afterEach(() => { @@ -51,6 +61,7 @@ describe('src/database/uiTestResult', () => { savedEntity.overallLoadTime = 30; saveRecordsStub.resolves([savedEntity]); + generateValidAlertsStub.resolves([]); // When const result = await saveUiTestResult([dto]); @@ -66,6 +77,46 @@ describe('src/database/uiTestResult', () => { overallLoadTime: 30, }, ]); + expect(generateValidAlertsStub).to.be.calledOnce; + expect(alertInfoStub).to.not.be.called; + }); + + it('should save alerts if any valid alert entries are returned', async () => { + // Given + const dto: UiTestResultDTO = { + testSuiteName: 'suite', + individualTestName: 'test', + componentLoadTime: 10, + salesforceLoadTime: 20, + overallLoadTime: 30, + }; + + const savedEntity = new UiTestResult(); + savedEntity.id = 1; + savedEntity.testSuiteName = 'suite'; + savedEntity.individualTestName = 'test'; + savedEntity.componentLoadTime = 10; + savedEntity.salesforceLoadTime = 20; + savedEntity.overallLoadTime = 30; + + const alert: UiAlert = new UiAlert(); + alert.testSuiteName = savedEntity.testSuiteName; + alert.individualTestName = savedEntity.individualTestName; + alert.componentLoadTimeDegraded = 2; + alert.alertType = 'normal'; + + saveRecordsStub.resolves([savedEntity]); + generateValidAlertsStub.resolves([alert]); + + // When + await saveUiTestResult([dto]); + + // Then + expect(saveRecordsStub).to.be.calledOnce; + expect(generateValidAlertsStub).to.be.calledOnce; + expect(alertInfoStub).to.be.calledOnce; + expect(alertInfoStub.args[0][0][0].id).to.equal(savedEntity.id); + expect(alertInfoStub.args[0][1][0].alertType).to.equal('normal'); }); }); From 252ca7e3d410598d326fed0fdd3f1aab8044196a Mon Sep 17 00:00:00 2001 From: Vijay Choudhary Date: Fri, 24 Oct 2025 13:23:31 +0530 Subject: [PATCH 18/20] updating test --- test/database/uiAlertInfo.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/database/uiAlertInfo.test.ts b/test/database/uiAlertInfo.test.ts index 644379c..30de2f8 100644 --- a/test/database/uiAlertInfo.test.ts +++ b/test/database/uiAlertInfo.test.ts @@ -179,7 +179,6 @@ describe('src/database/alertInfo', () => { manager: { save: saveStub }, } as unknown as DataSource); - const results = [new UiAlert()]; const savedEntity = new UiTestResult(); savedEntity.id = 1; savedEntity.testSuiteName = 'suite'; @@ -188,12 +187,20 @@ describe('src/database/alertInfo', () => { savedEntity.salesforceLoadTime = 20; savedEntity.overallLoadTime = 30; + const alert: UiAlert = new UiAlert(); + alert.testSuiteName = savedEntity.testSuiteName; + alert.individualTestName = savedEntity.individualTestName; + alert.componentLoadTimeDegraded = 2; + alert.alertType = 'normal'; + const results = [alert]; + // When const savedRecords = await saveAlerts([savedEntity], results); // Then expect(saveStub).to.be.calledOnce; expect(savedRecords).to.eql(results); + expect(savedRecords[0].uiTestResultId).to.equal(1); }); }); }); From 0d3590e4dd4e3df7d31ddaf9bfa553195eb51ba6 Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Mon, 27 Oct 2025 16:51:01 +0530 Subject: [PATCH 19/20] review changes --- docs/user/alerts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/alerts.md b/docs/user/alerts.md index 1772d79..3fd9c92 100644 --- a/docs/user/alerts.md +++ b/docs/user/alerts.md @@ -112,7 +112,7 @@ Note: If the test level threshold is misconfigured below the average, you get an ``` # UI Alerts -UI Alerts can be stored in the database to monitor performance degradation over time. Each record’s degradation value represents the difference between the average of the first 5 ui test results and the average of the subsequent 10 ui test results recorded within the past 30 days. If there have not been at least 15 results in the last 30 days, an ui alert is not stored. +UI Alerts can be stored in the database to monitor performance degradation over time. Each record’s degradation value represents the difference between the average of the first 5 ui test results and the average of the subsequent 10 ui test results recorded within the past 30 days. A minimum of 15 results within a 30-day window is required to trigger UI alert storage. ## Usage From 6bc69e199261d011f19c9e82ca80b43c9bbdf62a Mon Sep 17 00:00:00 2001 From: rjaiswalcertinia Date: Tue, 28 Oct 2025 14:15:39 +0530 Subject: [PATCH 20/20] updated default range for dml and soql --- src/services/defaultRanges.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/services/defaultRanges.ts b/src/services/defaultRanges.ts index 5804c43..fcb5006 100644 --- a/src/services/defaultRanges.ts +++ b/src/services/defaultRanges.ts @@ -6,16 +6,14 @@ import { RangeCollection } from './ranges'; export const DEFAULT_RANGES: RangeCollection = { dml_ranges: [ - { start_range: 0, end_range: 50, offset_threshold: 5 }, - { start_range: 51, end_range: 100, offset_threshold: 3 }, - { start_range: 101, end_range: 120, offset_threshold: 2 }, - { start_range: 121, end_range: 149, offset_threshold: 1 }, + { start_range: 0, end_range: 50, offset_threshold: 3 }, + { start_range: 51, end_range: 100, offset_threshold: 2 }, + { start_range: 101, end_range: 149, offset_threshold: 1 }, ], soql_ranges: [ - { start_range: 0, end_range: 50, offset_threshold: 10 }, - { start_range: 51, end_range: 80, offset_threshold: 5 }, - { start_range: 81, end_range: 90, offset_threshold: 2 }, - { start_range: 91, end_range: 99, offset_threshold: 1 }, + { start_range: 0, end_range: 30, offset_threshold: 3 }, + { start_range: 31, end_range: 60, offset_threshold: 2 }, + { start_range: 61, end_range: 99, offset_threshold: 1 }, ], cpu_ranges: [ { start_range: 0, end_range: 2000, offset_threshold: 3000 },