blob: af83699afed856ad19b800950b00362896e81685 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
Ben Rohlfs94fcbbc2022-05-27 10:45:03 +02003 * Copyright 2016 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04005 */
Ben Rohlfs5b5475e2024-07-02 18:32:31 +02006import {FlagsService, KnownExperimentId} from '../flags/flags';
Ben Rohlfsa7ab9502021-02-15 17:45:45 +01007import {EventValue, ReportingService, Timer} from './gr-reporting';
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +02008import {hasOwnProperty} from '../../utils/common-util';
Milutin Kristofic3a844522021-01-21 10:01:55 +01009import {NumericChangeId} from '../../types/common';
Ben Rohlfsbd4c91b2021-12-22 11:37:41 +010010import {Deduping, EventDetails, ReportingOptions} from '../../api/reporting';
Ben Rohlfs94f34a12021-02-26 18:37:32 +010011import {PluginApi} from '../../api/plugin';
Milutin Kristofica7c24c12021-06-07 23:09:26 +020012import {
13 Execution,
14 Interaction,
15 LifeCycle,
16 Timing,
17} from '../../constants/reporting';
Ben Rohlfsed456c12024-08-14 09:19:44 +020018import {onCLS, onLCP, Metric, onINP} from 'web-vitals';
Ben Rohlfsf4313302023-02-23 11:57:05 +010019import {getEventPath, isElementTarget} from '../../utils/dom-util';
Dhruv Srivastava7cfec742023-09-28 10:25:47 +020020import {Finalizable} from '../../types/types';
Viktar Donich5ad42282016-08-26 16:07:24 -070021
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010022// Latency reporting constants.
Milutin Kristofic0fe96792020-03-26 13:17:35 +010023
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010024const TIMING = {
25 TYPE: 'timing-report',
Milutin Kristofic0fe96792020-03-26 13:17:35 +010026 CATEGORY: {
27 UI_LATENCY: 'UI Latency',
28 RPC: 'RPC Timing',
29 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010030};
Viktar Donichc1c5f772018-05-04 15:04:44 -070031
Milutin Kristofic0fe96792020-03-26 13:17:35 +010032const LIFECYCLE = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010033 TYPE: 'lifecycle',
Milutin Kristofic0fe96792020-03-26 13:17:35 +010034 CATEGORY: {
35 DEFAULT: 'Default',
36 EXTENSION_DETECTED: 'Extension detected',
37 PLUGINS_INSTALLED: 'Plugins installed',
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +020038 VISIBILITY: 'Visibility',
Ben Rohlfsbad48d42020-12-17 15:03:33 +010039 EXECUTION: 'Execution',
Milutin Kristofic0fe96792020-03-26 13:17:35 +010040 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010041};
Viktar Donich761ce28e2018-05-04 15:34:10 -070042
Milutin Kristofic0fe96792020-03-26 13:17:35 +010043const INTERACTION = {
44 TYPE: 'interaction',
45 CATEGORY: {
46 DEFAULT: 'Default',
47 VISIBILITY: 'Visibility',
48 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010049};
Viktar Donich5ad42282016-08-26 16:07:24 -070050
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010051const NAVIGATION = {
52 TYPE: 'nav-report',
Milutin Kristofic0fe96792020-03-26 13:17:35 +010053 CATEGORY: {
54 LOCATION_CHANGED: 'Location Changed',
55 },
56 EVENT: {
57 PAGE: 'Page',
58 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010059};
Viktar Donich1c97ee72017-01-17 11:13:20 -080060
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010061const ERROR = {
62 TYPE: 'error',
Milutin Kristofic0fe96792020-03-26 13:17:35 +010063 CATEGORY: {
64 EXCEPTION: 'exception',
65 ERROR_DIALOG: 'Error Dialog',
66 },
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010067};
Viktar Donichd155b8b2018-04-19 22:18:31 -070068
Milutin Kristofice1a562f2021-06-01 19:51:11 +020069const PLUGIN = {
70 TYPE: 'plugin-log',
71 CATEGORY: {
72 LIFECYCLE: 'lifecycle',
73 INTERACTION: 'interaction',
74 },
75};
76
Milutin Kristofic48a0f432021-03-16 22:53:08 +010077const STARTUP_TIMERS: {[name: string]: number} = {
78 [Timing.PLUGINS_LOADED]: 0,
79 [Timing.METRICS_PLUGIN_LOADED]: 0,
80 [Timing.STARTUP_CHANGE_DISPLAYED]: 0,
81 [Timing.STARTUP_CHANGE_LOAD_FULL]: 0,
82 [Timing.STARTUP_DASHBOARD_DISPLAYED]: 0,
83 [Timing.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED]: 0,
84 [Timing.STARTUP_DIFF_VIEW_DISPLAYED]: 0,
Milutin Kristofic48a0f432021-03-16 22:53:08 +010085 [Timing.STARTUP_FILE_LIST_DISPLAYED]: 0,
86 [Timing.APP_STARTED]: 0,
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +020087 // WebComponentsReady timer is triggered from gr-router.
Milutin Kristofic48a0f432021-03-16 22:53:08 +010088 [Timing.WEB_COMPONENTS_READY]: 0,
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +020089};
Wyatt Allenf87244c2017-02-24 15:35:57 -080090
Ben Rohlfs055d4a42023-05-03 13:55:08 +020091// List of timers that should NOT be reset before a location change.
92const LOCATION_CHANGE_OK_TIMERS: (string | Timing)[] = [Timing.SEND_REPLY];
93
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010094const SLOW_RPC_THRESHOLD = 500;
Milutin Kristoficc9a35972019-08-21 12:35:57 +020095
Chris Poucetc6e880b2021-11-15 19:57:06 +010096export function initErrorReporter(reportingService: ReportingService) {
Milutin Kristofic380b9f62020-12-03 09:24:17 +010097 const normalizeError = (err: Error | unknown) => {
98 if (err instanceof Error) {
99 return err;
100 }
101 let msg = '';
102 if (typeof err === 'string') {
103 msg += err;
104 } else {
105 msg += JSON.stringify(err);
106 }
107 const error = new Error(msg);
108 error.stack = 'unknown';
109 return error;
110 };
111 // TODO(dmfilippov): TS-fix-any oldOnError - define correct type
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200112 const onError = function (
113 oldOnError: Function,
Tao Zhou38b3e0d2020-09-09 17:02:21 +0200114 msg: Event | string,
115 url?: string,
116 line?: number,
117 column?: number,
118 error?: Error
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200119 ) {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100120 if (oldOnError) {
121 oldOnError(msg, url, line, column, error);
Viktar Donich1c97ee72017-01-17 11:13:20 -0800122 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100123 if (error) {
Milutin Kristofic380b9f62020-12-03 09:24:17 +0100124 line = line ?? error.lineNumber;
125 column = column ?? error.columnNumber;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100126 }
Kamil Musinf0ece022022-10-14 15:22:44 +0200127 reportingService.error('onError', normalizeError(error), {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100128 line,
129 column,
Milutin Kristofic380b9f62020-12-03 09:24:17 +0100130 url,
131 msg,
132 });
Milutin Kristoficda88b332020-03-24 10:19:12 +0100133 return true;
134 };
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200135 // TODO(dmfilippov): TS-fix-any unclear what is context
Dhruv Srivastavaf97b0552020-11-30 14:14:09 +0100136 // eslint-disable-next-line @typescript-eslint/no-explicit-any
Dhruv Srivastavab3ed8742023-03-24 14:42:06 +0100137 const catchErrors = function (context?: any) {
138 context = context || window;
Ben Rohlfscf59bdd2020-09-11 11:27:54 +0200139 const oldOnError = context.onerror;
Tao Zhou38b3e0d2020-09-09 17:02:21 +0200140 context.onerror = (
141 event: Event | string,
142 source?: string,
143 lineno?: number,
144 colno?: number,
145 error?: Error
Frank Borden6988bdf2021-04-07 14:42:00 +0200146 ) => onError(oldOnError, event, source, lineno, colno, error);
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200147 context.addEventListener(
148 'unhandledrejection',
149 (e: PromiseRejectionEvent) => {
Kamil Musinf0ece022022-10-14 15:22:44 +0200150 reportingService.error('unhandledrejection', normalizeError(e.reason));
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200151 }
152 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100153 };
154
155 catchErrors();
156
157 // for testing
158 return {catchErrors};
159}
160
Chris Poucetc6e880b2021-11-15 19:57:06 +0100161export function initPerformanceReporter(reportingService: ReportingService) {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100162 // PerformanceObserver interface is a browser API.
163 if (window.PerformanceObserver) {
164 const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
165 // Safari doesn't support longtask yet
166 if (supportedEntryTypes.includes('longtask')) {
167 const catchLongJsTasks = new PerformanceObserver(list => {
168 for (const task of list.getEntries()) {
Milutin Kristofic94625702022-09-09 10:17:18 +0200169 // We are interested in longtask longer than 400 ms (default is 50 ms)
170 if (task.duration > 400) {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200171 reportingService.reporter(
172 TIMING.TYPE,
173 TIMING.CATEGORY.UI_LATENCY,
174 `Task ${task.name}`,
175 Math.round(task.duration),
176 {},
177 false
178 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100179 }
180 }
181 });
182 catchLongJsTasks.observe({entryTypes: ['longtask']});
183 }
Tao Zhoud59cb8b2019-11-13 20:01:09 +0000184 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100185}
Tao Zhoud59cb8b2019-11-13 20:01:09 +0000186
Chris Poucetc6e880b2021-11-15 19:57:06 +0100187export function initVisibilityReporter(reportingService: ReportingService) {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100188 document.addEventListener('visibilitychange', () => {
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200189 reportingService.onVisibilityChange();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100190 });
Ben Rohlfs8cabee12023-04-27 13:32:59 +0200191 window.addEventListener('blur', () => {
192 reportingService.onFocusChange();
193 });
194 window.addEventListener('focus', () => {
195 reportingService.onFocusChange();
196 });
Milutin Kristoficda88b332020-03-24 10:19:12 +0100197}
Milutin Kristofic74de4842020-01-29 14:09:01 +0100198
Ben Rohlfsf4313302023-02-23 11:57:05 +0100199export function initClickReporter(reportingService: ReportingService) {
200 document.addEventListener('click', (e: MouseEvent) => {
201 const anchorEl = e
202 .composedPath()
203 .find(el => isElementTarget(el) && el.tagName.toUpperCase() === 'A') as
204 | HTMLAnchorElement
205 | undefined;
206 if (!anchorEl) return;
207 reportingService.reportInteraction(Interaction.LINK_CLICK, {
208 path: getEventPath(e),
209 link: anchorEl.href,
210 text: anchorEl.innerText,
211 });
212 });
213}
214
Ben Rohlfs991c03b2023-04-25 11:20:38 +0200215/**
216 * Reports generic user interaction every x seconds to detect, if the user is
217 * present and is using the application somehow. If you just look at
218 * `document.visibilityState`, then the user may have left the browser open
219 * without locking the screen. So it helps to know whether some interaction is
220 * actually happening.
221 */
222export class InteractionReporter implements Finalizable {
223 /** Accumulates event names until the next round of interaction reporting. */
224 private interactionEvents = new Set<string>();
225
226 /** Allows clearing the interval timer. Mostly useful for tests. */
227 private intervalId?: number;
228
229 constructor(
230 private readonly reportingService: ReportingService,
231 private readonly reportingIntervalMs = 10 * 1000
232 ) {
233 const events = ['mousemove', 'scroll', 'wheel', 'keydown', 'pointerdown'];
234 for (const eventName of events) {
235 document.addEventListener(eventName, () =>
236 this.interactionEvents.add(eventName)
237 );
238 }
239
240 this.intervalId = window.setInterval(
241 () => this.report(),
242 this.reportingIntervalMs
243 );
244 }
245
246 finalize() {
247 window.clearInterval(this.intervalId);
248 }
249
250 private report() {
251 const active = this.interactionEvents.size > 0;
252 if (active) {
253 this.reportingService.reportInteraction(Interaction.USER_ACTIVE, {
254 events: [...this.interactionEvents],
255 });
256 } else if (document.visibilityState === 'visible') {
257 this.reportingService.reportInteraction(Interaction.USER_PASSIVE, {});
258 }
259 this.interactionEvents.clear();
260 }
261}
262
263let interactionReporter: InteractionReporter;
264
265export function initInteractionReporter(reportingService: ReportingService) {
266 if (!interactionReporter) {
267 interactionReporter = new InteractionReporter(reportingService);
268 }
269}
270
Milutin Kristoficaad3ce42022-06-01 20:21:24 +0200271export function initWebVitals(reportingService: ReportingService) {
272 function reportWebVitalMetric(name: Timing, metric: Metric) {
Milutin Kristofice19583e2023-02-07 15:13:21 +0100273 let score = metric.value;
274 // CLS good score is 0.1 and poor score is 0.25. Logging system
275 // prefers integers, so we multiple by 100;
276 if (name === Timing.CLS) {
277 score *= 100;
278 }
Milutin Kristoficaad3ce42022-06-01 20:21:24 +0200279 reportingService.reporter(
280 TIMING.TYPE,
281 TIMING.CATEGORY.UI_LATENCY,
282 name,
Milutin Kristofice19583e2023-02-07 15:13:21 +0100283 score,
284 {
285 navigationType: metric.navigationType,
286 rating: metric.rating,
287 entries: metric.entries,
288 }
Milutin Kristoficaad3ce42022-06-01 20:21:24 +0200289 );
290 }
291
Milutin Kristoficd9b92d82023-02-02 21:20:47 +0100292 onCLS(metric => reportWebVitalMetric(Timing.CLS, metric));
Milutin Kristoficd9b92d82023-02-02 21:20:47 +0100293 onLCP(metric => reportWebVitalMetric(Timing.LCP, metric));
294 onINP(metric => reportWebVitalMetric(Timing.INP, metric));
Milutin Kristoficaad3ce42022-06-01 20:21:24 +0200295}
296
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200297// Calculates the time of Gerrit being in a background tab. When Gerrit reports
298// a pageLoad metric it’s attached to its details for latency analysis.
299// It resets on locationChange.
300class HiddenDurationTimer {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200301 public accHiddenDurationMs = 0;
302
303 public lastVisibleTimestampMs: number | null = null;
304
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200305 constructor() {
306 this.reset();
307 }
308
309 reset() {
310 this.accHiddenDurationMs = 0;
311 this.lastVisibleTimestampMs = 0;
312 }
313
314 onVisibilityChange() {
315 if (document.visibilityState === 'hidden') {
316 this.lastVisibleTimestampMs = now();
317 } else if (document.visibilityState === 'visible') {
318 if (this.lastVisibleTimestampMs !== null) {
319 this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
320 // Set to null for guarding against two 'visible' events in a row.
321 this.lastVisibleTimestampMs = null;
322 }
323 }
324 }
325
326 get hiddenDurationMs() {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200327 if (
328 document.visibilityState === 'hidden' &&
329 this.lastVisibleTimestampMs !== null
330 ) {
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200331 return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
332 }
333 return this.accHiddenDurationMs;
334 }
335}
336
337export function now() {
338 return Math.round(window.performance.now());
339}
340
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200341type PeformanceTimingEventName = keyof Omit<PerformanceTiming, 'toJSON'>;
342
343interface EventInfo {
344 type: string;
345 category: string;
346 name: string;
347 value?: EventValue;
348 eventStart: number;
349 eventDetails?: string;
350 repoName?: string;
Milutin Kristofic3a844522021-01-21 10:01:55 +0100351 changeId?: string;
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200352 inBackgroundTab?: boolean;
353 enabledExperiments?: string;
354}
355
356interface PageLoadDetails {
357 rpcList: SlowRpcCall[];
358 hiddenDurationMs: number;
359 screenSize?: {width: number; height: number};
360 viewport?: {width: number; height: number};
361 usedJSHeapSizeMb?: number;
Ben Rohlfs5b5475e2024-07-02 18:32:31 +0200362 parallelRequestsEnabled?: boolean;
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200363}
364
365interface SlowRpcCall {
366 anonymizedUrl: string;
367 elapsed: number;
368}
369
370type PendingReportInfo = [EventInfo, boolean | undefined];
371
Chris Poucet55cbccb2021-11-16 03:17:06 +0100372export class GrReporting implements ReportingService, Finalizable {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200373 private readonly _flagsService: FlagsService;
374
375 private readonly _baselines = STARTUP_TIMERS;
376
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100377 private reportRepoName: string | undefined;
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200378
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100379 private reportChangeId: NumericChangeId | undefined;
Milutin Kristofic3a844522021-01-21 10:01:55 +0100380
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100381 private pending: PendingReportInfo[] = [];
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200382
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100383 private slowRpcList: SlowRpcCall[] = [];
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200384
Ben Rohlfsbad48d42020-12-17 15:03:33 +0100385 /**
Ben Rohlfsbd4c91b2021-12-22 11:37:41 +0100386 * Keeps track of which ids were already reported for events that should only
387 * be reported once per session.
Ben Rohlfsbad48d42020-12-17 15:03:33 +0100388 */
Ben Rohlfsbd4c91b2021-12-22 11:37:41 +0100389 private reportedIds = new Set<string>();
Ben Rohlfsbad48d42020-12-17 15:03:33 +0100390
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200391 public readonly hiddenDurationTimer = new HiddenDurationTimer();
392
393 constructor(flagsService: FlagsService) {
Milutin Kristoficda88b332020-03-24 10:19:12 +0100394 this._flagsService = flagsService;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100395 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100396
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200397 private get performanceTiming() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100398 return window.performance.timing;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100399 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100400
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200401 private get slowRpcSnapshot() {
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100402 return (this.slowRpcList || []).slice();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100403 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100404
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200405 private _arePluginsLoaded() {
406 return (
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100407 this._baselines && !hasOwnProperty(this._baselines, Timing.PLUGINS_LOADED)
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200408 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100409 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100410
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200411 private _isMetricsPluginLoaded() {
412 return (
413 this._arePluginsLoaded() ||
414 (this._baselines &&
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100415 !hasOwnProperty(this._baselines, Timing.METRICS_PLUGIN_LOADED))
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200416 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100417 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100418
Chris Poucet55cbccb2021-11-16 03:17:06 +0100419 finalize() {}
420
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100421 /**
422 * Reporter reports events. Events will be queued if metrics plugin is not
423 * yet installed.
424 *
Dmitrii Filippov486c4882020-08-06 18:34:08 +0200425 * @param noLog If true, the event will not be logged to the JS console.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100426 */
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200427 reporter(
428 type: string,
429 category: string,
430 eventName: string,
431 eventValue?: EventValue,
432 eventDetails?: EventDetails,
Dmitrii Filippov486c4882020-08-06 18:34:08 +0200433 noLog?: boolean
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200434 ) {
435 const eventInfo = this._createEventInfo(
436 type,
437 category,
438 eventName,
439 eventValue,
440 eventDetails
441 );
Milutin Kristofic0fe96792020-03-26 13:17:35 +0100442 if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
Dhruv Srivastavab68c85f2020-11-30 19:04:34 +0100443 console.error(
444 (typeof eventValue === 'object' && eventValue.error) || eventName
445 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100446 }
447
448 // We report events immediately when metrics plugin is loaded
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100449 if (this._isMetricsPluginLoaded() && !this.pending.length) {
Dmitrii Filippov486c4882020-08-06 18:34:08 +0200450 this._reportEvent(eventInfo, noLog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100451 } else {
452 // We cache until metrics plugin is loaded
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100453 this.pending.push([eventInfo, noLog]);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100454 if (this._isMetricsPluginLoaded()) {
Dhruv Srivastavab3ed8742023-03-24 14:42:06 +0100455 this.pending.forEach(([eventInfo, noLog]) => {
456 this._reportEvent(eventInfo, noLog);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100457 });
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100458 this.pending = [];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100459 }
460 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100461 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100462
Dhruv Srivastavab3ed8742023-03-24 14:42:06 +0100463 private _reportEvent(eventInfo: EventInfo, noLog?: boolean) {
Ben Rohlfs53998552022-03-30 17:47:28 +0200464 const {type, value, name, eventDetails} = eventInfo;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100465 document.dispatchEvent(new CustomEvent(type, {detail: eventInfo}));
Dhruv Srivastavab3ed8742023-03-24 14:42:06 +0100466 if (noLog) {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200467 return;
468 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100469 if (type !== ERROR.TYPE) {
470 if (value !== undefined) {
Dhruv Srivastava805f49102022-07-19 13:17:20 +0200471 console.debug(
472 `Reporting(${new Date().toISOString()}): ${name}: ${value}`
473 );
Ben Rohlfs53998552022-03-30 17:47:28 +0200474 } else if (eventDetails !== undefined) {
Dhruv Srivastava805f49102022-07-19 13:17:20 +0200475 console.debug(
476 `Reporting(${new Date().toISOString()}): ${name}: ${eventDetails}`
477 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100478 } else {
Dhruv Srivastava805f49102022-07-19 13:17:20 +0200479 console.debug(`Reporting(${new Date().toISOString()}): ${name}`);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100480 }
481 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100482 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100483
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200484 private _createEventInfo(
485 type: string,
486 category: string,
487 name: string,
488 value?: EventValue,
489 eventDetails?: EventDetails
490 ): EventInfo {
491 const eventInfo: EventInfo = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100492 type,
493 category,
494 name,
495 value,
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200496 eventStart: now(),
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100497 };
498
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200499 if (
500 typeof eventDetails === 'object' &&
501 Object.entries(eventDetails).length !== 0
502 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100503 eventInfo.eventDetails = JSON.stringify(eventDetails);
504 }
Tao Zhoucef79172020-04-08 16:05:31 +0200505
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100506 if (this.reportRepoName) {
507 eventInfo.repoName = this.reportRepoName;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100508 }
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100509 if (this.reportChangeId) {
510 eventInfo.changeId = `${this.reportChangeId}`;
Milutin Kristofic3a844522021-01-21 10:01:55 +0100511 }
Tao Zhoucef79172020-04-08 16:05:31 +0200512
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100513 const isInBackgroundTab = document.visibilityState === 'hidden';
514 if (isInBackgroundTab !== undefined) {
515 eventInfo.inBackgroundTab = isInBackgroundTab;
516 }
517
Milutin Kristoficb131af92021-07-13 15:42:34 +0200518 if (
519 name === Timing.APP_STARTED &&
520 this._flagsService.enabledExperiments.length
521 ) {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200522 eventInfo.enabledExperiments = JSON.stringify(
523 this._flagsService.enabledExperiments
524 );
Tao Zhoucef79172020-04-08 16:05:31 +0200525 }
526
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100527 return eventInfo;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100528 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100529
530 /**
531 * User-perceived app start time, should be reported when the app is ready.
532 */
533 appStarted() {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100534 this.timeEnd(Timing.APP_STARTED);
Milutin Kristofic091ab762020-03-19 16:37:48 +0100535 this._reportNavResTimes();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100536 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100537
Ben Rohlfs8cabee12023-04-27 13:32:59 +0200538 onFocusChange() {
539 this.reporter(
540 LIFECYCLE.TYPE,
541 LIFECYCLE.CATEGORY.VISIBILITY,
542 LifeCycle.FOCUS,
543 undefined,
544 {
545 isVisible: document.visibilityState === 'visible',
546 hasFocus: document.hasFocus(),
547 },
548 false
549 );
550 }
551
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200552 onVisibilityChange() {
553 this.hiddenDurationTimer.onVisibilityChange();
Milutin Kristoficaaef49a2021-03-10 13:30:19 +0100554 let eventName;
555 if (document.visibilityState === 'hidden') {
556 eventName = LifeCycle.VISIBILILITY_HIDDEN;
557 } else if (document.visibilityState === 'visible') {
558 eventName = LifeCycle.VISIBILILITY_VISIBLE;
559 }
560 if (eventName)
561 this.reporter(
562 LIFECYCLE.TYPE,
563 LIFECYCLE.CATEGORY.VISIBILITY,
564 eventName,
565 undefined,
566 {
567 hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
Ben Rohlfs8cabee12023-04-27 13:32:59 +0200568 isVisible: document.visibilityState === 'visible',
569 hasFocus: document.hasFocus(),
Milutin Kristoficaaef49a2021-03-10 13:30:19 +0100570 },
571 false
572 );
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200573 }
574
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100575 /**
Milutin Kristofic091ab762020-03-19 16:37:48 +0100576 * Browser's navigation and resource timings
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100577 */
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200578 private _reportNavResTimes() {
Milutin Kristofic091ab762020-03-19 16:37:48 +0100579 const perfEvents = Object.keys(this.performanceTiming.toJSON());
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200580 perfEvents.forEach(eventName =>
581 this._reportPerformanceTiming(eventName as PeformanceTimingEventName)
Milutin Kristofic091ab762020-03-19 16:37:48 +0100582 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100583 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100584
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200585 private _reportPerformanceTiming(
586 eventName: PeformanceTimingEventName,
587 eventDetails?: EventDetails
588 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100589 const eventTiming = this.performanceTiming[eventName];
590 if (eventTiming > 0) {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200591 const elapsedTime = eventTiming - this.performanceTiming.navigationStart;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100592 // NavResTime - Navigation and resource timings.
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200593 this.reporter(
594 TIMING.TYPE,
595 TIMING.CATEGORY.UI_LATENCY,
596 `NavResTime - ${eventName}`,
597 elapsedTime,
598 eventDetails,
599 true
600 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100601 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100602 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100603
604 beforeLocationChanged() {
605 for (const prop of Object.keys(this._baselines)) {
Ben Rohlfs055d4a42023-05-03 13:55:08 +0200606 if (LOCATION_CHANGE_OK_TIMERS.includes(prop)) continue;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100607 delete this._baselines[prop];
608 }
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100609 this.time(Timing.CHANGE_DISPLAYED);
610 this.time(Timing.CHANGE_LOAD_FULL);
611 this.time(Timing.DASHBOARD_DISPLAYED);
612 this.time(Timing.DIFF_VIEW_CONTENT_DISPLAYED);
613 this.time(Timing.DIFF_VIEW_DISPLAYED);
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100614 this.time(Timing.FILE_LIST_DISPLAYED);
Ben Rohlfs17f3575d2024-02-26 11:27:00 +0100615
616 this.setRepoName(undefined);
617 this.setChangeId(undefined);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100618 // reset slow rpc list since here start page loads which report these rpcs
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100619 this.slowRpcList = [];
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200620 this.hiddenDurationTimer.reset();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100621 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100622
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200623 locationChanged(page: string) {
624 this.reporter(
625 NAVIGATION.TYPE,
626 NAVIGATION.CATEGORY.LOCATION_CHANGED,
627 NAVIGATION.EVENT.PAGE,
628 page
629 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100630 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100631
632 dashboardDisplayed() {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100633 if (hasOwnProperty(this._baselines, Timing.STARTUP_DASHBOARD_DISPLAYED)) {
634 this.timeEnd(Timing.STARTUP_DASHBOARD_DISPLAYED, this._pageLoadDetails());
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100635 } else {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100636 this.timeEnd(Timing.DASHBOARD_DISPLAYED, this._pageLoadDetails());
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100637 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100638 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100639
Milutin Kristofic933e6bd2021-06-07 20:19:45 +0200640 changeDisplayed(eventDetails?: EventDetails) {
641 eventDetails = {...eventDetails, ...this._pageLoadDetails()};
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100642 if (hasOwnProperty(this._baselines, Timing.STARTUP_CHANGE_DISPLAYED)) {
Milutin Kristofic933e6bd2021-06-07 20:19:45 +0200643 this.timeEnd(Timing.STARTUP_CHANGE_DISPLAYED, eventDetails);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100644 } else {
Milutin Kristofic933e6bd2021-06-07 20:19:45 +0200645 this.timeEnd(Timing.CHANGE_DISPLAYED, eventDetails);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100646 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100647 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100648
649 changeFullyLoaded() {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100650 if (hasOwnProperty(this._baselines, Timing.STARTUP_CHANGE_LOAD_FULL)) {
651 this.timeEnd(Timing.STARTUP_CHANGE_LOAD_FULL);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100652 } else {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100653 this.timeEnd(Timing.CHANGE_LOAD_FULL);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100654 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100655 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100656
657 diffViewDisplayed() {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100658 if (hasOwnProperty(this._baselines, Timing.STARTUP_DIFF_VIEW_DISPLAYED)) {
659 this.timeEnd(Timing.STARTUP_DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100660 } else {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100661 this.timeEnd(Timing.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100662 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100663 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100664
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100665 diffViewContentDisplayed() {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200666 if (
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100667 hasOwnProperty(
668 this._baselines,
669 Timing.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED
670 )
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200671 ) {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100672 this.timeEnd(Timing.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100673 } else {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100674 this.timeEnd(Timing.DIFF_VIEW_CONTENT_DISPLAYED);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100675 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100676 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100677
678 fileListDisplayed() {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100679 if (hasOwnProperty(this._baselines, Timing.STARTUP_FILE_LIST_DISPLAYED)) {
680 this.timeEnd(Timing.STARTUP_FILE_LIST_DISPLAYED);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100681 } else {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100682 this.timeEnd(Timing.FILE_LIST_DISPLAYED);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100683 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100684 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100685
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200686 private _pageLoadDetails(): PageLoadDetails {
687 const details: PageLoadDetails = {
Milutin Kristofica3580122020-04-14 14:31:48 +0200688 rpcList: this.slowRpcSnapshot,
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200689 hiddenDurationMs: this.hiddenDurationTimer.accHiddenDurationMs,
Ben Rohlfs5b5475e2024-07-02 18:32:31 +0200690 parallelRequestsEnabled: this._flagsService.isEnabled(
691 KnownExperimentId.PARALLEL_DASHBOARD_REQUESTS
692 ),
Milutin Kristofica3580122020-04-14 14:31:48 +0200693 };
694
695 if (window.screen) {
696 details.screenSize = {
697 width: window.screen.width,
698 height: window.screen.height,
699 };
700 }
701
Tao Zhou34b0f122020-08-31 13:44:50 +0200702 if (document?.documentElement) {
Milutin Kristofica3580122020-04-14 14:31:48 +0200703 details.viewport = {
704 width: document.documentElement.clientWidth,
705 height: document.documentElement.clientHeight,
706 };
707 }
708
Tao Zhou34b0f122020-08-31 13:44:50 +0200709 if (window.performance?.memory) {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200710 const toMb = (bytes: number) =>
711 Math.round((bytes / (1024 * 1024)) * 100) / 100;
712 details.usedJSHeapSizeMb = toMb(window.performance.memory.usedJSHeapSize);
Milutin Kristofica3580122020-04-14 14:31:48 +0200713 }
714
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200715 details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
Milutin Kristofica3580122020-04-14 14:31:48 +0200716 return details;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100717 }
Milutin Kristofica3580122020-04-14 14:31:48 +0200718
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200719 reportExtension(name: string) {
Milutin Kristoficaaef49a2021-03-10 13:30:19 +0100720 this.reporter(
721 LIFECYCLE.TYPE,
722 LIFECYCLE.CATEGORY.EXTENSION_DETECTED,
723 LifeCycle.EXTENSION_DETECTED,
724 undefined,
725 {name}
726 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100727 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100728
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200729 pluginLoaded(name: string) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100730 if (name.startsWith('metrics-')) {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100731 this.timeEnd(Timing.METRICS_PLUGIN_LOADED);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100732 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100733 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100734
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200735 pluginsLoaded(pluginsList?: string[]) {
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100736 this.timeEnd(Timing.PLUGINS_LOADED);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100737 this.reporter(
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200738 LIFECYCLE.TYPE,
739 LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
Milutin Kristoficaaef49a2021-03-10 13:30:19 +0100740 LifeCycle.PLUGINS_INSTALLED,
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200741 undefined,
742 {pluginsList: pluginsList || []},
Ben Rohlfs53998552022-03-30 17:47:28 +0200743 false
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200744 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100745 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100746
Ben Rohlfs89418582021-09-27 12:15:57 +0200747 pluginsFailed(pluginsList?: string[]) {
748 if (!pluginsList || pluginsList.length === 0) return;
749 this.reporter(
750 LIFECYCLE.TYPE,
751 LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
752 LifeCycle.PLUGINS_FAILED,
753 undefined,
754 {pluginsList: pluginsList || []},
Ben Rohlfs53998552022-03-30 17:47:28 +0200755 false
Ben Rohlfs89418582021-09-27 12:15:57 +0200756 );
757 }
758
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100759 /**
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100760 * Reset named Timing.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100761 */
Milutin Kristofic48a0f432021-03-16 22:53:08 +0100762 time(name: Timing) {
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200763 this._baselines[name] = now();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100764 window.performance.mark(`${name}-start`);
Milutin Kristofic6e6f42e2024-04-09 19:29:05 +0200765 // When time(Timing.DASHBOARD_DISPLAYED) is called gr-dashboard-view
766 // we need to clean-up slowRpcList, otherwise it can accumulate to big size
767 if (name === Timing.DASHBOARD_DISPLAYED) {
768 this.slowRpcList = [];
769 this.hiddenDurationTimer.reset();
770 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100771 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100772
773 /**
774 * Finish named timer and report it to server.
775 */
Ben Rohlfsdd88cd92024-05-08 13:29:39 +0200776 timeEnd(name: Timing, eventDetails?: EventDetails): number {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200777 if (!hasOwnProperty(this._baselines, name)) {
Ben Rohlfsdd88cd92024-05-08 13:29:39 +0200778 return 0;
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200779 }
Ben Rohlfsdd88cd92024-05-08 13:29:39 +0200780 const begin = this._baselines[name];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100781 delete this._baselines[name];
Ben Rohlfsdd88cd92024-05-08 13:29:39 +0200782 const end = now();
783 const elapsed = end - begin;
784 this._reportTiming(name, elapsed, eventDetails);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100785
786 // Finalize the interval. Either from a registered start mark or
787 // the navigation start time (if baseTime is 0).
Ben Rohlfsdd88cd92024-05-08 13:29:39 +0200788 if (begin !== 0) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100789 window.performance.measure(name, `${name}-start`);
790 } else {
David Ostrovskyf91f9662021-02-22 19:34:37 +0100791 // Microsoft Edge does not handle the 2nd param correctly
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100792 // (if undefined).
793 window.performance.measure(name);
794 }
Ben Rohlfsdd88cd92024-05-08 13:29:39 +0200795 return elapsed;
Milutin Kristoficda88b332020-03-24 10:19:12 +0100796 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100797
798 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100799 * Send a timing report with an arbitrary time value.
800 *
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200801 * @param name Timing name.
802 * @param time The time to report as an integer of milliseconds.
803 * @param eventDetails non sensitive details
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100804 */
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200805 private _reportTiming(
806 name: string,
807 time: number,
808 eventDetails?: EventDetails
809 ) {
810 this.reporter(
811 TIMING.TYPE,
812 TIMING.CATEGORY.UI_LATENCY,
813 name,
814 time,
815 eventDetails
816 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100817 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100818
819 /**
David Ostrovskyf91f9662021-02-22 19:34:37 +0100820 * Get a timer object to for reporting a user timing. The start time will be
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100821 * the time that the object has been created, and the end time will be the
822 * time that the "end" method is called on the object.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100823 */
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200824 getTimer(name: string): Timer {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100825 let called = false;
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200826 let start: number;
827 let max: number | null = null;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100828
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200829 const timer: Timer = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100830 // Clear the timer and reset the start time.
831 reset: () => {
832 called = false;
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200833 start = now();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100834 return timer;
Viktar Doniche8680ea2016-10-05 11:58:58 -0700835 },
Wyatt Allen18f03542018-06-19 17:03:43 -0700836
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100837 // Stop the timer and report the intervening time.
Ben Rohlfs23152eb2021-04-30 09:01:30 +0200838 end: (eventDetails?: EventDetails) => {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100839 if (called) {
840 throw new Error(`Timer for "${name}" already ended.`);
841 }
842 called = true;
Milutin Kristofic0b221a62020-04-30 19:57:10 +0200843 const time = now() - start;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100844
845 // If a maximum is specified and the time exceeds it, do not report.
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200846 if (max && time > max) {
847 return timer;
848 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100849
Ben Rohlfs23152eb2021-04-30 09:01:30 +0200850 this._reportTiming(name, time, eventDetails);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100851 return timer;
Wyatt Allen18f03542018-06-19 17:03:43 -0700852 },
Viktar Donich78b7cd22016-08-18 16:45:05 -0700853
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100854 // Set a maximum reportable time. If a maximum is set and the timer is
855 // ended after the specified amount of time, the value is not reported.
856 withMaximum(maximum) {
857 max = maximum;
858 return timer;
859 },
860 };
Viktar Donich78b7cd22016-08-18 16:45:05 -0700861
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100862 // The timer is initialized to its creation time.
863 return timer.reset();
Milutin Kristoficda88b332020-03-24 10:19:12 +0100864 }
Milutin Kristofic623ecd12020-02-15 21:46:58 +0100865
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100866 /**
867 * Log timing information for an RPC.
868 *
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200869 * @param anonymizedUrl The URL of the RPC with tokens obfuscated.
870 * @param elapsed The time elapsed of the RPC.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100871 */
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200872 reportRpcTiming(anonymizedUrl: string, elapsed: number) {
873 this.reporter(
874 TIMING.TYPE,
875 TIMING.CATEGORY.RPC,
876 'RPC-' + anonymizedUrl,
877 elapsed,
878 {},
879 true
880 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100881 if (elapsed >= SLOW_RPC_THRESHOLD) {
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100882 this.slowRpcList.push({anonymizedUrl, elapsed});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100883 }
Milutin Kristoficda88b332020-03-24 10:19:12 +0100884 }
Viktar Donich78b7cd22016-08-18 16:45:05 -0700885
Milutin Kristoficaaef49a2021-03-10 13:30:19 +0100886 reportLifeCycle(eventName: LifeCycle, details: EventDetails) {
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200887 this.reporter(
888 LIFECYCLE.TYPE,
889 LIFECYCLE.CATEGORY.DEFAULT,
890 eventName,
891 undefined,
892 details,
Ben Rohlfs53998552022-03-30 17:47:28 +0200893 false
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200894 );
Milutin Kristofic0fe96792020-03-26 13:17:35 +0100895 }
896
Milutin Kristofice1a562f2021-06-01 19:51:11 +0200897 reportPluginLifeCycleLog(eventName: string, details: EventDetails) {
898 this.reporter(
899 PLUGIN.TYPE,
900 PLUGIN.CATEGORY.LIFECYCLE,
901 eventName,
902 undefined,
903 details,
Ben Rohlfs53998552022-03-30 17:47:28 +0200904 false
Milutin Kristofice1a562f2021-06-01 19:51:11 +0200905 );
906 }
907
908 reportPluginInteractionLog(eventName: string, details: EventDetails) {
909 this.reporter(
910 PLUGIN.TYPE,
911 PLUGIN.CATEGORY.INTERACTION,
912 eventName,
913 undefined,
914 details,
915 true
916 );
917 }
918
Ben Rohlfsbd4c91b2021-12-22 11:37:41 +0100919 /**
920 * Returns true when the event was deduped and thus should not be reported.
921 */
922 _dedup(
923 eventName: string | Interaction,
924 details: EventDetails,
925 deduping?: Deduping
926 ): boolean {
927 if (!deduping) return false;
928 let id = '';
929 switch (deduping) {
930 case Deduping.DETAILS_ONCE_PER_CHANGE:
931 id = `${eventName}-${this.reportChangeId}-${JSON.stringify(details)}`;
932 break;
933 case Deduping.DETAILS_ONCE_PER_SESSION:
934 id = `${eventName}-${JSON.stringify(details)}`;
935 break;
936 case Deduping.EVENT_ONCE_PER_CHANGE:
937 id = `${eventName}-${this.reportChangeId}`;
938 break;
939 case Deduping.EVENT_ONCE_PER_SESSION:
940 id = `${eventName}`;
941 break;
942 default:
943 throw new Error(`Invalid 'deduping' option '${deduping}'.`);
944 }
945 if (this.reportedIds.has(id)) return true;
946 this.reportedIds.add(id);
947 return false;
948 }
949
950 reportInteraction(
951 eventName: string | Interaction,
952 details: EventDetails,
953 options?: ReportingOptions
954 ) {
955 if (this._dedup(eventName, details, options?.deduping)) return;
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200956 this.reporter(
957 INTERACTION.TYPE,
958 INTERACTION.CATEGORY.DEFAULT,
959 eventName,
960 undefined,
961 details,
Ben Rohlfs53998552022-03-30 17:47:28 +0200962 false
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +0200963 );
Milutin Kristoficda88b332020-03-24 10:19:12 +0100964 }
Viktar Donich82ad0ec2018-05-17 14:33:22 -0700965
Milutin Kristofic2fddf922021-03-11 10:35:19 +0100966 reportExecution(name: Execution, details?: EventDetails) {
Ben Rohlfsbd4c91b2021-12-22 11:37:41 +0100967 if (this._dedup(name, details, Deduping.DETAILS_ONCE_PER_SESSION)) return;
Ben Rohlfsbad48d42020-12-17 15:03:33 +0100968 this.reporter(
969 LIFECYCLE.TYPE,
970 LIFECYCLE.CATEGORY.EXECUTION,
Milutin Kristofic2fddf922021-03-11 10:35:19 +0100971 name,
Ben Rohlfsbad48d42020-12-17 15:03:33 +0100972 undefined,
973 details,
Ben Rohlfs94f34a12021-02-26 18:37:32 +0100974 true // skip console log
Ben Rohlfsbad48d42020-12-17 15:03:33 +0100975 );
976 }
977
Ben Rohlfs629d89d2021-03-13 22:04:19 +0100978 trackApi(
979 pluginApi: Pick<PluginApi, 'getPluginName'>,
980 object: string,
981 method: string
982 ) {
Ben Rohlfs13edc7c2021-03-08 08:50:07 +0100983 const plugin = pluginApi?.getPluginName() ?? 'unknown';
Milutin Kristofic2fddf922021-03-11 10:35:19 +0100984 this.reportExecution(Execution.PLUGIN_API, {plugin, object, method});
Ben Rohlfs94f34a12021-02-26 18:37:32 +0100985 }
986
Kamil Musinf0ece022022-10-14 15:22:44 +0200987 error(errorSource: string, error: Error, details?: EventDetails) {
988 const message = `${errorSource}: ${error.message}`;
989 const eventDetails = {
990 errorMessage: message,
991 ...details,
992 stack: error.stack,
993 };
Milutin Kristofic380b9f62020-12-03 09:24:17 +0100994
995 this.reporter(
996 ERROR.TYPE,
997 ERROR.CATEGORY.EXCEPTION,
Kamil Musinf0ece022022-10-14 15:22:44 +0200998 errorSource,
Milutin Kristofic380b9f62020-12-03 09:24:17 +0100999 {error},
Kamil Musinf0ece022022-10-14 15:22:44 +02001000 eventDetails
Milutin Kristofic380b9f62020-12-03 09:24:17 +01001001 );
1002 }
1003
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +02001004 reportErrorDialog(message: string) {
1005 this.reporter(
1006 ERROR.TYPE,
1007 ERROR.CATEGORY.ERROR_DIALOG,
Kamil Musinf0ece022022-10-14 15:22:44 +02001008 'ErrorDialog',
1009 {error: new Error(message)},
1010 {errorMessage: message}
Dmitrii Filippova5ace2f2020-07-29 09:30:56 +02001011 );
Milutin Kristoficda88b332020-03-24 10:19:12 +01001012 }
Milutin Kristofic68a27a32020-01-27 15:53:17 +01001013
Ben Rohlfs17f3575d2024-02-26 11:27:00 +01001014 setRepoName(repoName?: string) {
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +01001015 this.reportRepoName = repoName;
Milutin Kristoficda88b332020-03-24 10:19:12 +01001016 }
Milutin Kristofic3a844522021-01-21 10:01:55 +01001017
Ben Rohlfs17f3575d2024-02-26 11:27:00 +01001018 setChangeId(changeId?: NumericChangeId) {
1019 const originalChangeId = this.reportChangeId;
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +01001020 this.reportChangeId = changeId;
Ben Rohlfs17f3575d2024-02-26 11:27:00 +01001021
1022 if (!!changeId && changeId !== originalChangeId) {
1023 this.reportInteraction(Interaction.CHANGE_ID_CHANGED, {changeId});
1024 }
Milutin Kristofic3a844522021-01-21 10:01:55 +01001025 }
Milutin Kristoficda88b332020-03-24 10:19:12 +01001026}
Milutin Kristofic68a27a32020-01-27 15:53:17 +01001027
Tao Zhou4cd35cb2020-07-22 11:28:22 +02001028export const DEFAULT_STARTUP_TIMERS = {...STARTUP_TIMERS};