New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Shield study #4
Shield study #4
Conversation
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'm leaving some review comments but not approving for now, I'll need a little bit more looking at this, I think.
Overall I don't think there should be any major blockers, this looks solid and works, as far as I can tell.
<header> | ||
<h1 data-i18n-message="studyTitle"></h1> | ||
<form id="disable"> | ||
<button data-i18n-message="disableButtonText"></button> |
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.
This button does not have a hover or active state, which made it very confusing to me whether I could click it or not.
Please add one.
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.
✔️
src/study.js
Outdated
@@ -0,0 +1,67 @@ | |||
async function init() { | |||
// TODO speak with gandalf about new i18n lib to use fragments instead of this |
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 suppose this isn't happening anymore? :(
For a study (and since we can inspect the localizations in here) I can live with this, but it would be great if we could avoid this in the future.
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.
Removed the comment. Yeah the lib isn't ready yet :(
}); | ||
} | ||
|
||
const brandStrings = await browser.experiments.settings.getStrings(["brandShortName", "brandFullName"]); |
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 don't really mind this (it's pretty clean), but I'm also not sure why you need brandStrings for a Nightly-only experiment :)
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.
Just in case we then reuse this code and roll it out to other releases.
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.
👍
url: STUDY_URL | ||
}); | ||
browser.tabs.remove(tabs.map((tab) => tab.id)); | ||
browser.experiments.notifications.clear("rollout-prompt"); |
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.
So why are you not uninstalling the study if the user opts out? Is there anything left that this addon does?
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 think this is actually quite an important point, considering that the about:addons page says If you don’t want to participate in this study, you can select the Disable button on this page
, even if the pref is disabled...
Then there's also the question whether you should flip the rollout pref to -1 when the user disables the add-on explicitly. Maybe that's overthinking it, but leaving it at 1 wouldn't correctly reflect user choice for follow-up studies.
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.
- Added addon uninstall ✔️
- State of disable needs to persist ✔️
src/background.js
Outdated
/** | ||
* Ensure that the user hasn't modified any pref in the prerequisite list | ||
*/ | ||
async checkPrerequisites() { |
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.
nit: it's hard to tell what checkPrerequisites
returns, you should probably call it hasUnmodifiedPrerequisites
or something like that.
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.
✔️
src/background.js
Outdated
} | ||
}, | ||
|
||
async getStateName() { |
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.
nit: is there a reason why this is not called getState()
?
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.
✔️
src/background.js
Outdated
case null: | ||
case "loaded": | ||
if (await stateManager.checkPrerequisites()) { | ||
await stateManager.setState("loaded"); |
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.
Why are you manually setting "loaded"
again here? Aren't we already in loaded state? Shouldn't you set "enabled"
here instead of in this.show();
?
src/experiments/notifications/api.js
Outdated
let buttonsOutput = []; | ||
if (options.buttons) { | ||
let buttonIndex = 0; | ||
for (let buttonIndex in options.buttons) { |
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.
bah. If this code were landing in m-c I'd strongly prefer you to use for .. of
and just count buttonIndex++
:)
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.
Heh so I went for a hybrid of those I didn't need to init the buttonIndex. I think I meant to do what you want then realised the event closure received the latest value where using the index works as the closure didn't need it. I'll throw it into a function instead. ✔️
src/experiments/settings/api.js
Outdated
}, | ||
async add(id, settingConfig) { | ||
await this.init(); | ||
await ExtensionSettingsStore.addSetting(id, SETTING_TYPE, `settingConfig_${settingConfig.name}`, settingConfig); |
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.
Having looked into this for a while I get the feeling you're just duplicating efforts with ExtensionPreferencesManager by maintaining a separate ExtensionSettingsStore setting.
Can you try just ripping out ExtensionSettingsStore usage here and using ExtensionPreferencesManager.getSetting()
for settings.get()
instead?
If that works this code would get much simpler...
35474e7
to
db969ec
Compare
db969ec
to
57c32ea
Compare
…ch design more. Fix feedback from johannh on functin naming.
Added most of the changes. However it could do with the settings cleanup now for certain! Lunchtime though. |
…hich will be set for anything other than a disable.
@johannhof I think I have covered all your feedback. Notable changes include (1e418ae):
|
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.
A couple of things to fix (mostly small nits, but please look closely at the disable issue)
Thanks!
margin-inline-end: auto; | ||
} | ||
|
||
button { |
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.
Sorry to be pedantic about this, but please give this cursor: pointer
and a different background color for :active
.
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.
✔️
src/experiments/settings/api.js
Outdated
// TODO Decide if we need to do something here | ||
break; | ||
case "ADDON_UNINSTALL": | ||
settingManager.clear(extension.id); |
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.
Moving away from ExtensionPreferencesManager means you don't get the prefs cleanup for free when the extension is disabled, which may be what we want, because that means you can set the -1
state on ADDON_DISABLE
(because arguably disable means the user wanted to opt out of the experiment). I'm not sure how to guard this against disables that happen without user interaction, though...
In any case, please update this to call .clear()
when the add-on is disabled, because currently you're leaking the pref :)
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 set this to clear which sets the state to -2
due to the issue mentioned about guarding reasons. This is better than not having clearing at all... we could re-address though to change to set to -1
src/experiments/settings/api.js
Outdated
}, | ||
getUserPref(name, value) { | ||
if (!Services.prefs.prefHasUserValue(name)) { | ||
return null; |
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.
shouldn't this return value
?
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.
Ah yeah it was... thanks ✔️
src/experiments/settings/api.js
Outdated
} | ||
await ExtensionSettingsStore.removeSetting(id, SETTING_TYPE, key); | ||
} | ||
return true; |
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.
nit: why return true?
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.
✔️ Just habit of always having a positive return when things went well. I will remove.
}, | ||
async add(id, settingConfig) { | ||
await this.init(); | ||
await ExtensionSettingsStore.addSetting(id, SETTING_TYPE, `${SETTING_PREFIX}${settingConfig.name}`, settingConfig); |
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.
Ok, this went a little in the opposite direction of what I thought (only using ExtensionPreferencesManager and not using ExtensionSettingsStore directly). I didn't mean to make you rewrite this whole component, sorry for that.
I can live with this, even though I'd still say we're using ExtensionSettingsStore a bit outside of what it was intented to be :)
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.
Yeah it wasn't possible to remove the ExtensionPreferencesManager. Likely we could migrate to just basic storage now the code is simpler anyway.
As I said on slack it was likely super over engineered to make this as reusable as possible.
src/study.js
Outdated
continue; | ||
} | ||
const textOutput = childEl.textContent; | ||
el.appendChild(document.createTextNode(textOutput)); |
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.
nit: you could use el.append(textOutput);
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.
✔️
src/study.js
Outdated
const brandStrings = await browser.experiments.settings.getStrings(["brandShortName", "brandFullName"]); | ||
|
||
function createNodes(el, tagConfig) { | ||
for (const childEl of [...tagConfig.childNodes]) { |
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.
nit: NodeList is iterable, no need for the spread here
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.
✔️
src/study.js
Outdated
@@ -0,0 +1,66 @@ | |||
async function init() { | |||
const elements = [...document.querySelectorAll("[data-i18n-message]")]; |
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.
nit: NodeList is iterable, no need for the spread here
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.
✔️
src/study.js
Outdated
const el = document.createElement(tagConfig.tagName); | ||
for (const attr of [...tagConfig.attributes]) { | ||
el[attr.name] = attr.value; | ||
} |
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.
you could just call .cloneNode(false)
instead, right?
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.
Yup 👍
src/study.js
Outdated
return el; | ||
} | ||
|
||
const templates = [...document.querySelectorAll("template")]; |
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.
don't need the spread
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.
Yup 👍
…ll state for disabled extension.
Update Russian localization
…he addon is removed.
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 to say that the uninstall-on-disable flow doesn't feel as bad as I thought, so I'm in favor of that!
I had, however, a few final notes before I'd consider this good to ship.
src/background.js
Outdated
case null: | ||
case "loaded": | ||
if (await stateManager.hasUnmodifiedPrerequisites()) { | ||
await stateManager.setState("loaded"); |
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.
Maybe I missed it, was this question answered?
Why are you manually setting "loaded" again here? Aren't we already in loaded state? Shouldn't you set "enabled" here instead of in this.show();?
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.
The loaded state is mostly to track if we broke before enabling... it shouldn't ever be observable. However the user needs to go through it when in null state. I think the loaded state case couldn't be removed from this switch for some reason, will check...
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.
Ah I remember now... as I'm not observing the state changing, I'm just doing the init
on addon install. My hope was if we ever had an error in showing the extension we could ship a new study to the users with loaded
as a state. I can remove the loaded state and make null
go straight to calling show()
which will set the enabled
state.
That said enabled
really should be at the bottom of the show
function to make this worthwhile.
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 pushed a change to make null
fall through to loaded
which is always going to be the normal loading path. I moved enabled
to the bottom of the fn as mentioned.
src/experiments/settings/api.js
Outdated
} | ||
await ExtensionSettingsStore.removeSetting(id, SETTING_TYPE, key); | ||
const addon = await AddonManager.getAddonByID(id); | ||
addon.uninstall(); |
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 don't think these lines are supposed to happen inside the while loop...
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.
✔️ thanks, no it wasn't however this addon wouldn't have an observable difference here given we only have one settings thing. Either way I made the change. Thanks
src/experiments/settings/api.js
Outdated
const state = await this.get(settingName); | ||
const config = await this.getSettingConfig(settingName); | ||
// If we don't have a valid state or it's not a minus state then set uninstalled | ||
if (!config.states[state] || config.states[state].id >= 0) { |
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.
So currently that means once the user clicks "OK" and gets into state -4
, uninstalling or disabling in the add-on manager will not overwrite that.
Is that intended? It doesn't seem like it.
This is again the dilemma of "we're uninstalling but did the user really uninstall?". I'm not sure how to solve this :(
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.
This is the intended, as soon as the user clicks OK then we consider that as a final state. The only change we make is clearing the non persistent prefs.
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.
As I was putting it in though I felt the same, it's very hard to track what users are intending to do here. -4
is basically just a different uninstall giving us slightly more data to reason with on the next study.
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.
You probably want to note that briefly in the _comment for the "disabled" state.
cb6aa16
to
011581b
Compare
… end of function.
src/background.js
Outdated
}, | ||
|
||
async show() { | ||
browser.experiments.notifications.onButtonClicked.addListener((options) => { |
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.
Just a passing note that you're not handling [x] button clicks, which is probably intended...
src/experiments/settings/api.js
Outdated
const state = await this.get(settingName); | ||
const config = await this.getSettingConfig(settingName); | ||
// If we don't have a valid state or it's not a minus state then set uninstalled | ||
if (!config.states[state] || config.states[state].id >= 0) { |
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.
You probably want to note that briefly in the _comment for the "disabled" state.
…to avoid loading without prerequisites check.
…mit API into extension.
Fix ESLint config
src/background.js
Outdated
await this.show(); | ||
} else { | ||
// If the user hasn't met the criteria clean up | ||
browser.management.uninstallSelf(); |
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.
This will reset the user prefs through calling the UNINSTALL handler in api.js. Please make sure that doesn't happen, e.g. through defining a new state for this.
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.
✔️ Done, I moved this to use the new null clear state that is in the experiment.
No description provided.