Skip to content
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

Merged
merged 39 commits into from Jun 4, 2018
Merged

Shield study #4

merged 39 commits into from Jun 4, 2018

Conversation

jonathanKingston
Copy link
Owner

No description provided.

Copy link

@johannhof johannhof left a 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>

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.

Copy link
Owner Author

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

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.

Copy link
Owner Author

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"]);

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 :)

Copy link
Owner Author

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.

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");

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?

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.

Copy link
Owner Author

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 ✔️

/**
* Ensure that the user hasn't modified any pref in the prerequisite list
*/
async checkPrerequisites() {

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.

Copy link
Owner Author

Choose a reason for hiding this comment

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

✔️

}
},

async getStateName() {

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()?

Copy link
Owner Author

Choose a reason for hiding this comment

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

✔️

case null:
case "loaded":
if (await stateManager.checkPrerequisites()) {
await stateManager.setState("loaded");

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();?

let buttonsOutput = [];
if (options.buttons) {
let buttonIndex = 0;
for (let buttonIndex in options.buttons) {

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++ :)

Copy link
Owner Author

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. ✔️

},
async add(id, settingConfig) {
await this.init();
await ExtensionSettingsStore.addSetting(id, SETTING_TYPE, `settingConfig_${settingConfig.name}`, settingConfig);

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?

https://searchfox.org/mozilla-central/rev/d4b9e50875ad7e5d20f2fee6a53418315f6dfcc0/toolkit/components/extensions/ExtensionPreferencesManager.jsm#314

If that works this code would get much simpler...

@jonathanKingston
Copy link
Owner Author

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.
@jonathanKingston
Copy link
Owner Author

@johannhof I think I have covered all your feedback.

Notable changes include (1e418ae):

  • I removed all the Preferences code and essentially rolling it myself. We clear the prefs everytime it changes "state"
  • I added an "uninstall" state which is triggered whenever the addon is removed causing all the prefs to be reset. The exception is "disable" which is also an exit state (I added your feedback of uninstalling too), disabling sets the trr state still which is persisted.

Copy link

@johannhof johannhof left a 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 {

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.

Copy link
Owner Author

Choose a reason for hiding this comment

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

✔️

// TODO Decide if we need to do something here
break;
case "ADDON_UNINSTALL":
settingManager.clear(extension.id);

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 :)

Copy link
Owner Author

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

},
getUserPref(name, value) {
if (!Services.prefs.prefHasUserValue(name)) {
return null;

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?

Copy link
Owner Author

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 ✔️

}
await ExtensionSettingsStore.removeSetting(id, SETTING_TYPE, key);
}
return true;

Choose a reason for hiding this comment

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

nit: why return true?

Copy link
Owner Author

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);

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 :)

Copy link
Owner Author

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));

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);

Copy link
Owner Author

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]) {

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

Copy link
Owner Author

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]")];

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

Copy link
Owner Author

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;
}

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?

Copy link
Owner Author

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")];

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

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yup 👍

Copy link

@johannhof johannhof left a 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.

case null:
case "loaded":
if (await stateManager.hasUnmodifiedPrerequisites()) {
await stateManager.setState("loaded");

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();?

Copy link
Owner Author

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...

Copy link
Owner Author

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.

Copy link
Owner Author

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.

}
await ExtensionSettingsStore.removeSetting(id, SETTING_TYPE, key);
const addon = await AddonManager.getAddonByID(id);
addon.uninstall();

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...

Copy link
Owner Author

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

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) {

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 :(

Copy link
Owner Author

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.

Copy link
Owner Author

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.

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.

},

async show() {
browser.experiments.notifications.onButtonClicked.addListener((options) => {

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...

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) {

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.

await this.show();
} else {
// If the user hasn't met the criteria clean up
browser.management.uninstallSelf();

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.

Copy link
Owner Author

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.

@jonathanKingston jonathanKingston merged commit f6c2d9a into master Jun 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants