blob: 2d90e3e93dd457f287923b09273360490c986b1d [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001/* Copyright 2018 The Chromium Authors. All Rights Reserved.
2 *
3 * Use of this source code is governed by a BSD-style
4 * license that can be found in the LICENSE file.
5 */
6
7import {TSMonClient} from '@chopsui/tsmon-client';
8
9export const tsMonClient = new TSMonClient();
10import AutoRefreshPrpcClient from 'prpc.js';
11
12const TS_MON_JS_PATH = '/_/jstsmon.do';
13const TS_MON_CLIENT_GLOBAL_NAME = '__tsMonClient';
14const PAGE_LOAD_MAX_THRESHOLD = 60000;
15export const PAGE_TYPES = Object.freeze({
16 ISSUE_DETAIL_SPA: 'issue_detail_spa',
17 ISSUE_ENTRY: 'issue_entry',
18 ISSUE_LIST_SPA: 'issue_list_spa',
19});
20
21export default class MonorailTSMon extends TSMonClient {
22 /** @override */
23 constructor() {
24 super(TS_MON_JS_PATH);
25 this.clientId = MonorailTSMon.generateClientId();
26 this.disableAfterNextFlush();
27 // Create an instance of pRPC client for refreshing XSRF tokens.
28 this.prpcClient = new AutoRefreshPrpcClient(
29 window.CS_env.token, window.CS_env.tokenExpiresSec);
30
31 // TODO(jeffcarp, 4415): Deduplicate metric defs.
32 const standardFields = new Map([
33 ['client_id', TSMonClient.stringField('client_id')],
34 ['host_name', TSMonClient.stringField('host_name')],
35 ['document_visible', TSMonClient.boolField('document_visible')],
36 ]);
37 this._userTimingMetrics = [
38 {
39 category: 'issues',
40 eventName: 'new-issue',
41 eventLabel: 'server-time',
42 metric: this.cumulativeDistribution(
43 'monorail/frontend/issue_create_latency',
44 'Latency between issue entry form submit and issue detail page load.',
45 null, standardFields,
46 ),
47 },
48 {
49 category: 'issues',
50 eventName: 'issue-update',
51 eventLabel: 'computer-time',
52 metric: this.cumulativeDistribution(
53 'monorail/frontend/issue_update_latency',
54 'Latency between issue update form submit and issue detail page load.',
55 null, standardFields,
56 ),
57 },
58 {
59 category: 'autocomplete',
60 eventName: 'populate-options',
61 eventLabel: 'user-time',
62 metric: this.cumulativeDistribution(
63 'monorail/frontend/autocomplete_populate_latency',
64 'Latency between page load and autocomplete options loading.',
65 null, standardFields,
66 ),
67 },
68 ];
69
70 this.dateRangeMetric = this.counter(
71 'monorail/frontend/charts/switch_date_range',
72 'Number of times user changes date range.',
73 null, (new Map([
74 ['client_id', TSMonClient.stringField('client_id')],
75 ['host_name', TSMonClient.stringField('host_name')],
76 ['document_visible', TSMonClient.boolField('document_visible')],
77 ['date_range', TSMonClient.intField('date_range')],
78 ])),
79 );
80
81 this.issueCommentsLoadMetric = this.cumulativeDistribution(
82 'monorail/frontend/issue_comments_load_latency',
83 'Time from navigation or click to issue comments loaded.',
84 null, (new Map([
85 ['client_id', TSMonClient.stringField('client_id')],
86 ['host_name', TSMonClient.stringField('host_name')],
87 ['template_name', TSMonClient.stringField('template_name')],
88 ['document_visible', TSMonClient.boolField('document_visible')],
89 ['full_app_load', TSMonClient.boolField('full_app_load')],
90 ])),
91 );
92
93 this.issueListLoadMetric = this.cumulativeDistribution(
94 'monorail/frontend/issue_list_load_latency',
95 'Time from navigation or click to search issues list loaded.',
96 null, (new Map([
97 ['client_id', TSMonClient.stringField('client_id')],
98 ['host_name', TSMonClient.stringField('host_name')],
99 ['template_name', TSMonClient.stringField('template_name')],
100 ['document_visible', TSMonClient.boolField('document_visible')],
101 ['full_app_load', TSMonClient.boolField('full_app_load')],
102 ])),
103 );
104
105
106 this.pageLoadMetric = this.cumulativeDistribution(
107 'frontend/dom_content_loaded',
108 'domContentLoaded performance timing.',
109 null, (new Map([
110 ['client_id', TSMonClient.stringField('client_id')],
111 ['host_name', TSMonClient.stringField('host_name')],
112 ['template_name', TSMonClient.stringField('template_name')],
113 ['document_visible', TSMonClient.boolField('document_visible')],
114 ])),
115 );
116 }
117
118 fetchImpl(rawMetricValues) {
119 return this.prpcClient.ensureTokenIsValid().then(() => {
120 return fetch(this._reportPath, {
121 method: 'POST',
122 credentials: 'same-origin',
123 body: JSON.stringify({
124 metrics: rawMetricValues,
125 token: this.prpcClient.token,
126 }),
127 });
128 });
129 }
130
131 recordUserTiming(category, eventName, eventLabel, elapsed) {
132 const metricFields = new Map([
133 ['client_id', this.clientId],
134 ['host_name', window.CS_env.app_version],
135 ['document_visible', MonorailTSMon.isPageVisible()],
136 ]);
137 for (const metric of this._userTimingMetrics) {
138 if (category === metric.category &&
139 eventName === metric.eventName &&
140 eventLabel === metric.eventLabel) {
141 metric.metric.add(elapsed, metricFields);
142 }
143 }
144 }
145
146 recordDateRangeChange(dateRange) {
147 const metricFields = new Map([
148 ['client_id', this.clientId],
149 ['host_name', window.CS_env.app_version],
150 ['document_visible', MonorailTSMon.isPageVisible()],
151 ['date_range', dateRange],
152 ]);
153 this.dateRangeMetric.add(1, metricFields);
154 }
155
156 // Make sure this function runs after the page is loaded.
157 recordPageLoadTiming(pageType, maxThresholdMs=null) {
158 if (!pageType) return;
159 // See timing definitions here:
160 // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming
161 const t = window.performance.timing;
162 const domContentLoadedMs = t.domContentLoadedEventEnd - t.navigationStart;
163
164 const measurePageTypes = new Set([
165 PAGE_TYPES.ISSUE_DETAIL_SPA,
166 PAGE_TYPES.ISSUE_ENTRY,
167 ]);
168
169 if (measurePageTypes.has(pageType)) {
170 if (maxThresholdMs !== null && domContentLoadedMs > maxThresholdMs) {
171 return;
172 }
173 const metricFields = new Map([
174 ['client_id', this.clientId],
175 ['host_name', window.CS_env.app_version],
176 ['template_name', pageType],
177 ['document_visible', MonorailTSMon.isPageVisible()],
178 ]);
179 this.pageLoadMetric.add(domContentLoadedMs, metricFields);
180 }
181 }
182
183 recordIssueCommentsLoadTiming(value, fullAppLoad) {
184 const metricFields = new Map([
185 ['client_id', this.clientId],
186 ['host_name', window.CS_env.app_version],
187 ['template_name', PAGE_TYPES.ISSUE_DETAIL_SPA],
188 ['document_visible', MonorailTSMon.isPageVisible()],
189 ['full_app_load', fullAppLoad],
190 ]);
191 this.issueCommentsLoadMetric.add(value, metricFields);
192 }
193
194 recordIssueEntryTiming(maxThresholdMs=PAGE_LOAD_MAX_THRESHOLD) {
195 this.recordPageLoadTiming(PAGE_TYPES.ISSUE_ENTRY, maxThresholdMs);
196 }
197
198 recordIssueDetailSpaTiming(maxThresholdMs=PAGE_LOAD_MAX_THRESHOLD) {
199 this.recordPageLoadTiming(PAGE_TYPES.ISSUE_DETAIL_SPA, maxThresholdMs);
200 }
201
202
203 /**
204 * Adds a value to the 'issue_list_load_latency' metric.
205 * @param {timestamp} value duration of the load time.
206 * @param {Boolean} fullAppLoad true if this metric was collected from
207 * a full app load (cold) rather than from navigation within the
208 * app (hot).
209 */
210 recordIssueListLoadTiming(value, fullAppLoad) {
211 const metricFields = new Map([
212 ['client_id', this.clientId],
213 ['host_name', window.CS_env.app_version],
214 ['template_name', PAGE_TYPES.ISSUE_LIST_SPA],
215 ['document_visible', MonorailTSMon.isPageVisible()],
216 ['full_app_load', fullAppLoad],
217 ]);
218 this.issueListLoadMetric.add(value, metricFields);
219 }
220
221 // Uses the window object to ensure that only one ts_mon JS client
222 // exists on the page at any given time. Returns the object on window,
223 // instantiating it if it doesn't exist yet.
224 static getGlobalClient() {
225 const key = TS_MON_CLIENT_GLOBAL_NAME;
226 if (!window.hasOwnProperty(key)) {
227 window[key] = new MonorailTSMon();
228 }
229 return window[key];
230 }
231
232 static generateClientId() {
233 /**
234 * Returns a random string used as the client_id field in ts_mon metrics.
235 *
236 * Rationale:
237 * If we assume Monorail has sustained 40 QPS, assume every request
238 * generates a new ClientLogger (likely an overestimation), and we want
239 * the likelihood of a client ID collision to be 0.01% for all IDs
240 * generated in any given year (in other words, 1 collision every 10K
241 * years), we need to generate a random string with at least 2^30 different
242 * possible values (i.e. 30 bits of entropy, see log2(d) in Wolfram link
243 * below). Using an unsigned integer gives us 32 bits of entropy, more than
244 * enough.
245 *
246 * Returns:
247 * A string (the base-32 representation of a random 32-bit integer).
248
249 * References:
250 * - https://en.wikipedia.org/wiki/Birthday_problem
251 * - 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
252 * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString
253 */
254 const randomvalues = new Uint32Array(1);
255 window.crypto.getRandomValues(randomvalues);
256 return randomvalues[0].toString(32);
257 }
258
259 // Returns a Boolean, true if document is visible.
260 static isPageVisible(path) {
261 return document.visibilityState === 'visible';
262 }
263}
264
265// For integration with EZT pages, which don't use ES modules.
266window.getTSMonClient = MonorailTSMon.getGlobalClient;