Skip to content
Closed
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
11 changes: 11 additions & 0 deletions db/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
2 changes: 2 additions & 0 deletions src/database/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PackageInfo } from './entity/package';
import { TestResult } from './entity/result';
import { ExecutionInfo } from './entity/execution';
import { Alert } from './entity/alert';
import { UiAlert } from './entity/uiAlert';
import { TestInfo } from './entity/testInfo';
import { UiTestResult } from './entity/uiTestResult';

Expand All @@ -19,6 +20,7 @@ export const DB_ENTITIES = [
PackageInfo,
ExecutionInfo,
Alert,
UiAlert,
TestInfo,
UiTestResult,
];
Expand Down
34 changes: 34 additions & 0 deletions src/database/entity/uiAlert.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
93 changes: 93 additions & 0 deletions src/database/uiAlertInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/** @ignore */
/**
* Copyright (c) 2025 Certinia, Inc. All rights reserved.
*/
import { UiAlert } from './entity/uiAlert';
import { getConnection } from './connection';
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 }[]
) {
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 {};
}
}
26 changes: 26 additions & 0 deletions src/database/uiTestResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ 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
* 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
Expand All @@ -16,6 +39,7 @@ export interface UiTestResultDTO {
componentLoadTime?: number;
salesforceLoadTime?: number;
overallLoadTime: number;
alertInfo?: UiAlertInfo;
}

/**
Expand Down Expand Up @@ -59,6 +83,8 @@ export async function saveUiTestResult(
): Promise<UiTestResultDTO[]> {
const entities = testStepResults.map(dtoToEntity);
const savedEntities = await saveRecords<UiTestResult>(entities);
const alerts = await generateValidAlerts(testStepResults);
await saveAlerts(savedEntities, alerts);
return savedEntities.map(entityToDto);
}

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export {
loadUiTestResults,
UiTestResultDTO,
UiTestResultFilterOptions,
UiAlertInfo,
UiAlertThresholds,
} from './database/uiTestResult';

export namespace Constants {
Expand Down
103 changes: 103 additions & 0 deletions src/services/result/uiAlert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/** @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<UiAlert[]> {
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<UiAlert> {
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;
}
12 changes: 12 additions & 0 deletions src/shared/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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';
}
Loading
Loading