Bug 1413830 - Data schema change for onboarding telemetry;r=fischer,francois
authorFred Lin <gasolin@gmail.com>
Mon, 15 Jan 2018 15:29:39 +0800
changeset 453583 14a96e730a866b8e7c882f7c6005c8eb3c5c0853
parent 453582 3ae6cbde5225f24eb512aff7acba6be65efd29ec
child 453584 209252da2b5ed67d672db8a3923369a6bceaa662
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfischer, francois
bugs1413830, 1417769
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1413830 - Data schema change for onboarding telemetry;r=fischer,francois Implement new processPings and _sendPing, handle sessions via registerNewOnboardingSession This patch covers all new events except onboarding-noshow-smallscreen and overlay-disapear-resize events will be implemented in bug 1417769 MozReview-Commit-ID: 3jNAmOcrvEa
browser/extensions/onboarding/OnboardingTelemetry.jsm
browser/extensions/onboarding/content/onboarding.js
browser/extensions/onboarding/data_events.md
--- a/browser/extensions/onboarding/OnboardingTelemetry.jsm
+++ b/browser/extensions/onboarding/OnboardingTelemetry.jsm
@@ -13,17 +13,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   PingCentre: "resource:///modules/PingCentre.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
   "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
 
 // Flag to control if we want to send new/old telemetry
 // TODO: remove this flag and the legacy code in Bug 1419996
