// Copyright 2022 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js'; import {assert} from 'chrome://resources/js/assert.js'; import {addWebUiListener} from 'chrome://resources/js/cr.js'; import {CustomElement} from 'chrome://resources/js/custom_element.js'; import {getTemplate} from './app.html.js'; import type {KeyValue, Log, LogData, MetricsInternalsBrowserProxy} from './browser_proxy.js'; import {MetricsInternalsBrowserProxyImpl} from './browser_proxy.js'; import {getEventsPeekString, logEventToString, sizeToString, timestampToString, umaLogTypeToString} from './log_utils.js'; /** * An empty log. It is appended to a logs table when there are no logs (for * purely aesthetic reasons). */ const EMPTY_LOG: Log = { type: 'N/A', hash: 'N/A', timestamp: '', size: -1, events: [], }; export class MetricsInternalsAppElement extends CustomElement { static get is(): string { return 'metrics-internals-app'; } static override get template() { return getTemplate(); } /** * Resolves once the component has finished loading. */ initPromise: Promise; private browserProxy_: MetricsInternalsBrowserProxy = MetricsInternalsBrowserProxyImpl.getInstance(); /** * Previous summary tables data. Used to prevent re-renderings of the tables * when the data has not changed. */ private previousVariationsSummaryData_: string = ''; private previousUmaSummaryData_: string = ''; constructor() { super(); this.initPromise = this.init_(); } /** * Returns UMA logs data (with their proto) as a JSON string. Used when * exporting UMA logs data. Returns a promise. */ getUmaLogsExportContent(): Promise { return this.browserProxy_.getUmaLogData(/*includeLogProtoData*/ true); } private async init_(): Promise { // Fetch variations summary data and set up a recurring timer. await this.updateVariationsSummary_(); setInterval(() => this.updateVariationsSummary_(), 3000); // Fetch UMA summary data and set up a recurring timer. await this.updateUmaSummary_(); setInterval(() => this.updateUmaSummary_(), 3000); // Set up the UMA table caption. const umaTableCaption = this.$('#uma-table-caption') as HTMLElement; const isUsingMetricsServiceObserver = await this.browserProxy_.isUsingMetricsServiceObserver(); umaTableCaption.textContent = isUsingMetricsServiceObserver ? 'List of all UMA logs closed since browser startup.' : 'List of UMA logs closed since opening this page. Starting the browser \ with the --export-uma-logs-to-file command line flag will instead show \ all logs closed since browser startup.'; // Set up a listener for UMA logs. Also update UMA log data immediately in // case there are logs that we already have data on. addWebUiListener( 'uma-log-created-or-event', () => this.updateUmaLogsData_()); await this.updateUmaLogsData_(); // Set up the UMA "Export logs" button. const exportUmaLogsButton = this.$('#export-uma-logs') as HTMLElement; exportUmaLogsButton.addEventListener('click', () => this.exportUmaLogs_()); } /** * Callback function to expand/collapse an element on click. * @param e The click event. */ private toggleEventsExpand_(e: MouseEvent): void { let umaLogEventsDiv = e.target as HTMLElement; // It is possible we have clicked a descendant. Keep checking the parent // until we are the the root div of the events. while (!umaLogEventsDiv.classList.contains('uma-log-events')) { umaLogEventsDiv = umaLogEventsDiv.parentElement as HTMLElement; } umaLogEventsDiv.classList.toggle('uma-log-events-expanded'); } /** * Fills the passed table element with the given summary. */ private updateSummaryTable_(tableBody: HTMLElement, summary: KeyValue[]): void { // Clear the table first. tableBody.replaceChildren(); const template = this.$('#summary-row-template') as HTMLTemplateElement; for (const info of summary) { const row = template.content.cloneNode(true) as HTMLElement; const [key, value] = row.querySelectorAll('td'); assert(key); key.textContent = info.key; assert(value); value.textContent = info.value; tableBody.appendChild(row); } } /** * Fetches variations summary data and updates the view. */ private async updateVariationsSummary_(): Promise { const summary: KeyValue[] = await this.browserProxy_.fetchVariationsSummary(); const variationsSummaryTableBody = this.$('#variations-summary-body') as HTMLElement; // Don't re-render the table if the data has not changed. const newDataString = summary.toString(); if (newDataString === this.previousVariationsSummaryData_) { return; } this.previousVariationsSummaryData_ = newDataString; this.updateSummaryTable_(variationsSummaryTableBody, summary); } /** * Fetches UMA summary data and updates the view. */ private async updateUmaSummary_(): Promise { const summary: KeyValue[] = await this.browserProxy_.fetchUmaSummary(); const umaSummaryTableBody = this.$('#uma-summary-body') as HTMLElement; // Don't re-render the table if the data has not changed. const newDataString = summary.toString(); if (newDataString === this.previousUmaSummaryData_) { return; } this.previousUmaSummaryData_ = newDataString; this.updateSummaryTable_(umaSummaryTableBody, summary); } /** * Fills the passed table element with the given logs. */ private updateLogsTable_(tableBody: HTMLElement, logs: Log[]): void { // Clear the table first. tableBody.replaceChildren(); const template = this.$('#uma-log-row-template') as HTMLTemplateElement; // Iterate through the logs in reverse order so that the most recent log // shows up first. for (const log of logs.slice(0).reverse()) { const row = template.content.cloneNode(true) as HTMLElement; const [type, hash, timestamp, size, events] = row.querySelectorAll('td'); assert(type); type.textContent = umaLogTypeToString(log.type); assert(hash); hash.textContent = log.hash; assert(timestamp); timestamp.textContent = timestampToString(log.timestamp); assert(size); size.textContent = sizeToString(log.size); assert(events); const eventsPeekDiv = events.querySelector('.uma-log-events-peek'); assert(eventsPeekDiv); eventsPeekDiv.addEventListener('click', this.toggleEventsExpand_, false); const eventsPeekText = events.querySelector('.uma-log-events-peek-text'); assert(eventsPeekText); eventsPeekText.textContent = getEventsPeekString(log.events); const eventsText = events.querySelector('.uma-log-events-text'); assert(eventsText); // Iterate through the events in reverse order so that the most recent // event shows up first. for (const event of log.events.slice(0).reverse()) { const div = document.createElement('div'); div.textContent = logEventToString(event); eventsText.appendChild(div); } tableBody.appendChild(row); } } /** * Fetches the latest UMA logs and renders them. This is called when the page * is loaded and whenever there is a log that created or changed. */ private async updateUmaLogsData_(): Promise { const logsData: string = await this.browserProxy_.getUmaLogData(/*includeLogProtoData=*/ false); const logs: LogData = JSON.parse(logsData); // If there are no logs, append an empty log. This is purely for aesthetic // reasons. Otherwise, the table may look confusing. if (!logs.logs.length) { logs.logs = [EMPTY_LOG]; } // We don't compare the new data with the old data to prevent re-renderings // because this should only be called when there is an actual change. const umaLogsTableBody = this.$('#uma-logs-body') as HTMLElement; this.updateLogsTable_(umaLogsTableBody, logs.logs); } /** * Exports the accumulated UMA logs, including their proto data, as a JSON * file. This will initiate a download. */ private async exportUmaLogs_(): Promise { const logsData: string = await this.getUmaLogsExportContent(); const file = new Blob([logsData], {type: 'text/plain'}); const a = document.createElement('a'); a.href = URL.createObjectURL(file); a.download = `uma_logs_${new Date().getTime()}.json`; a.click(); } } declare global { interface HTMLElementTagNameMap { 'metrics-internals-app': MetricsInternalsAppElement; } } customElements.define( MetricsInternalsAppElement.is, MetricsInternalsAppElement);