| // Copyright 2018 The Chromium Authors |
| // 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; |