Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
feat(mc): Implement TelemetryFeed.jsm to replace tab tracker
Browse files Browse the repository at this point in the history
This patch adds a TelemetryFeed, which tracks several different kinds of
events, including sessions. It replaces the functionality our old Tab
Tracker code used to handle.
  • Loading branch information
k88hudson committed Apr 28, 2017
1 parent 7e44239 commit d661e2d
Show file tree
Hide file tree
Showing 18 changed files with 880 additions and 203 deletions.
2 changes: 1 addition & 1 deletion karma.mc.config.js
Expand Up @@ -80,7 +80,7 @@ module.exports = function(config) {
loader: "istanbul-instrumenter-loader",
include: [path.resolve("system-addon")],
exclude: [
/\.test\.js$/,
path.resolve("system-addon/test/"),
path.resolve("system-addon/vendor")
]
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -53,6 +53,7 @@
"github": "8.1.1",
"husky": "0.11.9",
"istanbul-instrumenter-loader": "0.2.0",
"joi-browser": "10.0.6",
"jpm": "1.2.2",
"karma": "1.3.0",
"karma-chai": "0.1.0",
Expand Down
86 changes: 77 additions & 9 deletions system-addon/common/Actions.jsm
Expand Up @@ -3,19 +3,34 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const MAIN_MESSAGE_TYPE = "ActivityStream:Main";
const CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
this.UI_CODE = 1;
this.BACKGROUND_PROCESS = 2;

/**
* globalImportContext - Are we in UI code (i.e. react, a dom) or some kind of background process?
* Use this in action creators if you need different logic
* for ui/background processes.
*/
const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
// Export for tests
this.globalImportContext = globalImportContext;

const actionTypes = [
"INIT",
"UNINIT",
"NEW_TAB_INITIAL_STATE",
"NEW_TAB_LOAD",
"NEW_TAB_UNLOAD",
"NEW_TAB_VISIBLE",
"PERFORM_SEARCH",
"SCREENSHOT_UPDATED",
"SEARCH_STATE_UPDATED",
"TOP_SITES_UPDATED"
"TELEMETRY_PERFORMANCE_EVENT",
"TELEMETRY_UNDESIRED_EVENT",
"TELEMETRY_USER_EVENT",
"TOP_SITES_UPDATED",
"UNINIT"
// The line below creates an object like this:
// {
// INIT: "INIT",
Expand Down Expand Up @@ -48,14 +63,14 @@ function _RouteMessage(action, options) {
*
* @param {object} action Any redux action (required)
* @param {object} options
* @param {string} options.fromTarget The id of the content port from which the action originated. (optional)
* @param {string} fromTarget The id of the content port from which the action originated. (optional)
* @return {object} An action with added .meta properties
*/
function SendToMain(action, options = {}) {
function SendToMain(action, fromTarget) {
return _RouteMessage(action, {
from: CONTENT_MESSAGE_TYPE,
to: MAIN_MESSAGE_TYPE,
fromTarget: options.fromTarget
fromTarget
});
}

Expand Down Expand Up @@ -90,12 +105,59 @@ function SendToContent(action, target) {
});
}

/**
* UserEvent - A telemetry ping indicating a user action. This should only
* be sent from the UI during a user session.
*
* @param {object} data Fields to include in the ping (source, etc.)
* @return {object} An SendToMain action
*/
function UserEvent(data) {
return SendToMain({
type: actionTypes.TELEMETRY_USER_EVENT,
data
});
}

/**
* UndesiredEvent - A telemetry ping indicating an undesired state.
*
* @param {object} data Fields to include in the ping (value, etc.)
* @param {int} importContext (For testing) Override the import context for testing.
* @return {object} An action. For UI code, a SendToMain action.
*/
function UndesiredEvent(data, importContext = globalImportContext) {
const action = {
type: actionTypes.TELEMETRY_UNDESIRED_EVENT,
data
};
return importContext === UI_CODE ? SendToMain(action) : action;
}

/**
* PerfEvent - A telemetry ping indicating a performance-related event.
*
* @param {object} data Fields to include in the ping (value, etc.)
* @param {int} importContext (For testing) Override the import context for testing.
* @return {object} An action. For UI code, a SendToMain action.
*/
function PerfEvent(data, importContext = globalImportContext) {
const action = {
type: actionTypes.TELEMETRY_PERFORMANCE_EVENT,
data
};
return importContext === UI_CODE ? SendToMain(action) : action;
}

this.actionTypes = actionTypes;

this.actionCreators = {
SendToMain,
BroadcastToContent,
UserEvent,
UndesiredEvent,
PerfEvent,
SendToContent,
BroadcastToContent
SendToMain
};

// These are helpers to test for certain kinds of actions
Expand Down Expand Up @@ -124,13 +186,19 @@ this.actionUtils = {
}
return false;
},
getPortIdOfSender(action) {
return (action.meta && action.meta.fromTarget) || null;
},
_RouteMessage
};

