From f9fff85e6699d812c34407ecf0bcbfad23de74a0 Mon Sep 17 00:00:00 2001 From: Matthew Oladimeji Date: Mon, 15 Dec 2025 10:39:44 +0000 Subject: [PATCH 01/10] Filter out recent ui alerts for a specific tests and relevant tests --- src/database/uiAlertInfo.ts | 34 ++++++++++++++++ src/services/result/uiAlert.ts | 21 ++++++++-- src/shared/constants.ts | 1 - test/database/uiAlertInfo.test.ts | 68 +++++++++++++++++++++++++++++++ test/services/uiAlert.test.ts | 52 +++++++++++++++++++++-- 5 files changed, 168 insertions(+), 8 deletions(-) diff --git a/src/database/uiAlertInfo.ts b/src/database/uiAlertInfo.ts index fca28c5..f5bb2fa 100644 --- a/src/database/uiAlertInfo.ts +++ b/src/database/uiAlertInfo.ts @@ -128,3 +128,37 @@ export async function getAverageLimitValuesFromDB( return {}; } } + +export async function checkRecentUiAlerts( + suiteAndTestNamePairs: { testSuiteName: string; individualTestName: string }[] +) { + const connection = await getConnection(); + + const suiteAndTestNameConditions = suiteAndTestNamePairs + .map(pair => `('${pair.testSuiteName}', '${pair.individualTestName}')`) + .join(', '); + + const query = ` + SELECT test_suite_name, individual_test_name + FROM performance.ui_alert + WHERE create_date_time >= CURRENT_DATE - INTERVAL '3 days' + AND (test_suite_name, individual_test_name) IN (${suiteAndTestNameConditions}) + `; + + const existingAlerts = new Set(); + + try { + const result = await connection.query(query); + result.forEach( + (row: { test_suite_name: string; individual_test_name: string }) => { + existingAlerts.add( + `${row.test_suite_name}_${row.individual_test_name}` + ); + } + ); + } catch (error) { + console.error('Error checking recent UI alerts:', error); + } + + return existingAlerts; +} diff --git a/src/services/result/uiAlert.ts b/src/services/result/uiAlert.ts index 8b0d4f6..9c79042 100644 --- a/src/services/result/uiAlert.ts +++ b/src/services/result/uiAlert.ts @@ -7,10 +7,13 @@ import { getNormalComponentLoadThreshold, getCriticalComponentLoadThreshold, } from '../../shared/env'; -import { getAverageLimitValuesFromDB } from '../../database/uiAlertInfo'; +import { + getAverageLimitValuesFromDB, + checkRecentUiAlerts, +} from '../../database/uiAlertInfo'; import { UiAlert } from '../../database/entity/uiAlert'; import { UiTestResultDTO } from '../../database/uiTestResult'; -import { NORMAL, CRITICAL } from '../../shared/constants'; +import { CRITICAL } from '../../shared/constants'; export async function generateValidAlerts( testResultOutput: UiTestResultDTO[] @@ -31,6 +34,17 @@ export async function generateValidAlerts( individualTestName: result.individualTestName, })); + const existingAlerts = await checkRecentUiAlerts(suiteAndTestNamePairs); + + const alertsToProcess = needToStoreAlert.filter( + item => + !existingAlerts.has(`${item.testSuiteName}_${item.individualTestName}`) + ); + + if (alertsToProcess.length === 0) { + return []; + } + // Fetch average values const preFetchedAverages = await getAverageLimitValuesFromDB( suiteAndTestNamePairs @@ -38,7 +52,7 @@ export async function generateValidAlerts( // Generate alerts const alerts = await Promise.all( - needToStoreAlert.map(async item => + alertsToProcess.map(async item => addAlertByComparingAvg(item, preFetchedAverages) ) ); @@ -94,7 +108,6 @@ async function addAlertByComparingAvg( componentLoadThresholdDegraded < criticalComponentLoadThreshold ) { alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; - alert.alertType = NORMAL; } else if (componentLoadThresholdDegraded >= criticalComponentLoadThreshold) { alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; alert.alertType = CRITICAL; diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 0126501..4895f6c 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -62,5 +62,4 @@ export const ERROR_OPENING_BROWSER = /***************************************************************************** * UI ALERT TYPES ******************************************************************************/ -export const NORMAL = 'normal'; export const CRITICAL = 'critical'; diff --git a/test/database/uiAlertInfo.test.ts b/test/database/uiAlertInfo.test.ts index 61b62b9..5ac8833 100644 --- a/test/database/uiAlertInfo.test.ts +++ b/test/database/uiAlertInfo.test.ts @@ -5,6 +5,7 @@ import { getAverageLimitValuesFromDB, saveAlerts, + checkRecentUiAlerts, } from '../../src/database/uiAlertInfo'; import * as db from '../../src/database/connection'; import sinon from 'sinon'; @@ -329,4 +330,71 @@ describe('src/database/uiAlertInfo', () => { expect(savedRecords[0].uiTestResultId).to.equal(1); }); }); + + describe('checkRecentUiAlerts', () => { + it('should return a Set of keys for alerts found in the last 3 days', async () => { + // Given + const pairs = [ + { testSuiteName: 'SuiteA', individualTestName: 'Test1' }, + { testSuiteName: 'SuiteB', individualTestName: 'Test2' }, + ]; + + const mockDbRows = [ + { test_suite_name: 'SuiteA', individual_test_name: 'Test1' }, + ]; + + mockQuery.resolves(mockDbRows); + + // When + const result = await checkRecentUiAlerts(pairs); + + // Then + expect(mockQuery).to.have.been.calledOnce; + + const sqlQuery = mockQuery.firstCall.args[0]; + expect(sqlQuery).to.include("INTERVAL '3 days'"); + + expect(sqlQuery).to.include("('SuiteA', 'Test1')"); + expect(sqlQuery).to.include("('SuiteB', 'Test2')"); + + expect(result).to.be.instanceOf(Set); + expect(result.size).to.equal(1); + expect(result.has('SuiteA_Test1')).to.be.true; + expect(result.has('SuiteB_Test2')).to.be.false; + }); + + it('should return an empty Set if no recent alerts are found', async () => { + // Given + mockQuery.resolves([]); + + // When + const result = await checkRecentUiAlerts([ + { testSuiteName: 'SuiteA', individualTestName: 'Test1' }, + ]); + + // Then + expect(result).to.be.instanceOf(Set); + expect(result.size).to.equal(0); + }); + + it('should handle database errors gracefully by returning an empty Set', async () => { + // Given + const consoleStub = sinon.stub(console, 'error'); + mockQuery.rejects(new Error('Connection failed')); + + // When + const result = await checkRecentUiAlerts([ + { testSuiteName: 'SuiteA', individualTestName: 'Test1' }, + ]); + + // Then + expect(result).to.be.instanceOf(Set); + expect(result.size).to.equal(0); + expect(consoleStub).to.have.been.calledWith( + sinon.match('Error checking recent UI alerts') + ); + + consoleStub.restore(); + }); + }); }); diff --git a/test/services/uiAlert.test.ts b/test/services/uiAlert.test.ts index 9870a0a..9fdb4a4 100644 --- a/test/services/uiAlert.test.ts +++ b/test/services/uiAlert.test.ts @@ -7,7 +7,7 @@ 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'; +import { CRITICAL } from '../../src/shared/constants'; chai.use(sinonChai); @@ -26,6 +26,7 @@ describe('generateValidAlerts', () => { let getAveragesStub: sinon.SinonStub; let normalThresholdStub: sinon.SinonStub; let criticalThresholdStub: sinon.SinonStub; + let checkRecentStub: sinon.SinonStub; before(() => { sandbox = sinon.createSandbox(); @@ -38,6 +39,7 @@ describe('generateValidAlerts', () => { ); getAveragesStub = sandbox.stub(uiAlertInfo, 'getAverageLimitValuesFromDB'); + checkRecentStub = sandbox.stub(uiAlertInfo, 'checkRecentUiAlerts'); }); beforeEach(() => { @@ -47,6 +49,7 @@ describe('generateValidAlerts', () => { normalThresholdStub.returns('20'); criticalThresholdStub.returns('50'); getAveragesStub.resolves({}); + checkRecentStub.resolves(new Set()); }); after(() => { @@ -100,6 +103,51 @@ describe('generateValidAlerts', () => { }); }); + describe('recent alert filtering', () => { + const avgFirst5 = 200; + it('should skip processing if an alert was already triggered for this test in the last 3 days', async () => { + // Given + const avgNext10 = 200; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_load_time_past_5_days: avgFirst5, + avg_load_time_6_to_15_days_ago: avgNext10, + }, + }; + checkRecentStub.resolves( + new Set(['ComponentLoadSuite_ComponentXLoadTime']) + ); + getAveragesStub.resolves(mockAverages); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.be.an('array').that.is.empty; + expect(getAveragesStub).to.not.have.been.called; + }); + + it('should process the test normally if no recent alerts exist', async () => { + // Given + const avgNext10 = 200; + const mockAverages = { + ['ComponentLoadSuite_ComponentXLoadTime']: { + avg_load_time_past_5_days: avgFirst5, + avg_load_time_6_to_15_days_ago: avgNext10, + }, + }; + checkRecentStub.resolves(new Set()); + getAveragesStub.resolves(mockAverages); + + // When + const results = await generateValidAlerts([MOCK_TEST_DTO_BASE]); + + // Then + expect(results).to.have.lengthOf(1); + expect(getAveragesStub).to.have.been.called; + }); + }); + describe('alert generation logic', () => { const avgFirst5 = 200; @@ -139,7 +187,6 @@ describe('generateValidAlerts', () => { // Then expect(results).to.have.lengthOf(1); - expect(results[0].alertType).to.equal(NORMAL); expect(results[0].componentLoadTimeDegraded).to.equal(35); }); @@ -220,7 +267,6 @@ describe('generateValidAlerts', () => { // 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; }); From 34a6923a793e6defb295d559837efb3e572e141c Mon Sep 17 00:00:00 2001 From: Matthew Oladimeji Date: Mon, 15 Dec 2025 11:50:47 +0000 Subject: [PATCH 02/10] fix failing test --- test/services/uiAlert.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/services/uiAlert.test.ts b/test/services/uiAlert.test.ts index 9fdb4a4..149b91c 100644 --- a/test/services/uiAlert.test.ts +++ b/test/services/uiAlert.test.ts @@ -107,7 +107,7 @@ describe('generateValidAlerts', () => { const avgFirst5 = 200; it('should skip processing if an alert was already triggered for this test in the last 3 days', async () => { // Given - const avgNext10 = 200; + const avgNext10 = 100; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { avg_load_time_past_5_days: avgFirst5, @@ -129,7 +129,7 @@ describe('generateValidAlerts', () => { it('should process the test normally if no recent alerts exist', async () => { // Given - const avgNext10 = 200; + const avgNext10 = 100; const mockAverages = { ['ComponentLoadSuite_ComponentXLoadTime']: { avg_load_time_past_5_days: avgFirst5, From 73fef0f5f907ad1dabe75ca865c1e0c3682ad7d4 Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:04:47 +0000 Subject: [PATCH 03/10] Revert change to alertType --- src/services/result/uiAlert.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/result/uiAlert.ts b/src/services/result/uiAlert.ts index 9c79042..cf3458b 100644 --- a/src/services/result/uiAlert.ts +++ b/src/services/result/uiAlert.ts @@ -13,7 +13,7 @@ import { } from '../../database/uiAlertInfo'; import { UiAlert } from '../../database/entity/uiAlert'; import { UiTestResultDTO } from '../../database/uiTestResult'; -import { CRITICAL } from '../../shared/constants'; +import { CRITICAL, NORMAL } from '../../shared/constants'; export async function generateValidAlerts( testResultOutput: UiTestResultDTO[] @@ -108,6 +108,7 @@ async function addAlertByComparingAvg( componentLoadThresholdDegraded < criticalComponentLoadThreshold ) { alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; + alert.alertType = NORMAL; } else if (componentLoadThresholdDegraded >= criticalComponentLoadThreshold) { alert.componentLoadTimeDegraded = componentLoadThresholdDegraded; alert.alertType = CRITICAL; From cc6a7b41b29277c151608cff7c6621b4544b9e99 Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:05:07 +0000 Subject: [PATCH 04/10] Revert removal of constant --- src/shared/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 4895f6c..0126501 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -62,4 +62,5 @@ export const ERROR_OPENING_BROWSER = /***************************************************************************** * UI ALERT TYPES ******************************************************************************/ +export const NORMAL = 'normal'; export const CRITICAL = 'critical'; From 022f50ba8902bdeb7fa3f60d9057ca8b9015167c Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:05:30 +0000 Subject: [PATCH 05/10] Update benchmarker version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc522e7..8ae8796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apexdevtools/benchmarker", - "version": "6.0.2", + "version": "6.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apexdevtools/benchmarker", - "version": "6.0.2", + "version": "6.1.2", "dependencies": { "@jsforce/jsforce-node": "3.2.0", "@salesforce/core": "7.3.6", diff --git a/package.json b/package.json index 956c8cf..205a0a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apexdevtools/benchmarker", - "version": "6.0.2", + "version": "6.1.2", "description": "Benchmarks performance of processes on Salesforce Orgs", "author": { "name": "Apex Dev Tools Team", From e53c9454ad2d1e6c0de460080317300be72b8006 Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:05:44 +0000 Subject: [PATCH 06/10] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12db50f..63f9d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Benchmarker - Changelog +## 6.1.0 + +- UI performance alerts are now suppressed if the same test suite and individual test have already triggered an alert within the last 3 days. + ## 6.0.2 - Update logic to generate UI Alerts to handle averages that are decimals. @@ -41,6 +45,7 @@ ### Added - New property on `TestStepDescription`, `additionalData`. Provide custom string information to link to results. + - Stored against `flowName`, `action` and `product` of the test. - To set it use `createApexExecutionTestStepFlow(connection, path, { flowName, action, additionalData })`. @@ -76,6 +81,7 @@ ### Added - New alerts config for global and per test usage. Used for reporting on degradations over time. + - Configure global behaviour with a JSON file, or per test with new parameter on `TransactionProcess.executeTestStep`. - See [the documentation on alerts](./docs/user/alerts.md) for more details. From b74bbd81016afebd6c6d8776391a78644c1928cc Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:11:27 +0000 Subject: [PATCH 07/10] Revert removal of assertions --- test/services/uiAlert.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/services/uiAlert.test.ts b/test/services/uiAlert.test.ts index 149b91c..b9f59d0 100644 --- a/test/services/uiAlert.test.ts +++ b/test/services/uiAlert.test.ts @@ -187,6 +187,7 @@ describe('generateValidAlerts', () => { // Then expect(results).to.have.lengthOf(1); + expect(results[0].alertType).to.equal(NORMAL); expect(results[0].componentLoadTimeDegraded).to.equal(35); }); @@ -267,6 +268,7 @@ describe('generateValidAlerts', () => { // 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; }); From 947e084f99c25c0f15fa05be597a1fa39ccbb332 Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:11:49 +0000 Subject: [PATCH 08/10] Update benchmarker version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ae8796..7c439f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apexdevtools/benchmarker", - "version": "6.1.2", + "version": "6.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apexdevtools/benchmarker", - "version": "6.1.2", + "version": "6.1.0", "dependencies": { "@jsforce/jsforce-node": "3.2.0", "@salesforce/core": "7.3.6", diff --git a/package.json b/package.json index 205a0a5..b1d7a55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apexdevtools/benchmarker", - "version": "6.1.2", + "version": "6.1.0", "description": "Benchmarks performance of processes on Salesforce Orgs", "author": { "name": "Apex Dev Tools Team", From 649a08bdea44e8192817165b96bdf88a016bbfd0 Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:18:10 +0000 Subject: [PATCH 09/10] Revert formatting --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f9d5f..d5f28aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,6 @@ ### Added - New property on `TestStepDescription`, `additionalData`. Provide custom string information to link to results. - - Stored against `flowName`, `action` and `product` of the test. - To set it use `createApexExecutionTestStepFlow(connection, path, { flowName, action, additionalData })`. @@ -81,7 +80,6 @@ ### Added - New alerts config for global and per test usage. Used for reporting on degradations over time. - - Configure global behaviour with a JSON file, or per test with new parameter on `TransactionProcess.executeTestStep`. - See [the documentation on alerts](./docs/user/alerts.md) for more details. From fdad062a2bbde86ed39571cc9a1e6b5ceaddcafe Mon Sep 17 00:00:00 2001 From: Shaun Brocklehurst Date: Tue, 16 Dec 2025 11:19:08 +0000 Subject: [PATCH 10/10] Revert removal of import --- src/services/result/uiAlert.ts | 2 +- test/services/uiAlert.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/result/uiAlert.ts b/src/services/result/uiAlert.ts index cf3458b..8c312fc 100644 --- a/src/services/result/uiAlert.ts +++ b/src/services/result/uiAlert.ts @@ -13,7 +13,7 @@ import { } from '../../database/uiAlertInfo'; import { UiAlert } from '../../database/entity/uiAlert'; import { UiTestResultDTO } from '../../database/uiTestResult'; -import { CRITICAL, NORMAL } from '../../shared/constants'; +import { NORMAL, CRITICAL } from '../../shared/constants'; export async function generateValidAlerts( testResultOutput: UiTestResultDTO[] diff --git a/test/services/uiAlert.test.ts b/test/services/uiAlert.test.ts index b9f59d0..c3df051 100644 --- a/test/services/uiAlert.test.ts +++ b/test/services/uiAlert.test.ts @@ -7,7 +7,7 @@ import * as env from '../../src/shared/env'; import * as uiAlertInfo from '../../src/database/uiAlertInfo'; import { UiTestResultDTO } from '../../src/database/uiTestResult'; -import { CRITICAL } from '../../src/shared/constants'; +import { NORMAL, CRITICAL } from '../../src/shared/constants'; chai.use(sinonChai);