Bug 1389424 - enable onboarding telemetry via ping-centre;r=emtwo,fischer,liuche draft
authorgasolin <gasolin@gmail.com>
Thu, 31 Aug 2017 10:19:04 +0800
changeset 681999 ae8cb062663c7b573928f444afae9fa06f6f2b89
parent 681956 f78d5947333422ab09ec23e3dab0d48538c9d6ad
child 736284 0f24649ccf6631da33cfbda8772b1f5b4f0748c2
push id84984
push userbmo:gasolin@mozilla.com
push dateWed, 18 Oct 2017 00:57:48 +0000
reviewersemtwo, fischer, liuche
bugs1389424
milestone58.0a1
Bug 1389424 - enable onboarding telemetry via ping-centre;r=emtwo,fischer,liuche MozReview-Commit-ID: KWRCnD4lFh9
browser/extensions/onboarding/OnboardingTelemetry.jsm
browser/extensions/onboarding/README.md
browser/extensions/onboarding/bootstrap.js
browser/extensions/onboarding/content/onboarding.js
browser/extensions/onboarding/data_events.md
browser/extensions/onboarding/install.rdf.in
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/OnboardingTelemetry.jsm
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["OnboardingTelemetry"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PingCentre: "resource:///modules/PingCentre.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
+XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
+  "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+
+/**
+ * We send 2 kinds (firefox-onboarding-event, firefox-onboarding-session) of pings to ping centre
+ * server (they call it `topic`). The `internal` state in `topic` field means this event is used internaly to
+ * track states and will not send out any message.
+ *
+ * To save server space and make query easier, we track session begin and end but only send pings
+ * when session end. Therefore the server will get single "onboarding/overlay/notification-session"
+ * event which includes both `session_begin` and `session_end` timestamp.
+ *
+ * We send `session_begin` and `session_end` timestamps instead of `session_duration` diff because
+ * of analytics engineer's request.
+ */
+const EVENT_WHITELIST = {
+  // track when click the notification close button
+  "notification-close-button-click": {topic: "firefox-onboarding-event", category: "notification-interactions"},
+  // track when click the notification Call-To-Action button
+  "notification-cta-click": {topic: "firefox-onboarding-event", category: "notification-interactions"},
+  // track when notification is shown
+  "notification-session-begin": {topic: "internal"},
+  // track when the notification closed
+  "notification-session-end": {topic: "firefox-onboarding-session", category: "notification-interactions"},
+  // init onboarding session with session_key and page url
+  "onboarding-register-session": {topic: "internal"},
+  // track when the onboarding script inited
+  "onboarding-session-begin": {topic: "internal"},
+  // track when the onboarding script destoryed
+  "onboarding-session-end": {topic: "firefox-onboarding-session", category: "overlay-interactions"},
+  // track when click the overlay Call-To-Action button
+  "overlay-cta-click": {topic: "firefox-onboarding-event", category: "overlay-interactions"},
+  // track when click or auto select the overlay navigate item
+  "overlay-nav-click": {topic: "firefox-onboarding-event", category: "overlay-interactions"},
+  // track when the overlay is shown
+  "overlay-session-begin": {topic: "internal"},
+  // track when the overlay is closed
+  "overlay-session-end": {topic: "firefox-onboarding-session", category: "overlay-interactions"},
+  // track when click the overlay "skip tour" button
+  "overlay-skip-tour": {topic: "firefox-onboarding-event", category: "overlay-interactions"},
+};
+const ONBOARDING_STATE_DEFAULT = "default";
+const ONBOARDING_ID = "onboarding";
+
+let OnboardingTelemetry = {
+  sessionProbe: null,
+  eventProbe: null,
+  state: {
+    sessions: {},
+  },
+
+  init(startupData) {
+    this.sessionProbe = new PingCentre({topic: "firefox-onboarding-session"});
+    this.eventProbe = new PingCentre({topic: "firefox-onboarding-event"});
+    this.state.addon_version = startupData.version;
+  },
+
+  // register per tab session data
+  registerNewTelemetrySession(session_key, page) {
+    if (this.state.sessions[session_key]) {
+      return;
+    }
+    // session_key and page url are must have
+    if (!session_key || !page) {
+      throw new Error("session_key and page url are required for onboarding-register-session");
+    }
+    let session_id = gUUIDGenerator.generateUUID().toString();
+    this.state.sessions[session_key] = {
+      session_id,
+      page,
+    };
+  },
+
+  process(data) {
+    let { event, page, session_key } = data;
+
+    let topic = EVENT_WHITELIST[event] && EVENT_WHITELIST[event].topic;
+    if (!topic) {
+      throw new Error(`ping-centre doesn't know ${event}, only knows ${Object.keys(EVENT_WHITELIST)}`);
+    }
+
+    if (event === "onboarding-register-session") {
+      this.registerNewTelemetrySession(session_key, page);
+    }
+
+    if (!this.state.sessions[session_key]) {
+      throw new Error(`should pass valid session_key`);
+    }
+
+    if (topic === "internal") {
+      switch (event) {
+        case "onboarding-session-begin":
+          this.state.sessions[session_key].onboarding_session_begin = Date.now();
+          break;
+        case "overlay-session-begin":
+          this.state.sessions[session_key].overlay_session_begin = Date.now();
+          break;
+        case "notification-session-begin":
+          this.state.sessions[session_key].notification_session_begin = Date.now();
+          break;
+      }
+    } else {
+      this._send(topic, data);
+    }
+  },
+
+  // send out pings by topic
+  _send(topic, data) {
+    let {
+      addon_version,
+    } = this.state;
+    let {
+      event,
+      tour_id = "",
+      session_key,
+    } = data;
+    let {
+      notification_session_begin,
+      onboarding_session_begin,
+      overlay_session_begin,
+      page,
+      session_id,
+    } = this.state.sessions[session_key];
+    let category = EVENT_WHITELIST[event].category;
+    let session_begin;
+    switch (topic) {
+      case "firefox-onboarding-session":
+        switch (event) {
+          case "onboarding-session-end":
+            if (!onboarding_session_begin) {
+              throw new Error(`should fire onboarding-session-begin event before ${event}`);
+            }
+            event = "onboarding-session";
+            session_begin = onboarding_session_begin;
+            delete this.state.sessions[session_key];
+            break;
+          case "overlay-session-end":
+            if (!overlay_session_begin) {
+              throw new Error(`should fire overlay-session-begin event before ${event}`);
+            }
+            event = "overlay-session";
+            session_begin = overlay_session_begin;
+            break;
+          case "notification-session-end":
+            if (!notification_session_begin) {
+              throw new Error(`should fire notification-session-begin event before ${event}`);
+            }
+            event = "notification-session";
+            session_begin = notification_session_begin;
+            break;
+        }
+
+        // the field is used to identify how user open the overlay (through default logo or watermark),
+        // the number of open from notification can be retrieved via `notification-cta-click` event
+        const tour_source = Services.prefs.getStringPref("browser.onboarding.state",
+          ONBOARDING_STATE_DEFAULT);
+        const session_end = Date.now();
+        this.sessionProbe && this.sessionProbe.sendPing({
+          addon_version,
+          category,
+          event,
+          page,
+          session_begin,
+          session_end,
+          session_id,
+          tour_id,
+          tour_source,
+        }, {filter: ONBOARDING_ID});
+        break;
+      case "firefox-onboarding-event":
+        let impression = (event === "notification-close-button-click" ||
+          event === "notification-cta-click") ?
+          Services.prefs.getIntPref("browser.onboarding.notification.prompt-count", 0) : -1;
+        this.eventProbe && this.eventProbe.sendPing({
+          addon_version,
+          category,
+          event,
+          impression,
+          page,
+          session_id,
+          tour_id,
+        }, {filter: ONBOARDING_ID});
+        break;
+    }
+  }
+};
--- a/browser/extensions/onboarding/README.md
+++ b/browser/extensions/onboarding/README.md
@@ -19,19 +19,23 @@ Besides above settings, notification wil
 To manually remove the mute duration, set pref `browser.onboarding.notification.mute-duration-on-first-session-ms` to `0` and notification will be shown at the next time you open `about:home` or `about:newtab`.
 
 ## How to show the snippets
 
 Snippets (the remote notification that handled by activity stream) will only be shown after onboarding notifications are all done. You can set preference `browser.onboarding.notification.finished` to `true` to disable onboarding notification and accept snippets right away.
 
 ## Architecture
 
-Everytime `about:home` or `about:newtab` page is opened, onboarding overlay is injected into that page.
+![](https://i.imgur.com/7RK89Zw.png)
+
+During booting from `bootstrap.js`, `OnboardingTourType.jsm` will check the onboarding tour type (`new` and `update` are supported types) and set required initial states into preferences.
 
-`OnboardingTourType.jsm` will check the onboarding tour type (currently support `new` and `update`). Then in `onboarding.js`, All tours are defined inside of `onboardingTourset` dictionary. `getTourIDList` function will load tours from proper preferences. (Check `How to change the order of tours` section for more detail).
+Everytime `about:home` or `about:newtab` page is opened, `onboarding.js` is injected into that page via [frame scripts](https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Message_Manager/Communicating_with_frame_scripts).
+
+Then in `onboarding.js`, all tours are defined inside of `onboardingTourset` dictionary. `getTourIDList` function will load tours from proper preferences. (Check `How to change the order of tours` section for more detail).
 
 When user clicks the action button in each tour, We use [UITour](http://bedrock.readthedocs.io/en/latest/uitour.html) to highlight the correspondent browser UI element. The UITour client is bundled in onboarding addon via `jar.mn`.
 
 ## Landing rules
 
 We would apply some rules:
 
 * To avoid conflict with the origin page, all styles and ids should be formatted as `onboarding-*`.
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -2,22 +2,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 /* globals  APP_STARTUP, ADDON_INSTALL */
 "use strict";
 
 const {utils: Cu, interfaces: Ci} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OnboardingTourType",
-  "resource://onboarding/modules/OnboardingTourType.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
-  "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  OnboardingTourType: "resource://onboarding/modules/OnboardingTourType.jsm",
+  OnboardingTelemetry: "resource://onboarding/modules/OnboardingTelemetry.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+  fxAccounts: "resource://gre/modules/FxAccounts.jsm",
+});
 
 const {PREF_STRING, PREF_BOOL, PREF_INT} = Ci.nsIPrefBranch;
 
 const BROWSER_READY_NOTIFICATION = "browser-delayed-startup-finished";
 const BROWSER_SESSION_STORE_NOTIFICATION = "sessionstore-windows-restored";
 const PREF_WHITELIST = [
   ["browser.onboarding.enabled", PREF_BOOL],
   ["browser.onboarding.state", PREF_STRING],
@@ -35,16 +35,17 @@ const PREF_WHITELIST = [
   "onboarding-tour-performance",
   "onboarding-tour-private-browsing",
   "onboarding-tour-screenshots",
   "onboarding-tour-singlesearch",
   "onboarding-tour-sync",
 ].forEach(tourId => PREF_WHITELIST.push([`browser.onboarding.tour.${tourId}.completed`, PREF_BOOL]));
 
 let waitingForBrowserReady = true;
+let startupData;
 
 /**
  * Set pref. Why no `getPrefs` function is due to the priviledge level.
  * We cannot set prefs inside a framescript but can read.
  * For simplicity and effeciency, we still read prefs inside the framescript.
  *
  * @param {Array} prefs the array of prefs to set.
  *   The array element carrys info to set pref, should contain
@@ -59,25 +60,22 @@ function setPrefs(prefs) {
     }
 
     let [name, type] = prefObj;
 
     switch (type) {
       case PREF_BOOL:
         Services.prefs.setBoolPref(name, pref.value);
         break;
-
       case PREF_INT:
         Services.prefs.setIntPref(name, pref.value);
         break;
-
       case PREF_STRING:
         Services.prefs.setStringPref(name, pref.value);
         break;
-
       default:
         throw new TypeError(`Unexpected type (${type}) for preference ${name}.`);
     }
   });
 }
 
 /**
  * syncTourChecker listens to and maintains the login status inside, and can be
@@ -150,27 +148,35 @@ function initContentMessageListener() {
       case "set-prefs":
         setPrefs(msg.data.params);
         break;
       case "get-login-status":
         msg.target.messageManager.sendAsyncMessage("Onboarding:ResponseLoginStatus", {
           isLoggedIn: syncTourChecker.isLoggedIn()
         });
         break;
+      case "ping-centre":
+        try {
+          OnboardingTelemetry.process(msg.data.params.data);
+        } catch (e) {
+          Cu.reportError(e);
+        }
+        break;
     }
   });
 }
 
 /**
  * onBrowserReady - Continues startup of the add-on after browser is ready.
  */
 function onBrowserReady() {
   waitingForBrowserReady = false;
 
   OnboardingTourType.check();
+  OnboardingTelemetry.init(startupData);
   Services.mm.loadFrameScript("resource://onboarding/onboarding.js", true);
   initContentMessageListener();
 }
 
 /**
  * observe - nsIObserver callback to handle various browser notifications.
  */
 function observe(subject, topic, data) {
@@ -190,25 +196,29 @@ function observe(subject, topic, data) {
   }
 }
 
 function install(aData, aReason) {}
 
 function uninstall(aData, aReason) {}
 
 function startup(aData, aReason) {
+  // Cache startup data which contains stuff like the version number, etc.
+  // so we can use it when we init the telemetry
+  startupData = aData;
   // Only start Onboarding when the browser UI is ready
   if (Services.startup.startingUp) {
     Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
     Services.obs.addObserver(observe, BROWSER_SESSION_STORE_NOTIFICATION);
   } else {
     onBrowserReady();
     syncTourChecker.init();
   }
 }
 
 function shutdown(aData, aReason) {
+  startupData = null;
   // Stop waiting for browser to be ready
   if (waitingForBrowserReady) {
     Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
   }
   syncTourChecker.uninit();
 }
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -2,17 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env mozilla/frame-script */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css";
 const ABOUT_HOME_URL = "about:home";
 const ABOUT_NEWTAB_URL = "about:newtab";
 const BUNDLE_URI = "chrome://onboarding/locale/onboarding.properties";
 const UITOUR_JS_URI = "resource://onboarding/lib/UITour-lib.js";
 const TOUR_AGENT_JS_URI = "resource://onboarding/onboarding-tour-agent.js";
@@ -328,34 +327,52 @@ var onboardingTourset = {
       `;
       return div;
     },
   },
 };
 
 /**
  * @param {String} action the action to ask the chrome to do
- * @param {Array} params the parameters for the action
+ * @param {Array | Object} params the parameters for the action
  */
 function sendMessageToChrome(action, params) {
   sendAsyncMessage("Onboarding:OnContentMessage", {
     action, params
   });
 }
+
+/**
+ * Template code for talking to `PingCentre`
+ * @param {Object} data the payload for the telemetry
+ */
+function telemetry(data) {
+   sendMessageToChrome("ping-centre", {data});
+}
+
+function registerNewTelemetrySession(data) {
+  telemetry(Object.assign(data, {
+    event: "onboarding-register-session",
+  }));
+}
+
 /**
  * The script won't be initialized if we turned off onboarding by
  * setting "browser.onboarding.enabled" to false.
  */
 class Onboarding {
   constructor(contentWindow) {
     this.init(contentWindow);
   }
 
   async init(contentWindow) {
     this._window = contentWindow;
+    // session_key is used for telemetry to track the current tab.
+    // The number will renew after reload the page.
+    this._session_key = Date.now();
     this._tours = [];
     this._tourType = Services.prefs.getStringPref("browser.onboarding.tour-type", "update");
 
     let tourIds = this._getTourIDList();
     tourIds.forEach(tourId => {
       if (onboardingTourset[tourId]) {
         this._tours.push(onboardingTourset[tourId]);
       }
@@ -376,16 +393,24 @@ class Onboarding {
 
     // Destroy on unload. This is to ensure we remove all the stuff we left.
     // No any leak out there.
     this._window.addEventListener("unload", () => this.destroy());
 
     this.uiInitialized = false;
     this._resizeTimerId =
       this._window.requestIdleCallback(() => this._resizeUI());
+    registerNewTelemetrySession({
+      page: this._window.location.href,
+      session_key: this._session_key,
+    });
+    telemetry({
+      event: "onboarding-session-begin",
+      session_key: this._session_key,
+    });
   }
 
   _resizeUI() {
     let width = this._window.document.body.getBoundingClientRect().width;
     if (width < ONBOARDING_MIN_WIDTH_PX) {
       // Don't show the overlay UI before we get to a better, responsive design.
       this.destroy();
       return;
@@ -520,34 +545,50 @@ class Onboarding {
       case "onboarding-overlay-close-btn":
       // If the clicking target is directly on the outer-most overlay,
       // that means clicking outside the tour content area.
       // Let's toggle the overlay.
       case "onboarding-overlay":
         this.hideOverlay();
         break;
       case "onboarding-notification-close-btn":
+        let tour_id = this._notificationBar.dataset.targetTourId;
         this.hideNotification();
-        this._removeTourFromNotificationQueue(this._notificationBar.dataset.targetTourId);
+        this._removeTourFromNotificationQueue(tour_id);
+        telemetry({
+          event: "notification-close-button-click",
+          tour_id,
+          session_key: this._session_key,
+        });
         break;
       case "onboarding-notification-action-btn":
         let tourId = this._notificationBar.dataset.targetTourId;
         this.showOverlay();
         this.gotoPage(tourId);
+        telemetry({
+          event: "notification-cta-click",
+          tour_id: tourId,
+          session_key: this._session_key,
+        });
         this._removeTourFromNotificationQueue(tourId);
         break;
     }
     if (classList.contains("onboarding-tour-item")) {
       this.gotoPage(id);
       // Keep focus (not visible) on current item for potential keyboard
       // navigation.
       target.focus();
     } else if (classList.contains("onboarding-tour-action-button")) {
       let activeItem = this._tourItems.find(item => item.classList.contains("onboarding-active"));
       this.setToursCompleted([ activeItem.id ]);
+      telemetry({
+        event: "overlay-cta-click",
+        tour_id: activeItem.id,
+        session_key: this._session_key,
+      });
     }
   }
 
   /**
    * Wrap keyboard focus within the dialog and focus on first element after last
    * when moving forward or last element after first when moving backwards. Do
    * nothing if focus is moving in the middle of the list of dialog's focusable
    * elements.
@@ -666,16 +707,20 @@ class Onboarding {
     this._clearPrefObserver();
     this._overlayIcon.remove();
     this._overlay.remove();
     if (this._notificationBar) {
       this._notificationBar.remove();
     }
     this._tourItems = this._tourPages =
     this._overlayIcon = this._overlay = this._notificationBar = null;
+    telemetry({
+      event: "onboarding-session-end",
+      session_key: this._session_key,
+    });
   }
 
   _onIconStateChange(state) {
     switch (state) {
       case ICON_STATE_DEFAULT:
         this._overlayIcon.classList.remove("onboarding-watermark");
         break;
       case ICON_STATE_WATERMARK:
@@ -688,20 +733,28 @@ class Onboarding {
   showOverlay() {
     if (this._tourItems.length == 0) {
       // Lazy loading until first toggle.
       this._loadTours(this._tours);
     }
 
     this.hideNotification();
     this.toggleModal(this._overlay.classList.toggle("onboarding-opened"));
+    telemetry({
+      event: "overlay-session-begin",
+      session_key: this._session_key
+    });
   }
 
   hideOverlay() {
     this.toggleModal(this._overlay.classList.toggle("onboarding-opened"));
+    telemetry({
+      event: "overlay-session-end",
+      session_key: this._session_key,
+    });
   }
 
   /**
    * Set modal dialog state and properties for accessibility purposes.
    * @param  {Boolean} opened  whether the dialog is opened or closed.
    */
   toggleModal(opened) {
     let { document: doc } = this._window;
@@ -743,28 +796,39 @@ class Onboarding {
       } else {
         page.style.display = "none";
       }
     }
     for (let tab of this._tourItems) {
       if (tab.id == tourId) {
         tab.classList.add("onboarding-active");
         tab.setAttribute("aria-selected", true);
+        telemetry({
+          event: "overlay-nav-click",
+          tour_id: tourId,
+          session_key: this._session_key,
+        });
       } else {
         tab.classList.remove("onboarding-active");
         tab.setAttribute("aria-selected", false);
       }
     }
 
     switch (tourId) {
       // These tours should tagged completed instantly upon showing.
       case "onboarding-tour-default-browser":
       case "onboarding-tour-sync":
       case "onboarding-tour-performance":
         this.setToursCompleted([tourId]);
+        // also track auto completed tour so we can filter data with the same event
+        telemetry({
+          event: "overlay-cta-click",
+          tour_id: tourId,
+          session_key: this._session_key,
+        });
         break;
     }
   }
 
   isTourCompleted(tourId) {
     return Services.prefs.getBoolPref(`browser.onboarding.tour.${tourId}.completed`, false);
   }
 
@@ -996,21 +1060,32 @@ class Onboarding {
     } else {
       let promptCount = Services.prefs.getIntPref(PROMPT_COUNT_PREF, 0);
       params.push({
         name: PROMPT_COUNT_PREF,
         value: promptCount + 1
       });
     }
     sendMessageToChrome("set-prefs", params);
+    telemetry({
+      event: "notification-session-begin",
+      session_key: this._session_key
+    });
   }
 
   hideNotification() {
     if (this._notificationBar) {
-      this._notificationBar.classList.remove("onboarding-opened");
+      if (this._notificationBar.classList.contains("onboarding-opened")) {
+        this._notificationBar.classList.remove("onboarding-opened");
+        telemetry({
+          event: "notification-session-end",
+          tour_id: this._notificationBar.dataset.targetTourId,
+          session_key: this._session_key,
+        });
+      }
     }
   }
 
   _renderNotificationBar() {
     let footer = this._window.document.createElement("footer");
     footer.id = "onboarding-notification-bar";
     footer.setAttribute("aria-live", "polite");
     footer.setAttribute("aria-labelledby", "onboarding-notification-tour-title");
@@ -1041,16 +1116,20 @@ class Onboarding {
         name: "browser.onboarding.notification.finished",
         value: true
       },
       {
         name: "browser.onboarding.state",
         value: ICON_STATE_WATERMARK
       }
     ]);
+    telemetry({
+      event: "overlay-skip-tour",
+      session_key: this._session_key
+    });
   }
 
   _renderOverlay() {
     let div = this._window.document.createElement("div");
     div.id = "onboarding-overlay";
     // We use `innerHTML` for more friendly reading.
     // The security should be fine because this is not from an external input.
     div.innerHTML = `
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/data_events.md
@@ -0,0 +1,116 @@
+# Metrics we collect
+
+We adhere to [Activity Stream's data collection policy](https://github.com/mozilla/activity-stream/blob/master/docs/v2-system-addon/data_events.md). Data about your specific browsing behavior or the sites you visit is **never transmitted to any Mozilla server**.  At any time, it is easy to **turn off** this data collection by [opting out of Firefox telemetry](https://support.mozilla.org/kb/share-telemetry-data-mozilla-help-improve-firefox).
+
+## User event pings
+
+The Onboarding system add-on sends 2 types of pings(HTTPS POST) to the backend [Onyx server](https://github.com/mozilla/onyx) :
+- a `session` ping that describes the ending of an Onboarding session (a new tab is closed or refreshed, an overlay is closed, a notification bar is closed), and
+- an `event` ping that records specific data about individual user interactions while interacting with Onboarding
+
+For reference, Onyx is a Mozilla owned service to serve tiles for the current newtab in Firefox. It also receives all the telemetry from the about:newtab and about:home page as well as Activity Stream. It's operated and monitored by the Cloud Services team.
+
+# Example Onboarding `session` Log
+
+```js
+{
+  // These fields are sent from the client
+  "addon_version": "1.0.0",
+  "category": ["overlay-interactions"|"notification-interactions"],
+  "client_id": "374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e",
+  "locale": "en-US",
+  "event": ["onboarding_session" | "overlay_session" | "notification_session"],
+  "page": ["about:newtab" | "about:home"],
+  "session_begin": 1505440017018,
+  "session_end": 1505440021992,
+  "session_id": "{12dasd-213asda-213dkakj}",
+  "tour_source": ["default" | "watermark"],
+  "tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset'
+
+  // These fields are generated on the server
+  "date": "2016-03-07",
+  "ip": "10.192.171.13",
+  "ua": "python-requests/2.9.1",
+  "receive_at": 1457396660000
+}
+```
+
+# Example Onboarding `event` Log
+
+```js
+{
+  "addon_version": "1.0.0",
+  "category": ["overlay-interactions"|"notification-interactions"],
+  "client_id": "374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e",
+  "event": ["notification-cta-click" | "overlay-cta-click" | "overlay-nav-click" | "overlay-skip-tour"],
+  "impression": [0-8],
+  "locale": "en-US",
+  "page": ["about:newtab" | "about:home"],
+  "session_id": "{12dasd-213asda-213dkakj}",
+  "tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset'
+
+  // These fields are generated on the server
+  "ip": "10.192.171.13",
+  "ua": "python-requests/2.9.1",
+  "receive_at": 1457396660000,
+  "date": "2016-03-07",
+}
+```
+
+
+| KEY | DESCRIPTION | &nbsp; |
+|-----|-------------|:-----:|
+| `addon_version` | [Required] The version of the Onboarding addon. | :one:
+| `category` | [Required] Either ("overlay-interactions", "notification-interactions") to identify which kind of the interaction | :one:
+| `client_id` | [Required] An identifier generated by [ClientID](https://github.com/mozilla/gecko-dev/blob/master/toolkit/modules/ClientID.jsm) module to provide an identifier for this device. Auto append by `ping-centre` module | :one:
+| `event` | [Required] The type of event. allowed event strings are defined in the below section | :one:
+| `impression` | [Optional] An integer to record how many times the current notification tour is shown to the user. Each Notification tour can show not more than 8 times. We put `-1` when this field is not relevant to this event | :one:
+| `ip` | [Auto populated by Onyx] The IP address of the client. Onyx does use (with the permission) the IP address to infer user's geo information so that it could prepare the corresponding tiles for the country she lives in. However, Ping-centre will NOT store IP address in the database, where only authorized Mozilla employees can access the telemetry data, and all the raw logs are being strictly managed by the Ops team and will expire according to the Mozilla's data retention policy.| :two:
+| `locale` | The browser chrome's language (eg. en-US). | :two:
+| `page` | [Required] One of ["about:newtab", "about:home"]| :one:
+| `session_begin` | Timestamp in (integer) milliseconds when onboarding/overlay/notification becoming visible. | :one:
+| `session_end` | Timestamp in (integer) milliseconds when onboarding/overlay/notification losing focus. | :one:
+| `session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify the specific user session when onboarding is inited/when overlay is opened/when notification is shown. | :one:
+| `tour_source` | Either ("default", "watermark") indicates the overlay is opened while in the default or the watermark state. Open from the notification bar is counted via 'notification-cta-click event'. | :two:
+| `tour_id` | id of the current tour. The number of open from notification can be retrieved via 'notification-cta-click event'. We put ` ` when this field is not relevant to this event | :two:
+| `ua` | [Auto populated by Onyx] The user agent string. | :two:
+| `ver` | [Auto populated by Onyx] The version of the Onyx API the ping was sent to. | :one:
+
+**Where:**
+
+:one: Firefox data
+:two: HTTP protocol data
+
+## Events
+
+Here are all allowed `event` strings that defined in `OnboardingTelemetry::EVENT_WHITELIST`.
+### Session events
+
+| EVENT | DESCRIPTION |
+|-----------|---------------------|
+| `onboarding-register-session` | internal event triggered to register a new page session. Called when the onboarding script is inited in a browser tab. Will not send out any data. |
+| `onboarding-session-begin` | internal event triggered when the onboarding script is inited, will not send out any data. |
+| `onboarding-session-end` | internal event triggered when the onboarding script is destoryed. `onboarding-session` event is sent to the server. |
+| `onboarding-session` | event is sent when the onboarding script is destoryed |
+
+### Overlay events
+
+| EVENT | DESCRIPTION |
+|-----------|---------------------|
+| `overlay-session-begin` | internal event triggered when user open the overlay, will not send out any data. |
+| `overlay-session-end` | internal event is triggered when user close the overlay. `overlay-session` event is sent to the server. |
+| `overlay-session` | event is sent when user close the overlay |
+| `overlay-nav-click` | event is sent when click or auto select the overlay navigate item |
+| `overlay-cta-click` | event is sent when user click the overlay CTA button |
+| `overlay-skip-tour` | event is sent when click the overlay `skip tour` button |
+
+### Notification events
+
+| EVENT | DESCRIPTION |
+|-----------|---------------------|
+| `notification-session-begin` | internal event triggered when user open the notification, will not send out any data. |
+| `notification-session-end` | internal event is triggered when user close the notification. `notification-session` event is sent to the server. |
+| `notification-session` | event is sent when user close the notification |
+| `notification-close-button-click` | event is sent when click the notification close button |
+| `notification-cta-click` | event is sent when click the notification CTA button |
+
--- a/browser/extensions/onboarding/install.rdf.in
+++ b/browser/extensions/onboarding/install.rdf.in
@@ -6,17 +6,17 @@
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>onboarding@mozilla.org</em:id>
     <em:name>Photon onboarding</em:name>
     <em:description>Photon onboarding</em:description>
-    <em:version>0.1</em:version>
+    <em:version>1.0</em:version>
     <em:bootstrap>true</em:bootstrap>
     <em:type>2</em:type>
     <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
         <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>