From b2437ab9552eb2014b01d8f1546b46fe547f5096 Mon Sep 17 00:00:00 2001 From: Alejandro Baez Date: Thu, 9 Mar 2017 15:34:30 -0500 Subject: [PATCH 01/10] removing tabcenter css for #354 --- data/usercontext.css | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/data/usercontext.css b/data/usercontext.css index 5d1fb37..2497287 100644 --- a/data/usercontext.css +++ b/data/usercontext.css @@ -1,5 +1,5 @@ /* HACK: Custom Container vars do not propigate correctly -until the container tab is blurred and refocused, +until the container tab is blurred and refocused, adding the data-identity-color with the default hex value, or chrome url path as an alternate selector mitiages this bug.*/ [data-identity-color="blue"], @@ -190,18 +190,6 @@ special cases are addressed below */ width: 400%; } -/* this fixes containers tab center */ -#verticaltabs-box .tabbrowser-tab[usercontextid] .tab-content::after { - background-color: var(--identity-tab-color); - content: ''; - height: 100% !important; - left: 0; - position: absolute; - top: 0; - width: 3px !important; - z-index: 999; -} - .tabs-newtab-button .toolbarbutton-icon[type="menu"] { margin-inline-end: 0; } From 82cad13630e98f34298da87c9c5aaea889895d70 Mon Sep 17 00:00:00 2001 From: baku Date: Mon, 20 Mar 2017 14:39:07 +0100 Subject: [PATCH 02/10] Fix the opening of a link from a context menu - issue #327 --- index.js | 59 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index e17c655..f680c69 100644 --- a/index.js +++ b/index.js @@ -1370,25 +1370,48 @@ ContainerWindow.prototype = { }, _configureContextMenu() { - return this._configureMenu("context-openlinkinusercontext-menu", - () => { - // This userContextId is what we want to exclude. - const tab = modelFor(this._window).tabs.activeTab; - return ContainerService._getUserContextIdFromTab(tab); - }, - e => { - // This is a super internal method. Hopefully it will be stable in the - // next FF releases. - this._window.gContextMenu.openLinkInTab(e); + return Promise.all([ + this._configureMenu("context-openlinkinusercontext-menu", + () => { + // This userContextId is what we want to exclude. + const tab = modelFor(this._window).tabs.activeTab; + return ContainerService._getUserContextIdFromTab(tab); + }, + e => { + // This is a super internal method. Hopefully it will be stable in the + // next FF releases. + this._window.gContextMenu.openLinkInTab(e); - const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); - ContainerService.showTabs({ - userContextId, - nofocus: true, - window: this._window, - }); - } - ); + const userContextId = parseInt(e.target.getAttribute("data-usercontextid"), 10); + ContainerService.showTabs({ + userContextId, + nofocus: true, + window: this._window, + }); + } + ), + this._configureContextMenuOpenLink(), + ]); + }, + + _configureContextMenuOpenLink() { + return new Promise(resolve => { + const self = this; + this._window.gSetUserContextIdAndClick = function(event) { + const tab = modelFor(self._window).tabs.activeTab; + const userContextId = ContainerService._getUserContextIdFromTab(tab); + event.target.setAttribute("data-usercontextid", userContextId); + self._window.gContextMenu.openLinkInTab(event); + }; + + let item = this._window.document.getElementById("context-openlinkincontainertab"); + item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)"); + + item = this._window.document.getElementById("context-openlinkintab"); + item.setAttribute("oncommand", "gSetUserContextIdAndClick(event)"); + + resolve(); + }); }, // Generic menu configuration. From 5e94941377bb07e54ae825ed5b709316c532b1b3 Mon Sep 17 00:00:00 2001 From: groovecoder Date: Thu, 16 Mar 2017 13:46:51 -0500 Subject: [PATCH 03/10] fix #209: page-requests-completed-per-tab metric --- docs/metrics.md | 14 +++++++++++++- webextension/background.js | 37 ++++++++++++++++++++++++++++++++++++- webextension/manifest.json | 4 +++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index 3a0bb86..4a13647 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -49,7 +49,7 @@ whether they are actively interacting with it. * Average number of container tabs when new window was clicked * How many containers do users have hidden at the same time? (when should we measure this? each time a container is hidden?) * Do users pin container tabs? (do we have existing Telemetry for pinning?) -* Do users change URLs in a container tab? (sounds like it could be a flood unless we only record the first URL change?) +* Do users visit more pages in container tabs than non-container tabs? ### Follow-up Questions @@ -178,6 +178,17 @@ of a `testpilottest` telemetry ping for each scenario. } ``` +* The user closes a tab + +```js + { + "uuid": , + "userContextId": , + "event": "page-requests-completed-per-tab", + "pageRequestCount": + } +``` + ### A Redshift schema for the payload: ```lua @@ -188,6 +199,7 @@ local schema = { {"clickedContainerTabCount", "INTEGER", 255, nil, "Fields[payload.clickedContainerTabCount]"}, {"eventSource", "VARCHAR", 255, nil, "Fields[payload.eventSource]"}, {"event", "VARCHAR", 255, nil, "Fields[payload.event]"}, + {"pageRequestCount", "INTEGER", 255, nil, "Fields[payload.pageRequestCount]"} {"hiddenContainersCount", "INTEGER", 255, nil, "Fields[payload.hiddenContainersCount]"}, {"shownContainersCount", "INTEGER", 255, nil, "Fields[payload.shownContainersCount]"}, {"totalContainersCount", "INTEGER", 255, nil, "Fields[payload.totalContainersCount]"}, diff --git a/webextension/background.js b/webextension/background.js index e909c6b..75669a6 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -1,4 +1,3 @@ - const themeManager = { existingTheme: null, init() { @@ -43,7 +42,43 @@ const themeManager = { } }; +const tabPageCounter = { + counter: {}, + + init() { + browser.tabs.onCreated.addListener(this.initTabCounter.bind(this)); + browser.tabs.onRemoved.addListener(this.sendTabCountAndDelete.bind(this)); + browser.webRequest.onCompleted.addListener(this.incrementTabCount.bind(this), {urls: [""], types: ["main_frame"]}); + }, + + initTabCounter(tab) { + this.counter[tab.id] = { + "cookieStoreId": tab.cookieStoreId, + "pageRequests": 0 + }; + }, + + sendTabCountAndDelete(tab) { + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: "page-requests-completed-per-tab", + userContextId: this.counter[tab].cookieStoreId, + pageRequestCount: this.counter[tab].pageRequests + }); + delete this.counter[tab.id]; + }, + + incrementTabCount(details) { + browser.tabs.get(details.tabId).then(tab => { + this.counter[tab.id].pageRequests++; + }).catch(e => { + throw e; + }); + } +}; + themeManager.init(); +tabPageCounter.init(); browser.runtime.sendMessage({ method: "getPreference", diff --git a/webextension/manifest.json b/webextension/manifest.json index 6341619..93a6b16 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -20,7 +20,9 @@ "permissions": [ "cookies", - "tabs" + "tabs", + "webRequest", + "" ], "browser_action": { From 7d9b4c15617dd044c5e9915f049311d95c512dd0 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 3 Apr 2017 17:09:57 +0100 Subject: [PATCH 04/10] Fixing shopping icon on tab first focused for 52+. Fixes #410 --- data/usercontext.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/usercontext.css b/data/usercontext.css index 5d1fb37..f23b2b8 100644 --- a/data/usercontext.css +++ b/data/usercontext.css @@ -66,7 +66,8 @@ value, or chrome url path as an alternate selector mitiages this bug.*/ } [data-identity-icon="cart"], -[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"] { +[data-identity-icon="chrome://browser/skin/usercontext/cart.svg"], +[data-identity-icon="chrome://browser/skin/usercontext/shopping.svg"] { --identity-icon: url("resource://testpilot-containers/data/usercontext.svg#cart"); } From 3e657a2e8d952da30c963ae929bc8a774db9a425 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 13 Mar 2017 00:58:40 +0000 Subject: [PATCH 05/10] Auto send users to sites they assigned to a container. Fixes #306 --- index.js | 30 ++-- package.json | 2 +- webextension/background.js | 269 +++++++++++++++++++++++++++++- webextension/confirm-page.html | 33 ++++ webextension/css/confirm-page.css | 37 ++++ webextension/css/popup.css | 2 +- webextension/js/confirm-page.js | 26 +++ webextension/manifest.json | 12 +- 8 files changed, 383 insertions(+), 28 deletions(-) create mode 100644 webextension/confirm-page.html create mode 100644 webextension/css/confirm-page.css create mode 100644 webextension/js/confirm-page.js diff --git a/index.js b/index.js index f680c69..ba86ad3 100644 --- a/index.js +++ b/index.js @@ -117,7 +117,7 @@ const ContainerService = { _identitiesState: {}, _windowMap: new Map(), _containerWasEnabled: false, - _onThemeChangedCallback: null, + _onBackgroundConnectCallback: null, init(installation, reason) { // If we are just been installed, we must store some information for the @@ -260,7 +260,7 @@ const ContainerService = { } }); - this.registerThemeConnection(api); + this.registerBackgroundConnection(api); }).catch(() => { throw new Error("WebExtension startup failed. Unable to continue."); }); @@ -307,28 +307,28 @@ const ContainerService = { Services.obs.addObserver(this, "lightweight-theme-changed", false); }, - registerThemeConnection(api) { - // This is only used for theme notifications + registerBackgroundConnection(api) { + // This is only used for theme and container deletion notifications api.browser.runtime.onConnect.addListener((port) => { - this.onThemeChanged((theme, topic) => { + this._onBackgroundConnectCallback = (message, topic) => { port.postMessage({ type: topic, - theme + message }); - }); + }; }); }, - triggerThemeChanged(theme, topic) { - if (this._onThemeChangedCallback) { - this._onThemeChangedCallback(theme, topic); + triggerBackgroundCallback(message, topic) { + if (this._onBackgroundConnectCallback) { + this._onBackgroundConnectCallback(message, topic); } }, observe(subject, topic) { if (topic === "lightweight-theme-changed") { this.getTheme().then((theme) => { - this.triggerThemeChanged(theme, topic); + this.triggerBackgroundCallback(theme, topic); }).catch(() => { throw new Error("Unable to get theme"); }); @@ -346,10 +346,6 @@ const ContainerService = { }); }, - onThemeChanged(callback) { - this._onThemeChangedCallback = callback; - }, - // utility methods _containerTabCount(userContextId) { @@ -960,12 +956,13 @@ const ContainerService = { }, removeIdentity(args) { + const eventName = "delete-container"; if (!("userContextId" in args)) { return Promise.reject("removeIdentity must be called with userContextId argument."); } this.sendTelemetryPayload({ - "event": "delete-container", + "event": eventName, "userContextId": args.userContextId }); @@ -976,6 +973,7 @@ const ContainerService = { return this._closeTabs(tabsToClose).then(() => { const removed = ContextualIdentityProxy.remove(args.userContextId); + this.triggerBackgroundCallback({userContextId: args.userContextId}, eventName); this._forgetIdentity(args.userContextId); return this._refreshNeeded().then(() => removed ); }); diff --git a/package.json b/package.json index 33c1ab3..e34dd94 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "testpilot-containers", "title": "Containers Experiment", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", - "version": "2.0.0", + "version": "2.1.0", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/testpilot-containers/issues" diff --git a/webextension/background.js b/webextension/background.js index 75669a6..c6dc283 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -1,14 +1,266 @@ +const assignManager = { + CLOSEABLE_WINDOWS: new Set([ + "about:startpage", + "about:newtab", + "about:home", + "about:blank" + ]), + MENU_ASSIGN_ID: "open-in-this-container", + MENU_REMOVE_ID: "remove-open-in-this-container", + storageArea: { + area: browser.storage.local, + + getSiteStoreKey(pageUrl) { + const url = new window.URL(pageUrl); + const storagePrefix = "siteContainerMap@@_"; + return `${storagePrefix}${url.hostname}`; + }, + + get(pageUrl) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + return new Promise((resolve, reject) => { + this.area.get([siteStoreKey]).then((storageResponse) => { + if (storageResponse && siteStoreKey in storageResponse) { + resolve(storageResponse[siteStoreKey]); + } + resolve(null); + }).catch((e) => { + reject(e); + }); + }); + }, + + set(pageUrl, data) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + return this.area.set({ + [siteStoreKey]: data + }); + }, + + remove(pageUrl) { + const siteStoreKey = this.getSiteStoreKey(pageUrl); + return this.area.remove([siteStoreKey]); + }, + + deleteContainer(userContextId) { + const removeKeys = []; + this.area.get().then((siteConfigs) => { + Object.keys(siteConfigs).forEach((key) => { + // For some reason this is stored as string... lets check them both as that + if (String(siteConfigs[key].userContextId) === String(userContextId)) { + removeKeys.push(key); + } + }); + this.area.remove(removeKeys); + }).catch((e) => { + throw e; + }); + } + }, + + init() { + browser.tabs.onActivated.addListener((info) => { + browser.tabs.get(info.tabId).then((tab) => { + this.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + }); + + browser.windows.onFocusChanged.addListener((windowId) => { + browser.tabs.query({active: true, windowId}).then((tabs) => { + if (tabs && tabs[0]) { + this.calculateContextMenu(tabs[0]); + } + }).catch((e) => { + throw e; + }); + }); + + browser.runtime.onMessage.addListener((neverAskMessage) => { + const pageUrl = neverAskMessage.pageUrl; + if (neverAskMessage.neverAsk === true) { + // If we have existing data and for some reason it hasn't been deleted etc lets update it + this.storageArea.get(pageUrl).then((siteSettings) => { + if (siteSettings) { + siteSettings.neverAsk = true; + this.storageArea.set(pageUrl, siteSettings); + } + }).catch((e) => { + throw e; + }); + } + }); + + browser.contextMenus.onClicked.addListener((info, tab) => { + const userContextId = this.getUserContextIdFromCookieStore(tab); + // Mapping ${URL(info.pageUrl).hostname} to ${userContextId} + if (userContextId) { + let actionName; + let storageAction; + if (info.menuItemId === this.MENU_ASSIGN_ID) { + actionName = "added"; + storageAction = this.storageArea.set(info.pageUrl, { + userContextId, + neverAsk: false + }); + } else { + actionName = "removed"; + storageAction = this.storageArea.remove(info.pageUrl); + } + storageAction.then(() => { + browser.notifications.create({ + type: "basic", + title: "Containers", + message: `Successfully ${actionName} site to always open in this container`, + iconUrl: browser.extension.getURL("/img/onboarding-1.png") + }); + this.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + } + }); + + browser.webRequest.onBeforeRequest.addListener((options) => { + if (options.frameId !== 0 || options.tabId === -1) { + return {}; + } + return Promise.all([ + browser.tabs.get(options.tabId), + this.storageArea.get(options.url) + ]).then(([tab, siteSettings]) => { + const userContextId = this.getUserContextIdFromCookieStore(tab); + if (!siteSettings + || userContextId === siteSettings.userContextId + || tab.incognito) { + return {}; + } + + this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index, siteSettings.neverAsk); + this.calculateContextMenu(tab); + // If the user just opened the tab, we can auto close it + if (this.CLOSEABLE_WINDOWS.has(tab.url)) { + browser.tabs.remove(tab.id); + } + return { + cancel: true, + }; + }).catch((e) => { + throw e; + }); + },{urls: [""], types: ["main_frame"]}, ["blocking"]); + + browser.webRequest.onCompleted.addListener((options) => { + if (options.frameId !== 0 || options.tabId === -1) { + return {}; + } + browser.tabs.get(options.tabId).then((tab) => { + this.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + },{urls: [""], types: ["main_frame"]}); + }, + + + deleteContainer(userContextId) { + this.storageArea.deleteContainer(userContextId); + }, + + getUserContextIdFromCookieStore(tab) { + if (!("cookieStoreId" in tab)) { + return false; + } + const cookieStore = tab.cookieStoreId; + const container = cookieStore.replace("firefox-container-", ""); + if (container !== cookieStore) { + return container; + } + return false; + }, + + isTabPermittedAssign(tab) { + // Ensure we are not an important about url + // Ensure we are not in incognito mode + if (this.CLOSEABLE_WINDOWS.has(tab.url) + || tab.incognito) { + return false; + } + return true; + }, + + calculateContextMenu(tab) { + // There is a focus issue in this menu where if you change window with a context menu click + // you get the wrong menu display because of async + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1215376#c16 + // We also can't change for always private mode + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1352102 + const cookieStore = this.getUserContextIdFromCookieStore(tab); + browser.contextMenus.remove(this.MENU_ASSIGN_ID); + browser.contextMenus.remove(this.MENU_REMOVE_ID); + // Ensure we have a cookieStore to assign to + if (cookieStore + && this.isTabPermittedAssign(tab)) { + this.storageArea.get(tab.url).then((siteSettings) => { + // ✓ This is to mitigate https://bugzilla.mozilla.org/show_bug.cgi?id=1351418 + let prefix = " "; // Alignment of non breaking space, unknown why this requires so many spaces to align with the tick + let menuId = this.MENU_ASSIGN_ID; + if (siteSettings) { + prefix = "✓"; + menuId = this.MENU_REMOVE_ID; + } + browser.contextMenus.create({ + id: menuId, + title: `${prefix} Always Open in This Container`, + checked: true, + contexts: ["all"], + }); + }).catch((e) => { + throw e; + }); + } + }, + + reloadPageInContainer(url, userContextId, index, neverAsk = false) { + const loadPage = browser.extension.getURL("confirm-page.html"); + // If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there + if (neverAsk) { + browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index}); + } else { + const confirmUrl = `${loadPage}?url=${url}`; + browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => { + // We don't want to sync this URL ever nor clutter the users history + browser.history.deleteUrl({url: confirmUrl}); + }).catch((e) => { + throw e; + }); + } + } +}; + +const messageHandler = { + init() { + const port = browser.runtime.connect(); + port.onMessage.addListener(m => { + switch (m.type) { + case "lightweight-theme-changed": + themeManager.update(m.message); + break; + case "delete-container": + assignManager.deleteContainer(m.message.userContextId); + break; + default: + throw new Error(`Unhandled message type: ${m.message}`); + } + }); + } +}; + const themeManager = { existingTheme: null, init() { this.check(); - - const port = browser.runtime.connect(); - port.onMessage.addListener(m => { - if (m.type === "lightweight-theme-changed") { - this.update(m.theme); - } - }); }, setPopupIcon(theme) { let icons = { @@ -77,8 +329,11 @@ const tabPageCounter = { } }; +assignManager.init(); themeManager.init(); tabPageCounter.init(); +// Lets do this last as theme manager did a check before connecting before +messageHandler.init(); browser.runtime.sendMessage({ method: "getPreference", diff --git a/webextension/confirm-page.html b/webextension/confirm-page.html new file mode 100644 index 0000000..ca182bb --- /dev/null +++ b/webextension/confirm-page.html @@ -0,0 +1,33 @@ + + + + Containers confirm navigation + + + + +
+
+

