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