Skip to content

Commit

Permalink
Revert "Removed study add-on src and tests"
Browse files Browse the repository at this point in the history
This reverts commit bd87b7124e8ada62b12ad02b210e0c50b7c70f11.
  • Loading branch information
motin committed Jul 4, 2019
1 parent 031ac6b commit dc47d20
Show file tree
Hide file tree
Showing 38 changed files with 2,105 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/_locales/en-US/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extensionName": {
"message": "Preparatory Survey for NAAR",
"description": "Name of the extension"
},
"extensionDescription": {
"message":
"Prompts users to participate in a questionnaire where you can let Mozilla know why you chose your particular Extensions. With your participation you can help others discover better Extensions.",
"description": "Description of the extension."
}
}
74 changes: 74 additions & 0 deletions src/addonsData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "getSelfInstalledEnabledAddonsWithAmoData" }]*/

let fetchedAmoData;

const getSelfInstalledEnabledAddonsWithAmoData = async() => {
// Users need to have at least 3 self-installed add-ons to be eligible
const listOfInstalledAddons = await browser.addonsMetadata.getListOfInstalledAddons();
const listOfSelfInstalledEnabledAddons = listOfInstalledAddons.filter(
addon =>
!addon.isSystem && !addon.userDisabled && addon.id !== browser.runtime.id,
);
if (listOfSelfInstalledEnabledAddons.length === 0) {
await browser.study.logger.info(
"No self-installed enabled add-ons found. Returning an empty result",
);
return [];
}

if (!fetchedAmoData) {
// Fetching AMO metadata about the extensions allows us to verify that
// they are listed in AMO and retrieve public extension names and icon URLs
const guids = listOfSelfInstalledEnabledAddons.map(addon => addon.id);
const amoDataUrl = `https://addons.mozilla.org/api/v3/addons/search/?guid=${encodeURIComponent(
guids.join(","),
)}`;
const amoDataResponse = await fetch(amoDataUrl).catch(async error => {
await browser.study.logger.error(
"Error when fetching metadata from AMO. Returning an empty result",
);
await browser.study.logger.error({ error });
return false;
});
if (!amoDataResponse) {
await browser.study.logger.error(
"Fetched AMO response empty. Returning an empty result",
);
return [];
}

const amoData = await amoDataResponse.json();
if (!amoData || !amoData.results) {
await browser.study.logger.error(
"Fetched metadata from AMO empty. Returning an empty result",
);
return false;
}

fetchedAmoData = amoData;
}

return listOfSelfInstalledEnabledAddons
.map(addon => {
const matchingAmoDataResultsEntry = fetchedAmoData.results.find(
amoDataResultsEntry => amoDataResultsEntry.guid === addon.id,
);
return {
...addon,
amoData: matchingAmoDataResultsEntry
? {
guid: matchingAmoDataResultsEntry.guid,
icon_url: matchingAmoDataResultsEntry.icon_url,
name_en_us: matchingAmoDataResultsEntry.name["en-US"],
}
: null,
};
})
.filter(
addon =>
addon.amoData !== null &&
addon.amoData.guid &&
addon.amoData.icon_url &&
addon.amoData.name_en_us,
);
};
140 changes: 140 additions & 0 deletions src/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/* global getStudySetup, feature */

/**
* Goal: Implement an instrumented feature using `browser.study` API
*
* Every runtime:
* - Prepare
*
* - listen for `onEndStudy` (study endings)
* - listen for `study.onReady`
*
* - Startup the feature
*
* - attempt to `browser.study.setup` the study using our studySetup
*
* - will fire EITHER
* - `endStudy` (`expired`, `ineligible`)
* - onReady
* - (see docs for `browser.study.setup`)
*
* - onReady: configure the feature to match the `variation` study selected
* - or, if we got an `onEndStudy` cleanup and uninstall.
*
* During the feature:
* - `sendTelemetry` to send pings
* - `endStudy` to force an ending (for positive or negative reasons!)
*
* Interesting things to try next:
* - `browser.study.validateJSON` your pings before sending
* - `endStudy` different endings in response to user action
* - force an override of setup.testing to choose branches.
*
*/

