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

Fix #158: Add non-UI telemetry probes #240

Merged
merged 11 commits into from
Nov 6, 2018

Conversation

biancadanforth
Copy link
Collaborator

@biancadanforth biancadanforth commented Nov 1, 2018

Broke this up into a few commits. Also updated the extension's name and telemetry category in METRICS.md and added a new Appendix section to explain how to find the probes (mostly for QA).

I don’t like how I get the “previous price” (I refetch state) for the detect_price_change probe, but I didn't see another way.

@biancadanforth biancadanforth requested a review from Osmose November 1, 2018 18:43
@biancadanforth
Copy link
Collaborator Author

@Osmose , ready for your review!

Copy link
Contributor

@Osmose Osmose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, I'm glad we were able to get to this in the end!

@@ -86,10 +95,12 @@ export async function handleNotificationClicked(notificationId) {
// Record open_external_page event
const latestPrice = getLatestPriceForProduct(state, product.id);
const originalPrice = getOldestPriceForProduct(state, product.id);
const highPriceAmount = Dinero({amount: alert.highPriceAmount});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to convert this; if we're not comparing it to other amounts or displaying it, the Dinero object adds nothing (and honestly it's adding almost nothing right now anyway, I kinda regret including it before supporting multiple currencies).

const state = store.getState();
const id = getProductIdFromExtracted(data);
const product = getProduct(state, id);
if (product) {
store.dispatch(addPriceFromExtracted(data));
const isPriceChange = await store.dispatch(addPriceFromExtracted(data));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only run when there's an existing product, which means we already have at least one price logged. This method has an if check inside that checks whether the price being added is the first price logged, but given that we only care about the return value here, and we already know that the if statement will be true, we can skip it completely.

Instead, what if addPriceFromExtracted returns either the price that was added or null? That way, we don't need to fetch the latest price below at all. If the only prices we need to fetch are the previous price and original price, we can fetch those using the previous state object and avoid calling getState again.

function extractProduct() {
for (const extract of EXTRACTION_METHODS) {
function extractProduct(isBackgroundUpdate) {
const baseExtra = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The amount of events in this method are making it difficult to follow, what if we had a more concise API?

function extractProduct() {
  const attempt = new ExtractionAttempt();
  attempt.start();
  for (const extract of EXTRACTION_METHODS) {
    const extractedProduct = extract(window.document);
    if (extractedProduct) {
      attempt.succeed(methodName);
      return extractedProduct;
    }
  }

  attempt.fail();
  return null;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea; thank you!

@@ -51,6 +52,11 @@ export async function handleExtractedProductData({extractedProduct}, sender) {
tabId,
});
browser.browserAction.setBadgeText({text: '✚', tabId});
if (sendTelemetry) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this? To prevent sending events when the badge isn't changing from its previous value?

If so, couldn't we just compare the old and new values instead of passing a parameter around?

Copy link
Collaborator Author

@biancadanforth biancadanforth Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added that second argument to sendProductToBackground because when we call resend (the messy workaround for Bug 1493470 in PR #220 ), we are also re-recording the badge_toolbar_button event (for badge type of 'add'). It is a temporary addition to the temporary code to also be removed when resend is removed.

Although your idea of only recording the event (for badge types of 'add' and 'price_alert') if the previous badge value is different would also work here. I'll do that instead, since that can stay even after resend is removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Geeze this was a lot trickier than I anticipated. More soon when I resubmit the PR...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still seeing duplicate detect_price_change events due to resend:

[STR]:

  1. Open the fake product page in Tab 1, say it has Price $A.
  2. Add the product via the browserAction toolbar button
  3. Refresh the page until a Price Alert is triggered, Price $B.
  4. Immediately click on the System Notification (within 5 seconds of refreshing the page that triggered the alert)
  5. This will open another fake product page in Tab 2 (at Price $C) and perform extraction before resend has been executed even once in Tab 1 fake product page.
  6. Because the price is different between Tabs 1 and 2, the price history will look something like: [A, ... (refreshing to trigger a change), B, C, B (resend), C (resend), B (resend), C (resend)], where all the resend prices are duplicate events which can incorrectly trigger price alerts.

I don't think this will be a problem in any real world scenario, since it would require a product page's price to change within 5 seconds of visiting the page.

@Osmose , do you agree that this is a non-issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this should be fine.

async function attemptExtraction() {
const extractedProduct = extractProduct();
async function attemptExtraction(isBackgroundUpdate) {
const extractedProduct = extractProduct(isBackgroundUpdate);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing this parameter through a bunch of methods, if we move the isBackgroundUpdate code into a function, we can call it again when we need to log the value a second time. We could also assign it to a global variable, as whether the page is a background update or not is not expected to change within this script.

@biancadanforth
Copy link
Collaborator Author

@Osmose Ready for another look!

@teonbrooks I haven't heard back from you re: collapsing the privacy extra keys into a single key; if there's still no feedback by Monday (11/5/18), I will proceed with that approach in this PR.

@biancadanforth biancadanforth added the [ENG]: Do not merge Do not merge this pull request label Nov 3, 2018
@teonbrooks
Copy link

sorry for the delay. for the extra keys, I would go with the splitting, if possible, that follow the event telemetry schema. it would make for straightforward analysis

@biancadanforth biancadanforth removed the [ENG]: Do not merge Do not merge this pull request label Nov 3, 2018
@biancadanforth biancadanforth removed the request for review from teonbrooks November 3, 2018 20:58
Copy link
Contributor

@Osmose Osmose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, nice work!

export async function hasBadgeTextChanged(nextText, tabId = null) {
// If the nextText contains a digit, we know it's a price alert badge, which only affects
// the global badge text.
const returnGlobal = (/\d+/.test(nextText));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't tabId being null be a better signal that we want to check the global badge text?

if (prevText !== nextText) {
return true;
}
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can replace this conditional with:

return prevText !== nextText;

@@ -229,6 +231,19 @@ export async function getBadgeType() {
}
}

export async function hasBadgeTextChanged(nextText, tabId = null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is difficult to read at the point of usage; hasBadgeTextChanged('+') doesn't really make it clear that the parameter is the badge text we're intending to change to.

Since it's being passed in anyway, maybe change this to getToolbarBadgeText(tabId) and do the comparison in place?

if (getToolbarBadgeText(tabId) !== '+') {

}

@@ -28,40 +28,72 @@ const EXTRACTION_METHODS = {
open_graph: extractProductWithOpenGraph,
};

const IS_BACKGROUND_UPDATE = (function isBackgroundUpdate() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun fact, since we use let and const now we don't need to use functions to hide one-time computations, blocks work just as well:

let isBackgroundUpdate = false;
{ // This is a block! They are neat!
  let result = false;
  try {
    isBackgroundUpdate = window.top.location.href.startsWith(
      browser.runtime.getURL('/'), // URL is unique per-install / hard to forge
    );
  } catch (err) {
    // Non-background updates may throw a cross-origin error
  }
}

although since we don't need the intermediate result value anyway we can simplify even more:

let isBackgroundUpdate = false;
try {
  isBackgroundUpdate = window.top.location.href.startsWith(
    browser.runtime.getURL('/'), // URL is unique per-install / hard to forge
  );
} catch (err) {
  // Non-background updates may throw a cross-origin error
}

Copy link
Collaborator Author

@biancadanforth biancadanforth Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I haven't really made use of blocks, but I know what you mean in that const and let are block-scoped, and I don't need a function in this case. Makes sense.

/**
* Helper class to record extraction-related telemetry events.
*/
class ExtractionAttempt {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

68747470733a2f2f332e62702e626c6f6773706f742e636f6d2f2d6d7852764e586977584a592f575042776e554d55574b492f41414141414141414957412f665a57714b335a58414a6762344c61574d6e335955494a7477784f457252654341434c63422f733332302f4865616425324

Copy link
Collaborator Author

@biancadanforth biancadanforth Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That guy looks pretty smug. He wishes he had Osmose's antlers!

@biancadanforth
Copy link
Collaborator Author

@Osmose , thanks for the feedback. This is ready for another look!

Copy link
Contributor

@Osmose Osmose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thank you for this!

// The 'price_alert' badge modifies the global badge text and will not have a tabId.
const returnGlobal = !tabId;
return browser.browserAction.getBadgeText(
returnGlobal ? {} : {tabId},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may optionally consider dropping the variable:

return browser.browserAction.getBadgeText(tabId ? {tabId} : {});

Add 'detect_price_change' probe
* Add `price_prev` extra key for `detect_price_change` event. This will be the same value as `price_orig` if there is only one price entry for the product when updating prices. This will help us answer questions around how much prices change in addition to how frequently.
Additionally:
* Update the name of the extension in METRICS.md to Price Wise.
* Add Appendix B to METRICS.md which explains how to view recorded telemetry events in about:telemetry.
Add badge_toolbar_button and send_notification probes:
* Add an additional extra key, 'price_last_high' to 'send_notification' and 'open_external_page' (with 'element' value of 'system_notification') and update METRICS.md.
* Add a 'sendTelemetry' flag for the 'sendProductToBackground' function to also be removed when the 'resend' code is removed in './src/extraction/index.js'.
  * Due to the fix for Issue mozilla#192, I was seeing some extra 'badge_toolbar_button' events (with 'badge_type' of 'add') as we are resending extracted product information 5 and 10 seconds after the product page loads.
* Update METRICS.md to clarify when 'badge_toolbar_button' with a 'badge_type' of 'price_alert' fires, since it will continue to fire for the same price alert until that alert is dismissed which is not obvious.
…robes

* Update 'method' extra key for 'complete_extraction' event to have values of 'fathom', 'css_selectors', 'open_graph' or 'none' to distinguish between the two fallback extraction methods to Fathom: CSS Selectors or Open Graph attributes.
  * Note: Since none of our five supported sites use Open Graph attributes currently for product information, we should not expect to see any successful extraction using that method for the MVP.
* Important caveats for making conclusions using these probes as-is:
  * The coverage values that these probes will suggest will not be very accurate initially until we ensure we are extracting on only **product pages** for a supported site, rather than any page on the site (see Issues mozilla#225 and mozilla#181).
  * Successful extraction does not mean that the information extracted for the product is correct. It only means that _a_ value was extracted for each product feature (i.e. title, image and price) on the page.
Event Telemetry [limits the max number of extra keys](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html#limits) for an event to 10. In the fix for Issue mozilla#185, where three new privacy-related extra keys were added to every telemetry event, the number of extra keys for the 'open_external_page' event went to 11, which causes event telemetry to fail at the registration step.

This patch collapses these 3 privacy keys into a single key that points to a stringified JSON object.
Break up Extra Keys section into Common Extra Keys and Event-specific Extra Keys subsections. Update Sample Ping section to include additional common keys added from Issue mozilla#185.
Issue mozilla#157's patch removed this event from METRICS.md, but failed to remove it from the code. The uninstall event is recorded by the Addons Manager. It is not possible to record from a WE, as the extension is uninstalled before the event can be recorded.
* Note: This fix is complicated by the fact that a 'price alert' badge only changes the global badge state. An 'add' product badge changes the tab-specific badge state, which takes precedence over the global badge state.

To avoid event duplication for 'badge_toolbar_button':
  * Add 'hasBadgeTextChanged' function with optional 'tabId' argument, so that the event only fires if the badge text has changed.
    * Previously, we were getting a bunch of redundant `badge_toolbar_button` events for ‘price_alert’ any time the state was changed and we’re on a page with an ‘add’ badge. Since `handlePriceAlerts` is called every time state changes in any way (`store.subscribe(handlePriceAlerts)`), we now check to see if the badge text has changed for a price alert badge on the global browserAction button before recording the 'badge_toolbar_button' with badge type 'price_alert' event.
    * Previously, if tab 1 had badge type 'add' and tab 2 had badge type 'price_alert', and I switch to tab 2 before any calls to 'resend' (mozilla#220) are made from tab 1, I would get two extra 'badge_toolbar_button' of type 'add' events. Adding the 'tabId' optional argument fixes this bug.
… events

This patch:
* Reverts "Collapse privacy extra keys into a single key" (commit 32fcfc4) while preserving the Common Extra Keys and Event-Specific Extra Keys subsections and updating the Sample Ping section in the METRICS.md document.
* Breaks out 'open_external_page' event (11 extra keys) into two events: 'open_nonproduct_page' (5 extra keys) and 'open_product_page' (10 extra keys).
@biancadanforth biancadanforth merged commit 538268a into mozilla:master Nov 6, 2018
@biancadanforth biancadanforth deleted the 158-non-ui-probes branch November 6, 2018 21:12
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants