Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/monitoring/client-logger.js b/static_src/monitoring/client-logger.js
new file mode 100644
index 0000000..37959c0
--- /dev/null
+++ b/static_src/monitoring/client-logger.js
@@ -0,0 +1,272 @@
+/* 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 MonorailTSMon from './monorail-ts-mon.js';
+
+/**
+ * ClientLogger is a JavaScript library for tracking events with Google
+ * Analytics and ts_mon.
+ *
+ * @example
+ * // Example usage (tracking time to create a new issue, including time spent
+ * // by the user editing stuff):
+ *
+ *
+ * // t0: on page load for /issues/new:
+ * let l = new Clientlogger('issues');
+ * l.logStart('new-issue', 'user-time');
+ *
+ * // t1: on submit for /issues/new:
+ *
+ * l.logStart('new-issue', 'server-time');
+ *
+ * // t2: on page load for /issues/detail:
+ *
+ * let l = new Clientlogger('issues');
+ *
+ * if (l.started('new-issue') {
+ * l.logEnd('new-issue');
+ * }
+ *
+ * // This would record the following metrics:
+ *
+ * issues.new-issue {
+ * time: t2-t0
+ * }
+ *
+ * issues.new-issue["server-time"] {
+ * time: t2-t1
+ * }
+ *
+ * issues.new-issue["user-time"] {
+ * time: t1-t0
+ * }
+ */
+export default class ClientLogger {
+ /**
+ * @param {string} category Arbitrary string for categorizing metrics in
+ * this client. Used by Google Analytics for event logging.
+ */
+ constructor(category) {
+ this.category = category;
+ this.tsMon = MonorailTSMon.getGlobalClient();
+
+ const categoryKey = `ClientLogger.${category}.started`;
+ const startedEvtsStr = sessionStorage[categoryKey];
+ if (startedEvtsStr) {
+ this.startedEvents = JSON.parse(startedEvtsStr);
+ } else {
+ this.startedEvents = {};
+ }
+ }
+
+ /**
+ * @param {string} eventName Arbitrary string for the name of the event.
+ * ie: "issue-load"
+ * @return {Object} Event object for the string checked.
+ */
+ started(eventName) {
+ return this.startedEvents[eventName];
+ }
+
+ /**
+ * Log events that bookend some activity whose duration we’re interested in.
+ * @param {string} eventName Name of the event to start.
+ * @param {string} eventLabel Arbitrary string label to tie to event.
+ */
+ logStart(eventName, eventLabel) {
+ // Tricky situation: initial new issue POST gets rejected
+ // due to form validation issues. Start a new timer, or keep
+ // the original?
+
+ const startedEvent = this.startedEvents[eventName] || {
+ time: new Date().getTime(),
+ };
+
+ if (eventLabel) {
+ if (!startedEvent.labels) {
+ startedEvent.labels = {};
+ }
+ startedEvent.labels[eventLabel] = new Date().getTime();
+ }
+
+ this.startedEvents[eventName] = startedEvent;
+
+ sessionStorage[`ClientLogger.${this.category}.started`] =
+ JSON.stringify(this.startedEvents);
+
+ logEvent(this.category, `${eventName}-start`, eventLabel);
+ }
+
+ /**
+ * Pause the stopwatch for this event.
+ * @param {string} eventName Name of the event to pause.
+ * @param {string} eventLabel Arbitrary string label tied to the event.
+ */
+ logPause(eventName, eventLabel) {
+ if (!eventLabel) {
+ throw `logPause called for event with no label: ${eventName}`;
+ }
+
+ const startEvent = this.startedEvents[eventName];
+
+ if (!startEvent) {
+ console.warn(`logPause called for event with no logStart: ${eventName}`);
+ return;
+ }
+
+ if (!startEvent.labels[eventLabel]) {
+ console.warn(`logPause called for event label with no logStart: ` +
+ `${eventName}.${eventLabel}`);
+ return;
+ }
+
+ const elapsed = new Date().getTime() - startEvent.labels[eventLabel];
+ if (!startEvent.elapsed) {
+ startEvent.elapsed = {};
+ startEvent.elapsed[eventLabel] = 0;
+ }
+
+ // Save accumulated time.
+ startEvent.elapsed[eventLabel] += elapsed;
+
+ sessionStorage[`ClientLogger.${this.category}.started`] =
+ JSON.stringify(this.startedEvents);
+ }
+
+ /**
+ * Resume the stopwatch for this event.
+ * @param {string} eventName Name of the event to resume.
+ * @param {string} eventLabel Arbitrary string label tied to the event.
+ */
+ logResume(eventName, eventLabel) {
+ if (!eventLabel) {
+ throw `logResume called for event with no label: ${eventName}`;
+ }
+
+ const startEvent = this.startedEvents[eventName];
+
+ if (!startEvent) {
+ console.warn(`logResume called for event with no logStart: ${eventName}`);
+ return;
+ }
+
+ if (!startEvent.hasOwnProperty('elapsed') ||
+ !startEvent.elapsed.hasOwnProperty(eventLabel)) {
+ console.warn(`logResume called for event that was never paused:` +
+ `${eventName}.${eventLabel}`);
+ return;
+ }
+
+ // TODO(jeffcarp): Throw if an event is resumed twice.
+
+ startEvent.labels[eventLabel] = new Date().getTime();
+
+ sessionStorage[`ClientLogger.${this.category}.started`] =
+ JSON.stringify(this.startedEvents);
+ }
+
+ /**
+ * Stop ecording this event.
+ * @param {string} eventName Name of the event to stop recording.
+ * @param {string} eventLabel Arbitrary string label tied to the event.
+ * @param {number=} maxThresholdMs Avoid sending timing data if it took
+ * longer than this threshold.
+ */
+ logEnd(eventName, eventLabel, maxThresholdMs=null) {
+ const startEvent = this.startedEvents[eventName];
+
+ if (!startEvent) {
+ console.warn(`logEnd called for event with no logStart: ${eventName}`);
+ return;
+ }
+
+ // If they've specified a label, report the elapsed since the start
+ // of that label.
+ if (eventLabel) {
+ if (!startEvent.labels.hasOwnProperty(eventLabel)) {
+ console.warn(`logEnd called for event + label with no logStart: ` +
+ `${eventName}/${eventLabel}`);
+ return;
+ }
+
+ this._sendTiming(startEvent, eventName, eventLabel, maxThresholdMs);
+
+ delete startEvent.labels[eventLabel];
+ if (startEvent.hasOwnProperty('elapsed')) {
+ delete startEvent.elapsed[eventLabel];
+ }
+ } else {
+ // If no label is specified, report timing for the whole event.
+ this._sendTiming(startEvent, eventName, null, maxThresholdMs);
+
+ // And also end and report any labels they had running.
+ for (const label in startEvent.labels) {
+ this._sendTiming(startEvent, eventName, label, maxThresholdMs);
+ }
+
+ delete this.startedEvents[eventName];
+ }
+
+ sessionStorage[`ClientLogger.${this.category}.started`] =
+ JSON.stringify(this.startedEvents);
+ logEvent(this.category, `${eventName}-end`, eventLabel);
+ }
+
+ /**
+ * Helper to send data on the event to TSMon.
+ * @param {Object} event Data for the event being sent.
+ * @param {string} eventName Name of the event being sent.
+ * @param {string} recordOnlyThisLabel Label to record.
+ * @param {number=} maxThresholdMs Optional threshold to drop events
+ * if they took too long.
+ * @private
+ */
+ _sendTiming(event, eventName, recordOnlyThisLabel, maxThresholdMs=null) {
+ // Calculate elapsed.
+ let elapsed;
+ if (recordOnlyThisLabel) {
+ elapsed = new Date().getTime() - event.labels[recordOnlyThisLabel];
+ if (event.elapsed && event.elapsed[recordOnlyThisLabel]) {
+ elapsed += event.elapsed[recordOnlyThisLabel];
+ }
+ } else {
+ elapsed = new Date().getTime() - event.time;
+ }
+
+ // Return if elapsed exceeds maxThresholdMs.
+ if (maxThresholdMs !== null && elapsed > maxThresholdMs) {
+ return;
+ }
+
+ const options = {
+ 'timingCategory': this.category,
+ 'timingVar': eventName,
+ 'timingValue': elapsed,
+ };
+ if (recordOnlyThisLabel) {
+ options['timingLabel'] = recordOnlyThisLabel;
+ }
+ ga('send', 'timing', options);
+ this.tsMon.recordUserTiming(
+ this.category, eventName, recordOnlyThisLabel, elapsed);
+ }
+}
+
+/**
+ * Log single usr events with Google Analytics.
+ * @param {string} category Category of the event.
+ * @param {string} eventAction Name of the event.
+ * @param {string=} eventLabel Optional custom string value tied to the event.
+ * @param {number=} eventValue Optional custom number value tied to the event.
+ */
+export function logEvent(category, eventAction, eventLabel, eventValue) {
+ ga('send', 'event', category, eventAction, eventLabel,
+ eventValue);
+}
+
+// Until the rest of the app is in modules, this must be exposed on window.
+window.ClientLogger = ClientLogger;