1// Copyright 2023 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import {assert} from 'chrome://resources/js/assert.js'; 6 7/** 8 * Key-Value pair for the summary and metrics table. 9 */ 10interface KeyValue { 11 key: string; 12 value: number|string|boolean|[number]; 13} 14 15/** 16 * Sequence information for an event. 17 */ 18interface SequenceMetadata { 19 id: string; 20 systemUptimeMs: string; 21 resetCounter: string; 22} 23 24/** 25 * An event and its data. This includes metadata about the event and sequence 26 * information if applicable. 27 */ 28export interface StructuredMetricEvent { 29 project: string; 30 event: string; 31 type: string; 32 sequenceMetadata?: SequenceMetadata; 33 metrics: KeyValue[]; 34} 35 36/** 37 * Summary about Structured Metrics service. 38 */ 39export interface StructuredMetricsSummary { 40 enabled: boolean; 41 flags: KeyValue[]; 42 crosDeviceId: string; 43} 44 45/** 46 * Contains the search parameters by category. 47 * 48 * Valid categories are: project, event, metric. 49 */ 50export type SearchParams = Map<string, string>; 51 52/** 53 * Updates the Summary table with new information. 54 * 55 * @param summaryBody Body of the summary table. 56 * @param summary Summary object to populate the table. 57 * @param template Key-Value pair HTML template. 58 */ 59export function updateStructuredMetricsSummary( 60 summaryBody: HTMLElement, summary: StructuredMetricsSummary, 61 template: HTMLTemplateElement): void { 62 // Clear the table first. 63 summaryBody.replaceChildren(); 64 65 const enabled = 66 buildKeyValueRow('Enabled', summary.enabled.toString(), template); 67 summaryBody.append(enabled); 68 69 // If we do not get a value, do not display it. This value doesn't make sense 70 // on some platforms. 71 if (summary.crosDeviceId) { 72 const crosDeviceId = 73 buildKeyValueRow('CrOS Device Id', summary.crosDeviceId, template); 74 summaryBody.append(crosDeviceId); 75 } 76} 77 78/** 79 * Updates the events table with the events recorded by the client. 80 * 81 * @param eventBody Body of the event table. 82 * @param events List of events to populate the table. 83 * @param searchParams Optional search parameters. 84 * @param template HTML template for the event table row. 85 * @param kvTemplate Key-Value pair HTML template. 86 */ 87export function updateStructuredMetricsEvents( 88 eventBody: HTMLElement, events: StructuredMetricEvent[], 89 searchParams: SearchParams|null, eventTemplate: HTMLTemplateElement, 90 detailsTemplate: HTMLTemplateElement, 91 kvTemplate: HTMLTemplateElement): void { 92 // If chrome://metrics-internal is opened on Windows, Mac, or Linux and 93 // Structured Metrics is disabled, we should do nothing. 94 if (events === null) { 95 return; 96 } 97 98 eventBody.replaceChildren(); 99 100 for (const event of events) { 101 // If there is a |searchParams| and the event doesn't satisfy the 102 // |searchParams| then it can be skipped. 103 if (searchParams !== null && !checkSearch(event, searchParams)) { 104 continue; 105 } 106 107 const row = eventTemplate.content.cloneNode(true) as HTMLElement; 108 const [project, evn, type, uptime] = row.querySelectorAll('td'); 109 110 assert(project); 111 project.textContent = event.project; 112 113 assert(evn); 114 evn.textContent = event.event; 115 116 assert(type); 117 type.textContent = event.type; 118 119 assert(uptime); 120 uptime.textContent = event.sequenceMetadata?.systemUptimeMs ?? '-'; 121 122 const detailsRow = detailsTemplate.content.cloneNode(true) as HTMLElement; 123 const metricsRow = detailsRow.querySelector<HTMLElement>('#metrics-row'); 124 assert(metricsRow); 125 126 const [details, metrics] = detailsRow.querySelectorAll('tbody'); 127 assert(details); 128 assert(metrics); 129 130 updateEventDetailsTable(details, event, kvTemplate); 131 updateEventMetricsTable(metrics, event, kvTemplate); 132 133 const eventRow = row.querySelector('#event-row'); 134 assert(eventRow); 135 eventRow.addEventListener('click', () => { 136 if (metricsRow.style.display === 'none') { 137 metricsRow.style.display = 'table-row'; 138 } else { 139 metricsRow.style.display = 'none'; 140 } 141 }, false); 142 143 eventBody.append(row); 144 eventBody.append(detailsRow); 145 } 146} 147 148function updateEventDetailsTable( 149 detailTable: HTMLElement, event: StructuredMetricEvent, 150 template: HTMLTemplateElement): void { 151 detailTable.replaceChildren(); 152 153 const resetCounter = event.sequenceMetadata?.resetCounter ?? '-'; 154 const systemUptime = event.sequenceMetadata?.systemUptimeMs ?? '-'; 155 const eventId = event.sequenceMetadata?.id ?? '-'; 156 157 const resetCounterRow = buildKeyValueRow('Reset Id', resetCounter, template); 158 const systemUptimeRow = 159 buildKeyValueRow('System Uptime', systemUptime, template); 160 const eventIdRow = buildKeyValueRow('Event Id', eventId, template); 161 162 detailTable.append(resetCounterRow); 163 detailTable.append(systemUptimeRow); 164 detailTable.append(eventIdRow); 165} 166 167function checkSearch( 168 event: StructuredMetricEvent, searchParams: SearchParams): boolean { 169 const projectSearch = searchParams.get('project'); 170 const eventSearch = searchParams.get('event'); 171 const metricSearch = searchParams.get('metric'); 172 173 if (projectSearch && 174 event.project.toLowerCase().indexOf(projectSearch.toLowerCase()) === -1) { 175 return false; 176 } 177 178 if (eventSearch && 179 event.event.toLowerCase().indexOf(eventSearch.toLowerCase()) === -1) { 180 return false; 181 } 182 183 if (metricSearch && 184 event.metrics.find( 185 (metric: KeyValue) => 186 metric.key.toLowerCase().indexOf(metricSearch.toLowerCase()) !== 187 -1) === undefined) { 188 return false; 189 } 190 return true; 191} 192 193function updateEventMetricsTable( 194 metricsTable: HTMLElement, event: StructuredMetricEvent, 195 template: HTMLTemplateElement): void { 196 metricsTable.replaceChildren(); 197 for (const metric of event.metrics) { 198 const metricRow = 199 buildKeyValueRow(metric.key, metric.value.toString(), template); 200 metricsTable.append(metricRow); 201 } 202} 203 204function buildKeyValueRow( 205 key: string, value: string, template: HTMLTemplateElement): HTMLElement { 206 const kvRow = template.content.cloneNode(true) as HTMLElement; 207 208 const [k, v] = kvRow.querySelectorAll('td'); 209 assert(k); 210 k.textContent = key; 211 assert(v); 212 v.textContent = value; 213 214 return kvRow; 215} 216