diff --git a/.gitignore b/.gitignore index a38fdd1..d46f949 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules src/privileged/shieldUtils/jsm/StudyUtils.jsm src/privileged/shieldUtils/jsm/PioneerUtils.jsm -src/background.js +src/feature.js diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..fe02baa --- /dev/null +++ b/src/background.js @@ -0,0 +1,138 @@ +/* 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)); + } + + /** + * 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.storage.local.clear(); + 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) { + console.log("enabling feature", studyInfo); + if (studyInfo.timeUntilExpire) { + const alarmName = `${browser.runtime.id}:studyExpiration`; + const alarmListener = async alarm => { + if (alarm.name === alarmName) { + console.log("study will expire now!"); + browser.alarms.onAlarm.removeListener(alarmListener); + await browser.study.endStudy("expired"); + } + }; + browser.alarms.onAlarm.addListener(alarmListener); + browser.alarms.create(alarmName, { + delayInMinutes: studyInfo.timeUntilExpire / (1000 * 60), + }); + } + feature.configure(studyInfo); + } + + /** handles `study:end` signals + * + * - opens 'ending' urls (surveys, for example) + * - calls cleanup + * + * @param {object} ending An ending result + * + * @returns {undefined} + */ + async handleStudyEnding(ending) { + console.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: + console.log(`the ending: ${ending.endingName}`); + await this.cleanup(); + break; + } + // actually remove the addon. + console.log("about to actually uninstall"); + return browser.management.uninstallSelf(); + } +} + +// construct. will be configured after setup. +const feature = new Feature(); + +/** + * Run every startup to get config and instantiate the feature + * + * @returns {undefined} + */ +async function onEveryExtensionLoad() { + new StudyLifeCycleHandler(); + + const studySetup = await getStudySetup(); + console.log(`studySetup: ${JSON.stringify(studySetup)}`); + await browser.study.setup(studySetup); +} +onEveryExtensionLoad();