this.EXPORTED_SYMBOLS = [
"actionTypes",
"actionCreators",
"actionUtils",
"globalImportContext",
"UI_CODE",
"BACKGROUND_PROCESS",
"MAIN_MESSAGE_TYPE",
"CONTENT_MESSAGE_TYPE"
];
28 changes: 24 additions & 4 deletions system-addon/common/Reducers.jsm
Expand Up @@ -6,20 +6,40 @@
const {actionTypes: at} = Components.utils.import("resource://activity-stream/common/Actions.jsm", {});

const INITIAL_STATE = {
App: {
// Have we received real data from the app yet?
initialized: false,
// The version of the system-addon
version: null,
// The locale of the browser
locale: null
},
TopSites: {
init: false,
// Have we received real data from history yet?
initialized: false,
// The history (and possibly default) links
rows: []
},
Search: {
// The search engine currently set by the browser
currentEngine: {
name: "",
icon: ""
},
// All possible search engines
engines: []
}
};

// TODO: Handle some real actions here, once we have a TopSites feed working
function App(prevState = INITIAL_STATE.App, action) {
switch (action.type) {
case at.INIT:
return Object.assign({}, action.data || {}, {initialized: true});
default:
return prevState;
}
}

function TopSites(prevState = INITIAL_STATE.TopSites, action) {
let hasMatch;
let newRows;
Expand All @@ -28,7 +48,7 @@ function TopSites(prevState = INITIAL_STATE.TopSites, action) {
if (!action.data) {
return prevState;
}
return Object.assign({}, prevState, {init: true, rows: action.data});
return Object.assign({}, prevState, {initialized: true, rows: action.data});
case at.SCREENSHOT_UPDATED:
newRows = prevState.rows.map(row => {
if (row.url === action.data.url) {
Expand Down Expand Up @@ -60,6 +80,6 @@ function Search(prevState = INITIAL_STATE.Search, action) {
}
}
this.INITIAL_STATE = INITIAL_STATE;
this.reducers = {TopSites, Search};
this.reducers = {TopSites, App, Search};

this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
4 changes: 3 additions & 1 deletion system-addon/content-src/activity-stream.jsx
@@ -1,10 +1,12 @@
/* globals addMessageListener, removeMessageListener */
const React = require("react");
const ReactDOM = require("react-dom");
const Base = require("content-src/components/Base/Base");
const {Provider} = require("react-redux");
const initStore = require("content-src/lib/init-store");
const {reducers} = require("common/Reducers.jsm");
const DetectUserSessionStart = require("content-src/lib/detect-user-session-start");

new DetectUserSessionStart().sendEventOrAddListener();

const store = initStore(reducers);

Expand Down
50 changes: 50 additions & 0 deletions system-addon/content-src/lib/detect-user-session-start.js
@@ -0,0 +1,50 @@
const {actionTypes: at} = require("common/Actions.jsm");

const VISIBLE = "visible";
const VISIBILITY_CHANGE_EVENT = "visibilitychange";

module.exports = class DetectUserSessionStart {
constructor(options = {}) {
// Overrides for testing
this.sendAsyncMessage = options.sendAsyncMessage || window.sendAsyncMessage;
this.document = options.document || document;

this._onVisibilityChange = this._onVisibilityChange.bind(this);
}

/**
* sendEventOrAddListener - Notify immediately if the page is already visible,
* or else set up a listener for when visibility changes.
* This is needed for accurate session tracking for telemetry,
* because tabs are pre-loaded.
*/
sendEventOrAddListener() {
if (this.document.visibilityState === VISIBLE) {
// If the document is already visible, to the user, send a notification
// immediately that a session has started.
this._sendEvent();
} else {
// If the document is not visible, listen for when it does become visible.
this.document.addEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
}
}

/**
* _sendEvent - Sends a message to the main process to indicate the current tab
* is now visible to the user.
*/
_sendEvent() {
this.sendAsyncMessage("ActivityStream:ContentToMain", {type: at.NEW_TAB_VISIBLE});
}

/**
* _onVisibilityChange - If the visibility has changed to visible, sends a notification
* and removes the event listener. This should only be called once per tab.
*/
_onVisibilityChange() {
if (this.document.visibilityState === VISIBLE) {
this._sendEvent();
this.document.removeEventListener(VISIBILITY_CHANGE_EVENT, this._onVisibilityChange);
}
}
};
23 changes: 18 additions & 5 deletions system-addon/lib/ActivityStream.jsm
@@ -1,7 +1,8 @@
/* 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 XPCOMUtils, NewTabInit, TopSitesFeed, SearchFeed */

/* globals XPCOMUtils, NewTabInit, TopSitesFeed, SearchFeed, TelemetryFeed */

"use strict";

Expand All @@ -13,11 +14,16 @@ const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.j
// Feeds
XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
"resource://activity-stream/lib/NewTabInit.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
"resource://activity-stream/lib/TopSitesFeed.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
"resource://activity-stream/lib/SearchFeed.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFeed",
"resource://activity-stream/lib/TelemetryFeed.jsm");

const feeds = {
// When you add a feed here:
// 1. The key in this object should directly refer to a pref, not including the
Expand All @@ -27,8 +33,9 @@ const feeds = {
// 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
// so it isn't loaded until the feed is enabled.
"feeds.newtabinit": () => new NewTabInit(),
"feeds.topsites": () => new TopSitesFeed(),
"feeds.search": () => new SearchFeed()
"feeds.search": () => new SearchFeed(),
"feeds.telemetry": () => new TelemetryFeed(),
"feeds.topsites": () => new TopSitesFeed()
};

this.ActivityStream = class ActivityStream {
Expand All @@ -41,7 +48,7 @@ this.ActivityStream = class ActivityStream {
* @param {string} options.version Version of the add-on. e.g. "0.1.0"
* @param {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
*/
constructor(options) {
constructor(options = {}) {
this.initialized = false;
this.options = options;
this.store = new Store();
Expand All @@ -50,7 +57,13 @@ this.ActivityStream = class ActivityStream {
init() {
this.initialized = true;
this.store.init(this.feeds);
this.store.dispatch({type: at.INIT});
this.store.dispatch({
type: at.INIT,
data: {
version: this.options.version,
locale: null // TODO
}
});
}
uninit() {
this.store.dispatch({type: at.UNINIT});
Expand Down
2 changes: 1 addition & 1 deletion system-addon/lib/ActivityStreamMessageChannel.jsm
Expand Up @@ -89,7 +89,7 @@ this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
* @param {string} targetId The portID of the port that sent the message
*/
onActionFromContent(action, targetId) {
this.dispatch(ac.SendToMain(action, {fromTarget: targetId}));
this.dispatch(ac.SendToMain(action, targetId));
}

/**
Expand Down

0 comments on commit d661e2d

Please sign in to comment.