blob: 6d0a8284ba67928658eb43e71f9816f073e57728 [file]
import { ReactiveController, ReactiveControllerHost } from 'lit';
import {
Anomaly,
CalculateRegrShortcutResponse,
GetGroupReportResponse,
Timerange,
ShiftResponse,
} from '../json';
import { errorMessage } from '../errorMessage';
import { GraphConfig, updateShortcut } from '../common/graph-config';
import { ChromeTraceFormatter } from '../trace-details-formatter/traceformatter';
import { jsonOrThrow } from '../../../infra-sk/modules/jsonOrThrow';
import '../window/window';
const weekInSeconds = 7 * 24 * 60 * 60;
const SECONDS_IN_DAY = 24 * 60 * 60;
export class ReportNavigationController implements ReactiveController {
host: ReactiveControllerHost;
private traceFormatter: ChromeTraceFormatter;
private shortcutUrl: string = '';
constructor(host: ReactiveControllerHost) {
this.host = host;
this.host.addController(this);
this.traceFormatter = new ChromeTraceFormatter();
}
hostConnected() {}
async openReportForAnomalyIds(anomalies: Anomaly[], newTab?: Window | null): Promise<boolean> {
const idList = anomalies.map((a) => a.id);
if (newTab === undefined) {
newTab = null;
}
// If only one anomaly is selected, open the report page using
// the anomaly id directly.
if (idList.length === 1) {
const key = idList[0];
const url = `/u/?anomalyIDs=${key}`;
if (newTab) {
newTab.location.href = url;
} else {
newTab = window.open(url, '_blank');
}
return !!newTab;
}
const idString = idList.join(',');
const response = window.perf.fetch_anomalies_from_sql
? await this.fetchCreateShortcutApi(idString)
: await this.fetchGroupReportApi(idString);
if (!response) {
if (newTab) newTab.close();
// Return true to avoid triggering the "Popups blocked" error in the caller,
// as the error was due to an API failure, not a blocked popup.
return true;
}
const sid: string = response.sid || '';
const url = sid ? `/u/?sid=${sid}` : `/u/?anomalyIDs=${encodeURIComponent(idString)}`;
if (newTab) {
newTab.location.href = url;
} else {
newTab = window.open(url, '_blank');
}
return !!newTab;
}
public async openMultiGraphUrl(anomaly: Anomaly, newTab: Window | null, isDryRun = false) {
const response = await this.fetchGroupReportApi(String(anomaly.id));
let timerangeMap = response?.timerange_map || {};
if (!timerangeMap[anomaly.id] && isDryRun) {
const shiftResp = await this.fetchShiftApi(anomaly.start_revision, anomaly.end_revision);
if (shiftResp) {
timerangeMap = {
...timerangeMap,
[anomaly.id]: {
begin: shiftResp.begin,
end: shiftResp.end + SECONDS_IN_DAY, // Shift end time by a day to match backend getTimerangeMap
},
};
}
}
if (!timerangeMap[anomaly.id]) {
console.warn('ReportNavigationController: timerange missing for anomaly', anomaly.id);
if (newTab) newTab.close();
return;
}
const urlList = await this.generateMultiGraphUrl([anomaly], timerangeMap);
this.openAnomalyUrl(urlList[0], newTab);
}
private openAnomalyUrl(url: string | undefined, newTab: Window | null): void {
if (!newTab || !url) {
console.warn('Multi chart URL not found or tab was blocked.');
if (newTab) newTab.close(); // Clean up the blank tab on failure.
return;
}
// Navigate the already-opened tab to the final destination.
newTab.location.href = url;
}
private async fetchCreateShortcutApi(
idString: string
): Promise<CalculateRegrShortcutResponse | null> {
try {
const response = await fetch('/_/anomalies/calculate_regr_shortcut', {
method: 'POST',
body: JSON.stringify({
anomalyIDs: idString,
}),
headers: {
'Content-Type': 'application/json',
},
});
return jsonOrThrow(response);
} catch (msg) {
errorMessage(msg as string);
return null;
}
}
private async fetchGroupReportApi(idString: string): Promise<GetGroupReportResponse | null> {
try {
const response = await fetch('/_/anomalies/group_report', {
method: 'POST',
body: JSON.stringify({
anomalyIDs: idString,
}),
headers: {
'Content-Type': 'application/json',
},
});
return jsonOrThrow(response);
} catch (msg) {
errorMessage(msg as string);
return null;
}
}
private async fetchShiftApi(begin: number, end: number): Promise<ShiftResponse | null> {
try {
const response = await fetch('/_/shift', {
method: 'POST',
body: JSON.stringify({
begin: begin,
end: end,
}),
headers: {
'Content-Type': 'application/json',
},
});
return jsonOrThrow(response);
} catch (msg) {
errorMessage('Failed to fetch shift data: ' + msg);
return null;
}
}
private async generateMultiGraphUrl(
anomalies: Anomaly[],
timerangeMap: { [key: string]: Timerange }
): Promise<string[]> {
const shortcutUrlList: string[] = [];
for (let i = 0; i < anomalies.length; i++) {
const timerange = this.calculateTimeRange(timerangeMap[anomalies[i]!.id]);
const graphConfigs = [] as GraphConfig[];
const config: GraphConfig = {
keys: '',
formulas: [],
queries: [],
};
config.queries = [this.traceFormatter.formatQuery(anomalies[i]!.test_path)];
graphConfigs.push(config);
await updateShortcut(graphConfigs)
.then((shortcut) => {
if (shortcut === '') {
this.shortcutUrl = '';
return;
}
this.shortcutUrl = shortcut;
})
.catch(errorMessage);
// request_type=0 only selects data points for within the range
// rather than show 250 data points by default
let url =
`${window.location.protocol}//${window.location.host}` +
`/m/?begin=${timerange[0]}&end=${timerange[1]}` +
`&request_type=0&shortcut=${this.shortcutUrl}`;
if (window.perf && window.perf.default_to_manual_plot_mode) {
url += '&manual_plot_mode=true';
}
shortcutUrlList.push(url);
}
return shortcutUrlList;
}
private calculateTimeRange(timerange: Timerange): string[] {
if (!timerange) {
return ['', ''];
}
const timerangeBegin = timerange.begin;
const timerangeEnd = timerange.end;
// generate data one week ahead and one week behind to make it easier
// for user to discern trends
const newTimerangeBegin = timerangeBegin ? (timerangeBegin - weekInSeconds).toString() : '';
const newTimerangeEnd = timerangeEnd ? (timerangeEnd + weekInSeconds).toString() : '';
return [newTimerangeBegin, newTimerangeEnd];
}
}