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;