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.
This commit is contained in:
Bianca Danforth 2018-10-23 11:13:56 -07:00 коммит произвёл Michael Kelly
Родитель ce3f137d26
Коммит f2022d4dd0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 972176E09570E68A
9 изменённых файлов: 153 добавлений и 11 удалений

Просмотреть файл

@ -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"
// ...
}
]
]
}
// ...
}
// ...
}
// ...
}
```

Просмотреть файл

@ -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.

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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(),
},
};
}
};

Просмотреть файл

@ -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"
}
]
}
]
}
]

Просмотреть файл

@ -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 () => {

Просмотреть файл

@ -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": {

21
src/telemetry/content.js Normal file
Просмотреть файл

@ -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,
},
});
}

Просмотреть файл

@ -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(),
});
}
}