// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chrome://resources/js/assert.js';
/**
* Key-Value pair for the summary and metrics table.
*/
interface KeyValue {
key: string;
value: number|string|boolean|[number];
}
/**
* Sequence information for an event.
*/
interface SequenceMetadata {
id: string;
systemUptimeMs: string;
resetCounter: string;
}
/**
* An event and its data. This includes metadata about the event and sequence
* information if applicable.
*/
export interface StructuredMetricEvent {
project: string;
event: string;
type: string;
sequenceMetadata?: SequenceMetadata;
metrics: KeyValue[];
}
/**
* Summary about Structured Metrics service.
*/
export interface StructuredMetricsSummary {
enabled: boolean;
flags: KeyValue[];
crosDeviceId: string;
}
/**
* Contains the search parameters by category.
*
* Valid categories are: project, event, metric.
*/
export type SearchParams = Map<string, string>;
/**
* Updates the Summary table with new information.
*
* @param summaryBody Body of the summary table.
* @param summary Summary object to populate the table.
* @param template Key-Value pair HTML template.
*/
export function updateStructuredMetricsSummary(
summaryBody: HTMLElement, summary: StructuredMetricsSummary,
template: HTMLTemplateElement): void {
// Clear the table first.
summaryBody.replaceChildren();
const enabled =
buildKeyValueRow('Enabled', summary.enabled.toString(), template);
summaryBody.append(enabled);
// If we do not get a value, do not display it. This value doesn't make sense
// on some platforms.
if (summary.crosDeviceId) {
const crosDeviceId =
buildKeyValueRow('CrOS Device Id', summary.crosDeviceId, template);
summaryBody.append(crosDeviceId);
}
}
/**
* Updates the events table with the events recorded by the client.
*
* @param eventBody Body of the event table.
* @param events List of events to populate the table.
* @param searchParams Optional search parameters.
* @param template HTML template for the event table row.
* @param kvTemplate Key-Value pair HTML template.
*/
export function updateStructuredMetricsEvents(
eventBody: HTMLElement, events: StructuredMetricEvent[],
searchParams: SearchParams|null, eventTemplate: HTMLTemplateElement,
detailsTemplate: HTMLTemplateElement,
kvTemplate: HTMLTemplateElement): void {
// If chrome://metrics-internal is opened on Windows, Mac, or Linux and
// Structured Metrics is disabled, we should do nothing.
if (events === null) {
return;
}
eventBody.replaceChildren();
for (const event of events) {
// If there is a |searchParams| and the event doesn't satisfy the
// |searchParams| then it can be skipped.
if (searchParams !== null && !checkSearch(event, searchParams)) {
continue;
}
const row = eventTemplate.content.cloneNode(true) as HTMLElement;
const [project, evn, type, uptime] = row.querySelectorAll('td');
assert(project);
project.textContent = event.project;
assert(evn);
evn.textContent = event.event;
assert(type);
type.textContent = event.type;
assert(uptime);
uptime.textContent = event.sequenceMetadata?.systemUptimeMs ?? '-';
const detailsRow = detailsTemplate.content.cloneNode(true) as HTMLElement;
const metricsRow = detailsRow.querySelector<HTMLElement>('#metrics-row');
assert(metricsRow);
const [details, metrics] = detailsRow.querySelectorAll('tbody');
assert(details);
assert(metrics);
updateEventDetailsTable(details, event, kvTemplate);
updateEventMetricsTable(metrics, event, kvTemplate);
const eventRow = row.querySelector('#event-row');
assert(eventRow);
eventRow.addEventListener('click', () => {
if (metricsRow.style.display === 'none') {
metricsRow.style.display = 'table-row';
} else {
metricsRow.style.display = 'none';
}
}, false);
eventBody.append(row);
eventBody.append(detailsRow);
}
}
function updateEventDetailsTable(
detailTable: HTMLElement, event: StructuredMetricEvent,
template: HTMLTemplateElement): void {
detailTable.replaceChildren();
const resetCounter = event.sequenceMetadata?.resetCounter ?? '-';
const systemUptime = event.sequenceMetadata?.systemUptimeMs ?? '-';
const eventId = event.sequenceMetadata?.id ?? '-';
const resetCounterRow = buildKeyValueRow('Reset Id', resetCounter, template);
const systemUptimeRow =
buildKeyValueRow('System Uptime', systemUptime, template);
const eventIdRow = buildKeyValueRow('Event Id', eventId, template);
detailTable.append(resetCounterRow);
detailTable.append(systemUptimeRow);
detailTable.append(eventIdRow);
}
function checkSearch(
event: StructuredMetricEvent, searchParams: SearchParams): boolean {
const projectSearch = searchParams.get('project');
const eventSearch = searchParams.get('event');
const metricSearch = searchParams.get('metric');
if (projectSearch &&
event.project.toLowerCase().indexOf(projectSearch.toLowerCase()) === -1) {
return false;
}
if (eventSearch &&
event.event.toLowerCase().indexOf(eventSearch.toLowerCase()) === -1) {
return false;
}
if (metricSearch &&
event.metrics.find(
(metric: KeyValue) =>
metric.key.toLowerCase().indexOf(metricSearch.toLowerCase()) !==
-1) === undefined) {
return false;
}
return true;
}
function updateEventMetricsTable(
metricsTable: HTMLElement, event: StructuredMetricEvent,
template: HTMLTemplateElement): void {
metricsTable.replaceChildren();
for (const metric of event.metrics) {
const metricRow =
buildKeyValueRow(metric.key, metric.value.toString(), template);
metricsTable.append(metricRow);
}
}
function buildKeyValueRow(
key: string, value: string, template: HTMLTemplateElement): HTMLElement {
const kvRow = template.content.cloneNode(true) as HTMLElement;
const [k, v] = kvRow.querySelectorAll('td');
assert(k);
k.textContent = key;
assert(v);
v.textContent = value;
return kvRow;
}