blob: 0a20300ec985b275c152a68f5e0a1747ea3f2165 [file] [log] [blame]
/**
* @fileoverview This file defines a Telemetry class for sending frontend metrics.
* A singleton instance is exported for application-wide use. These metrics are
* used to track user interactions and performance.
*
* Metrics are buffered on the frontend for 5 seconds before being sent in batches
* to the `/_/fe_telemetry` endpoint. This reduces network traffic. Any pending metrics
* are also sent when the page visibility changes to 'hidden' (e.g., when the user
* navigates away or closes the tab) to prevent data loss.
*
* To add a new counter metric:
* 1. Add the metric name to the `CountMetric` enum.
* 2. Call `telemetry.increaseCounter()` with the new metric name and optional tags.
*
* To add a new summary metric:
* 1. Add the metric name to the `SummaryMetric` enum.
* 2. Call `telemetry.recordSummary()` with the new metric name, value, and optional tags.
*/
interface FrontendMetric {
metric_name: string;
metric_value: number;
tags: { [key: string]: string };
metric_type: 'counter' | 'summary';
}
export enum CountMetric {
// go/keep-sorted start
DataFetchFailure = 'fe_data_fetch_failure',
SIDRequiringActionTaken = 'fe_sid_requiring_action_taken',
TriageActionTaken = 'fe_triage_action_taken',
// go/keep-sorted end
}
export enum SummaryMetric {
// go/keep-sorted start
GoogleGraphPlotTime = 'fe_google_graph_plot_time_s',
MultiGraphDataLoadTime = 'fe_multi_graph_data_load_time_s',
ReportAnomaliesTableLoadTime = 'fe_report_anomalies_table_load_time_s',
ReportChartContainerLoadTime = 'fe_report_chart_container_load_time_s',
ReportGraphChunkLoadTime = 'fe_report_graph_chunk_load_time_s',
ReportPageLoadTime = 'fe_report_page_load_time_s',
SingleGraphLoadTime = 'fe_single_graph_load_time_s',
// go/keep-sorted end
}
class Telemetry {
private static readonly BUFFER_FLUSH_INTERVAL_MS = 5000; // 5 seconds
private static readonly MAX_BUFFER_SIZE = 1000; // Max 1000 metrics in buffer
private metricsBuffer: FrontendMetric[] = [];
private timerId: number | null = null;
constructor() {
// When the page visibility changes, flush the buffer. This helps ensure we
// capture metrics before the user navigates away or closes the tab.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
}
this.sendBufferedMetrics();
}
});
}
// Flushes the metrics buffer by sending the data to the telemetry endpoint.
private async sendBufferedMetrics() {
if (this.metricsBuffer.length === 0) {
return;
}
const metricsToSend = [...this.metricsBuffer];
this.metricsBuffer.length = 0;
try {
await fetch('/_/fe_telemetry', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(metricsToSend),
});
} catch (e) {
console.error(e, 'Failed to send frontend metrics:', metricsToSend);
this.queueMetrics(metricsToSend);
}
}
private queueMetric(metric: FrontendMetric) {
this.queueMetrics([metric]);
}
private queueMetrics(metrics: FrontendMetric[]) {
for (const m of metrics) {
if (this.metricsBuffer.length >= Telemetry.MAX_BUFFER_SIZE) {
console.warn('Frontend metrics buffer full, removing oldest metric to make space.');
this.metricsBuffer.shift(); // Remove the oldest metric (FIFO)
}
this.metricsBuffer.push(m);
}
if (!this.timerId) {
this.timerId = window.setTimeout(() => {
this.sendBufferedMetrics();
this.timerId = null;
}, Telemetry.BUFFER_FLUSH_INTERVAL_MS);
}
}
increaseCounter(metricName: CountMetric, tags = {}) {
this.queueMetric({
metric_name: metricName,
metric_value: 1,
tags: tags,
metric_type: 'counter',
});
}
recordSummary(metricName: SummaryMetric, val: number, tags = {}) {
this.queueMetric({
metric_name: metricName,
metric_value: val,
tags: tags,
metric_type: 'summary',
});
}
// The following are exposed for testing purposes.
_forTesting = {
reset: () => {
this.metricsBuffer.length = 0;
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
}
},
getBuffer: () => this.metricsBuffer,
MAX_BUFFER_SIZE: Telemetry.MAX_BUFFER_SIZE,
};
}
export const telemetry = new Telemetry();