blob: 068e00ead53eda159f090ab5566fece70202bb9d [file]
import { jsonOrThrow } from '../../../infra-sk/modules/jsonOrThrow';
import {
FrameRequest,
FrameResponse,
GetUserIssuesForTraceKeysRequest,
GetUserIssuesForTraceKeysResponse,
GraphConfig,
ShiftRequest,
ShiftResponse,
progress,
QueryConfig,
RegressionRangeRequest,
RegressionRangeResponse,
AnomalyMap,
} from '../json';
import {
messageByName,
messagesToErrorString,
messagesToPreString,
startRequest,
RequestOptions,
} from '../progress/progress';
export interface TraceValuesRequest {
ids: string[];
min_commit: number;
max_commit: number;
begin?: number;
end?: number;
}
export interface TraceRow {
commit_number: number;
createdat: number;
val: number;
}
export interface TraceValuesResponse {
results: Record<string, TraceRow[]>;
anomalymap?: AnomalyMap;
}
/**
* Custom error class for DataService operations.
*/
export class DataServiceError extends Error {
status?: number;
endpoint?: string;
method?: string;
constructor(message: string, status?: number, endpoint?: string, method?: string) {
super(message);
this.name = 'DataServiceError';
this.status = status;
this.endpoint = endpoint;
this.method = method;
}
}
export interface SendFrameRequestOptions {
onStart?: () => void;
onProgress?: (msg: string) => void;
onMessage?: (msg: string) => void;
onSettled?: () => void;
pollingIntervalMs?: number;
}
/**
* Handles all data fetching and manipulation requests to the backend.
*/
export class DataService {
private static instance: DataService = new DataService();
private constructor() {}
public static getInstance(): DataService {
return DataService.instance;
}
/**
* Helper to fetch JSON from a URL.
*/
private async fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const method = init?.method || 'GET';
try {
const response = await fetch(url, init);
return await jsonOrThrow(response);
} catch (error: any) {
throw new DataServiceError(error.message || error.toString(), error.status, url, method);
}
}
/**
* Creates a shortcut ID for the given Graph Configs.
*/
async updateShortcut(graphConfigs: GraphConfig[]): Promise<string> {
// Skip this call when running locally to avoid 500 errors from the proxy/backend.
if ((window as any).perf && (window as any).perf.disable_shortcut_update) {
return '';
}
if (graphConfigs.length === 0) {
return '';
}
const body = {
graphs: graphConfigs,
};
const json = await this.fetchJson<{ id: string }>('/_/shortcut/update', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
return json.id;
}
/**
* Fetches the Graph Configs for a given shortcut ID.
*/
async getShortcut(id: string): Promise<GraphConfig[]> {
const body = {
ID: id,
};
const json = await this.fetchJson<{ graphs: GraphConfig[] }>('/_/shortcut/get', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});
return json.graphs;
}
/**
* Fetches the initial page data.
*/
async getInitPage(tz: string): Promise<any> {
return await this.fetchJson(`/_/initpage/?tz=${tz}`, {
method: 'GET',
});
}
/**
* Fetches the default configuration.
*/
async getDefaults(): Promise<QueryConfig> {
return await this.fetchJson('/_/defaults/', {
method: 'GET',
});
}
/**
* Calculates the new range change based on a shift request.
*/
async shift(req: ShiftRequest): Promise<ShiftResponse> {
return await this.fetchJson('/_/shift/', {
method: 'POST',
body: JSON.stringify(req),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Fetches user issues for the given trace keys and commit range.
*/
async getUserIssues(
req: GetUserIssuesForTraceKeysRequest
): Promise<GetUserIssuesForTraceKeysResponse> {
return await this.fetchJson('/_/user_issues/', {
method: 'POST',
body: JSON.stringify(req),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Fetches regressions for a given range.
*/
async sendRegressionRangeRequest(req: RegressionRangeRequest): Promise<RegressionRangeResponse> {
return await this.fetchJson('/_/reg', {
method: 'POST',
body: JSON.stringify(req),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Fetches specific trace values for a given commit range.
*/
async fetchTraceValues(req: TraceValuesRequest): Promise<TraceValuesResponse> {
return await this.fetchJson('/_/trace_values', {
method: 'POST',
body: JSON.stringify(req),
headers: {
'Content-Type': 'application/json',
},
});
}
/**
* Starts the frame request and returns the resulting data.
*
* @param body - The frame request body.
* @param options - Optional configuration for the request lifecycle and callbacks.
*/
async sendFrameRequest(
body: FrameRequest,
options: SendFrameRequestOptions = {}
): Promise<FrameResponse> {
body.tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const requestOptions: RequestOptions = {
onStart: options.onStart,
onSettled: options.onSettled,
pollingIntervalMs: options.pollingIntervalMs,
onProgressUpdate: (prog: progress.SerializedProgress) => {
if (options.onProgress) {
options.onProgress(messagesToPreString(prog.messages || []));
}
},
};
const finishedProg = await startRequest('/_/frame/start', body, requestOptions);
if (finishedProg.status !== 'Finished') {
throw new DataServiceError(messagesToErrorString(finishedProg.messages));
}
const msg = messageByName(finishedProg.messages, 'Message');
if (msg && options.onMessage) {
options.onMessage(msg);
}
return finishedProg.results as FrameResponse;
}
/**
* Fetches the subrepo links for a batch of commits and traces.
*/
async getLinksBatch(
commitNumbers: number[],
traceIds: string[]
): Promise<Record<string, Record<string, Record<string, string>>>> {
const body = {
commit_numbers: commitNumbers,
trace_ids: traceIds,
};
return await this.fetchJson<Record<string, Record<string, Record<string, string>>>>(
'/_/links_batch',
{
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
}
);
}
}