class StudyLifeCycleHandler {
/**
* Listen to onEndStudy, onReady
* `browser.study.setup` fires onReady OR onEndStudy
*
* call `this.enableFeature` to actually do the feature/experience/ui.
*/
constructor() {
/*
* IMPORTANT: Listen for `onEndStudy` before calling `browser.study.setup`
* because:
* - `setup` can end with 'ineligible' due to 'allowEnroll' key in first session.
*
*/
browser.study.onEndStudy.addListener(this.handleStudyEnding.bind(this));
browser.study.onReady.addListener(this.enableFeature.bind(this));
this.expirationAlarmName = `${browser.runtime.id}:studyExpiration`;
}

/**
* Cleanup
*
* (If you have privileged code, you might need to clean
* that up as well.
* See: https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/lifecycle.html
*
* @returns {undefined}
*/
async cleanup() {
await browser.study.logger.log("Cleaning up study local storage");
await browser.storage.local.clear();
await browser.study.logger.log("Cleaning up alarm expiration");
await browser.alarms.clear(this.expirationAlarmName);
await browser.study.logger.log("Cleaning up feature artifacts");
await feature.cleanup();
}

/**
*
* side effects
* - set up expiration alarms
* - make feature/experience/ui with the particular variation for this user.
*
* @param {object} studyInfo browser.study.studyInfo object
*
* @returns {undefined}
*/
async enableFeature(studyInfo) {
await browser.study.logger.log(["Enabling experiment", studyInfo]);
const { delayInMinutes } = studyInfo;
if (delayInMinutes !== undefined) {
const alarmName = this.expirationAlarmName;
const alarmListener = async alarm => {
if (alarm.name === alarmName) {
browser.alarms.onAlarm.removeListener(alarmListener);
await browser.study.endStudy("expired");
}
};
browser.alarms.onAlarm.addListener(alarmListener);
browser.alarms.create(alarmName, {
delayInMinutes,
});
}
return feature.configure(studyInfo);

This comment has been minimized.

Copy link
@rhelmer

rhelmer Jul 8, 2019

hm, why return if the result is undefined?

This comment has been minimized.

Copy link
@motin

motin Jul 8, 2019

Author Contributor

Ah, configure is async so it returns a Promise. This just forwards the promise so that no promise gets left behind :)

}

/** handles `study:end` signals
*
* - opens 'ending' urls (surveys, for example)
* - calls cleanup
*
* @param {object} ending An ending result
*
* @returns {undefined}
*/
async handleStudyEnding(ending) {
await browser.study.logger.log([`Study wants to end:`, ending]);
for (const url of ending.urls) {
await browser.tabs.create({ url });
}
switch (ending.endingName) {
// could have different actions depending on positive / ending names
default:
await browser.study.logger.log(`The ending: ${ending.endingName}`);
await this.cleanup();
break;
}
// actually remove the addon.
await browser.study.logger.log("About to actually uninstall");
return browser.management.uninstallSelf();
}
}

/**
* Run every startup to get config and instantiate the feature
*
* @returns {undefined}
*/
async function onEveryExtensionLoad() {
new StudyLifeCycleHandler();

const studySetup = await getStudySetup();
await browser.study.logger.log([`Study setup: `, studySetup]);
await browser.study.setup(studySetup);
}
onEveryExtensionLoad();
153 changes: 153 additions & 0 deletions src/feature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "(feature)" }]*/
/* global getSelfInstalledEnabledAddonsWithAmoData */