Should we open this in your container?

+
+
+

+ Looks like you requested: +

+
+

+ You asked Firefox to always open in this type of container. Would you like to proceed?
+

+
+
+ +
+
+ +
+
+
+ + + + diff --git a/webextension/css/confirm-page.css b/webextension/css/confirm-page.css new file mode 100644 index 0000000..efbeb41 --- /dev/null +++ b/webextension/css/confirm-page.css @@ -0,0 +1,37 @@ +/* General Rules and Resets */ +.title { + background-image: none; +} + +main { + background: url(/img/onboarding-1.png) no-repeat; + background-position: -10px -15px; + background-size: 285px; + margin-inline-start: -285px; + padding-inline-start: 285px; +} + +@media only screen and (max-width: 1300px) { + main { + background: none; + } + + /* for a mid sized window we have enough for this but not our image */ + .title { + background-image: url("chrome://global/skin/icons/info.svg"); + } +} + +html { + box-sizing: border-box; + font: message-box; +} + +#redirect-url, +#redirect-origin { + font-weight: bold; +} + +dfn { + font-style: normal; +} diff --git a/webextension/css/popup.css b/webextension/css/popup.css index e2c8cd2..dd460d3 100644 --- a/webextension/css/popup.css +++ b/webextension/css/popup.css @@ -383,7 +383,7 @@ span ~ .panel-header-text { .panel-footer { align-items: center; background: #efefef; - block-size: 55px; + block-size: 54px; border-block-end: 1px solid #d8d8d8; color: #000; display: flex; diff --git a/webextension/js/confirm-page.js b/webextension/js/confirm-page.js new file mode 100644 index 0000000..19a64f9 --- /dev/null +++ b/webextension/js/confirm-page.js @@ -0,0 +1,26 @@ +const redirectUrl = new URL(window.location).searchParams.get("url"); +document.getElementById("redirect-url").textContent = redirectUrl; +const redirectSite = new URL(redirectUrl).hostname; +document.getElementById("redirect-site").textContent = redirectSite; + +document.getElementById("redirect-form").addEventListener("submit", (e) => { + e.preventDefault(); + const neverAsk = document.getElementById("never-ask").checked; + // Sending neverAsk message to background to store for next time we see this process + if (neverAsk) { + browser.runtime.sendMessage({ + neverAsk: true, + pageUrl: redirectUrl + }).then(() => { + redirect(); + }).catch(() => { + // Can't really do much here user will have to click it again + }); + } + redirect(); +}); + +function redirect() { + const redirectUrl = document.getElementById("redirect-url").textContent; + document.location.replace(redirectUrl); +} diff --git a/webextension/manifest.json b/webextension/manifest.json index 93a6b16..675b179 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Containers Experiment", - "version": "2.0.0", + "version": "2.1.0", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", "icons": { @@ -19,10 +19,16 @@ "homepage_url": "https://testpilot.firefox.com/", "permissions": [ + "", + "activeTab", "cookies", + "contextMenus", + "history", + "notifications", + "storage", "tabs", - "webRequest", - "" + "webRequestBlocking", + "webRequest" ], "browser_action": { From d09de4764673f3541a9fa95c1ff7fd2135c7425e Mon Sep 17 00:00:00 2001 From: groovecoder Date: Mon, 3 Apr 2017 14:38:39 -0500 Subject: [PATCH 06/10] for #306: Telemetry for container assignment events --- docs/metrics.md | 39 +++++++++++++++++++++++++++++++++ package.json | 2 +- webextension/background.js | 15 +++++++++++++ webextension/js/confirm-page.js | 4 ++++ webextension/manifest.json | 2 +- 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index 4a13647..648af7f 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -189,6 +189,45 @@ of a `testpilottest` telemetry ping for each scenario. } ``` +* The user chooses "Always Open in this Container" context menu option. (Note: We send two separate event names: one for assigning a site to a container, one for removing a site from a container.) + +```js + { + "uuid": , + "userContextId": , + "event": "[added|removed]-container-assignment" + } +``` + +* Firefox prompts the user to reload a site into a container after the user picked "Always Open in this Container". + +```js + { + "uuid": , + "userContextId": , + "event": "prompt-reload-page-in-container" + } +``` + +* The user clicks "Take me there" to reload a site into a container after the user picked "Always Open in this Container". + +```js + { + "uuid": , + "event": "click-to-reload-page-in-container" + } +``` + +* Firefox automatically reloads a site into a container after the user picked "Always Open in this Container". + +```js + { + "uuid": , + "userContextId": , + "event": "auto-reload-page-in-container" + } +``` + ### A Redshift schema for the payload: ```lua diff --git a/package.json b/package.json index e34dd94..106488e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "testpilot-containers", "title": "Containers Experiment", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", - "version": "2.1.0", + "version": "2.1.1", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/testpilot-containers/issues" diff --git a/webextension/background.js b/webextension/background.js index c6dc283..bb70dac 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -115,6 +115,11 @@ const assignManager = { message: `Successfully ${actionName} site to always open in this container`, iconUrl: browser.extension.getURL("/img/onboarding-1.png") }); + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: `${actionName}-container-assignment`, + userContextId: userContextId, + }); this.calculateContextMenu(tab); }).catch((e) => { throw e; @@ -227,7 +232,17 @@ const assignManager = { // If the user has explicitly checked "Never Ask Again" on the warning page we will send them straight there if (neverAsk) { browser.tabs.create({url, cookieStoreId: `firefox-container-${userContextId}`, index}); + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: "auto-reload-page-in-container", + userContextId: userContextId, + }); } else { + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: "prompt-to-reload-page-in-container", + userContextId: userContextId, + }); const confirmUrl = `${loadPage}?url=${url}`; browser.tabs.create({url: confirmUrl, cookieStoreId: `firefox-container-${userContextId}`, index}).then(() => { // We don't want to sync this URL ever nor clutter the users history diff --git a/webextension/js/confirm-page.js b/webextension/js/confirm-page.js index 19a64f9..feefba1 100644 --- a/webextension/js/confirm-page.js +++ b/webextension/js/confirm-page.js @@ -17,6 +17,10 @@ document.getElementById("redirect-form").addEventListener("submit", (e) => { // Can't really do much here user will have to click it again }); } + browser.runtime.sendMessage({ + method: "sendTelemetryPayload", + event: "click-to-reload-page-in-container", + }); redirect(); }); diff --git a/webextension/manifest.json b/webextension/manifest.json index 675b179..a3f4c61 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Containers Experiment", - "version": "2.1.0", + "version": "2.1.1", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", "icons": { From 5a996c1dea0ec16824982f2a26a90eae44f810ce Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Mon, 3 Apr 2017 22:59:32 +0100 Subject: [PATCH 07/10] Moving code into messageHandler to share assignManager and tabPageCounter code. Fixes bind functions not working correctly. --- webextension/background.js | 106 +++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/webextension/background.js b/webextension/background.js index bb70dac..21d07f6 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -59,24 +59,6 @@ const assignManager = { }, init() { - browser.tabs.onActivated.addListener((info) => { - browser.tabs.get(info.tabId).then((tab) => { - this.calculateContextMenu(tab); - }).catch((e) => { - throw e; - }); - }); - - browser.windows.onFocusChanged.addListener((windowId) => { - browser.tabs.query({active: true, windowId}).then((tabs) => { - if (tabs && tabs[0]) { - this.calculateContextMenu(tabs[0]); - } - }).catch((e) => { - throw e; - }); - }); - browser.runtime.onMessage.addListener((neverAskMessage) => { const pageUrl = neverAskMessage.pageUrl; if (neverAskMessage.neverAsk === true) { @@ -127,6 +109,7 @@ const assignManager = { } }); + // Before a request is handled by the browser we decide if we should route through a different container browser.webRequest.onBeforeRequest.addListener((options) => { if (options.frameId !== 0 || options.tabId === -1) { return {}; @@ -155,17 +138,6 @@ const assignManager = { throw e; }); },{urls: [""], types: ["main_frame"]}, ["blocking"]); - - browser.webRequest.onCompleted.addListener((options) => { - if (options.frameId !== 0 || options.tabId === -1) { - return {}; - } - browser.tabs.get(options.tabId).then((tab) => { - this.calculateContextMenu(tab); - }).catch((e) => { - throw e; - }); - },{urls: [""], types: ["main_frame"]}); }, @@ -256,6 +228,7 @@ const assignManager = { const messageHandler = { init() { + // Handles messages from index.js const port = browser.runtime.connect(); port.onMessage.addListener(m => { switch (m.type) { @@ -269,6 +242,55 @@ const messageHandler = { throw new Error(`Unhandled message type: ${m.message}`); } }); + + browser.tabs.onCreated.addListener((tab, other) => { + // This works at capturing the tabs as they are created + // However we need onFocusChanged and onActivated to capture the initial tab + if (tab.id === -1) { + return {}; + } + tabPageCounter.initTabCounter(tab); + }); + + browser.tabs.onRemoved.addListener((tabId, other) => { + if (tabId === -1) { + return {}; + } + tabPageCounter.sendTabCountAndDelete(tabId); + }); + + browser.tabs.onActivated.addListener((info) => { + browser.tabs.get(info.tabId).then((tab) => { + tabPageCounter.initTabCounter(tab); + assignManager.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + }); + + browser.windows.onFocusChanged.addListener((windowId) => { + browser.tabs.query({active: true, windowId}).then((tabs) => { + if (tabs && tabs[0]) { + tabPageCounter.initTabCounter(tabs[0]); + assignManager.calculateContextMenu(tabs[0]); + } + }).catch((e) => { + throw e; + }); + }); + + browser.webRequest.onCompleted.addListener((details) => { + if (details.frameId !== 0 || details.tabId === -1) { + return {}; + } + + browser.tabs.get(details.tabId).then((tab) => { + tabPageCounter.incrementTabCount(tab); + assignManager.calculateContextMenu(tab); + }).catch((e) => { + throw e; + }); + }, {urls: [""], types: ["main_frame"]}); } }; @@ -312,41 +334,33 @@ const themeManager = { const tabPageCounter = { counter: {}, - init() { - browser.tabs.onCreated.addListener(this.initTabCounter.bind(this)); - browser.tabs.onRemoved.addListener(this.sendTabCountAndDelete.bind(this)); - browser.webRequest.onCompleted.addListener(this.incrementTabCount.bind(this), {urls: [""], types: ["main_frame"]}); - }, - initTabCounter(tab) { + if (tab.id in this.counter) { + return; + } this.counter[tab.id] = { "cookieStoreId": tab.cookieStoreId, "pageRequests": 0 }; }, - sendTabCountAndDelete(tab) { + sendTabCountAndDelete(tabId) { browser.runtime.sendMessage({ method: "sendTelemetryPayload", event: "page-requests-completed-per-tab", - userContextId: this.counter[tab].cookieStoreId, - pageRequestCount: this.counter[tab].pageRequests + userContextId: this.counter[tabId].cookieStoreId, + pageRequestCount: this.counter[tabId].pageRequests }); - delete this.counter[tab.id]; + delete this.counter[tabId]; }, - incrementTabCount(details) { - browser.tabs.get(details.tabId).then(tab => { - this.counter[tab.id].pageRequests++; - }).catch(e => { - throw e; - }); + incrementTabCount(tab) { + this.counter[tab.id].pageRequests++; } }; assignManager.init(); themeManager.init(); -tabPageCounter.init(); // Lets do this last as theme manager did a check before connecting before messageHandler.init(); From 8a0f9a99cd80d66ac2482149cbf1f3d484ce4d3f Mon Sep 17 00:00:00 2001 From: groovecoder Date: Tue, 4 Apr 2017 07:53:24 -0500 Subject: [PATCH 08/10] eslint fixes --- webextension/background.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webextension/background.js b/webextension/background.js index 21d07f6..57e7c6a 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -243,7 +243,7 @@ const messageHandler = { } }); - browser.tabs.onCreated.addListener((tab, other) => { + browser.tabs.onCreated.addListener((tab) => { // This works at capturing the tabs as they are created // However we need onFocusChanged and onActivated to capture the initial tab if (tab.id === -1) { @@ -252,7 +252,7 @@ const messageHandler = { tabPageCounter.initTabCounter(tab); }); - browser.tabs.onRemoved.addListener((tabId, other) => { + browser.tabs.onRemoved.addListener((tabId) => { if (tabId === -1) { return {}; } From d91a81ffffe4ba3b0620298c71250cd58fb564e0 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 4 Apr 2017 15:33:11 +0100 Subject: [PATCH 09/10] Fixing about: scheme assignment. Fixes #413 --- webextension/background.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webextension/background.js b/webextension/background.js index 57e7c6a..9ae91e8 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -160,7 +160,8 @@ const assignManager = { isTabPermittedAssign(tab) { // Ensure we are not an important about url // Ensure we are not in incognito mode - if (this.CLOSEABLE_WINDOWS.has(tab.url) + const url = new URL(tab.url); + if (url.protocol === "about:" || tab.incognito) { return false; } From b92e44d8e52a0778b49d4165efd22d4e5ce0a0c2 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Tue, 4 Apr 2017 16:45:04 +0100 Subject: [PATCH 10/10] Assign tab order should appear after website they came from. Fixes #415 --- package.json | 2 +- webextension/background.js | 2 +- webextension/manifest.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 106488e..ab0f5bc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "testpilot-containers", "title": "Containers Experiment", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", - "version": "2.1.1", + "version": "2.1.2", "author": "Andrea Marchesini, Luke Crouch and Jonathan Kingston", "bugs": { "url": "https://github.com/mozilla/testpilot-containers/issues" diff --git a/webextension/background.js b/webextension/background.js index 57e7c6a..4fd82a5 100644 --- a/webextension/background.js +++ b/webextension/background.js @@ -125,7 +125,7 @@ const assignManager = { return {}; } - this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index, siteSettings.neverAsk); + this.reloadPageInContainer(options.url, siteSettings.userContextId, tab.index + 1, siteSettings.neverAsk); this.calculateContextMenu(tab); // If the user just opened the tab, we can auto close it if (this.CLOSEABLE_WINDOWS.has(tab.url)) { diff --git a/webextension/manifest.json b/webextension/manifest.json index a3f4c61..4e30c9d 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Containers Experiment", - "version": "2.1.1", + "version": "2.1.2", "description": "Containers works by isolating cookie jars using separate origin-attributes defined visually by colored ‘Container Tabs’. This add-on is a modified version of the containers feature for Firefox Test Pilot.", "icons": {