Skip to content
This repository has been archived by the owner on Dec 3, 2020. It is now read-only.

Commit

Permalink
Fix #156: Add extension UI probes
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
biancadanforth committed Oct 16, 2018
1 parent 8070d93 commit 502c6c2
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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"
}
}
1 change: 1 addition & 0 deletions src/background/extraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function handleExtractedProductData(contentExtractedProduct, 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) {
Expand Down
12 changes: 12 additions & 0 deletions src/background/price_alerts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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));
Expand Down
24 changes: 23 additions & 1 deletion src/background/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
* @module
*/

import store from 'commerce/state';
import {getAllProducts} from 'commerce/state/products';

const CATEGORY = 'extension.price_alerts';

const EVENTS = {
Expand Down Expand Up @@ -164,6 +167,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 changes: 11 additions & 1 deletion src/browser_action/components/BrowserActionApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import TrackedProductList from 'commerce/browser_action/components/TrackedProduc
import {extractedProductShape, getAllProducts, productShape} from 'commerce/state/products';
import * as syncActions from 'commerce/state/sync';
import {removeMarkedProducts} from 'commerce/state/products';
import {recordEvent, getBadgeType} from 'commerce/background/telemetry';

import 'commerce/browser_action/components/BrowserActionApp.css';

Expand All @@ -34,6 +35,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,
Expand All @@ -45,6 +47,7 @@ export default class BrowserActionApp extends React.Component {

static defaultProps = {
extractedProduct: null,
tabId: null,
}

constructor(props) {
Expand All @@ -54,10 +57,15 @@ export default class BrowserActionApp extends React.Component {
};
}

componentDidMount() {
async componentDidMount() {
this.props.loadStateFromStorage();
window.addEventListener('unload', this.handleUnload);

// 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});
Expand All @@ -81,6 +89,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();
}

Expand All @@ -89,6 +98,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();
}

Expand Down
15 changes: 9 additions & 6 deletions src/browser_action/components/EmptyOnboarding.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
}
}
Expand All @@ -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>
);
Expand Down
18 changes: 18 additions & 0 deletions src/browser_action/components/ProductCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
Expand All @@ -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() {
Expand Down
8 changes: 7 additions & 1 deletion src/browser_action/components/TrackProductButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions src/browser_action/components/TrackedProductList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions src/browser_action/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand Down
21 changes: 16 additions & 5 deletions src/state/products.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import pt from 'prop-types';
import uuidv4 from 'uuid/v4';

// Types

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

/**
Expand Down Expand Up @@ -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;
});
}

/**
Expand Down Expand Up @@ -188,5 +198,6 @@ export function getProductFromExtracted(data) {
image: data.image,
vendorFaviconUrl: data.vendorFaviconUrl || '',
isDeleted: false,
key: data.key,
};
}

0 comments on commit 502c6c2

Please sign in to comment.