Bug 952568 - [australis-measuring] Determine if Australis Update experience tab is/was active. r=mconley,MattN

This commit is contained in:
Blair McBride 2014-02-07 16:19:12 +13:00
parent 279a231dd6
commit 621bbde00a
7 changed files with 436 additions and 14 deletions

View File

@ -1092,8 +1092,13 @@
// Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
if (!this._previewMode) {
// We've selected the new tab, so go ahead and notify listeners.
let event = document.createEvent("Events");
event.initEvent("TabSelect", true, false);
let event = new CustomEvent("TabSelect", {
bubbles: true,
cancelable: false,
detail: {
previousTab: oldTab
}
});
this.mCurrentTab.dispatchEvent(event);
this._tabAttrModified(oldTab);

View File

@ -17,6 +17,15 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyGetter(this, "Timer", function() {
let timer = {};
Cu.import("resource://gre/modules/Timer.jsm", timer);
return timer;
});
const MS_SECOND = 1000;
const MS_MINUTE = MS_SECOND * 60;
const MS_HOUR = MS_MINUTE * 60;
XPCOMUtils.defineLazyGetter(this, "DEFAULT_AREA_PLACEMENTS", function() {
let result = {
@ -149,6 +158,14 @@ const MOUSEDOWN_MONITORED_ITEMS = [
// lasted.
const WINDOW_DURATION_MAP = new WeakMap();
// Default bucket name, when no other bucket is active.
const BUCKET_DEFAULT = "__DEFAULT__";
// Bucket prefix, for named buckets.
const BUCKET_PREFIX = "bucket_";
// Standard separator to use between different parts of a bucket name, such
// as primary name and the time step string.
const BUCKET_SEPARATOR = "|";
this.BrowserUITelemetry = {
init: function() {
UITelemetry.addSimpleMeasureFunction("toolbars",
@ -204,6 +221,7 @@ this.BrowserUITelemetry = {
_ensureObjectChain: function(aKeys, aEndWith) {
let current = this._countableEvents;
let parent = null;
aKeys.unshift(this._bucket);
for (let [i, key] of Iterator(aKeys)) {
if (!(key in current)) {
if (i == aKeys.length - 1) {
@ -538,17 +556,153 @@ this.BrowserUITelemetry = {
durationMap = {};
WINDOW_DURATION_MAP.set(aWindow, durationMap);
}
durationMap.customization = aWindow.performance.now();
durationMap.customization = {
start: aWindow.performance.now(),
bucket: this._bucket,
};
},
onCustomizeEnd: function(aWindow) {
let durationMap = WINDOW_DURATION_MAP.get(aWindow);
if (durationMap && "customization" in durationMap) {
let duration = aWindow.performance.now() - durationMap.customization;
this._durations.customization.push(duration);
let duration = aWindow.performance.now() - durationMap.customization.start;
this._durations.customization.push({
duration: duration,
bucket: durationMap.customization.bucket,
});
delete durationMap.customization;
}
},
_bucket: BUCKET_DEFAULT,
_bucketTimer: null,
/**
* Default bucket name, when no other bucket is active.
*/
get BUCKET_DEFAULT() BUCKET_DEFAULT,
/**
* Bucket prefix, for named buckets.
*/
get BUCKET_PREFIX() BUCKET_PREFIX,
/**
* Standard separator to use between different parts of a bucket name, such
* as primary name and the time step string.
*/
get BUCKET_SEPARATOR() BUCKET_SEPARATOR,
get currentBucket() {
return this._bucket;
},
/**
* Sets a named bucket for all countable events and select durections to be
* put into.
*
* @param aName Name of bucket, or null for default bucket name (__DEFAULT__)
*/
setBucket: function(aName) {
if (this._bucketTimer) {
Timer.clearTimeout(this._bucketTimer);
this._bucketTimer = null;
}
if (aName)
this._bucket = BUCKET_PREFIX + aName;
else
this._bucket = BUCKET_DEFAULT;
},
/**
* Sets a bucket that expires at the rate of a given series of time steps.
* Once the bucket expires, the current bucket will automatically revert to
* the default bucket. While the bucket is expiring, it's name is postfixed
* by '|' followed by a short string representation of the time step it's
* currently in.
* If any other bucket (expiring or normal) is set while an expiring bucket is
* still expiring, the old expiring bucket stops expiring and the new bucket
* immediately takes over.
*
* @param aName Name of bucket.
* @param aTimeSteps An array of times in milliseconds to count up to before
* reverting back to the default bucket. The array of times
* is expected to be pre-sorted in ascending order.
* For example, given a bucket name of 'bucket', the times:
* [60000, 300000, 600000]
* will result in the following buckets:
* * bucket|1m - for the first 1 minute
* * bucket|5m - for the following 4 minutes
* (until 5 minutes after the start)
* * bucket|10m - for the following 5 minutes
* (until 10 minutes after the start)
* * __DEFAULT__ - until a new bucket is set
* @param aTimeOffset Time offset, in milliseconds, from which to start
* counting. For example, if the first time step is 1000ms,
* and the time offset is 300ms, then the next time step
* will become active after 700ms. This affects all
* following time steps also, meaning they will also all be
* timed as though they started expiring 300ms before
* setExpiringBucket was called.
*/
setExpiringBucket: function(aName, aTimeSteps, aTimeOffset = 0) {
if (aTimeSteps.length === 0) {
this.setBucket(null);
return;
}
if (this._bucketTimer) {
Timer.clearTimeout(this._bucketTimer);
this._bucketTimer = null;
}
// Make a copy of the time steps array, so we can safely modify it without
// modifying the original array that external code has passed to us.
let steps = [...aTimeSteps];
let msec = steps.shift();
let postfix = this._toTimeStr(msec);
this.setBucket(aName + BUCKET_SEPARATOR + postfix);
this._bucketTimer = Timer.setTimeout(() => {
this._bucketTimer = null;
this.setExpiringBucket(aName, steps, aTimeOffset + msec);
}, msec - aTimeOffset);
},
/**
* Formats a time interval, in milliseconds, to a minimal non-localized string
* representation. Format is: 'h' for hours, 'm' for minutes, 's' for seconds,
* 'ms' for milliseconds.
* Examples:
* 65 => 65ms
* 1000 => 1s
* 60000 => 1m
* 61000 => 1m01s
*
* @param aTimeMS Time in milliseconds
*
* @return Minimal string representation.
*/
_toTimeStr: function(aTimeMS) {
let timeStr = "";
function reduce(aUnitLength, aSymbol) {
if (aTimeMS >= aUnitLength) {
let units = Math.floor(aTimeMS / aUnitLength);
aTimeMS = aTimeMS - (units * aUnitLength)
timeStr += units + aSymbol;
}
}
reduce(MS_HOUR, "h");
reduce(MS_MINUTE, "m");
reduce(MS_SECOND, "s");
reduce(1, "ms");
return timeStr;
},
};
/**

View File

@ -18,14 +18,30 @@ XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
"resource://gre/modules/PermissionsUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
"resource://gre/modules/UITelemetry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
"resource:///modules/BrowserUITelemetry.jsm");
const UITOUR_PERMISSION = "uitour";
const PREF_PERM_BRANCH = "browser.uitour.";
const MAX_BUTTONS = 4;
const BUCKET_NAME = "UITour";
const BUCKET_TIMESTEPS = [
1 * 60 * 1000, // Until 1 minute after tab is closed/inactive.
3 * 60 * 1000, // Until 3 minutes after tab is closed/inactive.
10 * 60 * 1000, // Until 10 minutes after tab is closed/inactive.
60 * 60 * 1000, // Until 1 hour after tab is closed/inactive.
];
this.UITour = {
seenPageIDs: new Set(),
pageIDSourceTabs: new WeakMap(),
pageIDSourceWindows: new WeakMap(),
originTabs: new WeakMap(),
pinnedTabs: new WeakMap(),
urlbarCapture: new WeakMap(),
@ -80,7 +96,7 @@ this.UITour = {
let element = aDocument.getAnonymousElementByAttribute(selectedtab,
"anonid",
"tab-icon-image");
if (!element || !_isElementVisible(element)) {
if (!element || !this.isElementVisible(element)) {
return null;
}
return element;
@ -92,6 +108,11 @@ this.UITour = {
}],
]),
init: function() {
UITelemetry.addSimpleMeasureFunction("UITour",
this.getTelemetry.bind(this));
},
onPageEvent: function(aEvent) {
let contentDocument = null;
if (aEvent.target instanceof Ci.nsIDOMHTMLDocument)
@ -117,8 +138,26 @@ this.UITour = {
return false;
let window = this.getChromeWindow(contentDocument);
let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
switch (action) {
case "registerPageID": {
// We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the
// pageID, as it could make parsing the telemetry bucket name difficult.
if (typeof data.pageID == "string" &&
!data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
this.seenPageIDs.add(data.pageID);
// Store tabs and windows separately so we don't need to loop over all
// tabs when a window is closed.
this.pageIDSourceTabs.set(tab, data.pageID);
this.pageIDSourceWindows.set(window, data.pageID);
this.setTelemetryBucket(data.pageID);
}
break;
}
case "showHighlight": {
let targetPromise = this.getTarget(window, data.target);
targetPromise.then(target => {
@ -258,7 +297,6 @@ this.UITour = {
}
}
let tab = window.gBrowser._getTabForContentWindow(contentDocument.defaultView);
if (!this.originTabs.has(window))
this.originTabs.set(window, new Set());
this.originTabs.get(window).add(tab);
@ -279,12 +317,34 @@ this.UITour = {
}
case "TabClose": {
let window = aEvent.target.ownerDocument.defaultView;
let tab = aEvent.target;
if (this.pageIDSourceTabs.has(tab)) {
let pageID = this.pageIDSourceTabs.get(tab);
// Delete this from the window cache, so if the window is closed we
// don't expire this page ID twice.
let window = tab.ownerDocument.defaultView;
if (this.pageIDSourceWindows.get(window) == pageID)
this.pageIDSourceWindows.delete(window);
this.setExpiringTelemetryBucket(pageID, "closed");
}
let window = tab.ownerDocument.defaultView;
this.teardownTour(window);
break;
}
case "TabSelect": {
if (aEvent.detail && aEvent.detail.previousTab) {
let previousTab = aEvent.detail.previousTab;
if (this.pageIDSourceTabs.has(previousTab)) {
let pageID = this.pageIDSourceTabs.get(previousTab);
this.setExpiringTelemetryBucket(pageID, "inactive");
}
}
let window = aEvent.target.ownerDocument.defaultView;
let pinnedTab = this.pinnedTabs.get(window);
if (pinnedTab && pinnedTab.tab == window.gBrowser.selectedTab)
@ -299,6 +359,11 @@ this.UITour = {
case "SSWindowClosing": {
let window = aEvent.target;
if (this.pageIDSourceWindows.has(window)) {
let pageID = this.pageIDSourceWindows.get(window);
this.setExpiringTelemetryBucket(pageID, "closed");
}
this.teardownTour(window, true);
break;
}
@ -321,6 +386,25 @@ this.UITour = {
}
},
setTelemetryBucket: function(aPageID) {
let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID;
BrowserUITelemetry.setBucket(bucket);
},
setExpiringTelemetryBucket: function(aPageID, aType) {
let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID +
BrowserUITelemetry.BUCKET_SEPARATOR + aType;
BrowserUITelemetry.setExpiringBucket(bucket,
BUCKET_TIMESTEPS);
},
getTelemetry: function() {
return {
seenPageIDs: [...this.seenPageIDs],
};
},
teardownTour: function(aWindow, aWindowClosing = false) {
aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
aWindow.PanelUI.panel.removeEventListener("popuphiding", this.onAppMenuHiding);
@ -425,6 +509,11 @@ this.UITour = {
aDocument.dispatchEvent(event);
},
isElementVisible: function(aElement) {
let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
return (targetStyle.display != "none" && targetStyle.visibility == "visible");
},
getTarget: function(aWindow, aTargetName, aSticky = false) {
let deferred = Promise.defer();
if (typeof aTargetName != "string" || !aTargetName) {
@ -621,7 +710,7 @@ this.UITour = {
}
// Prevent showing a panel at an undefined position.
if (!_isElementVisible(aTarget.node))
if (!this.isElementVisible(aTarget.node))
return;
this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
@ -694,7 +783,7 @@ this.UITour = {
}
// Prevent showing a panel at an undefined position.
if (!_isElementVisible(aAnchor.node))
if (!this.isElementVisible(aAnchor.node))
return;
this._setAppMenuStateForAnnotation(aAnchor.node.ownerDocument.defaultView, "info",
@ -833,7 +922,4 @@ this.UITour = {
},
};
function _isElementVisible(aElement) {
let targetStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
return (targetStyle.display != "none" && targetStyle.visibility == "visible");
}
this.UITour.init();

View File

@ -4,6 +4,7 @@ support-files =
uitour.*
image.png
[browser_BrowserUITelemetry_buckets.js]
[browser_NetworkPrioritizer.js]
[browser_SignInToWebsite.js]
[browser_UITour.js]
@ -11,6 +12,7 @@ skip-if = os == "linux" # Intermittent failures, bug 951965
[browser_UITour2.js]
[browser_UITour3.js]
[browser_UITour_panel_close_annotation.js]
[browser_UITour_registerPageID.js]
[browser_UITour_sync.js]
[browser_taskbar_preview.js]
run-if = os == "win"

View File

@ -0,0 +1,102 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
WHERE'S MAH BUCKET?!
\
___
.-9 9 `\
=(:(::)= ;
|||| \
|||| `-.
,\|\| `,
/ \
; `'---.,
| `\
; / |
\ | /
) \ __,.--\ /
.-' \,..._\ \` .-' .-'
`-=`` `: | /-/-/`
`.__/
*/
"use strict";
function generatorTest() {
let s = {};
Components.utils.import("resource:///modules/BrowserUITelemetry.jsm", s);
let BUIT = s.BrowserUITelemetry;
registerCleanupFunction(function() {
BUIT.setBucket(null);
});
// setBucket
is(BUIT.currentBucket, BUIT.BUCKET_DEFAULT, "Bucket should be default bucket");
BUIT.setBucket("mah-bucket");
is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "mah-bucket", "Bucket should have correct name");
BUIT.setBucket(null);
is(BUIT.currentBucket, BUIT.BUCKET_DEFAULT, "Bucket should be reset to default");
// _toTimeStr
is(BUIT._toTimeStr(10), "10ms", "Checking time string reprentation, 10ms");
is(BUIT._toTimeStr(1000 + 10), "1s10ms", "Checking time string reprentation, 1s10ms");
is(BUIT._toTimeStr((20 * 1000) + 10), "20s10ms", "Checking time string reprentation, 20s10ms");
is(BUIT._toTimeStr(60 * 1000), "1m", "Checking time string reprentation, 1m");
is(BUIT._toTimeStr(3 * 60 * 1000), "3m", "Checking time string reprentation, 3m");
is(BUIT._toTimeStr((3 * 60 * 1000) + 1), "3m1ms", "Checking time string reprentation, 3m1ms");
is(BUIT._toTimeStr((60 * 60 * 1000) + (10 * 60 * 1000)), "1h10m", "Checking time string reprentation, 1h10m");
is(BUIT._toTimeStr(100 * 60 * 60 * 1000), "100h", "Checking time string reprentation, 100h");
// setExpiringBucket
BUIT.setExpiringBucket("walrus", [1001, 2001, 3001, 10001]);
is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "walrus" + BUIT.BUCKET_SEPARATOR + "1s1ms",
"Bucket should be expiring and have time step of 1s1ms");
waitForCondition(function() {
return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "walrus" + BUIT.BUCKET_SEPARATOR + "2s1ms");
}, nextStep, "Bucket should be expiring and have time step of 2s1ms");
yield undefined;
waitForCondition(function() {
return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "walrus" + BUIT.BUCKET_SEPARATOR + "3s1ms");
}, nextStep, "Bucket should be expiring and have time step of 3s1ms");
yield undefined;
// Interupt previous expiring bucket
BUIT.setExpiringBucket("walrus2", [1002, 2002]);
is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "walrus2" + BUIT.BUCKET_SEPARATOR + "1s2ms",
"Should be new expiring bucket, with time step of 1s2ms");
waitForCondition(function() {
return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "walrus2" + BUIT.BUCKET_SEPARATOR + "2s2ms");
}, nextStep, "Should be new expiring bucket, with time step of 2s2ms");
yield undefined;
// Let expiring bucket expire
waitForCondition(function() {
return BUIT.currentBucket == BUIT.BUCKET_DEFAULT;
}, nextStep, "Bucket should have expired, default bucket should now be active");
yield undefined;
// Interupt expiring bucket with normal bucket
BUIT.setExpiringBucket("walrus3", [1003, 2003]);
is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "walrus3" + BUIT.BUCKET_SEPARATOR + "1s3ms",
"Should be new expiring bucket, with time step of 1s3ms");
BUIT.setBucket("mah-bucket");
is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "mah-bucket", "Bucket should have correct name");
waitForCondition(function() {
return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "mah-bucket");
}, nextStep, "Next step of old expiring bucket shouldn't have progressed");
yield undefined;
}

View File

@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gTestTab;
let gContentAPI;
let gContentWindow;
Components.utils.import("resource:///modules/UITour.jsm");
Components.utils.import("resource:///modules/BrowserUITelemetry.jsm");
function test() {
registerCleanupFunction(function() {
UITour.seenPageIDs.clear();
BrowserUITelemetry.setBucket(null);
delete window.BrowserUITelemetry;
});
UITourTest();
}
let tests = [
function test_seenPageIDs_1(done) {
gContentAPI.registerPageID("testpage1");
is(UITour.seenPageIDs.size, 1, "Should be 1 seen page ID");
ok(UITour.seenPageIDs.has("testpage1"), "Should have seen 'testpage1' page ID");
const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
const SEP = BrowserUITelemetry.BUCKET_SEPARATOR;
let bucket = PREFIX + "UITour" + SEP + "testpage1";
is(BrowserUITelemetry.currentBucket, bucket, "Bucket should have correct name");
gBrowser.selectedTab = gBrowser.addTab("about:blank");
bucket = PREFIX + "UITour" + SEP + "testpage1" + SEP + "inactive" + SEP + "1m";
is(BrowserUITelemetry.currentBucket, bucket,
"After switching tabs, bucket should be expiring");
gBrowser.removeTab(gBrowser.selectedTab);
gBrowser.selectedTab = gTestTab;
BrowserUITelemetry.setBucket(null);
done();
},
function test_seenPageIDs_2(done) {
gContentAPI.registerPageID("testpage2");
is(UITour.seenPageIDs.size, 2, "Should be 2 seen page IDs");
ok(UITour.seenPageIDs.has("testpage1"), "Should have seen 'testpage1' page ID");
ok(UITour.seenPageIDs.has("testpage2"), "Should have seen 'testpage2' page ID");
const PREFIX = BrowserUITelemetry.BUCKET_PREFIX;
const SEP = BrowserUITelemetry.BUCKET_SEPARATOR;
let bucket = PREFIX + "UITour" + SEP + "testpage2";
is(BrowserUITelemetry.currentBucket, bucket, "Bucket should have correct name");
gBrowser.removeTab(gTestTab);
gTestTab = null;
bucket = PREFIX + "UITour" + SEP + "testpage2" + SEP + "closed" + SEP + "1m";
is(BrowserUITelemetry.currentBucket, bucket,
"After closing tab, bucket should be expiring");
BrowserUITelemetry.setBucket(null);
done();
},
];

View File

@ -60,6 +60,12 @@ if (typeof Mozilla == 'undefined') {
Mozilla.UITour.DEFAULT_THEME_CYCLE_DELAY = 10 * 1000;
Mozilla.UITour.registerPageID = function(pageID) {
_sendEvent('registerPageID', {
pageID: pageID
});
};
Mozilla.UITour.showHighlight = function(target, effect) {
_sendEvent('showHighlight', {
target: target,