// Displays an error message to the UI. Any previous message will be erased.
function displayError(message) {
// Clear the body of all children.
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
const element = document.createElement("p");
element.innerText = "Error: " + message;
element.style.color = "red";
element.style.fontFamily = "Arial";
element.style.fontWeight = "bold";
element.style.margin = "5rem";
document.body.appendChild(element);
}
const testGopoghLink = (jobId, environment, testName, status) => {
return `https://storage.googleapis.com/minikube-builds/logs/master/${jobId}/${environment}.html${testName ? `#${status}_${testName}` : ``}`;
}
// Parse URL search `query` into [{key, value}].
function parseUrlQuery(query) {
if (query[0] === '?') {
query = query.substring(1);
}
return Object.fromEntries((query === "" ? [] : query.split("&")).map(element => {
const keyValue = element.split("=");
return [unescape(keyValue[0]), unescape(keyValue[1])];
}));
}
function createRecentNumberOfFailTable(summaryTable) {
const createCell = (elementType, text) => {
const element = document.createElement(elementType);
element.innerHTML = text;
return element;
}
const table = document.createElement("table");
const tableHeaderRow = document.createElement("tr");
tableHeaderRow.appendChild(createCell("th", "Rank"));
tableHeaderRow.appendChild(createCell("th", "Env Name")).style.textAlign = "left";
tableHeaderRow.appendChild(createCell("th", "Recent Number of Fails"));
tableHeaderRow.appendChild(createCell("th", "Growth (since last 15 days)"));
table.appendChild(tableHeaderRow);
const tableBody = document.createElement("tbody");
for (let i = 0; i < summaryTable.length; i++) {
const {
envName,
recentNumberOfFail,
growth
} = summaryTable[i];
const row = document.createElement("tr");
row.appendChild(createCell("td", "" + (i + 1))).style.textAlign = "center";
row.appendChild(createCell("td", `${envName}`));
row.appendChild(createCell("td", recentNumberOfFail)).style.textAlign = "right";
row.appendChild(createCell("td", ` 0 ? "red" : "green")}">${growth > 0 ? '+' + growth : growth}`));
tableBody.appendChild(row);
}
table.appendChild(tableBody);
new Tablesort(table);
return table;
}
function createRecentFlakePercentageTable(recentFlakePercentTable, query) {
const createCell = (elementType, text) => {
const element = document.createElement(elementType);
element.innerHTML = text;
return element;
}
const table = document.createElement("table");
const tableHeaderRow = document.createElement("tr");
tableHeaderRow.appendChild(createCell("th", "Rank"));
tableHeaderRow.appendChild(createCell("th", "Test Name")).style.textAlign = "left";
tableHeaderRow.appendChild(createCell("th", "Recent Flake Percentage"));
tableHeaderRow.appendChild(createCell("th", "Growth (since last 15 days)"));
table.appendChild(tableHeaderRow);
const tableBody = document.createElement("tbody");
for (let i = 0; i < recentFlakePercentTable.length; i++) {
const {
testName,
recentFlakePercentage,
growthRate
} = recentFlakePercentTable[i];
const row = document.createElement("tr");
row.appendChild(createCell("td", "" + (i + 1))).style.textAlign = "center";
row.appendChild(createCell("td", `${testName}`));
row.appendChild(createCell("td", recentFlakePercentage + "%")).style.textAlign = "right";
row.appendChild(createCell("td", ` 0 ? "red" : "green")}">${growthRate > 0 ? '+' + growthRate : growthRate}%`));
tableBody.appendChild(row);
}
table.appendChild(tableBody);
new Tablesort(table);
return table;
}
function displayTestAndEnvironmentChart(data, query) {
const chartsContainer = document.getElementById('chart_div');
const dayData = data.flakeByDay
const dayChart = new google.visualization.DataTable();
dayChart.addColumn('date', 'Date');
dayChart.addColumn('number', 'Flake Percentage');
dayChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
dayChart.addColumn('number', 'Duration');
dayChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
dayChart.addRows(
dayData
.map(groupData => {
let dataArr = groupData.commitResultsAndDurations.split(',')
dataArr = dataArr.map((commit) => commit.split(":"))
const resultArr = dataArr.map((commit) => ({
id: commit[commit.length - 3],
status: (commit[commit.length - 2]).trim()
}))
const durationArr = dataArr.map((commit) => ({
id: commit[commit.length - 3],
status: (commit[commit.length - 2]).trim(),
duration: (commit[commit.length - 1]).trim()
}))
return [
new Date(groupData.startOfDate),
groupData.flakePercentage,
`
Date: ${groupData.startOfDate.toLocaleString([], {dateStyle: 'medium'})}
Flake Percentage: ${groupData.flakePercentage.toFixed(2)}%
Jobs:
${resultArr.map(({ id, status }) => ` -
${id} (${status})`).join("
")}
`,
groupData.avgDuration,
`
Date: ${groupData.startOfDate.toLocaleString([], {dateStyle: 'medium'})}
Average Duration: ${groupData.avgDuration.toFixed(2)}s
Jobs:
${durationArr.map(({ id, duration, status }) => ` -
${id} (${duration}s)`).join("
")}
`,
]
})
);
const dayOptions = {
title: `Flake rate and duration by day of ${query.test} on ${query.env}`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
series: {
0: {
targetAxisIndex: 0
},
1: {
targetAxisIndex: 1
},
},
vAxes: {
0: {
title: "Flake rate",
minValue: 0,
maxValue: 100
},
1: {
title: "Duration (seconds)"
},
},
colors: ['#dc3912', '#3366cc'],
tooltip: {
trigger: "selection",
isHtml: true
}
};
const flakeRateDayContainer = document.createElement("div");
flakeRateDayContainer.style.width = "100vw";
flakeRateDayContainer.style.height = "100vh";
chartsContainer.appendChild(flakeRateDayContainer);
const dChart = new google.visualization.LineChart(flakeRateDayContainer);
dChart.draw(dayChart, dayOptions);
const weekData = data.flakeByWeek
const weekChart = new google.visualization.DataTable();
weekChart.addColumn('date', 'Date');
weekChart.addColumn('number', 'Flake Percentage');
weekChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
weekChart.addColumn('number', 'Duration');
weekChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
console.log(weekChart)
weekChart.addRows(
weekData
.map(groupData => {
let dataArr = groupData.commitResultsAndDurations.split(',')
dataArr = dataArr.map((commit) => commit.split(":"))
const resultArr = dataArr.map((commit) => ({
id: commit[commit.length - 3],
status: (commit[commit.length - 2]).trim()
}))
const durationArr = dataArr.map((commit) => ({
id: commit[commit.length - 3],
status: (commit[commit.length - 2]).trim(),
duration: (commit[commit.length - 1]).trim()
}))
return [
new Date(groupData.startOfDate),
groupData.flakePercentage,
`
Date: ${groupData.startOfDate.toLocaleString([], {dateStyle: 'medium'})}
Flake Percentage: ${groupData.flakePercentage.toFixed(2)}%
Jobs:
${resultArr.map(({ id, status }) => ` -
${id} (${status})`).join("
")}
`,
groupData.avgDuration,
`
Date: ${groupData.startOfDate.toLocaleString([], {dateStyle: 'medium'})}
Average Duration: ${groupData.avgDuration.toFixed(2)}s
Jobs:
${durationArr.map(({ id, duration, status }) => ` -
${id} (${duration}s)`).join("
")}
`,
]
})
);
const weekOptions = {
title: `Flake rate and duration by week of ${query.test} on ${query.env}`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
series: {
0: {
targetAxisIndex: 0
},
1: {
targetAxisIndex: 1
},
},
vAxes: {
0: {
title: "Flake rate",
minValue: 0,
maxValue: 100
},
1: {
title: "Duration (seconds)"
},
},
colors: ['#dc3912', '#3366cc'],
tooltip: {
trigger: "selection",
isHtml: true
}
};
const flakeRateWeekContainer = document.createElement("div");
flakeRateWeekContainer.style.width = "100vw";
flakeRateWeekContainer.style.height = "100vh";
chartsContainer.appendChild(flakeRateWeekContainer);
const wChart = new google.visualization.LineChart(flakeRateWeekContainer);
wChart.draw(weekChart, weekOptions);
}
function displaySummaryChart(data) {
const chartsContainer = document.getElementById('chart_div');
const summaryData = data.summaryAvgFail
const uniqueDayDates = new Set();
const summaryEnvDateMap = {};
for (const envDay of summaryData) {
const {
startOfDate,
envName,
avgFailedTests,
avgDuration
} = envDay
uniqueDayDates.add(startOfDate)
if (!summaryEnvDateMap[envName]) {
summaryEnvDateMap[envName] = {};
}
summaryEnvDateMap[envName][startOfDate] = {
avgFailedTests,
avgDuration
}
}
const uniqueEnvs = Object.keys(summaryEnvDateMap);
const orderedDayDates = Array.from(uniqueDayDates).sort()
const dayChart = new google.visualization.DataTable();
dayChart.addColumn('date', 'Date');
for (const env of uniqueEnvs) {
dayChart.addColumn('number', `${env}`);
dayChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
}
dayChart.addRows(orderedDayDates.map(dateTime => [new Date(dateTime)].concat(uniqueEnvs.map(name => {
const avgVal = summaryEnvDateMap[name][dateTime];
if (avgVal !== undefined) {
const {
avgFailedTests,
} = avgVal
return [
avgFailedTests,
`
${name}
Date: ${dateTime.toLocaleString([], {dateStyle: 'medium'})}
Number of Failed Tests (avg): ${+avgFailedTests.toFixed(2)}
`
]
}
return [null, null];
})).flat()))
const dayOptions = {
title: `Average Daily Failed Tests`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
vAxes: {
0: {
title: "# of Failed Tests",
minValue: 0
},
},
tooltip: {
trigger: "selection",
isHtml: true
}
};
// Create the chart and draw it
const summaryDayContainer = document.createElement("div");
summaryDayContainer.style.width = "100vw";
summaryDayContainer.style.height = "100vh";
chartsContainer.appendChild(summaryDayContainer);
const dChart = new google.visualization.LineChart(summaryDayContainer);
dChart.draw(dayChart, dayOptions);
const durChart = new google.visualization.DataTable();
durChart.addColumn('date', 'Date');
for (const env of uniqueEnvs) {
durChart.addColumn('number', `${env}`);
durChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
}
durChart.addRows(orderedDayDates.map(dateTime => [new Date(dateTime)].concat(uniqueEnvs.map(name => {
const avgVal = summaryEnvDateMap[name][dateTime];
if (avgVal !== undefined) {
const {
avgDuration
} = avgVal
return [
avgDuration,
`
${name}
Date: ${dateTime.toLocaleString([], {dateStyle: 'medium'})}
Duration (avg): ${+avgDuration.toFixed(2)}
`
]
}
return [null, null];
})).flat()))
const durOptions = {
title: `Average Total Duration per day`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
vAxes: {
0: {
title: "Total Duration",
minValue: 0
},
},
tooltip: {
trigger: "selection",
isHtml: true
}
};
// Create the chart and draw it
const summaryDurContainer = document.createElement("div");
summaryDurContainer.style.width = "100vw";
summaryDurContainer.style.height = "100vh";
chartsContainer.appendChild(summaryDurContainer);
const durationChart = new google.visualization.LineChart(summaryDurContainer);
durationChart.draw(durChart, durOptions);
chartsContainer.appendChild(createRecentNumberOfFailTable(data.summaryTable))
}
function displayEnvironmentChart(data, query) {
const chartsContainer = document.getElementById('chart_div');
//By Day Chart
const dayData = data.flakeRateByDay
const uniqueDayTestNames = new Set();
const uniqueDayDates = new Set();
for (const flakeDay of dayData) {
uniqueDayTestNames.add(flakeDay.testName);
uniqueDayDates.add(flakeDay.startOfDate)
}
const uniqueDayTestNamesArray = Array.from(uniqueDayTestNames);
const orderedDayDates = Array.from(uniqueDayDates).sort();
const flakeDayDataMap = {};
dayData.forEach((day) => {
const {
testName,
startOfDate,
flakePercentage,
commitResults
} = day;
// If the test name doesn't exist in the map, create a new entry
if (!flakeDayDataMap[testName]) {
flakeDayDataMap[testName] = {};
}
// Set the flakePercentage for the corresponding startOfDate
flakeDayDataMap[testName][startOfDate] = {
fp: flakePercentage,
cr: commitResults
};
});
const dayChart = new google.visualization.DataTable();
dayChart.addColumn('date', 'Date');
for (const testName of uniqueDayTestNamesArray) {
dayChart.addColumn('number', `${testName}`);
dayChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
}
dayChart.addRows(orderedDayDates.map(dateTime => [new Date(dateTime)].concat(uniqueDayTestNamesArray.map(name => {
const fpAndCr = flakeDayDataMap[name][dateTime];
if (fpAndCr !== undefined) {
const {
fp,
cr
} = fpAndCr
let commitArr = cr.split(",")
commitArr = commitArr.map((commit) => commit.split(":"))
commitArr = commitArr.map((commit) => ({
id: commit[commit.length - 2],
status: (commit[commit.length - 1]).trim()
}))
return [
fp,
`
${name}
Date: ${dateTime.toLocaleString([], {dateStyle: 'medium'})}
Flake Percentage: ${+fp.toFixed(2)}%
Jobs:
${commitArr.map(({ id, status }) => ` -
${id} (${status})`).join("
")}
`
]
}
return [null, null];
})).flat()))
const dayOptions = {
title: `Flake rate by day of top ${uniqueDayTestNamesArray.length} recent test flakiness (past 15 days) on ${query.env}`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
vAxes: {
0: {
title: "Flake rate",
minValue: 0,
maxValue: 100
},
},
tooltip: {
trigger: "selection",
isHtml: true
}
};
// Create the chart and draw it
const flakeRateDayContainer = document.createElement("div");
flakeRateDayContainer.style.width = "100vw";
flakeRateDayContainer.style.height = "100vh";
chartsContainer.appendChild(flakeRateDayContainer);
const dChart = new google.visualization.LineChart(flakeRateDayContainer);
dChart.draw(dayChart, dayOptions);
// Weekly Chart
const weekData = data.flakeRateByWeek
const uniqueWeekTestNames = new Set();
const uniqueWeekDates = new Set();
for (const flakeWeek of weekData) {
uniqueWeekTestNames.add(flakeWeek.testName);
uniqueWeekDates.add(flakeWeek.startOfDate)
}
const uniqueWeekTestNamesArray = Array.from(uniqueWeekTestNames);
const orderedWeekDates = Array.from(uniqueWeekDates).sort();
const flakeWeekDataMap = {};
weekData.forEach((week) => {
const {
testName,
startOfDate,
flakePercentage,
commitResults
} = week;
// If the test name doesn't exist in the map, create a new entry
if (!flakeWeekDataMap[testName]) {
flakeWeekDataMap[testName] = {};
}
// Set the flakePercentage for the corresponding startOfDate
flakeWeekDataMap[testName][startOfDate] = {
fp: flakePercentage,
cr: commitResults
};
});
{
// Create the DataTable
const weekChart = new google.visualization.DataTable();
// Add the columns to the DataTable
weekChart.addColumn('date', 'Date');
for (const testName of uniqueWeekTestNamesArray) {
weekChart.addColumn('number', `${testName}`);
weekChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
}
weekChart.addRows(orderedWeekDates.map(dateTime => [new Date(dateTime)].concat(uniqueWeekTestNamesArray.map(name => {
const fpAndcr = flakeWeekDataMap[name][dateTime];
if (fpAndcr != undefined) {
const {
fp,
cr
} = fpAndcr
let commitArr = cr.split(",")
commitArr = commitArr.map((commit) => commit.split(":"))
commitArr = commitArr.map((commit) => ({
id: commit[commit.length - 2],
status: (commit[commit.length - 1]).trim()
}))
return [
fp,
`
${name}
Date: ${dateTime.toLocaleString([], {dateStyle: 'medium'})}
Flake Percentage: ${+fp.toFixed(2)}%
Jobs:
${commitArr.map(({ id, status }) => ` -
${id} (${status})`).join("
")}
`
];
}
return [null, null];
})).flat()))
const weekOptions = {
title: `Flake rate by week of top ${uniqueWeekTestNamesArray.length} of recent test flakiness (past week) on ${query.env}`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
vAxes: {
0: {
title: "Flake rate",
minValue: 0,
maxValue: 100
},
},
tooltip: {
trigger: "selection",
isHtml: true
}
};
// Create the chart and draw it
const flakeRateWeekContainer = document.createElement("div");
flakeRateWeekContainer.style.width = "100vw";
flakeRateWeekContainer.style.height = "100vh";
chartsContainer.appendChild(flakeRateWeekContainer);
const wChart = new google.visualization.LineChart(flakeRateWeekContainer);
wChart.draw(weekChart, weekOptions);
}
// Duration Chart
{
const durationChart = new google.visualization.DataTable();
const durationData = data.countsAndDurations
durationChart.addColumn('date', 'Date');
durationChart.addColumn('number', 'Test Count');
durationChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
durationChart.addColumn('number', 'Duration');
durationChart.addColumn({
type: 'string',
role: 'tooltip',
'p': {
'html': true
}
});
durationChart.addRows(
durationData.map(dateInfo => {
let countArr = dateInfo.commitCounts.split(",")
countArr = countArr.map((commit) => commit.split(":"))
countArr = countArr.map((commit) => ({
rootJob: commit[commit.length - 2],
testCount: +(commit[commit.length - 1]).trim()
}))
let durationArr = dateInfo.commitDurations.split(",")
durationArr = durationArr.map((commit) => commit.split(":"))
durationArr = durationArr.map((commit) => ({
rootJob: commit[commit.length - 2],
totalDuration: +(commit[commit.length - 1]).trim()
}))
return [
new Date(dateInfo.startOfDate),
dateInfo.testCount,
`
Date: ${dateInfo.startOfDate.toLocaleString([], {dateStyle: 'medium'})}
Test Count (averaged): ${+dateInfo.testCount.toFixed(2)}
Jobs:
${countArr.map(job => ` -
${job.rootJob} Test count: ${job.testCount}`).join("
")}
`,
dateInfo.duration,
`
Date: ${dateInfo.startOfDate.toLocaleString([], {dateStyle: 'medium'})}
Total Duration (averaged): ${+dateInfo.duration.toFixed(2)}
Jobs:
${durationArr.map(job => ` -
${job.rootJob} Total Duration: ${+job.totalDuration.toFixed(2)}s`).join("
")}
`,
]
}));
const durOptions = {
title: `Test count and total duration by day on ${query.env}`,
width: window.innerWidth,
height: window.innerHeight,
pointSize: 10,
pointShape: "circle",
series: {
0: {
targetAxisIndex: 0
},
1: {
targetAxisIndex: 1
},
},
vAxes: {
0: {
title: "Test Count",
minValue: 0
},
1: {
title: "Duration (seconds)",
minValue: 0
},
},
tooltip: {
trigger: "selection",
isHtml: true
}
};
const envDurationContainer = document.createElement("div");
envDurationContainer.style.width = "100vw";
envDurationContainer.style.height = "100vh";
chartsContainer.appendChild(envDurationContainer);
const durChart = new google.visualization.LineChart(envDurationContainer);
durChart.draw(durationChart, durOptions);
}
chartsContainer.appendChild(createRecentFlakePercentageTable(data.recentFlakePercentTable, query))
}
function createTopnDropdown(currentTopn) {
const dropdownContainer = document.createElement("div");
dropdownContainer.style.margin = "1rem";
const dropdownLabel = document.createElement("label");
dropdownLabel.innerText = "Select topn value: ";
const dropdown = document.createElement("select");
dropdown.id = "topnDropdown";
const values = [3, 5, 10, 15];
values.forEach(value => {
const option = document.createElement("option");
option.value = value;
option.text = value;
if (value.toString() === currentTopn) {
option.selected = true;
}
dropdown.appendChild(option);
});
dropdown.addEventListener("change", () => {
const selectedValue = dropdown.value;
const currentURL = new URL(window.location.href);
currentURL.searchParams.set("tests_in_top", selectedValue);
window.location.href = currentURL.href;
});
dropdownContainer.appendChild(dropdownLabel);
dropdownContainer.appendChild(dropdown);
document.getElementById('dropdown_container').appendChild(dropdownContainer)
}
function displayGopoghVersion(verData) {
const footerElement = document.getElementById('version_div');
const version = verData.version
footerElement.className = "mdl-mega-footer";
footerElement.innerHTML = "generated by Gopogh " + version + "";
}
async function init() {
const query = parseUrlQuery(window.location.search);
const desiredTest = query.test,
desiredEnvironment = query.env,
desiredPeriod = query.period || "",
desiredTestNumber = query.tests_in_top || "";
const currentTopn = query.tests_in_top || "10"; // Default to 10 (for top 10 tests)
google.charts.load('current', {
'packages': ['corechart']
});
try {
// Wait for Google Charts to load
await new Promise(resolve => google.charts.setOnLoadCallback(resolve));
let url;
const basePath = 'https://gopogh-server-tts3vkcpgq-uc.a.run.app' // Base Server Path. Modify to actual server path if deploying
if (desiredEnvironment === undefined) {
// URL for displaySummaryChart
url = basePath + '/summary'
} else if (desiredTest === undefined) {
// URL for displayEnvironmentChart
url = basePath + '/env' + '?env=' + desiredEnvironment + '&tests_in_top=' + desiredTestNumber;
} else {
// URL for displayTestAndEnvironmentChart
url = basePath + '/test' + '?env=' + desiredEnvironment + '&test=' + desiredTest;
}
// Fetch data from the determined URL
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log(data)
// Call the appropriate chart display function based on the desired condition
if (desiredTest == undefined && desiredEnvironment === undefined) {
displaySummaryChart(data)
} else if (desiredTest === undefined) {
createTopnDropdown(currentTopn);
displayEnvironmentChart(data, query);
} else {
displayTestAndEnvironmentChart(data, query);
}
url = basePath + '/version'
const verResponse = await fetch(url);
if (!verResponse.ok) {
throw new Error('Network response was not ok');
}
const verData = await verResponse.json();
console.log(verData)
displayGopoghVersion(verData)
} catch (err) {
displayError(err);
}
}
init();