-const NEW_TABLE = false;
+const NEW_TABLE = true;
 
 // Validate the content has non-empty string
 function hasString(str) {
   return typeof str == "string" && str.length > 0;
 }
 
 // Validate the content is an empty string
 function isEmptyString(str) {
@@ -133,191 +133,206 @@ const BASIC_EVENT_SCHEMA = {
  * We send `session_begin` and `session_end` timestamps instead of `session_duration` diff because
  * of analytics engineer's request.
  */
 const EVENT_WHITELIST = {
   // track when a notification appears.
   "notification-appear": {
     topic: "firefox-onboarding-event2",
     category: "notification-interactions",
+    parent: "notification-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isValidBubbleState,
       current_tour_id: hasString,
       logo_state: isValidLogoState,
       notification_impression: isPositiveInteger,
       notification_state: isValidNotificationState,
       target_tour_id: isEmptyString,
     }),
   },
   // track when a user clicks close notification button
   "notification-close-button-click": {
     topic: "firefox-onboarding-event2",
     category: "notification-interactions",
+    parent: "notification-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isValidBubbleState,
       current_tour_id: hasString,
       logo_state: isValidLogoState,
       notification_impression: isPositiveInteger,
       notification_state: isValidNotificationState,
       target_tour_id: hasString,
     }),
   },
   // track when a user clicks notification's Call-To-Action button
   "notification-cta-click": {
     topic: "firefox-onboarding-event2",
     category: "notification-interactions",
+    parent: "notification-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isValidBubbleState,
       current_tour_id: hasString,
       logo_state: isValidLogoState,
       notification_impression: isPositiveInteger,
       notification_state: isValidNotificationState,
       target_tour_id: hasString,
     }),
   },
   // track the start and end time of the notification session
   "notification-session": {
     topic: "firefox-onboarding-session2",
     category: "notification-interactions",
+    parent: "onboarding-session",
     validators: BASIC_SESSION_SCHEMA,
   },
   // track the start of a notification
   "notification-session-begin": {topic: "internal"},
   // track the end of a notification
   "notification-session-end": {topic: "internal"},
   // track when a user clicks the Firefox logo
   "onboarding-logo-click": {
     topic: "firefox-onboarding-event2",
     category: "logo-interactions",
+    parent: "onboarding-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isValidBubbleState,
       current_tour_id: isEmptyString,
       logo_state: isValidLogoState,
       notification_impression: isMinusOne,
       notification_state: isValidNotificationState,
       target_tour_id: isEmptyString,
     }),
   },
   // track when the onboarding is not visisble due to small screen in the 1st load
   "onboarding-noshow-smallscreen": {
     topic: "firefox-onboarding-event2",
     category: "onboarding-interactions",
+    parent: "onboarding-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: isEmptyString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: isEmptyString,
     }),
   },
   // init onboarding session with session_key, page url, and tour_type
   "onboarding-register-session": {topic: "internal"},
   // track the start and end time of the onboarding session
   "onboarding-session": {
     topic: "firefox-onboarding-session2",
     category: "onboarding-interactions",
+    parent: "onboarding-session",
     validators: BASIC_SESSION_SCHEMA,
   },
   // track onboarding start time (when user loads about:home or about:newtab)
   "onboarding-session-begin": {topic: "internal"},
   // track onboarding end time (when user unloads about:home or about:newtab)
   "onboarding-session-end": {topic: "internal"},
   // track when a user clicks the close overlay button
   "overlay-close-button-click": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: hasString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: hasString,
     }),
   },
   // track when a user clicks outside the overlay area to end the tour
   "overlay-close-outside-click": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: hasString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: hasString,
     }),
   },
   // track when a user clicks overlay's Call-To-Action button
   "overlay-cta-click": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: hasString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: hasString,
     }),
   },
   // track when a tour is shown in the overlay
   "overlay-current-tour": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: hasString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: isEmptyString,
     }),
   },
   // track when an overlay is opened and disappeared because the window is resized too small
   "overlay-disapear-resize": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: isEmptyString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: isEmptyString,
     }),
   },
   // track when a user clicks a navigation button in the overlay
   "overlay-nav-click": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: hasString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: hasString,
     }),
   },
   // track the start and end time of the overlay session
   "overlay-session": {
     topic: "firefox-onboarding-session2",
     category: "overlay-interactions",
+    parent: "onboarding-session",
     validators:  BASIC_SESSION_SCHEMA,
   },
   // track the start of an overlay session
   "overlay-session-begin": {topic: "internal"},
   // track the end of an overlay session
   "overlay-session-end":  {topic: "internal"},
   // track when a user clicks 'Skip Tour' button in the overlay
   "overlay-skip-tour": {
     topic: "firefox-onboarding-event2",
     category: "overlay-interactions",
+    parent: "overlay-session",
     validators: Object.assign({}, BASIC_EVENT_SCHEMA, {
       bubble_state: isEmptyString,
       current_tour_id: hasString,
       logo_state: isEmptyString,
       notification_impression: isMinusOne,
       notification_state: isEmptyString,
       target_tour_id: isEmptyString,
     }),
@@ -367,18 +382,23 @@ 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"});
+    if (NEW_TABLE) {
+      this.sessionProbe = new PingCentre({topic: "firefox-onboarding-session2"});
+      this.eventProbe = new PingCentre({topic: "firefox-onboarding-event2"});
+    } else {
+      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(data) {
     let { page, session_key, tour_type } = data;
     if (this.state.sessions[session_key]) {
       return;
@@ -390,24 +410,222 @@ let OnboardingTelemetry = {
     let session_id = gUUIDGenerator.generateUUID().toString();
     this.state.sessions[session_key] = {
       page,
       session_id,
       tour_type,
     };
   },
 
+  // register per tab session data
+  registerNewOnboardingSession(data) {
+    let { page, session_key, tour_type } = data;
+    if (this.state.sessions[session_key]) {
+      return;
+    }
+    // session_key and page url are must have
+    if (!session_key || !page || !tour_type) {
+      throw new Error("session_key, page url, and tour_type are required for onboarding-register-session");
+    }
+    let onboarding_session_id = gUUIDGenerator.generateUUID().toString();
+    this.state.sessions[session_key] = {
+      onboarding_session_id,
+      overlay_session_id: "",
+      notification_session_id: "",
+      page,
+      tour_type,
+    };
+  },
+
   process(data) {
     if (NEW_TABLE) {
-      throw new Error("Will implement in bug 1413830");
+      this.processPings(data);
     } else {
       this.processOldPings(data);
     }
   },
 
+  processPings(data) {
+    let { type, session_key } = data;
+    if (type === "onboarding-register-session") {
+      this.registerNewOnboardingSession(data);
+      return;
+    }
+
+    if (!this.state.sessions[session_key]) {
+      throw new Error(`${type} should pass valid session_key`);
+    }
+
+    switch (type) {
+      case "onboarding-session-begin":
+        if (!this.state.sessions[session_key].onboarding_session_id) {
+          throw new Error(`should fire onboarding-register-session event before ${type}`);
+        }
+        this.state.sessions[session_key].onboarding_session_begin = Date.now();
+        return;
+      case "onboarding-session-end":
+        data = Object.assign({}, data, {
+          type: "onboarding-session"
+        });
+        this.state.sessions[session_key].onboarding_session_end = Date.now();
+        break;
+      case "overlay-session-begin":
+        this.state.sessions[session_key].overlay_session_id = gUUIDGenerator.generateUUID().toString();
+        this.state.sessions[session_key].overlay_session_begin = Date.now();
+        return;
+      case "overlay-session-end":
+        data = Object.assign({}, data, {
+          type: "overlay-session"
+        });
+        this.state.sessions[session_key].overlay_session_end = Date.now();
+        break;
+      case "notification-session-begin":
+        this.state.sessions[session_key].notification_session_id = gUUIDGenerator.generateUUID().toString();
+        this.state.sessions[session_key].notification_session_begin = Date.now();
+        return;
+      case "notification-session-end":
+        data = Object.assign({}, data, {
+          type: "notification-session"
+        });
+        this.state.sessions[session_key].notification_session_end = Date.now();
+        break;
+    }
+    let topic = EVENT_WHITELIST[data.type] && EVENT_WHITELIST[data.type].topic;
+    if (!topic) {
+      throw new Error(`ping-centre doesn't know ${type} after processPings, only knows ${Object.keys(EVENT_WHITELIST)}`);
+    }
+    this._sendPing(topic, data);
+  },
+
+  // send out pings by topic
+  _sendPing(topic, data) {
+    if (topic === "internal") {
+      throw new Error(`internal ping ${data.type} should be processed within processPings`);
+    }
+
+    let {
+      addon_version,
+    } = this.state;
+    let {
+      bubble_state = "",
+      current_tour_id = "",
+      logo_state = "",
+      notification_impression = -1,
+      notification_state = "",
+      session_key,
+      target_tour_id = "",
+      type,
+      width,
+    } = data;
+    let {
+      notification_session_begin,
+      notification_session_end,
+      notification_session_id,
+      onboarding_session_begin,
+      onboarding_session_end,
+      onboarding_session_id,
+      overlay_session_begin,
+      overlay_session_end,
+      overlay_session_id,
+      page,
+      tour_type,
+    } = this.state.sessions[session_key];
+    let {
+      category,
+      parent,
+    } = EVENT_WHITELIST[type];
+    let parent_session_id;
+    let payload;
+    let session_begin;
+    let session_end;
+    let session_id;
+    let root_session_id = onboarding_session_id;
+
+    // assign parent_session_id
+    switch (parent) {
+      case "onboarding-session":
+        parent_session_id = onboarding_session_id;
+        break;
+      case "overlay-session":
+        parent_session_id = overlay_session_id;
+        break;
+      case "notification-session":
+        parent_session_id = notification_session_id;
+        break;
+    }
+    if (!parent_session_id) {
+      throw new Error(`Unable to find the ${parent} parent session for the event ${type}`);
+    }
+
+    switch (topic) {
+      case "firefox-onboarding-session2":
+        switch (type) {
+          case "onboarding-session":
+            session_id = onboarding_session_id;
+            session_begin = onboarding_session_begin;
+            session_end = onboarding_session_end;
+            delete this.state.sessions[session_key];
+            break;
+          case "overlay-session":
+            session_id = overlay_session_id;
+            session_begin = overlay_session_begin;
+            session_end = overlay_session_end;
+            break;
+          case "notification-session":
+            session_id = notification_session_id;
+            session_begin = notification_session_begin;
+            session_end = notification_session_end;
+            break;
+        }
+        if (!session_id || !session_begin || !session_end) {
+          throw new Error(`should fire ${type}-begin and ${type}-end event before ${type}`);
+        }
+
+        payload = {
+          addon_version,
+          category,
+          page,
+          parent_session_id,
+          root_session_id,
+          session_begin,
+          session_end,
+          session_id,
+          tour_type,
+          type,
+        };
+        this._validatePayload(payload);
+        this.sessionProbe && this.sessionProbe.sendPing(payload,
+          {filter: ONBOARDING_ID});
+        break;
+      case "firefox-onboarding-event2":
+        let timestamp = Date.now();
+        payload = {
+          addon_version,
+          bubble_state,
+          category,
+          current_tour_id,
+          logo_state,
+          notification_impression,
+          notification_state,
+          page,
+          parent_session_id,
+          root_session_id,
+          target_tour_id,
+          timestamp,
+          tour_type,
+          type,
+          width,
+        };
+        this._validatePayload(payload);
+        this.eventProbe && this.eventProbe.sendPing(payload,
+          {filter: ONBOARDING_ID});
+        break;
+    }
+  },
+
   processOldPings(data) {
     let { event, session_key } = data;
     let topic = OLD_EVENT_WHITELIST[event] && OLD_EVENT_WHITELIST[event].topic;
     if (!topic) {
       throw new Error(`ping-centre doesn't know ${event}, only knows ${Object.keys(OLD_EVENT_WHITELIST)}`);
     }
 
     if (event === "onboarding-register-session") {
@@ -517,37 +735,37 @@ let OnboardingTelemetry = {
           tour_type,
         }, {filter: ONBOARDING_ID});
         break;
     }
   },
 
   // validate data sanitation and make sure correct ping params are sent
   _validatePayload(payload) {
-    let event = payload.type;
-    let { validators } = EVENT_WHITELIST[event];
+    let type = payload.type;
+    let { validators } = EVENT_WHITELIST[type];
     if (!validators) {
-      throw new Error(`Event ${event} without validators should not be sent.`);
+      throw new Error(`Event ${type} without validators should not be sent.`);
     }
     let validatorKeys = Object.keys(validators);
     // Not send with undefined column
     if (Object.keys(payload).length > validatorKeys.length) {
-      throw new Error(`Event ${event} want to send more columns than expect, should not be sent.`);
+      throw new Error(`Event ${type} want to send more columns than expect, should not be sent.`);
     }
     let results = {};
     let failed = false;
     // Per column validation
     for (let key of validatorKeys) {
       if (payload[key] !== undefined) {
         results[key] = validators[key](payload[key]);
         if (!results[key]) {
           failed = true;
         }
       } else {
         results[key] = false;
         failed = true;
       }
     }
     if (failed) {
-      throw new Error(`Event ${event} contains incorrect data: ${JSON.stringify(results)}, should not be sent.`);
+      throw new Error(`Event ${type} contains incorrect data: ${JSON.stringify(results)}, should not be sent.`);
     }
   }
 };
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -14,16 +14,17 @@ 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";
 const BRAND_SHORT_NAME = Services.strings
                      .createBundle("chrome://branding/locale/brand.properties")
                      .GetStringFromName("brandShortName");
 const PROMPT_COUNT_PREF = "browser.onboarding.notification.prompt-count";
+const NOTIFICATION_FINISHED_PREF = "browser.onboarding.notification.finished";
 const ONBOARDING_DIALOG_ID = "onboarding-overlay-dialog";
 const ONBOARDING_MIN_WIDTH_PX = 960;
 const SPEECH_BUBBLE_MIN_WIDTH_PX = 1130;
 const SPEECH_BUBBLE_NEWTOUR_STRING_ID = "onboarding.overlay-icon-tooltip2";
 const SPEECH_BUBBLE_UPDATETOUR_STRING_ID = "onboarding.overlay-icon-tooltip-updated2";
 const ICON_STATE_WATERMARK = "watermark";
 const ICON_STATE_DEFAULT = "default";
 /**
@@ -352,17 +353,17 @@ function sendMessageToChrome(action, par
  * @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",
+    type: "onboarding-register-session",
   }));
 }
 
 /**
  * The script won't be initialized if we turned off onboarding by
  * setting "browser.onboarding.enabled" to false.
  */
 class Onboarding {
@@ -391,46 +392,63 @@ class Onboarding {
 
     // We want to create and append elements after CSS is loaded so
     // no flash of style changes and no additional reflow.
     await this._loadCSS();
     this._bundle = Services.strings.createBundle(BUNDLE_URI);
 
     this._loadJS(UITOUR_JS_URI);
 
-    this._window.addEventListener("resize", this);
+    this.uiInitialized = false;
+    let doc = this._window.document;
+    if (doc.hidden) {
+      // When the preloaded-browser feature is on,
+      // it would preload a hidden about:newtab in the background.
+      // We don't want to show onboarding experience in that hidden state.
+      let onVisible = () => {
+        if (!doc.hidden) {
+          doc.removeEventListener("visibilitychange", onVisible);
+          this._startUI();
+        }
+      };
+      doc.addEventListener("visibilitychange", onVisible);
+    } else {
+      this._startUI();
+    }
+  }
 
-    // Destroy on unloading. 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());
+  _startUI() {
     registerNewTelemetrySession({
       page: this._window.location.href,
       session_key: this._session_key,
       tour_type: this._tourType,
     });
+
+    this._window.addEventListener("beforeunload", this);
+    this._window.addEventListener("unload", this);
+    this._window.addEventListener("resize", this);
+    this._resizeTimerId =
+      this._window.requestIdleCallback(() => this._resizeUI());
+    // start log the onboarding-session when the tab is visible
     telemetry({
-      event: "onboarding-session-begin",
+      type: "onboarding-session-begin",
       session_key: this._session_key,
     });
   }
 
   _resizeUI() {
-    let width = this._window.document.body.getBoundingClientRect().width;
-    if (width < ONBOARDING_MIN_WIDTH_PX) {
+    this._windowWidth = this._window.document.body.getBoundingClientRect().width;
+    if (this._windowWidth < ONBOARDING_MIN_WIDTH_PX) {
       // Don't show the overlay UI before we get to a better, responsive design.
       this.destroy();
       return;
     }
 
     this._initUI();
-    if (this._isFirstSession && width >= SPEECH_BUBBLE_MIN_WIDTH_PX) {
+    if (this._isFirstSession && this._windowWidth >= SPEECH_BUBBLE_MIN_WIDTH_PX) {
       this._overlayIcon.classList.add("onboarding-speech-bubble");
     } else {
       this._overlayIcon.classList.remove("onboarding-speech-bubble");
     }
   }
 
   _initUI() {
     if (this.uiInitialized) {
@@ -452,42 +470,24 @@ class Onboarding {
     body.appendChild(this._overlay);
 
     this._loadJS(TOUR_AGENT_JS_URI);
 
     this._initPrefObserver();
     this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
 
     // Doing tour notification takes some effort. Let's do it on idle.
-    this._window.requestIdleCallback(() => this._initNotification());
+    this._window.requestIdleCallback(() => this.showNotification());
   }
 
   _getTourIDList() {
     let tours = Services.prefs.getStringPref(`browser.onboarding.${this._tourType}tour`, "");
     return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim());
   }
 
-  _initNotification() {
-    let doc = this._window.document;
-    if (doc.hidden) {
-      // When the preloaded-browser feature is on,
-      // it would preload a hidden about:newtab in the background.
-      // We don't want to show notification in that hidden state.
-      let onVisible = () => {
-        if (!doc.hidden) {
-          doc.removeEventListener("visibilitychange", onVisible);
-          this.showNotification();
-        }
-      };
-      doc.addEventListener("visibilitychange", onVisible);
-    } else {
-      this.showNotification();
-    }
-  }
-
   _initPrefObserver() {
     if (this._prefsObserved) {
       return;
     }
 
     this._prefsObserved = new Map();
     this._prefsObserved.set("browser.onboarding.state", () => {
       this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
@@ -527,75 +527,186 @@ class Onboarding {
    * Find a tour that should be selected. It is either a first tour that was not
    * yet complete or the first one in the tab list.
    */
   get _firstUncompleteTour() {
     return this._tours.find(tour => !this.isTourCompleted(tour.id)) ||
            this._tours[0];
   }
 
+  /*
+   * Return currently showing tour navigation item
+   */
+  get _activeTourId() {
+    // We are doing lazy load so there might be no items.
+    if (!this._tourItems) {
+      return "";
+    }
+
+    let tourItem = this._tourItems.find(item => item.classList.contains("onboarding-active"));
+    return tourItem ? tourItem.id : "";
+  }
+
+  /**
+   * Return current logo state as "logo" or "watermark".
+   */
+  get _logoState() {
+    return this._overlayIcon.classList.contains("onboarding-watermark") ?
+      "watermark" : "logo";
+  }
+
+  /**
+   * Return current speech bubble state as "bubble", "dot" or "hide".
+   */
+  get _bubbleState() {
+    let state;
+    if (this._overlayIcon.classList.contains("onboarding-watermark")) {
+      state = "hide";
+    } else if (this._overlayIcon.classList.contains("onboarding-speech-bubble")) {
+      state = "bubble";
+    } else {
+      state = "dot";
+    }
+    return state;
+  }
+
+  /**
+   * Return current notification state as "show", "hide" or "finished".
+   */
+  get _notificationState() {
+    if (this._notificationCachedState === "finished") {
+      return this._notificationCachedState;
+    }
+
+    if (Services.prefs.getBoolPref(NOTIFICATION_FINISHED_PREF, false)) {
+      this._notificationCachedState = "finished";
+    } else if (this._notification) {
+      this._notificationCachedState = "show";
+    } else {
+      // we know it is in the hidden state if there's no notification bar
+      this._notificationCachedState = "hide";
+    }
+
+    return this._notificationCachedState;
+  }
+
+  /**
+   * Return current notification prompt count.
+   */
+  get _notificationPromptCount() {
+    return Services.prefs.getIntPref(PROMPT_COUNT_PREF, 0);
+  }
+
+  /**
+   * Return current screen width and round it up to the nearest 50 pixels.
+   * Collecting rounded values reduces the risk that this could be used to
+   * derive a unique user identifier
+   */
+  get _windowWidthRounded() {
+    return Math.round(this._windowWidth / 50) * 50;
+  }
+
   handleClick(target) {
     let { id, classList } = target;
     // Only containers receive pointer events in onboarding tour tab list,
     // actual semantic tab is their first child.
     if (classList.contains("onboarding-tour-item-container")) {
       ({ id, classList } = target.firstChild);
     }
 
     switch (id) {
       case "onboarding-overlay-button":
+        telemetry({
+          type: "onboarding-logo-click",
+          bubble_state: this._bubbleState,
+          logo_state: this._logoState,
+          notification_state: this._notificationState,
+          session_key: this._session_key,
+          width: this._windowWidthRounded,
+        });
         this.showOverlay();
         this.gotoPage(this._firstUncompleteTour.id);
         break;
       case "onboarding-skip-tour-button":
         this.hideNotification();
         this.hideOverlay();
         this.skipTour();
         break;
       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":
+        let eventName = id === "onboarding-overlay-close-btn" ?
+          "overlay-close-button-click" : "overlay-close-outside-click";
+        telemetry({
+          type: eventName,
+          current_tour_id: this._activeTourId,
+          session_key: this._session_key,
+          target_tour_id: this._activeTourId,
+          width: this._windowWidthRounded,
+        });
         this.hideOverlay();
         break;
       case "onboarding-notification-close-btn":
-        let tour_id = this._notificationBar.dataset.targetTourId;
-        this.hideNotification();
-        this._removeTourFromNotificationQueue(tour_id);
+        let currentTourId = this._notificationBar.dataset.targetTourId;
+        // should trigger before notification-session event is sent
         telemetry({
-          event: "notification-close-button-click",
-          tour_id,
+          type: "notification-close-button-click",
+          bubble_state: this._bubbleState,
+          current_tour_id: currentTourId,
+          logo_state: this._logoState,
+          notification_impression: this._notificationPromptCount,
+          notification_state: this._notificationState,
           session_key: this._session_key,
+          target_tour_id: currentTourId,
+          width: this._windowWidthRounded,
         });
+        this.hideNotification();
+        this._removeTourFromNotificationQueue(currentTourId);
         break;
       case "onboarding-notification-action-btn":
         let tourId = this._notificationBar.dataset.targetTourId;
+        telemetry({
+          type: "notification-cta-click",
+          bubble_state: this._bubbleState,
+          current_tour_id: tourId,
+          logo_state: this._logoState,
+          notification_impression: this._notificationPromptCount,
+          notification_state: this._notificationState,
+          session_key: this._session_key,
+          target_tour_id: tourId,
+          width: this._windowWidthRounded,
+        });
         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")) {
+      telemetry({
+        type: "overlay-nav-click",
+        current_tour_id: this._activeTourId,
+        session_key: this._session_key,
+        target_tour_id: id,
+        width: this._windowWidthRounded,
+      });
       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 ]);
+      let activeTourId = this._activeTourId;
+      this.setToursCompleted([ activeTourId ]);
       telemetry({
-        event: "overlay-cta-click",
-        tour_id: activeItem.id,
+        type: "overlay-cta-click",
+        current_tour_id: activeTourId,
         session_key: this._session_key,
+        target_tour_id: activeTourId,
+        width: this._windowWidthRounded,
       });
     }
   }
 
   /**
    * Wrap keyboard focus within the dialog.
    * When moving forward, focus on the first element when the current focused
    * element is the last one.
@@ -686,16 +797,34 @@ class Onboarding {
       default:
         break;
     }
     event.stopPropagation();
   }
 
   handleEvent(evt) {
     switch (evt.type) {
+      case "beforeunload":
+        // To make sure the telemetry pings are sent,
+        // we send "onboarding-session-end" ping as well as
+        // "overlay-session-end" and "notification-session-end" ping
+        // (by hiding the overlay and notificaiton) on beforeunload.
+        this.hideOverlay();
+        this.hideNotification();
+        telemetry({
+          type: "onboarding-session-end",
+          session_key: this._session_key,
+        });
+        break;
+      case "unload":
+        // Notice: Cannot do `destroy` on beforeunload, must do on unload.
+        // Otherwise, we would hit the docShell leak in the test.
+        // See Bug 1413830#c190 and Bug 1429652 for details.
+        this.destroy();
+        break;
       case "resize":
         this._window.cancelIdleCallback(this._resizeTimerId);
         this._resizeTimerId =
           this._window.requestIdleCallback(() => this._resizeUI());
         break;
       case "keypress":
         this.handleKeypress(evt);
         break;
@@ -712,26 +841,28 @@ class Onboarding {
       return;
     }
     this.uiInitialized = false;
 
     this._overlayIcon.dispatchEvent(new this._window.CustomEvent("Agent:Destroy"));
 
     this._clearPrefObserver();
     this._overlayIcon.remove();
-    this._overlay.remove();
+    if (this._overlay) {
+      // send overlay-session telemetry
+      this.hideOverlay();
+      this._overlay.remove();
+    }
     if (this._notificationBar) {
+      // send notification-session telemetry
+      this.hideNotification();
       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:
@@ -742,30 +873,36 @@ 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
-    });
+    if (this._overlay && !this._overlay.classList.contains("onboarding-opened")) {
+      this.hideNotification();
+      this._overlay.classList.add("onboarding-opened");
+      this.toggleModal(true);
+      telemetry({
+        type: "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,
-    });
+    if (this._overlay && this._overlay.classList.contains("onboarding-opened")) {
+      this._overlay.classList.remove("onboarding-opened");
+      this.toggleModal(false);
+      telemetry({
+        type: "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;
@@ -793,34 +930,39 @@ class Onboarding {
         delete this._overlayIcon.dataset.keyboardFocus;
         this._overlayIcon.focus();
       } else {
         this._window.document.activeElement.blur();
       }
     }
   }
 
+  /**
+   * Switch to proper tour.
+   * @param {String} tourId specify which tour should be switched.
+   */
   gotoPage(tourId) {
     let targetPageId = `${tourId}-page`;
     for (let page of this._tourPages) {
       if (page.id === targetPageId) {
         page.style.display = "";
         page.dispatchEvent(new this._window.CustomEvent("beforeshow"));
       } 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,
+          type: "overlay-current-tour",
+          current_tour_id: tourId,
           session_key: this._session_key,
+          width: this._windowWidthRounded,
         });
 
         // Some tours should complete instantly upon showing.
         if (tab.getAttribute("data-instant-complete")) {
           this.setToursCompleted([tourId]);
         }
       } else {
         tab.classList.remove("onboarding-active");
@@ -919,19 +1061,18 @@ class Onboarding {
       }]);
       return true;
     }
     let muteDuration = Services.prefs.getIntPref("browser.onboarding.notification.mute-duration-on-first-session-ms");
     return Date.now() - lastTourChangeTime <= muteDuration;
   }
 
   _isTimeForNextTourNotification(lastTourChangeTime) {
-    let promptCount = Services.prefs.getIntPref("browser.onboarding.notification.prompt-count", 0);
     let maxCount = Services.prefs.getIntPref("browser.onboarding.notification.max-prompt-count-per-tour");
-    if (promptCount >= maxCount) {
+    if (this._notificationPromptCount >= maxCount) {
       return true;
     }
 
     let maxTime = Services.prefs.getIntPref("browser.onboarding.notification.max-life-time-per-tour-ms");
     if (lastTourChangeTime && Date.now() - lastTourChangeTime >= maxTime) {
       return true;
     }
 
@@ -974,24 +1115,25 @@ class Onboarding {
         name: "browser.onboarding.notification.tour-ids-queue",
         value: queue
       }]);
     }
     return queue ? queue.split(",") : [];
   }
 
   showNotification() {
-    if (Services.prefs.getBoolPref("browser.onboarding.notification.finished", false)) {
+    if (this._notificationState === "finished") {
       return;
     }
 
     let lastTime = this._getLastTourChangeTime();
     if (this._muteNotificationOnFirstSession(lastTime)) {
       return;
     }
+
     // After the notification mute on the 1st session,
     // we don't want to show the speech bubble by default
     this._overlayIcon.classList.remove("onboarding-speech-bubble");
 
     let queue = this._getNotificationQueue();
     let totalMaxTime = Services.prefs.getIntPref("browser.onboarding.notification.max-life-time-all-tours-ms");
     if (lastTime && Date.now() - lastTime >= totalMaxTime) {
       // Reach total max life time for all tour notifications.
@@ -1007,17 +1149,17 @@ class Onboarding {
     // We don't want to prompt the completed tour.
     while (queue.length > 0 && this.isTourCompleted(queue[0])) {
       queue.shift();
     }
 
     if (queue.length == 0) {
       sendMessageToChrome("set-prefs", [
         {
-          name: "browser.onboarding.notification.finished",
+          name: NOTIFICATION_FINISHED_PREF,
           value: true
         },
         {
           name: "browser.onboarding.notification.tour-ids-queue",
           value: ""
         },
         {
           name: "browser.onboarding.state",
@@ -1039,51 +1181,63 @@ class Onboarding {
     let tourTitle = this._notificationBar.querySelector("#onboarding-notification-tour-title");
     tourTitle.textContent = notificationStrings.title;
     let tourMessage = this._notificationBar.querySelector("#onboarding-notification-tour-message");
     tourMessage.textContent = notificationStrings.message;
     this._notificationBar.classList.add("onboarding-opened");
     this._window.document.body.appendChild(this._notificationBar);
 
     let params = [];
+    let promptCount = 1;
     if (startQueueLength != queue.length) {
       // We just change tour so update the time, the count and the queue
       params.push({
         name: "browser.onboarding.notification.last-time-of-changing-tour-sec",
         value: Math.floor(Date.now() / 1000)
       });
       params.push({
         name: PROMPT_COUNT_PREF,
-        value: 1
+        value: promptCount
       });
       params.push({
         name: "browser.onboarding.notification.tour-ids-queue",
         value: queue.join(",")
       });
     } else {
-      let promptCount = Services.prefs.getIntPref(PROMPT_COUNT_PREF, 0);
+      promptCount = this._notificationPromptCount + 1;
       params.push({
         name: PROMPT_COUNT_PREF,
-        value: promptCount + 1
+        value: promptCount
       });
     }
     sendMessageToChrome("set-prefs", params);
     telemetry({
-      event: "notification-session-begin",
+      type: "notification-session-begin",
       session_key: this._session_key
     });
+    // since set-perfs is async, pass promptCount directly to avoid gathering the wrong
+    // notification_impression.
+    telemetry({
+      type: "notification-appear",
+      bubble_state: this._bubbleState,
+      current_tour_id: targetTourId,
+      logo_state: this._logoState,
+      notification_impression: promptCount,
+      notification_state: this._notificationState,
+      session_key: this._session_key,
+      width: this._windowWidthRounded,
+    });
   }
 
   hideNotification() {
     if (this._notificationBar) {
       if (this._notificationBar.classList.contains("onboarding-opened")) {
         this._notificationBar.classList.remove("onboarding-opened");
         telemetry({
-          event: "notification-session-end",
-          tour_id: this._notificationBar.dataset.targetTourId,
+          type: "notification-session-end",
           session_key: this._session_key,
         });
       }
     }
   }
 
   _renderNotificationBar() {
     let footer = this._window.document.createElement("footer");
@@ -1109,27 +1263,29 @@ class Onboarding {
       this._bundle.GetStringFromName("onboarding.notification-close-button-tooltip"));
     return footer;
   }
 
   skipTour() {
     this.setToursCompleted(this._tours.map(tour => tour.id));
     sendMessageToChrome("set-prefs", [
       {
-        name: "browser.onboarding.notification.finished",
+        name: NOTIFICATION_FINISHED_PREF,
         value: true
       },
       {
         name: "browser.onboarding.state",
         value: ICON_STATE_WATERMARK
       }
     ]);
     telemetry({
-      event: "overlay-skip-tour",
-      session_key: this._session_key
+      type: "overlay-skip-tour",
+      current_tour_id: this._activeTourId,
+      session_key: this._session_key,
+      width: this._windowWidthRounded,
     });
   }
 
   _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.
--- a/browser/extensions/onboarding/data_events.md
+++ b/browser/extensions/onboarding/data_events.md
@@ -11,111 +11,144 @@ The Onboarding system add-on sends 2 typ
 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"],
+  "category": ["onboarding-interactions"|"overlay-interactions"|"notification-interactions"],
   "client_id": "374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e",
   "locale": "en-US",
-  "event": ["onboarding_session" | "overlay_session" | "notification_session"],
+  "type": ["onboarding_session" | "overlay_session" | "notification_session"],
   "page": ["about:newtab" | "about:home"],
+  "parent_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}",
+  "root_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}",
   "session_begin": 1505440017018,
   "session_end": 1505440021992,
   "session_id": "{12dasd-213asda-213dkakj}",
-  "tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset'
-  "tour_source": ["default" | "watermark"],
   "tour_type" ["new" | "update"],
 
   // 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
 }
 ```
 
+| 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. This data is automatically appended by `ping-centre` module | :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 (e.g. en-US). | :two:
+| `page` | [Required] One of ["about:newtab", "about:home"]| :one:
+| `parent_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which parent session. Events happen upon overlay will have the `overlay session uuid` as its `parent_session_id`. Events happen upon notification will have the `notification session uuid` as its `parent_session_id`. | :one:
+| `root_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which root session. Every event will have the same `onboarding session uuid` as its `root_session_id` when interact in the same tab. | :one:
+| `session_begin` | [Required] Timestamp in (integer) milliseconds when onboarding/overlay/notification becoming visible. | :one:
+| `session_end` | [Required] 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. We will log different uuid when onboarding is inited/when the overlay is opened/when notification is shown. | :one:
+| `tour_type` | [Required] One of ["new", "update"] indicates the user is a `new` user or the `update` user upgrade from the older version | :one:
+| `type` | [Required] The type of event. Allowed event strings are defined in the below section | :one:
+| `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:
+
 # Example Onboarding `event` Log
 
 ```js
 {
   "addon_version": "1.0.0",
-  "category": ["overlay-interactions"|"notification-interactions"],
+  "bubble_state": ["bubble" | "dot" | "hide"],
+  "category": ["logo-interactions"|"overlay-interactions"|"notification-interactions"],
   "client_id": "374dc4d8-0cb2-4ac5-a3cf-c5a9bc3c602e",
-  "timestamp": 1505440017019,
-  "event": ["notification-cta-click" | "overlay-cta-click" | "overlay-nav-click" | "overlay-skip-tour"],
-  "impression": [0-8],
   "locale": "en-US",
+  "logo_state": ["logo" | "watermark"],
+  "notification_impression": [1-8],
+  "notification_state": ["show" | "hide" | "finished"],
   "page": ["about:newtab" | "about:home"],
-  "session_id": "{12dasd-213asda-213dkakj}",
+  "parent_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}",
+  "root_session_id": "{45cddbeb-2bec-4f3a-bada-fb87d4b79a6c}",
+  "current_tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset'
+  "target_tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset',
   "tour_id": ["onboarding-tour-private-browsing" | "onboarding-tour-addons"|...], // tour ids defined in 'onboardingTourset'
-  "tour_source": ["default" | "watermark"],
+  "timestamp": 1505440017019,
   "tour_type" ["new" | "update"],
+  "type": ["notification-cta-click" | "overlay-cta-click" | "overlay-nav-click" | "overlay-skip-tour"...],
+  "width": 950,
 
   // 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:
+| `bubble_state` | [Optional] | One of ["bubble", "dot", "hide"] indicates the current visual state of the speach bubble (content dialog besides the onboarding logo). | :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. This data is automatically appended 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:
+| `current_tour_id` | [Optional] id of the current tour. We put "" 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 (e.g. en-US). | :two:
+| `logo_state` | [Optional] One of ["logo", "watermark"] indicates the overlay is opened while in the default or the watermark state. | :one:
+| `notification_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:
+| `notification_state` | [Optional] One of ["show", "hide", "finished"] indicates the current notification bar state. | :one:
 | `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 the overlay is opened/when notification is shown. | :one:
-| `timestamp` | Timestamp in (integer) milliseconds when the event triggered | :one:
-| `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 | :one:
-| `tour_source` | [Required] One of ["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'. | :one:
+| `parent_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which parent session. Events happen upon overlay will have the `overlay session uuid` as its `parent_session_id`. Events happen upon notification will have the `notification session uuid` as its `parent_session_id`. | :one:
+| `root_session_id` | [Required] The unique identifier generated by `gUUIDGenerator` service to identify this event belongs to which root session. Every event will have the same `onboarding session uuid` as its `root_session_id` when interact in the same tab. | :one:
+| `target_tour_id` | [Optional] id of the target switched tour. We put "" when this field is not relevant to this event. | :one:
+| `timestamp` | [Required] Timestamp in (integer) milliseconds when the event triggered | :one:
 | `tour_type` | [Required] One of ["new", "update"] indicates the user is a `new` user or the `update` user upgrade from the older version | :one:
+| `type` | [Required] The type of event. Allowed event strings are defined in the below section | :one:
 | `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:
+| `width` | [Required] Current browser window width rounded by 50 pixels. Collecting rounded values reduces the risk to use these values to derive a unique user identifier. | :one:
 
 **Where:**
 
 :one: Firefox data
 :two: HTTP protocol data
 
-## Events
+## Event types
 
-Here are all allowed `event` strings that defined in `OnboardingTelemetry::EVENT_WHITELIST`.
-### Session events
+Here are all allowed event `type` strings that defined in `OnboardingTelemetry::EVENT_WHITELIST`.
+
+### Onboarding 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 destroyed. `onboarding-session` event is sent to the server. |
-| `onboarding-session` | event is sent when the onboarding script is destroyed |
+| `onboarding-logo-click` | event is triggered when a user clicks the logo to open the overlay. |
+| `onboarding-register-session` | internal event triggered when loading the onboarding module, will not send out any data. |
+| `onboarding-session` | event is sent when the tab unload to track the start and end time of the onboarding session. |
+| `onboarding-session-begin` | internal event triggered when the onboarding starts, will not send out any data. |
+| `onboarding-session-end` | internal event triggered when the onboarding ends, `onboarding-session` event is the actual event that send to the server. |
 
 ### 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 closes 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 clicking 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 clicking the overlay `skip tour` button |
+| `overlay-close-button-click` | event is triggered when a user clicks close overlay button. |
+| `overlay-close-outside-click` | event is triggered when a user clicks outside the overlay area to end the tour. |
+| `overlay-cta-click` | event is triggered when a user clicks overlay's Call-To-Action button. |
+| `overlay-current-tour` | event is sent when a tour is shown in the overlay. |
+| `overlay-nav-click` | event is sent when a user clicks a navigation button in the overlay. |
+| `overlay-session` | event is sent when an overlay is closed to track the start and end time of the overlay session. |
+| `overlay-session-begin` | internal event triggered when open the overlay, will not send out any data. |
+| `overlay-session-end` | internal event is triggered when an overlay session ends. `overlay-session` event is the actual event that send to the server. |
+| `overlay-skip-tour` | event is sent when a user clicks `Skip Tour` button in the overlay. |
 
 ### Notification events
 
 | EVENT | DESCRIPTION |
 |-----------|---------------------|
+| `notification-appear` | event is sent when a notification appears. |
+| `notification-close-button-click` | event is sent when a user clicks close notification button. |
+| `notification-cta-click` | event is sent when a user clicks the notification's Call-To-Action button. |
+| `notification-session` | event is sent when user closes the notification  to track the start and end time of the notification session. |
 | `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 closes the notification. `notification-session` event is sent to the server. |
-| `notification-session` | event is sent when user closes the notification |
-| `notification-close-button-click` | event is sent when clicking the notification close button |
-| `notification-cta-click` | event is sent when clicking the notification CTA button |
+| `notification-session-end` | internal event is triggered when a notification session ends. `notification-session` event is the actual event that send to the server. |