* Add `open_popup`, `open_external_page`, `add_product`, `delete_product` and `undo_delete_product` probes (See ./docs/METRICS.md for their specifications).
* Update `badge_type` `extra_key` description in METRICS.md to add a new enum value of 'unknown' in the case that the badge text is unrecognized, and a badge type cannot be determined.
* Update `addProductFromExtracted` action creator to return a thunk, which returns a random UUID (v4) for use as a product key in telemetry. This allows us to differentiate one product from another for a particular user. This is NOT a universal product id across all users.
* Add `tabId` URL searchParam to browserAction popup URL when an extracted product is found on the current page in order to obtain the `badge_type` `extra_key` value for the `open_popup` event.
This commit is contained in:
Bianca Danforth 2018-10-09 17:56:42 -07:00
Родитель c5a10fd001
Коммит dbf9036fc8
13 изменённых файлов: 108 добавлений и 19 удалений

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

@ -137,7 +137,7 @@ Below is a sample ping for the `badge_toolbar_button` and `visit_supported_site`
`extra_keys` are keys in the [optional `extra` object field](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html#serialization-format) for telemetry events. All `extra_keys` and their values are strings.
- `'badge_type'`: Indicates what, if any, badge was present on the browserAction toolbar button. One of 'add', 'price_alert', or 'none'.
- `'badge_type'`: Indicates what, if any, badge was present on the browserAction toolbar button. One of 'add', 'price_alert', or 'none'. A value of 'unknown' is possible if the badge text is unrecognized.
- `'extraction_id'`: A unique identifier to associate an extraction attempt to an extraction completion event for a given page.
- `'is_bg_update'`: 'true' if the extraction is associated with a background price check; otherwise 'false'.
- `method`: The extraction method that was successful, if any. One of: 'fathom', 'fallback' or 'neither'. A value of 'neither' means that extraction failed.

2
package-lock.json сгенерированный
Просмотреть файл

@ -2524,7 +2524,7 @@
"color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha1-k4NDeaHMmgxh+C9S8NBDIiUb1aI=",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"dev": true
},
"colors": {

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

@ -58,6 +58,7 @@
"react-dom": "16.4.1",
"react-redux": "5.0.7",
"redux": "4.0.0",
"redux-thunk": "2.3.0"
"redux-thunk": "2.3.0",
"uuid": "3.3.2"
}
}

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