/**
* **Example template documentation - remove or replace this jsdoc in your study**
*
* Example Feature module for a Shield Study.
*
* UI:
* - during INSTALL only, show a notification bar with 2 buttons:
* - "Thanks". Accepts the study (optional)
* - "I don't want this". Uninstalls the study.
*
* Firefox code:
* - Implements the 'introduction' to the 'button choice' study, via notification bar.
*
* Demonstrates `studyUtils` API:
*
* - `telemetry` to instrument "shown", "accept", and "leave-study" events.
* - `endStudy` to send a custom study ending.
*
**/
class Feature {
constructor() {}

/**
* @returns {Promise<*>} Promise that resolves after configure
*/
async configure() {
const feature = this;

const { notificationShown } = await browser.storage.local.get(
"notificationShown",
);
if (!notificationShown) {
const selfInstalledEnabledAddonsWithAmoData = await getSelfInstalledEnabledAddonsWithAmoData();
await browser.study.logger.debug({
selfInstalledEnabledAddonsWithAmoData,
});

const baseUrl = await browser.study.fullSurveyUrl(
"https://qsurvey.mozilla.com/s3/extensions-satisfaction-survey-2019-1/",
"accept-survey",
);
await browser.study.logger.debug({
baseUrl,
});
const addonsSurveyData = selfInstalledEnabledAddonsWithAmoData.map(
addon => {
try {
return {
guid: addon.amoData.guid,
name: addon.amoData.name_en_us,
icon: addon.amoData.icon_url,
};
} catch (error) {
// Surfacing otherwise silent errors
// eslint-disable-next-line no-console
console.error(error.toString(), error.stack);
throw new Error(error.toString());
}
},
);
await browser.study.logger.debug({
addonsSurveyData,
});

// https://github.com/Daplie/knuth-shuffle
const shuffle = array => {
let currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}

return array;
};

// Send information about at most 10 randomly selected self-installed addons to the survey URL
const shuffledAddonsSurveyData = shuffle(addonsSurveyData);
await browser.study.logger.debug({
shuffledAddonsSurveyData,
});
const shuffledAndCappedAddonsSurveyData = shuffledAddonsSurveyData.slice(
0,
10,
);
await browser.study.logger.debug({
shuffledAndCappedAddonsSurveyData,
});

const surveyUrl =
baseUrl +
"&addons=" +
encodeURIComponent(JSON.stringify(shuffledAndCappedAddonsSurveyData));
await browser.study.logger.debug({
surveyUrl,
});

await browser.study.logger.log(
"First run. Showing faux Heartbeat prompt",
);

browser.fauxHeartbeat.onShown.addListener(async() => {
await browser.study.logger.log("notification-shown");
await browser.storage.local.set({ notificationShown: true });
});

browser.fauxHeartbeat.onAccept.addListener(async() => {
await browser.study.logger.log("accept-survey");

// Fire survey
await browser.study.logger.log("Firing survey");
await browser.tabs.create({ url: surveyUrl });

browser.study.endStudy("accept-survey");
});

browser.fauxHeartbeat.onReject.addListener(async() => {
await browser.study.logger.log("closed-notification-bar");
browser.study.endStudy("closed-notification-bar");
});

await browser.fauxHeartbeat.show({
notificationMessage: "Help others discover better Extensions!",
buttonLabel: "Take me to the questionnaire",
});
} else {
browser.study.endStudy("notification-already-shown");
}
}

/**
* Called at end of study, and if the user disables the study or it gets uninstalled by other means.
* @returns {Promise<*>} Promise that resolves after cleanup
*/
async cleanup() {
await browser.study.logger.log("Cleaning up fauxHeartbeat artifacts");
await browser.fauxHeartbeat.cleanup();
}
}

// make an instance of the feature class available to background.js
// construct only. will be configured after setup
window.feature = new Feature();
1 change: 1 addition & 0 deletions src/icons/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
All icons are public domain and came from https://openclipart.org/.
13 changes: 13 additions & 0 deletions src/icons/heartbeat-icon.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/icons/shield-icon.256.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/icons/shield-icon.48.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/icons/shield-icon.98.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

1 comment on commit dc47d20

@rhelmer
Copy link

@rhelmer rhelmer commented on dc47d20 Jul 8, 2019

Choose a reason for hiding this comment

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

I have a few random comments but the important parts lgtm, nice work! Note that I mostly focused on the privileged parts, I see that QA is ongoing but I'd also suggest that you double-check the brand assets ;)

Please sign in to comment.