From f2022d4dd0582dd8507c1794e73b929d212c3c0b Mon Sep 17 00:00:00 2001 From: Bianca Danforth Date: Tue, 23 Oct 2018 11:13:56 -0700 Subject: [PATCH] Fix #157: Add Firefox UI telemetry probes * Add `visit_supported_site` and `hide_toolbar_button` probes. * `hide_toolbar_button` required adding a new experimental API, `customizeUI`, which allows the extension to be notified when the Firefox CustomizeUI module detects the `onWidgetRemoved` event. This event fires any time a widget is removed from the chrome, including browserAction buttons. The widget is identified by a widgetId. * Update METRICS.md to move `uninstall` probe to Appendix, since it is handled by the Addons Manager's event telemetry already. * Create a new ./src/telemetry/content.js file to handle sending messages to the background telemetry script to record events from content scripts. --- docs/METRICS.md | 56 +++++++++++++++---- src/background/index.js | 5 +- src/background/messages.js | 7 +++ src/experiment_apis/customizableUI/api.js | 31 ++++++++++ .../customizableUI/schema.json | 19 +++++++ src/extraction/index.js | 6 ++ src/manifest.json | 8 +++ src/telemetry/content.js | 21 +++++++ src/telemetry/extension.js | 11 ++++ 9 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 src/experiment_apis/customizableUI/api.js create mode 100644 src/experiment_apis/customizableUI/schema.json create mode 100644 src/telemetry/content.js diff --git a/docs/METRICS.md b/docs/METRICS.md index 056083f..ec0f819 100644 --- a/docs/METRICS.md +++ b/docs/METRICS.md @@ -261,16 +261,7 @@ Fired when the user clicks an undo button in a Product Card in the browserAction ### `uninstall` -Fired when the user uninstalls the extension. - -#### Payload properties - -- `methods`: String - - `'uninstall'` -- `objects`: String - - `'uninstall'` -- `extra_keys`: Object - - `'tracked_prods'` +See Appendix A. ### `hide_toolbar_button` @@ -381,3 +372,48 @@ No telemetry will be sent from the extension in the following additional cases: - The user is in a [Private Browsing](https://support.mozilla.org/en-US/kb/private-browsing-use-firefox-without-history?redirectlocale=en-US&redirectslug=Private+Browsing) window - Preference: `browser.privatebrowsing.autostart` - [`windows.Window`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/windows/Window) property: `window.incognito` + + +## Appendices + +### Appendix A: `uninstall` + +Fired when the user uninstalls the extension. + +This event, along with all other add-on lifecycle events, is recorded by the Addons Manager's event telemetry in Firefox. It will exist as part of the `main` ping under `payload.processes.parent.events` as an array in the `events` array. This event will be fired under the `addonsManager` telemetry category. + +#### Sample Ping + +Note: This is a sample ping. The exact value for the extension ID may differ, though the other values are correct. + +```javascript +{ + "type": "main", + // ... + "payload": { + // ... + "processes": { + // ... + "parent": { + // ... + "events": [ + [ + 9792, + "addonsManager", + "uninstall", + "extension", + "shopping-testpilot@mozilla.org", // the extension ID + { + "source": "testpilot" + // ... + } + ] + ] + } + // ... + } + // ... + } + // ... +} +``` diff --git a/src/background/index.js b/src/background/index.js index 84ebb7c..f25ec89 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -16,7 +16,7 @@ import {handleWebRequest, updatePrices} from 'commerce/background/price_updates' import store from 'commerce/state'; import {checkMigrations} from 'commerce/state/migrations'; import {loadStateFromStorage} from 'commerce/state/sync'; -import {registerEvents} from 'commerce/telemetry/extension'; +import {registerEvents, handleWidgetRemoved} from 'commerce/telemetry/extension'; (async function main() { registerEvents(); @@ -37,6 +37,9 @@ import {registerEvents} from 'commerce/telemetry/extension'; // Open the product page when an alert notification is clicked. browser.notifications.onClicked.addListener(handleNotificationClicked); + // Record hide_toolbar_button event when the toolbar button is hidden. + browser.customizableUI.onWidgetRemoved.addListener(handleWidgetRemoved); + // Enable content scripts now that the background listener is registered. // Store the return value globally to avoid destroying it, which would // unregister the content scripts. diff --git a/src/background/messages.js b/src/background/messages.js index 044f55f..ce3781f 100644 --- a/src/background/messages.js +++ b/src/background/messages.js @@ -19,12 +19,19 @@ import {handleConfigMessage} from 'commerce/config/background'; import {handleBrowserActionOpened} from 'commerce/background/browser_action'; import {handleExtractedProductData} from 'commerce/background/extraction'; +import {recordEvent} from 'commerce/telemetry/extension'; // sendMessage/onMessage handlers export const messageHandlers = new Map([ ['extracted-product', handleExtractedProductData], ['config', handleConfigMessage], + ['telemetry', async message => recordEvent( + message.data.method, + message.data.object, + message.data.value, + message.data.extra, + )], ]); export async function handleMessage(message, sender) { diff --git a/src/experiment_apis/customizableUI/api.js b/src/experiment_apis/customizableUI/api.js new file mode 100644 index 0000000..1af7a8b --- /dev/null +++ b/src/experiment_apis/customizableUI/api.js @@ -0,0 +1,31 @@ +/* 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/. */ +/* global ChromeUtils ExtensionAPI ExtensionCommon */ + +this.customizableUI = class extends ExtensionAPI { + getAPI(context) { + ChromeUtils.import('resource://gre/modules/ExtensionCommon.jsm'); + const {EventManager} = ExtensionCommon; + const {CustomizableUI} = ChromeUtils.import('resource:///modules/CustomizableUI.jsm', {}); + return { + customizableUI: { + onWidgetRemoved: new EventManager( + context, + 'customizableUI.onWidgetRemoved', + (fire) => { + const toolbarButton = { + onWidgetRemoved(widgetId) { + fire.async(widgetId); + }, + }; + CustomizableUI.addListener(toolbarButton); + return () => { + CustomizableUI.removeListener(toolbarButton); + }; + }, + ).api(), + }, + }; + } +}; diff --git a/src/experiment_apis/customizableUI/schema.json b/src/experiment_apis/customizableUI/schema.json new file mode 100644 index 0000000..d177a40 --- /dev/null +++ b/src/experiment_apis/customizableUI/schema.json @@ -0,0 +1,19 @@ +[ + { + "namespace": "customizableUI", + "events": [ + { + "name": "onWidgetRemoved", + "type": "function", + "description": "Fired when a widget is removed from the browser chrome", + "parameters": [ + { + "name": "widgetId", + "description": "The unique identifier for the widget", + "type": "string" + } + ] + } + ] + } +] diff --git a/src/extraction/index.js b/src/extraction/index.js index 1852715..3cac318 100644 --- a/src/extraction/index.js +++ b/src/extraction/index.js @@ -13,6 +13,7 @@ import extractProductWithFathom from 'commerce/extraction/fathom'; import extractProductWithFallback from 'commerce/extraction/selector'; import extractProductWithOpenGraph from 'commerce/extraction/open_graph'; import {shouldExtract} from 'commerce/privacy'; +import recordEvent from 'commerce/telemetry/content'; /** * Extraction methods are given the document object for the page, and must @@ -89,6 +90,11 @@ async function attemptExtraction() { return; } + // Record visit_supported_site event + if (!isBackgroundUpdate) { + await recordEvent('visit_supported_site', 'supported_site'); + } + // Extract immediately, and again if the readyState changes. let extractedProduct = await attemptExtraction(); document.addEventListener('readystatechange', async () => { diff --git a/src/manifest.json b/src/manifest.json index de816ac..88b748a 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -37,6 +37,14 @@ "telemetry" ], "experiment_apis": { + "customizableUI": { + "schema": "experiment_apis/customizableUI/schema.json", + "parent": { + "scopes": ["addon_parent"], + "script": "experiment_apis/customizableUI/api.js", + "paths": [["customizableUI"]] + } + }, "shoppingPrefs": { "schema": "experiment_apis/shoppingPrefs/schema.json", "parent": { diff --git a/src/telemetry/content.js b/src/telemetry/content.js new file mode 100644 index 0000000..a964130 --- /dev/null +++ b/src/telemetry/content.js @@ -0,0 +1,21 @@ +/* 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/. */ + +/** + * Communication from content scripts to the background page for recording + * telemetry events. + * @module + */ + +export default async function recordEvent(method, object, value = null, extra = null) { + await browser.runtime.sendMessage({ + type: 'telemetry', + data: { + method, + object, + value, + extra, + }, + }); +} diff --git a/src/telemetry/extension.js b/src/telemetry/extension.js index f87b55e..c1e6b2a 100644 --- a/src/telemetry/extension.js +++ b/src/telemetry/extension.js @@ -212,3 +212,14 @@ export async function getBadgeType() { return 'unknown'; } } + +export async function handleWidgetRemoved(widgetId) { + const addonId = (await browser.management.getSelf()).id; + // widgetId replaces '@' and '.' in the addonId with _ + const modifiedAddonId = addonId.replace(/[@.+]/g, '_'); + if (`${modifiedAddonId}-browser-action` === widgetId) { + await recordEvent('hide_toolbar_button', 'toolbar_button', null, { + badge_type: await getBadgeType(), + }); + } +}