@ -40,6 +40,7 @@ export async function handleExtractedProductData(message, sender) {
// Update the toolbar icon's URL with the current page's product if we can
const url = new URL(await config.get('browserActionUrl'));
url.searchParams.set('extractedProduct', JSON.stringify(extractedProduct));
url.searchParams.set('tabId', JSON.stringify(tabId));
// Update the toolbar popup while it is open with the current page's product
if (sender.tab.active) {

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

@ -12,12 +12,14 @@ import store from 'commerce/state';
import {
deactivateAlert,
getActivePriceAlerts,
getLatestPriceForProduct,
getOldestPriceForProduct,
getPrice,
getPriceAlertForPrice,
showPriceAlert,
} from 'commerce/state/prices';
import {getProduct} from 'commerce/state/products';
import {recordEvent} from 'commerce/background/telemetry';
/**
* Update the extension UI based on the current state of active price alerts.
@ -75,6 +77,16 @@ export function handleNotificationClicked(notificationId) {
const product = getProduct(state, alert.productId);
browser.tabs.create({url: product.url});
const latestPrice = getLatestPriceForProduct(state, product.id);
const originalPrice = getOldestPriceForProduct(state, product.id);
recordEvent('open_external_page', 'ui_element', null, {
element: 'system_notification',
price: latestPrice.amount.getAmount().toString(),
price_alert: alert.active.toString(),
price_orig: originalPrice.amount.getAmount().toString(),
product_key: product.key,
});
// Mark the alert as inactive if necessary, since it was followed.
if (alert.active) {
store.dispatch(deactivateAlert(alert));

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

@ -8,6 +8,8 @@
*/
import {shouldCollectTelemetry} from 'commerce/privacy';
import store from 'commerce/state';
import {getAllProducts} from 'commerce/state/products';
const CATEGORY = 'extension.price_alerts';
@ -167,6 +169,25 @@ export async function recordEvent(method, object, value, extra) {
method,
object,
value,
extra,
{
...extra,
// Add extra_keys that are appended to every event
tracked_prods: getAllProducts(store.getState()).length.toString(),
},
);
}
export async function getBadgeType(tabId) {
const badgeText = await browser.browserAction.getBadgeText(tabId ? {tabId} : {});
switch (true) {
case (badgeText === ''):
return 'none';
case (badgeText === '✚'):
return 'add';
case (/\d+/.test(badgeText)):
return 'price_alert';
default:
console.warn(`Unexpected badge text ${badgeText}.`); // eslint-disable-line no-console
return 'unknown';
}
}

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

@ -12,6 +12,7 @@ import EmptyOnboarding from 'commerce/browser_action/components/EmptyOnboarding'
import TrackedProductList from 'commerce/browser_action/components/TrackedProductList';
import {extractedProductShape, getAllProducts, productShape} from 'commerce/state/products';
import * as syncActions from 'commerce/state/sync';
import {recordEvent, getBadgeType} from 'commerce/background/telemetry';
import 'commerce/browser_action/components/BrowserActionApp.css';
@ -32,6 +33,7 @@ export default class BrowserActionApp extends React.Component {
static propTypes = {
// Direct props
extractedProduct: extractedProductShape, // Product detected on the current page, if any
tabId: pt.number,
// State props
products: pt.arrayOf(productShape).isRequired,
@ -42,6 +44,7 @@ export default class BrowserActionApp extends React.Component {
static defaultProps = {
extractedProduct: null,
tabId: null,
}
constructor(props) {
@ -51,9 +54,14 @@ export default class BrowserActionApp extends React.Component {
};
}
componentDidMount() {
async componentDidMount() {
this.props.loadStateFromStorage();
// Record 'open_popup' event
recordEvent('open_popup', 'toolbar_button', null, {
badge_type: await getBadgeType(this.props.tabId),
});
browser.runtime.onMessage.addListener((message) => {
if (message.subject === 'extracted-product') {
this.setState({extractedProduct: message.extractedProduct});
@ -66,6 +74,7 @@ export default class BrowserActionApp extends React.Component {
*/
async handleClickHelp() {
browser.tabs.create({url: await config.get('supportUrl')});
recordEvent('open_external_page', 'ui_element', null, {element: 'help_button'});
window.close();
}
@ -74,6 +83,7 @@ export default class BrowserActionApp extends React.Component {
*/
async handleClickFeedback() {
browser.tabs.create({url: await config.get('feedbackUrl')});
recordEvent('open_external_page', 'ui_element', null, {element: 'feedback_button'});
window.close();
}

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

@ -8,6 +8,7 @@ import React from 'react';
import TrackProductButton from 'commerce/browser_action/components/TrackProductButton';
import config from 'commerce/config';
import {extractedProductShape} from 'commerce/state/products';
import {recordEvent} from 'commerce/background/telemetry';
import 'commerce/browser_action/components/EmptyOnboarding.css';
@ -49,6 +50,8 @@ export default class EmptyOnboarding extends React.Component {
if (event.target.href) {
event.preventDefault();
browser.tabs.create({url: event.target.href});
const element = `${event.target.id}_link`;
recordEvent('open_external_page', 'ui_element', null, {element});
window.close();
}
}
@ -69,14 +72,14 @@ export default class EmptyOnboarding extends React.Component {
*/}
<p className="description">
Add products you want to buy from
{' '}<a href="https://www.amazon.com">Amazon</a>,
{' '}<a href="https://www.bestbuy.com/">Best Buy</a>,
{' '}<a href="https://www.ebay.com/">eBay</a>,
{' '}<a href="https://www.homedepot.com/">Home Depot</a>, and
{' '}<a href="https://www.walmart.com/">Walmart</a>
{' '}<a id="amazon" href="https://www.amazon.com">Amazon</a>,
{' '}<a id="best_buy" href="https://www.bestbuy.com/">Best Buy</a>,
{' '}<a id="ebay" href="https://www.ebay.com/">eBay</a>,
{' '}<a id="home_depot" href="https://www.homedepot.com/">Home Depot</a>, and
{' '}<a id="walmart" href="https://www.walmart.com/">Walmart</a>
{' '}to your Price Watcher list, and Firefox will notify you if the price drops.
</p>
<a href={learnMoreHref} className="learn-more">Learn More</a>
<a id="learn_more" href={learnMoreHref} className="learn-more">Learn More</a>
<TrackProductButton className="button" extractedProduct={extractedProduct} />
</div>
);

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

@ -16,6 +16,7 @@ import {
} from 'commerce/state/prices';
import {productShape} from 'commerce/state/products';
import * as productActions from 'commerce/state/products';
import {recordEvent} from 'commerce/background/telemetry';
import 'commerce/browser_action/components/ProductCard.css';
@ -37,6 +38,7 @@ export default class ProductCard extends React.Component {
static propTypes = {
// Direct props
product: productShape.isRequired,
index: pt.number.isRequired,
// State props
latestPrice: priceWrapperShape.isRequired,
@ -57,16 +59,32 @@ export default class ProductCard extends React.Component {
*/
handleClick() {
browser.tabs.create({url: this.props.product.url});
this.recordClickEvent('open_external_page', 'ui_element', {element: 'product_card'});
window.close();
}
handleClickDelete(event) {
event.stopPropagation();
this.props.setDeletionFlag(this.props.product.id, true);
this.recordClickEvent('delete_product', 'delete_button');
}
handleClickUndo() {
this.props.setDeletionFlag(this.props.product.id, false);
this.recordClickEvent('undo_delete_product', 'undo_button');
}
recordClickEvent(method, object, extra = {}) {
const {activePriceAlert, latestPrice, originalPrice, product, index} = this.props;
recordEvent(method, object, null, {
...extra,
price: latestPrice.amount.getAmount().toString(),
// activePriceAlert is undefined if this product has never had a price alert
price_alert: activePriceAlert ? activePriceAlert.active.toString() : 'false',
price_orig: originalPrice.amount.getAmount().toString(),
product_index: index.toString(),
product_key: product.key,
});
}
render() {

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

@ -9,6 +9,7 @@ import {connect} from 'react-redux';
import * as productActions from 'commerce/state/products';
import {extractedProductShape, isProductTracked} from 'commerce/state/products';
import {recordEvent} from 'commerce/background/telemetry';
/**
* Button that tracks a product extracted from the current page when clicked.
@ -44,7 +45,12 @@ export default class TrackProductButton extends React.Component {
* Track the current tab's product when the track button is clicked.
*/
handleClickTrack() {
this.props.addProductFromExtracted(this.props.extractedProduct);
const {extractedProduct} = this.props;
const uuid = this.props.addProductFromExtracted(extractedProduct);
recordEvent('add_product', 'add_button', null, {
price: extractedProduct.price.toString(),
product_key: uuid,
});
}
render() {

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

@ -44,9 +44,9 @@ export default class TrackedProductList extends React.Component {
<React.Fragment>
<TrackProductButton className="menu-item" extractedProduct={extractedProduct} />
<ul className="product-list">
{sortedProducts.map(product => (
{sortedProducts.map((product, index) => (
<li className="product-list-item" key={product.id}>
<ProductCard product={product} />
<ProductCard product={product} index={index} />
</li>
))}
</ul>

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

@ -20,6 +20,12 @@ if (extractedProductJSON) {
appProps.extractedProduct = JSON.parse(extractedProductJSON);
}
// Pull tabId if present; only available via the url when there's a currently-viewed product
const tabIdJSON = url.searchParams.get('tabId');
if (tabIdJSON) {
appProps.tabId = JSON.parse(tabIdJSON);
}
ReactDOM.render(
<Provider store={store}>
<BrowserActionApp {...appProps} />

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

@ -8,6 +8,7 @@
*/
import pt from 'prop-types';
import uuidv4 from 'uuid/v4';
// Types
@ -23,6 +24,7 @@ export const productShape = pt.shape({
image: pt.string.isRequired,
isDeleted: pt.bool.isRequired,
vendorFaviconUrl: pt.string.isRequired,
key: pt.string.isRequired,
});
/**
@ -88,14 +90,22 @@ export default function reducer(state = initialState(), action) {
// Action Creators
/**
* Add a new product to the store.
* Add a new product to the store, adding an additional key with a random UUID (v4) value
* to help track this product for this user in telemetry.
* @param {ExtractedProduct} data
*/
export function addProductFromExtracted(data) {
return {
type: ADD_PRODUCT,
extractedProductData: data,
};
return ((dispatch) => {
const uuid = uuidv4();
dispatch({
type: ADD_PRODUCT,
extractedProductData: {
...data,
key: uuid,
},
});
return uuid;
});
}
/**
@ -188,5 +198,6 @@ export function getProductFromExtracted(data) {
image: data.image,
vendorFaviconUrl: data.vendorFaviconUrl || '',
isDeleted: false,
key: data.key,
};
}