-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revert "Removed study add-on src and tests"
This reverts commit bd87b7124e8ada62b12ad02b210e0c50b7c70f11.
- Loading branch information
Showing
38 changed files
with
2,105 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
motin
Author
Contributor
|
||
} | ||
|
||
/** 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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
All icons are public domain and came from https://openclipart.org/. |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
1 comment
on commit dc47d20
There was a problem hiding this comment.
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 ;)
hm, why return if the result is
undefined
?