• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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