Created
March 16, 2012 20:27
-
-
Save gregglind/2052443 to your computer and use it in GitHub Desktop.
combined-study
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* version NEW */ | |
BaseClasses = require("study_base_classes.js"); | |
const ORIGINAL_TEST_ID = 100; | |
const MY_TEST_ID = "heatmap"; // We are on second run | |
/* Non-numeric IDs would be nicer but this is not supported in the extension | |
* yet. */ | |
/* Explanation of the schema: | |
* Schema is highly generic so that it can handle everything from toolbar | |
* customizations to mouse events to menu selections. | |
* | |
* Column name Meaning | |
* Event = study metadata, customization, or action? (Int code) | |
* Item = Top-level element: "File menu", "Url bar", "tab bar", | |
* etc. (String) | |
* Sub-item = Menu item name, or like "right scroll button", etc. | |
* (String) | |
* Interaction = Click, menu-pick, right-click, click-and-hold, | |
* keyboard shortcut, etc. (String) | |
* Timestamp = Milliseconds since epoch. (Long int) | |
*/ | |
const EVENT_CODES = { | |
METADATA: "metadata", | |
ACTION: "action", | |
MENU_HUNT: "meta_hunt", | |
CUSTOMIZE: "customize" | |
}; | |
var COMBINED_EXPERIMENT_COLUMNS = [ | |
{property: "event", type: BaseClasses.TYPE_STRING, displayName: "Event", | |
displayValue: ["Study Metadata", "Action", "Menu Hunt", "Customization"]}, | |
{property: "item", type: BaseClasses.TYPE_STRING, displayName: "Element"}, | |
{property: "sub_item", type: BaseClasses.TYPE_STRING, | |
displayName: "Sub-Element"}, | |
{property: "interaction_type", type: BaseClasses.TYPE_STRING, | |
displayName: "Interaction"}, | |
{property: "timestamp", type: BaseClasses.TYPE_DOUBLE, displayName: "Time", | |
displayValue: function(value) {return new Date(value).toLocaleString();}} | |
]; | |
exports.experimentInfo = { | |
startDate: null, // Null start date means we can start immediately. | |
duration: 7, // Days | |
testName: "HeatMap", | |
testId: MY_TEST_ID, | |
testInfoUrl: "https://testpilot.mozillalabs.com/testcases/ongoingheatmap", | |
summary: "We are studying how the changes to the toolbar and menu bar in the" | |
+ " Firefox 4 beta affect usage of the interface.", | |
thumbnail: null, | |
optInRequired: false, | |
recursAutomatically: false, | |
recurrenceInterval: 0, | |
versionNumber: 2, | |
minTPVersion: "1.0rc1", | |
minFXVersion: "4.0b1", | |
runOrNotFunc: function() { | |
//Don't run if on Firefox 4b10 and Mac OS 10.6.6. | |
let version = Cc["@mozilla.org/xre/app-info;1"] | |
.getService(Ci.nsIXULAppInfo).version; | |
let os = Cc["@mozilla.org/network/protocol;1?name=http"] | |
.getService(Ci.nsIHttpProtocolHandler).oscpu; | |
return (version != "4.0b10" || os.indexOf("Mac OS X 10.6") == -1); | |
} | |
}; | |
exports.dataStoreInfo = { | |
fileName: "testpilot_heatmap_results.sqlite", | |
tableName: "heatmap_results", | |
columns: COMBINED_EXPERIMENT_COLUMNS | |
}; | |
/* Window observer class - one is instantiated per window; most of what | |
* we observe in this study is per-window, so this class registers a LOT | |
* of listeners. | |
*/ | |
function CombinedWindowObserver(window) { | |
CombinedWindowObserver.baseConstructor.call(this, window); | |
}; | |
BaseClasses.extend(CombinedWindowObserver, | |
BaseClasses.GenericWindowObserver); | |
// Window observer class, helper functions: | |
CombinedWindowObserver.prototype.compareSearchTerms = function(searchTerm, | |
searchEngine) { | |
/* Are two successive searches done with the same search term? | |
* Are they with the same search engine or not? | |
* Don't record the search term or the search engine, just whether it's the | |
* same or not. */ | |
if (searchTerm == this._lastSearchTerm) { | |
if (searchEngine == this._lastSearchEngine) { | |
exports.handlers.record(EVENT_CODES.ACTION, "search bar", "", | |
"same search same engine"); | |
} else { | |
exports.handlers.record(EVENT_CODES.ACTION, "search bar", "", | |
"same search different engine"); | |
} | |
} | |
this._lastSearchTerm = searchTerm; | |
this._lastSearchEngine = searchEngine; | |
}; | |
CombinedWindowObserver.prototype.urlLooksMoreLikeSearch = function(url) { | |
/* Trying to tell whether user is inputting searches in the URL bar. | |
* Heuristic to tell whether a "url" is really a search term: | |
* If there are spaces in it, and/or it has no periods in it. | |
*/ | |
return ( (url.indexOf(" ") > -1) || (url.indexOf(".") == -1) ); | |
}; | |
CombinedWindowObserver.prototype.recordPanoramaState = function() { | |
/* Record panorama state - Record number of panorama tab groups, then | |
* record number of tabs in each group. */ | |
if (this.window.TabView._window) { | |
let gi = this.window.TabView._window.GroupItems; | |
exports.handlers.record(EVENT_CODES.CUSTOMIZE, "Panorama", "Num Groups:", | |
gi.groupItems.length); | |
for each (let g in gi.groupItems) { | |
exports.handlers.record(EVENT_CODES.CUSTOMIZE, "Panorama", | |
"Num Tabs In Group:", g._children.length); | |
} | |
// some tabs not affiliated with any group (called "orphans") | |
let numOrphans = gi.getOrphanedTabs().length; | |
exports.handlers.record(EVENT_CODES.CUSTOMIZE, "Panorama", | |
"Num Orphaned Tabs", numOrphans); | |
} else { | |
// If TabView is uninitialized, just record total # of tabs | |
// in the window instead. | |
let tabCount = this.window.getBrowser().tabContainer.itemCount; | |
exports.handlers.record(EVENT_CODES.CUSTOMIZE, "Window", | |
"Total Number of Tabs", tabCount); | |
} | |
}; | |
// Window observer class, main listener registration | |
CombinedWindowObserver.prototype.install = function() { | |
console.info("Starting to install listeners for combined window observer."); | |
let window = this.window; | |
// Helper function for recording actions | |
let record = function( item, subItem, interaction ) { | |
exports.handlers.record(EVENT_CODES.ACTION, item, subItem, interaction); | |
}; | |
/* Register menu listeners: | |
* 1. listen for mouse-driven command events on the main menu bar: */ | |
let mainMenuBar = window.document.getElementById("main-menubar"); | |
this._listen(mainMenuBar, "command", function(evt) { | |
let menuItemId = "unknown"; | |
let menuId = "unknown"; | |
if (evt.target.id) { | |
menuItemId = evt.target.id; | |
} | |
let node = evt.target; | |
while(node) { | |
if (node.tagName == "menupopup") { | |
menuId = node.id; | |
break; | |
} | |
if (node.id && menuItemId == "unknown") { | |
menuItemId = node.id; | |
} | |
node = node.parentNode; | |
} | |
record(menuId, menuItemId, "mouse"); | |
}, | |
true); | |
/* 2. Listen for keyboard shortcuts and mouse command events on the | |
* main command set: */ | |
let mainCommandSet = window.document.getElementById("mainCommandSet"); | |
this._listen(mainCommandSet, "command", function(evt) { | |
let tag = evt.sourceEvent.target; | |
if (tag.tagName == "menuitem") { | |
let menuItemId = tag.id?tag.id:tag.command; | |
let menuId = "unknown"; | |
let node = evt.sourceEvent.target; | |
while(node) { | |
if (node.tagName == "menupopup") { | |
menuId = node.id; | |
break; | |
} | |
node = node.parentNode; | |
} | |
record(menuId, menuItemId, "mouse"); | |
} else if (tag.tagName == "key") { | |
record("menus", tag.command?tag.command:tag.id, "key shortcut"); | |
}}, | |
true); | |
/* Intentionally omitted the code from the menu study that tracks | |
* number of menus hunted through and time spent hunting */ | |
// Record clicks in tab bar right-click context menu: | |
let tabContext = window.document.getElementById("tabContextMenu"); | |
this._listen(tabContext, "command", function(evt) { | |
if (evt.target && evt.target.id) { | |
record("tab context menu", evt.target.id, "click"); | |
if (evt.target.id == "context_pinTab" || | |
evt.target.id == "context_unpinTab") { | |
/* When you pin or unpin an app tab, record | |
* number of pinned tabs (number recorded is number | |
* BEFORE the change)*/ | |
let numAppTabs = window.gBrowser._numPinnedTabs; | |
exports.handlers.record(EVENT_CODES.CUSTOMIZE, | |
"Tab Bar", "Num App Tabs", | |
numAppTabs); | |
} | |
} | |
}, true); | |
// TODO: Other context menus? | |
// Register listeners on all the main toolbar buttons we care about: | |
let buttonIds = ["back-button", "forward-button", "reload-button", | |
"stop-button", "home-button", "feed-button", "star-button", | |
"identity-popup-more-info-button", | |
"back-forward-dropmarker", "security-button", | |
"downloads-button", "print-button", "bookmarks-button", | |
"history-button", "new-window-button", "tabview-button", | |
"cut-button", "copy-button", "paste-button", | |
"fullscreen-button"]; | |
for (let i = 0; i < buttonIds.length; i++) { | |
let id = buttonIds[i]; | |
let elem = window.document.getElementById(id); | |
if (!elem) { | |
// The element might not be there, if user customized it out | |
console.info("Can't install listener: no element with id " + id); | |
continue; | |
} | |
this._listen(elem, "mouseup", | |
function(evt) { | |
/* only count left button clicks and only on | |
* the element itself: */ | |
if (evt.target == elem && evt.button == 0) { | |
let tagName = evt.target.tagName; | |
/* There are a lot of spacer elements in the toolbar | |
* that we don't care about tracking individually: */ | |
if (tagName == "toolbarspacer" || | |
tagName == "toolbarspring" || | |
tagName == "toolbarseparator" || | |
tagName == "splitter" || | |
tagName == "hbox") { | |
id = "spacer"; | |
} else { | |
id = evt.target.id; | |
} | |
record(id, "", "click"); | |
} | |
}, false); | |
/* LONGTERM TODO: | |
* Problem with just listening for "mouseup" is that it triggers even | |
* if you clicked a greyed-out button... we really want something more | |
* like "button clicked". Try listening for "command"? */ | |
} | |
/* Listen on site ID button, see if page is SSL, or extended validation, | |
* or nothing. (TODO this is getting double-counted because it triggers | |
* again if you click to close; should trigger on popupshown or something.)*/ | |
let idBox = window.document.getElementById("identity-box"); | |
this._listen(idBox, "mouseup", function(evt) { | |
let idBoxClass = idBox.getAttribute("class"); | |
if (idBoxClass.indexOf("verifiedIdentity") > -1) { | |
record("site-id-button", "", "extended validation"); | |
} else if (idBoxClass.indexOf("verifiedDomain") > -1) { | |
record("site-id-button", "", "SSL"); | |
} else { | |
record("site-id-button", "", "none"); | |
} | |
}, false); | |
// Helper function for listening miscellaneous toolbar interactions | |
let self = this; | |
let register = function(elemId, event, item, subItem, interactionName) { | |
if (!self.window.document.getElementById(elemId)) { | |
console.info("Can't register " + elemId + ", no such element."); | |
return; | |
} | |
self._listen( self.window.document.getElementById(elemId), event, | |
function() { | |
record(item, subItem, interactionName); | |
}, false); | |
}; | |
// Observe item selection in the RSS feed drop down menu: | |
register( "feed-menu", "command", "rss icon", "menu item", "mouse pick"); | |
// Observe item selection in the search engine drop down menu: | |
register( "search-container", "popupshown", "search engine dropdown", | |
"menu item", "click"); | |
register( "search-container", "command", "search engine dropdown", | |
"menu item", "menu pick"); | |
/* Observe item selection in recent history menu - which you can get by | |
* clicking on the back button, forward button, and also (on Windows but | |
* not on Mac) the back-forward-dropmarker. */ | |
register( "back-forward-dropmarker", "command", "recent page dropdown", | |
"menu item", "mouse pick"); | |
this._listen(window.document.getElementById("back-button"), | |
"mouseup", function(evt) { | |
if (evt.originalTarget.tagName == "menuitem") { | |
record("back-button", "dropdown menu", "mouse pick"); | |
} | |
}, false); | |
this._listen(window.document.getElementById("forward-button"), | |
"mouseup", function(evt) { | |
if (evt.originalTarget.tagName == "menuitem") { | |
record("forward-button", "dropdown menu", "mouse pick"); | |
} | |
}, false); | |
// Observe clicks on bookmarks in the bookmarks toolbar | |
let bkmkToolbar = window.document.getElementById("personal-bookmarks"); | |
this._listen(bkmkToolbar, "mouseup", function(evt) { | |
if (evt.button == 0 && evt.target.tagName == "toolbarbutton") { | |
if (evt.target.id == "bookmarks-menu-button") { | |
record("bookmarks-menu-button", "", "click"); | |
} else { | |
record("bookmark toolbar", "personal bookmark", "click"); | |
} | |
}}, false); | |
// Observe clicks on the new unified Firefox menu button in the Windows beta | |
let firefoxButton = window.document.getElementById("appmenu-button"); | |
this._listen(firefoxButton, "mouseup", function(evt) { | |
let id = evt.target.id; | |
/* If the target event (i.e. the menu item) has an ID, then easy; just | |
* record that. The tricky part is all the elements with no ID... */ | |
if (!id) { | |
/* figure out which menu we're a child of, then decide | |
* what to record in place of the missing id. | |
* Recurse upwards (we might be in a sub-sub-sub-folder) | |
* until we hit something recognizable. */ | |
let parent = evt.target.parentNode; | |
while (!parent.id) { | |
parent = parent.parentNode; | |
if (!parent) { | |
record("appmenu-button", "unrecognized", "null"); | |
return; | |
} | |
} | |
switch( parent.id) { | |
case "appmenu_bookmarksMenupopup": | |
id = "User boomark item"; | |
break; | |
case "appmenu_historyMenupopup": | |
id = "User history item"; | |
break; | |
case "appmenu_recentlyClosedTabsMenupopup": | |
id = "Recently closed tab item"; | |
break; | |
case "appmenu_recentlyClosedWindowsMenupopup": | |
id = "Recently closed window item"; | |
break; | |
case "appmenu_developer_popup": | |
id = evt.target.label; | |
break; | |
case "appmenu_customizeMenu": | |
id = evt.target.label; | |
break; | |
default: | |
record("appmenu-button", "unrecognized", parent.id); | |
return; | |
} | |
} | |
record("appmenu-button", id, "click"); | |
}, false); | |
// Observe clicks on Feedback button | |
// TODO can we fold this into the generic button observer? | |
let feedbackToolbar = window.document.getElementById("feedback-menu-button"); | |
this._listen(feedbackToolbar, "mouseup", function(evt) { | |
record("feedback-toolbar", evt.target.id, "click"); | |
}, false); | |
/* Record clicks on new bookmark menu button; record "personal bookmark" | |
* rather than the name of the item picked */ | |
let bmkButton = window.document.getElementById("bookmarks-menu-button"); | |
this._listen(bmkButton, "mouseup", function(evt) { | |
record("bookmarks-menu-button", evt.target.id || "personal bookmark", "click"); | |
}, false); | |
// Listen on search bar ues by mouse and keyboard, including repeated | |
// searches (same engine or different engine?) | |
let searchBar = window.document.getElementById("searchbar"); | |
this._listen(searchBar, "keydown", function(evt) { | |
if (evt.keyCode == 13) { // Enter key | |
record("searchbar", "", "enter key"); | |
self.compareSearchTerms(searchBar.value, | |
searchBar.searchService.currentEngine.name); | |
} | |
}, false); | |
this._listen(searchBar, "mouseup", function(evt) { | |
if (evt.originalTarget.getAttribute("anonid") == "search-go-button") { | |
record("searchbar", "go button", "click"); | |
self.compareSearchTerms(searchBar.value, | |
searchBar.searchService.currentEngine.name); | |
} | |
}, false); | |
// Listen on URL bar: | |
let urlBar = window.document.getElementById("urlbar"); | |
this._listen(urlBar, "keydown", function(evt) { | |
if (evt.keyCode == 13) { // Enter key | |
if (self.urlLooksMoreLikeSearch(evt.originalTarget.value)) { | |
record("urlbar", "search term", "enter key"); | |
} else { | |
record("urlbar", "url", "enter key"); | |
} | |
} | |
}, false); | |
let urlGoButton = window.document.getElementById("go-button"); | |
this._listen(urlGoButton, "mouseup", function(evt) { | |
if (self.urlLooksMoreLikeSearch(urlBar.value)) { | |
record("urlbar", "search term", "go button click"); | |
} else { | |
record("urlbar", "url", "go button click"); | |
} | |
}, false); | |
/* Intentionally omitted: Code for observing individual mouseup/mousedown | |
* /change/select events in URL bar to distinguish click-and-insert, | |
* select-and-replace, or replace-all URL editing actions. */ | |
// Observe when the most-frequently-used menu in the URL bar is opened | |
this._listen(urlBar, "command", function(evt) { | |
if (evt.originalTarget.getAttribute("anonid") == "historydropmarker") { | |
record("urlbar", "most frequently used menu", "open"); | |
} | |
}, false); | |
/* TODO Get clicks on items in URL bar drop-down (or whether an awesomebar | |
* suggestion was hilighted when you hit enter?) */ | |
// Record Clicks on Scroll Buttons | |
let content = window.document.getElementById("content"); | |
this._listen(content, "mouseup", function(evt) { | |
if (evt.button == 0) { | |
let parent = evt.originalTarget.parentNode; | |
if (parent.tagName == "scrollbar") { | |
if (parent.parentNode.tagName == "HTML") { | |
let orientation = parent.getAttribute("orient"); | |
let widgetName = orientation + " scrollbar"; | |
let part = evt.originalTarget.tagName; | |
if (part == "xul:slider") { | |
// TODO can't distinguish slider from track... | |
record(widgetName, "slider", "drag"); | |
} else if (part == "xul:scrollbarbutton") { | |
let type = evt.originalTarget.getAttribute("type"); | |
if (type == "increment") { // vs. "decrement" | |
record(widgetName, "up scroll button", "click"); | |
} else { | |
record(widgetName, "down scroll button", "click"); | |
} | |
} | |
} | |
} | |
} | |
}, false); | |
// Record tab bar interactions | |
let tabBar = window.document.getElementById("TabsToolbar"); | |
this._listen(tabBar, "mouseup", function(evt) { | |
if (evt.button == 0) { | |
let targ = evt.originalTarget; | |
if (targ.id == "new-tab-button") { | |
record("tabbar", "new tab button", "click"); | |
} else if (targ.className == "tabs-newtab-button") { | |
record("tabbar", "new tab button", "click"); | |
} else if (targ.id == "alltabs-button") { | |
record("tabbar", "drop down menu", "click"); | |
} else { | |
switch (targ.getAttribute("anonid")) { | |
case "scrollbutton-up": | |
record("tabbar", "left scroll button", "mouseup"); | |
break; | |
case "scrollbutton-down": | |
record("tabbar", "right scroll button", "mouseup"); | |
break; | |
} | |
} | |
} | |
}, false); | |
// Record mouse-up and mouse-down on tab scroll buttons separately | |
// so that we can tell the difference between click vs click-and-hold | |
this._listen(tabBar, "mousedown", function(evt) { | |
if (evt.button == 0) { | |
let anonid = evt.originalTarget.getAttribute("anonid"); | |
if (anonid == "scrollbutton-up") { | |
record("tabbar", "left scroll button", "mouseup"); | |
} | |
if (anonid == "scrollbutton-down") { | |
record("tabbar", "right scroll button", "mouseup"); | |
} | |
} | |
}, false); | |
// Record picking an item from the tab drop down menu | |
this._listen(tabBar, "command", function(evt) { | |
if (evt.originalTarget.tagName == "menuitem") { | |
/* TODO this seems to get triggered when you edit | |
* something in about:config and click OK or cancel | |
* -- weird. */ | |
record("tabbar", "drop down menu", "menu pick"); | |
} | |
}, false); | |
/* LONGTERM TODO: | |
* Note we also get command events when you hit the tab scroll bars and | |
* they actually scroll (the tagName will be "xul:toolbarbutton") -- as | |
* opposed to moseup which triggers even if there's nowhere to scroll, this | |
* might be a more precise way to get that event. In fact look at using | |
* more command events on all the toolbar buttons...*/ | |
// Record opening of bookmark panel | |
let bkmkPanel = window.document.getElementById("editBookmarkPanel"); | |
this._listen(bkmkPanel, "popupshown", function(evt) { | |
record( "star-button", "edit bookmark panel", "panel open"); | |
}, false); | |
// Record clicks on "remove bookmark" button in bookmark panel: | |
this._listen(bkmkPanel, "command", function(evt) { | |
switch (evt.originalTarget.getAttribute("id")) { | |
case "editBookmarkPanelRemoveButton": | |
record( "star-button", "remove bookmark button", "click"); | |
break; | |
} | |
}, false); | |
// Record Tab view / panorama being shown/hidden: | |
// Try tabviewshown and tabviewhidden | |
this._listen(window, "tabviewshown", function(evt) { | |
record("Panorama", "Tab View Interface", "Opened"); | |
}, false); | |
let deck = window.document.getElementById("tab-view-deck"); | |
this._listen(deck, "tabviewhidden", function(evt) { | |
record("Panorama", "Tab View Interface", "Closed"); | |
// User has just finished interacting with Panorama, | |
// so record new number of tabs per group | |
self.recordPanoramaState(); | |
}, false); | |
// Record per-window customizations (tab-related): | |
record("window", exports.handlers.getNumWindows(), "new window opened"); | |
// Record number of app tabs: | |
exports.handlers.record(EVENT_CODES.CUSTOMIZE, "Tab Bar", "Num App Tabs", | |
window.gBrowser._numPinnedTabs); | |
// Record Panorama info - how many groups do you have right now, and how | |
// many tabs in each group? | |
this.recordPanoramaState(); | |
console.trace("Registering listeners complete.\n"); | |
}; | |
CombinedWindowObserver.prototype.uninstall = function() { | |
CombinedWindowObserver.superClass.uninstall.call(this); | |
exports.handlers.record(EVENT_CODES.ACTION, "window", | |
exports.handlers.getNumWindows(), | |
"window closed"); | |
}; | |
/* The global observer class, for things that we only want to observe once, | |
* rather than once-per-window. That mostly means observing toolbar | |
* customizations and other customizations and prefs. | |
*/ | |
function GlobalCombinedObserver() { | |
GlobalCombinedObserver.baseConstructor.call(this, CombinedWindowObserver); | |
} | |
BaseClasses.extend(GlobalCombinedObserver, BaseClasses.GenericGlobalObserver); | |
GlobalCombinedObserver.prototype.onExperimentStartup = function(store) { | |
GlobalCombinedObserver.superClass.onExperimentStartup.call(this, store); | |
// Record study version number. | |
this.record(EVENT_CODES.METADATA, "exp startup", "study version", | |
exports.experimentInfo.versionNumber); | |
/* The multiple Firefox Beta 4 Interface Studies are longitudial. | |
* The uploads need a shared GUID so we can match them up on the server. | |
* This is not supported by the extension yet so we do a hack right here. | |
* If there are multiple runs of the study, copy the | |
* GUID from the ORIGINAL run into my GUID -- (it's all just prefs). | |
* Now we can associate the different uploads with each other and with | |
* the survey upload.*/ | |
let prefs = require("preferences-service"); | |
let prefName = "extensions.testpilot.taskGUID." + ORIGINAL_TEST_ID; | |
let originalStudyGuid = prefs.get(prefName, ""); | |
if (originalStudyGuid != "") { | |
prefName = "extensions.testpilot.taskGUID." + MY_TEST_ID; | |
prefs.set(prefName, originalStudyGuid); | |
} | |
// Get the front browser window, use it to record customizations! | |
let wm = Cc["@mozilla.org/appshell/window-mediator;1"] | |
.getService(Ci.nsIWindowMediator); | |
let frontWindow = wm.getMostRecentWindow("navigator:browser"); | |
// Are tabs on top? | |
let toolbox = frontWindow.document.getElementById("navigator-toolbox"); | |
let tabPosition = (toolbox.getAttribute("tabsontop") == "true")?"true":"false"; | |
this.record(EVENT_CODES.CUSTOMIZE, "tab bar", "tabs on top?", tabPosition); | |
// Is the main menu bar hidden? (for unified Firefox Menu Bar on Windows) | |
let toolbarMenubar = frontWindow.document.getElementById("toolbar-menubar"); | |
let autohide = toolbarMenubar.getAttribute("autohide"); | |
this.record(EVENT_CODES.CUSTOMIZE, "menu bar", "hidden?", autohide); | |
// How many bookmarks in bookmark toolbar? Is bookmark toolbar shown? | |
let bkmkToolbar = frontWindow.document.getElementById("personal-bookmarks"); | |
let bkmks = bkmkToolbar.getElementsByClassName("bookmark-item"); | |
this.record(EVENT_CODES.CUSTOMIZE, "bookmark bar", "num. bookmarks", | |
bkmks.length); | |
this.record(EVENT_CODES.CUSTOMIZE, "bookmark bar", "hidden?", | |
"" + !!bkmkToolbar.parentNode.collapsed); | |
// Is status bar shown? | |
let statusBar = frontWindow.document.getElementById("status-bar"); | |
if (statusBar.getAttribute("hidden") == "true") { | |
this.record(EVENT_CODES.CUSTOMIZE, "status bar", "hidden?", "true"); | |
} else { | |
this.record(EVENT_CODES.CUSTOMIZE, "status bar", "hidden?", "false"); | |
} | |
// TODO Any change to toolbar buttons? (Copy code from toolbar study | |
// and see if user has added/removed/reoredered) | |
// Is Sync set up? What's the last time it synced? | |
let syncName = prefs.get("services.sync.username", ""); | |
this.record(EVENT_CODES.CUSTOMIZE, "Sync", "Configured?", | |
(syncName == "")?"False":"True"); | |
let lastSync = prefs.get("services.sync.lastSync", 0); | |
this.record(EVENT_CODES.CUSTOMIZE, "Sync", "Last Sync Time", lastSync); | |
}; | |
GlobalCombinedObserver.prototype.getNumWindows = function() { | |
return this._windowObservers.length; | |
}; | |
// Record app startup and shutdown events: | |
GlobalCombinedObserver.prototype.onAppStartup = function() { | |
GlobalCombinedObserver.superClass.onAppStartup.call(this); | |
this.record(EVENT_CODES.METADATA, "app", "", "startup"); | |
}; | |
GlobalCombinedObserver.prototype.onAppShutdown = function() { | |
GlobalCombinedObserver.superClass.onAppShutdown.call(this); | |
this.record(EVENT_CODES.METADATA, "app", "", "shutdown"); | |
}; | |
// Utility function for recording events: | |
GlobalCombinedObserver.prototype.record = function(event, item, subItem, | |
interactionType) { | |
dump("RECORDED: " + event + ", item: " + item + ", subItem: "+ subItem + ", interactionType: " + interactionType + "\n"); | |
if (!this.privateMode) { | |
// Make sure string columns are strings | |
if (typeof item != "string") { | |
item = item.toString(); | |
} | |
if (typeof subItem != "string") { | |
subItem = subItem.toString(); | |
} | |
if (typeof interactionType != "string") { | |
interactionType = interactionType.toString(); | |
} | |
this._store.storeEvent({ | |
event: event, | |
item: item, | |
sub_item: subItem, | |
interaction_type: interactionType, | |
timestamp: Date.now() | |
}); | |
// storeEvent can also take a callback, which we're not using here. | |
} | |
}; | |
exports.handlers = new GlobalCombinedObserver(); | |
// Web content | |
function CombinedStudyWebContent() { | |
CombinedStudyWebContent.baseConstructor.call(this, exports.experimentInfo); | |
} | |
BaseClasses.extend(CombinedStudyWebContent, BaseClasses.GenericWebContent); | |
CombinedStudyWebContent.prototype.__defineGetter__("dataViewExplanation", | |
function() { | |
return "This bar chart shows how often you used your 15 most frequently" | |
+ " used Firefox interface items."; | |
}); | |
CombinedStudyWebContent.prototype.__defineGetter__("dataCanvas", | |
function() { | |
return '<div class="dataBox"><h3>View Your Data:</h3>' + | |
this.dataViewExplanation + | |
this.rawDataLink + | |
'<div id="data-plot-div" style="width:480x;height:800px"></div>' + | |
this.saveButtons + '</div>'; | |
}); | |
CombinedStudyWebContent.prototype.__defineGetter__("inProgressHtml", | |
function() { | |
return '<h2>Thank you, Test Pilot!</h2>' + | |
'<p>The ' + this.titleLink + ' study is currently in progress.</p>' + | |
'<p>' + this.expInfo.summary + '</p>' + | |
'<p> The study will end in ' + this.expInfo.duration + ' days. ' + | |
'<ul><li>You can save your test graph or export the raw data now, or after you \ | |
submit your data.</li>' + this.thinkThereIsAnError + | |
'<li>If you don\'t want to submit your data this time, ' + | |
this.optOutLink + '.</li></ul>' + this.dataCanvas; | |
}); | |
/* Produce bar chart using flot lobrary; show 15 most frequently used items, | |
* sorted, in a bar chart. */ | |
CombinedStudyWebContent.prototype.onPageLoad = function(experiment, | |
document, | |
graphUtils) { | |
experiment.getDataStoreAsJSON(function(rawData) { | |
if (rawData.length == 0) { | |
return; | |
} | |
let stats = []; | |
let item; | |
let lastActionId; | |
for each( let row in rawData) { | |
if (row.event != EVENT_CODES.ACTION) { | |
continue; | |
} | |
// Skip the text selection events, they're not interesting | |
if (row.item == "urlbar" && row.sub_item == "text selection") { | |
continue; | |
} | |
// for window open/close we care about the interaction type more | |
// than the sub item. | |
if (row.item == "window") { | |
row.sub_item = row.interaction_type; | |
} | |
let match = false; | |
for (x in stats) { | |
if (stats[x].item == row.item && stats[x].sub_item == row.sub_item) { | |
match = true; | |
stats[x].quantity ++; | |
break; | |
} | |
} | |
if (!match) { | |
stats.push( {item: row.item, sub_item: row.sub_item, quantity: 1} ); | |
} | |
} | |
let numItems = stats.length<15?stats.length:15; | |
let d1 = []; | |
let yAxisLabels = []; | |
for (let i = 0; i < numItems; i++) { | |
let item = stats[i]; | |
d1.push([item.quantity, i - 0.5]); | |
let labelText = (item.item + ": <br/>" + item.sub_item).toLowerCase(); | |
yAxisLabels.push([i, labelText]); | |
} | |
try { | |
let plotDiv = document.getElementById("data-plot-div"); | |
if (plotDiv == null) | |
return; | |
graphUtils.plot(plotDiv, [{data: d1}], | |
{series: {bars: {show: true, horizontal: true}}, | |
yaxis: {ticks: yAxisLabels}, | |
xaxis: {tickDecimals: 0}}); | |
} catch(e) { | |
console.warn("Problem with graphutils: " + e + "\n"); | |
} | |
}); | |
}; | |
exports.webContent = new CombinedStudyWebContent(); | |
// Cleanup | |
require("unload").when( | |
function myDestructor() { | |
console.info("Combined study destructor called."); | |
exports.handlers.uninstallAll(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment