1// Copyright 2024 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'; 6import {CustomElement} from 'chrome://resources/js/custom_element.js'; 7 8import {getTemplate} from './app.html.js'; 9import {StructuredMetricsBrowserProxyImpl} from './structured_metrics_browser_proxy.js'; 10import type {SearchParams, StructuredMetricEvent, StructuredMetricsSummary} from './structured_utils.js'; 11import {updateStructuredMetricsEvents, updateStructuredMetricsSummary} from './structured_utils.js'; 12 13/** 14 * Gets search params from search url. 15 */ 16function getSearchParams(): string { 17 return window.location.search.substring(1); 18} 19 20export class StructuredMetricsInternalsAppElement extends CustomElement { 21 static get is(): string { 22 return 'structured-metrics-internals-app'; 23 } 24 25 static override get template() { 26 return getTemplate(); 27 } 28 29 private browserProxy_: StructuredMetricsBrowserProxyImpl = 30 StructuredMetricsBrowserProxyImpl.getInstance(); 31 private summaryIntervalId_: ReturnType<typeof setInterval>; 32 33 initPromise: Promise<void>; 34 35 textSearch: HTMLInputElement; 36 37 searchQuery: SearchParams|null = null; 38 39 searchError: boolean = false; 40 41 constructor() { 42 super(); 43 44 // This must be set before |initSearchParams_| is called. 45 this.textSearch = this.getRequiredElement<HTMLInputElement>('#search-bar'); 46 this.initSearchParams_(); 47 48 this.initPromise = this.init_(); 49 50 // Create periodic callbacks. 51 this.summaryIntervalId_ = 52 setInterval(() => this.updateStructuredMetricsSummary_(), 5000); 53 } 54 55 disconnectedCallback() { 56 clearInterval(this.summaryIntervalId_); 57 } 58 59 private async init_(): Promise<void> { 60 // Fetch Structured Metrics summary and events. 61 // TODO: Implement a push model as new events are recorded. 62 await this.updateStructuredMetricsSummary_(); 63 await this.updateStructuredMetricsEvents_(); 64 65 const eventRefreshButton = this.getRequiredElement('#sm-refresh-events'); 66 eventRefreshButton.addEventListener( 67 'click', () => this.updateStructuredMetricsEvents_()); 68 } 69 70 /** 71 * Fetches summary information of the Structured Metrics service and renders 72 * it. 73 */ 74 private async updateStructuredMetricsSummary_(): Promise<void> { 75 const summary: StructuredMetricsSummary = 76 await this.browserProxy_.fetchStructuredMetricsSummary(); 77 const template = 78 this.getRequiredElement<HTMLTemplateElement>('#summary-row-template'); 79 const smSummaryBody = this.getRequiredElement('#sm-summary-body'); 80 updateStructuredMetricsSummary(smSummaryBody, summary, template); 81 } 82 83 /** 84 * Fetches all events currently recorded by the Structured Metrics Service and 85 * renders them. It an event has been uploaded then it will not be shown 86 * again. This only shows Events recorded in Chromium. Platform2 events are 87 * not supported yet. 88 */ 89 private async updateStructuredMetricsEvents_(): Promise<void> { 90 const events: StructuredMetricEvent[] = 91 await this.browserProxy_.fetchStructuredMetricsEvents(); 92 const eventTemplate = this.getRequiredElement<HTMLTemplateElement>( 93 '#structured-metrics-event-row-template'); 94 const eventDetailsTemplate = this.getRequiredElement<HTMLTemplateElement>( 95 '#structured-metrics-event-details-template'); 96 97 const kvTemplate = 98 this.getRequiredElement<HTMLTemplateElement>('#summary-row-template'); 99 const eventTableBody = this.getRequiredElement('#sm-events-body'); 100 101 updateStructuredMetricsEvents( 102 eventTableBody, events, this.searchQuery, eventTemplate, 103 eventDetailsTemplate, kvTemplate); 104 } 105 106 /** 107 * Initializes search params from the URL. 108 */ 109 private initSearchParams_() { 110 const searchString = this.sanitizeUrlToSearch_(); 111 this.searchQuery = this.parseSearchString_(searchString); 112 113 this.textSearch.value = searchString; 114 this.textSearch.addEventListener('search', () => { 115 this.updateSearchCriteria_(); 116 }); 117 } 118 119 120 /** 121 * Updates the windows search url. 122 */ 123 private updateSearchCriteria_() { 124 // Update the url to reflect the search string. This will redirect the new 125 // url page, updating the searchQuery. 126 window.location.search = '?' + this.sanitizeSearchToUrl_(); 127 } 128 129 /** 130 * Sanitize the search format into a valid format for the URL. 131 */ 132 private sanitizeSearchToUrl_(): string { 133 return this.textSearch.value.replace(/\s+/gi, '&').replace(/:/gi, '='); 134 } 135 136 /** 137 * Sanitize the URL search parameters into the search format. 138 */ 139 private sanitizeUrlToSearch_(): string { 140 return getSearchParams().replace(/&/gi, ' ').replace(/=/gi, ':'); 141 } 142 143 /** 144 * Parse search format into an object. 145 * 146 * The format is a space separated lists of "key:value" pairs. Currently, a 147 * single search term is not supported. 148 */ 149 private parseSearchString_(text: string): SearchParams|null { 150 // Page should be rebuilt on a new search query, but leaving it because it 151 // is better to be safe then have an error message that doesn't disappear 152 // when the page is refreshed.. 153 if (this.searchError) { 154 this.hideSearchErrorMessage_(); 155 } 156 157 if (text.length === 0) { 158 return null; 159 } 160 161 // If an ':' is not found then we are doing a full text search. The string 162 // is the query as is. 163 if (text.indexOf(':') === -1) { 164 return null; 165 } 166 167 // If it is found, then we are doing a categorical search, this parses the 168 // string into a map of category and search value. 169 const categories = new Map<string, string>(); 170 const validSearchKeys = ['project', 'event', 'metric']; 171 172 text.split(' ').forEach((cat) => { 173 const [key, value] = cat.split(':', 2); 174 if (key !== undefined && value !== undefined) { 175 if (validSearchKeys.find((value) => value === key) === undefined) { 176 this.setSearchErrorMessage_(`invalid search key: ${ 177 key}. valid keys are project, event, metric`); 178 return; 179 } 180 categories.set(key, value); 181 } 182 }); 183 184 return categories; 185 } 186 187 /** 188 * Hides the search error message. 189 */ 190 private hideSearchErrorMessage_(): void { 191 this.searchError = false; 192 193 const errorMsg = 194 this.getRequiredElement<HTMLParagraphElement>('#search-error-msg'); 195 assert(errorMsg); 196 errorMsg.style.display = 'none'; 197 } 198 199 /** 200 * Sets and shows the error message. 201 */ 202 private setSearchErrorMessage_(msg: string): void { 203 this.searchError = true; 204 205 // Set the content of the error message. 206 const errorMsg = 207 this.getRequiredElement<HTMLParagraphElement>('#search-error-msg'); 208 assert(errorMsg); 209 errorMsg.innerText = msg; 210 211 // Shows the error message 212 errorMsg.style.display = 'block'; 213 } 214} 215 216declare global { 217 interface HTMLElementTagNameMap { 218 'structured-metrics-internals-app': StructuredMetricsInternalsAppElement; 219 } 220} 221 222customElements.define( 223 StructuredMetricsInternalsAppElement.is, 224 StructuredMetricsInternalsAppElement); 225