Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/monitoring/monorail-ts-mon.js b/static_src/monitoring/monorail-ts-mon.js
new file mode 100644
index 0000000..2d90e3e
--- /dev/null
+++ b/static_src/monitoring/monorail-ts-mon.js
@@ -0,0 +1,266 @@
+/* Copyright 2018 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file.
+ */
+
+import {TSMonClient} from '@chopsui/tsmon-client';
+
+export const tsMonClient = new TSMonClient();
+import AutoRefreshPrpcClient from 'prpc.js';
+
+const TS_MON_JS_PATH = '/_/jstsmon.do';
+const TS_MON_CLIENT_GLOBAL_NAME = '__tsMonClient';
+const PAGE_LOAD_MAX_THRESHOLD = 60000;
+export const PAGE_TYPES = Object.freeze({
+ ISSUE_DETAIL_SPA: 'issue_detail_spa',
+ ISSUE_ENTRY: 'issue_entry',
+ ISSUE_LIST_SPA: 'issue_list_spa',
+});
+
+export default class MonorailTSMon extends TSMonClient {
+ /** @override */
+ constructor() {
+ super(TS_MON_JS_PATH);
+ this.clientId = MonorailTSMon.generateClientId();
+ this.disableAfterNextFlush();
+ // Create an instance of pRPC client for refreshing XSRF tokens.
+ this.prpcClient = new AutoRefreshPrpcClient(
+ window.CS_env.token, window.CS_env.tokenExpiresSec);
+
+ // TODO(jeffcarp, 4415): Deduplicate metric defs.
+ const standardFields = new Map([
+ ['client_id', TSMonClient.stringField('client_id')],
+ ['host_name', TSMonClient.stringField('host_name')],
+ ['document_visible', TSMonClient.boolField('document_visible')],
+ ]);
+ this._userTimingMetrics = [
+ {
+ category: 'issues',
+ eventName: 'new-issue',
+ eventLabel: 'server-time',
+ metric: this.cumulativeDistribution(
+ 'monorail/frontend/issue_create_latency',
+ 'Latency between issue entry form submit and issue detail page load.',
+ null, standardFields,
+ ),
+ },
+ {
+ category: 'issues',
+ eventName: 'issue-update',
+ eventLabel: 'computer-time',
+ metric: this.cumulativeDistribution(
+ 'monorail/frontend/issue_update_latency',
+ 'Latency between issue update form submit and issue detail page load.',
+ null, standardFields,
+ ),
+ },
+ {
+ category: 'autocomplete',
+ eventName: 'populate-options',
+ eventLabel: 'user-time',
+ metric: this.cumulativeDistribution(
+ 'monorail/frontend/autocomplete_populate_latency',
+ 'Latency between page load and autocomplete options loading.',
+ null, standardFields,
+ ),
+ },
+ ];
+
+ this.dateRangeMetric = this.counter(
+ 'monorail/frontend/charts/switch_date_range',
+ 'Number of times user changes date range.',
+ null, (new Map([
+ ['client_id', TSMonClient.stringField('client_id')],
+ ['host_name', TSMonClient.stringField('host_name')],
+ ['document_visible', TSMonClient.boolField('document_visible')],
+ ['date_range', TSMonClient.intField('date_range')],
+ ])),
+ );
+
+ this.issueCommentsLoadMetric = this.cumulativeDistribution(
+ 'monorail/frontend/issue_comments_load_latency',
+ 'Time from navigation or click to issue comments loaded.',
+ null, (new Map([
+ ['client_id', TSMonClient.stringField('client_id')],
+ ['host_name', TSMonClient.stringField('host_name')],
+ ['template_name', TSMonClient.stringField('template_name')],
+ ['document_visible', TSMonClient.boolField('document_visible')],
+ ['full_app_load', TSMonClient.boolField('full_app_load')],
+ ])),
+ );
+
+ this.issueListLoadMetric = this.cumulativeDistribution(
+ 'monorail/frontend/issue_list_load_latency',
+ 'Time from navigation or click to search issues list loaded.',
+ null, (new Map([
+ ['client_id', TSMonClient.stringField('client_id')],
+ ['host_name', TSMonClient.stringField('host_name')],
+ ['template_name', TSMonClient.stringField('template_name')],
+ ['document_visible', TSMonClient.boolField('document_visible')],
+ ['full_app_load', TSMonClient.boolField('full_app_load')],
+ ])),
+ );
+
+
+ this.pageLoadMetric = this.cumulativeDistribution(
+ 'frontend/dom_content_loaded',
+ 'domContentLoaded performance timing.',
+ null, (new Map([
+ ['client_id', TSMonClient.stringField('client_id')],
+ ['host_name', TSMonClient.stringField('host_name')],
+ ['template_name', TSMonClient.stringField('template_name')],
+ ['document_visible', TSMonClient.boolField('document_visible')],
+ ])),
+ );
+ }
+
+ fetchImpl(rawMetricValues) {
+ return this.prpcClient.ensureTokenIsValid().then(() => {
+ return fetch(this._reportPath, {
+ method: 'POST',
+ credentials: 'same-origin',
+ body: JSON.stringify({
+ metrics: rawMetricValues,
+ token: this.prpcClient.token,
+ }),
+ });
+ });
+ }
+
+ recordUserTiming(category, eventName, eventLabel, elapsed) {
+ const metricFields = new Map([
+ ['client_id', this.clientId],
+ ['host_name', window.CS_env.app_version],
+ ['document_visible', MonorailTSMon.isPageVisible()],
+ ]);
+ for (const metric of this._userTimingMetrics) {
+ if (category === metric.category &&
+ eventName === metric.eventName &&
+ eventLabel === metric.eventLabel) {
+ metric.metric.add(elapsed, metricFields);
+ }
+ }
+ }
+
+ recordDateRangeChange(dateRange) {
+ const metricFields = new Map([
+ ['client_id', this.clientId],
+ ['host_name', window.CS_env.app_version],
+ ['document_visible', MonorailTSMon.isPageVisible()],
+ ['date_range', dateRange],
+ ]);
+ this.dateRangeMetric.add(1, metricFields);
+ }
+
+ // Make sure this function runs after the page is loaded.
+ recordPageLoadTiming(pageType, maxThresholdMs=null) {
+ if (!pageType) return;
+ // See timing definitions here:
+ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming
+ const t = window.performance.timing;
+ const domContentLoadedMs = t.domContentLoadedEventEnd - t.navigationStart;
+
+ const measurePageTypes = new Set([
+ PAGE_TYPES.ISSUE_DETAIL_SPA,
+ PAGE_TYPES.ISSUE_ENTRY,
+ ]);
+
+ if (measurePageTypes.has(pageType)) {
+ if (maxThresholdMs !== null && domContentLoadedMs > maxThresholdMs) {
+ return;
+ }
+ const metricFields = new Map([
+ ['client_id', this.clientId],
+ ['host_name', window.CS_env.app_version],
+ ['template_name', pageType],
+ ['document_visible', MonorailTSMon.isPageVisible()],
+ ]);
+ this.pageLoadMetric.add(domContentLoadedMs, metricFields);
+ }
+ }
+
+ recordIssueCommentsLoadTiming(value, fullAppLoad) {
+ const metricFields = new Map([
+ ['client_id', this.clientId],
+ ['host_name', window.CS_env.app_version],
+ ['template_name', PAGE_TYPES.ISSUE_DETAIL_SPA],
+ ['document_visible', MonorailTSMon.isPageVisible()],
+ ['full_app_load', fullAppLoad],
+ ]);
+ this.issueCommentsLoadMetric.add(value, metricFields);
+ }
+
+ recordIssueEntryTiming(maxThresholdMs=PAGE_LOAD_MAX_THRESHOLD) {
+ this.recordPageLoadTiming(PAGE_TYPES.ISSUE_ENTRY, maxThresholdMs);
+ }
+
+ recordIssueDetailSpaTiming(maxThresholdMs=PAGE_LOAD_MAX_THRESHOLD) {
+ this.recordPageLoadTiming(PAGE_TYPES.ISSUE_DETAIL_SPA, maxThresholdMs);
+ }
+
+
+ /**
+ * Adds a value to the 'issue_list_load_latency' metric.
+ * @param {timestamp} value duration of the load time.
+ * @param {Boolean} fullAppLoad true if this metric was collected from
+ * a full app load (cold) rather than from navigation within the
+ * app (hot).
+ */
+ recordIssueListLoadTiming(value, fullAppLoad) {
+ const metricFields = new Map([
+ ['client_id', this.clientId],
+ ['host_name', window.CS_env.app_version],
+ ['template_name', PAGE_TYPES.ISSUE_LIST_SPA],
+ ['document_visible', MonorailTSMon.isPageVisible()],
+ ['full_app_load', fullAppLoad],
+ ]);
+ this.issueListLoadMetric.add(value, metricFields);
+ }
+
+ // Uses the window object to ensure that only one ts_mon JS client
+ // exists on the page at any given time. Returns the object on window,
+ // instantiating it if it doesn't exist yet.
+ static getGlobalClient() {
+ const key = TS_MON_CLIENT_GLOBAL_NAME;
+ if (!window.hasOwnProperty(key)) {
+ window[key] = new MonorailTSMon();
+ }
+ return window[key];
+ }
+
+ static generateClientId() {
+ /**
+ * Returns a random string used as the client_id field in ts_mon metrics.
+ *
+ * Rationale:
+ * If we assume Monorail has sustained 40 QPS, assume every request
+ * generates a new ClientLogger (likely an overestimation), and we want
+ * the likelihood of a client ID collision to be 0.01% for all IDs
+ * generated in any given year (in other words, 1 collision every 10K
+ * years), we need to generate a random string with at least 2^30 different
+ * possible values (i.e. 30 bits of entropy, see log2(d) in Wolfram link
+ * below). Using an unsigned integer gives us 32 bits of entropy, more than
+ * enough.
+ *
+ * Returns:
+ * A string (the base-32 representation of a random 32-bit integer).
+
+ * References:
+ * - https://en.wikipedia.org/wiki/Birthday_problem
+ * - https://www.wolframalpha.com/input/?i=d%3D40+*+60+*+60+*+24+*+365,+p%3D0.0001,+n+%3D+sqrt(2d+*+ln(1%2F(1-p))),+d,+log2(d),+n
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString
+ */
+ const randomvalues = new Uint32Array(1);
+ window.crypto.getRandomValues(randomvalues);
+ return randomvalues[0].toString(32);
+ }
+
+ // Returns a Boolean, true if document is visible.
+ static isPageVisible(path) {
+ return document.visibilityState === 'visible';
+ }
+}
+
+// For integration with EZT pages, which don't use ES modules.
+window.getTSMonClient = MonorailTSMon.getGlobalClient;