-
Notifications
You must be signed in to change notification settings - Fork 63
[EMB-110] Add waffle feature flagging #117
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
[EMB-110] Add waffle feature flagging #117
Conversation
brianjgeiger
left a comment
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.
Since this is for the current functionality, could you add in support for the Dashboard and Support pages? They'll be ember_<page-name>_page, so ember_dashboard_page and ember_support_page based on https://github.com/CenterForOpenScience/osf.io/pull/8208/files#diff-7139e1d97ed3c74b51c423786cd04d66R7
app/mixins/waffle-route.ts
Outdated
|
|
||
| export default Mixin.create({ | ||
| features: service(), | ||
| currentUser: service(), |
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.
For TS to understand, we have to repeat the name of the service, e.g. service('features')
(Until we upgrade to using ember-decorators, that is)
app/services/current-user.js
Outdated
| }); | ||
| }), | ||
|
|
||
| _setWaflle: task(function* () { |
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.
Typo: Waflle
app/services/current-user.js
Outdated
|
|
||
| _setWaflle: task(function* () { | ||
| const url = `${config.OSF.apiUrl}/v2/_waffle/`; | ||
| const { data } = yield $.ajax(url, 'GET'); |
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 this will be a logged-in request; should probably use authenticatedAjax
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.
What do you mean by not a logged-in request?
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.
Like including the cookie, so the API knows what user to get the waffle for. But I just realized that's only a concern for cross-origin ajax; it'll do the right thing by default for same-origin. In case we ever use this code in other apps, though, we should probably use authenticatedAjax from ember-osf-web/utils/ajax-helpers for all our ajax needs.
app/mixins/waffle-route.ts
Outdated
| return this.get('currentUser._setWaflle').perform().then(() => this._super(...args)); | ||
| }, | ||
| actions: { | ||
| didTransition(...args) { |
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 should ideally go in willTransition, so we can reload the page as soon as possible after the user clicks. didTransition waits for all the route hooks to resolve, which can take a while.
Actually, would it be possible to do this check in the router willTransition, instead of the route? You could get the target route from the transition object, and maybe have route-to-flag map in the config? It might be nicer to have all this in a central place, rather than a mixin in every route.
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.
Will try it! willTransititon was the ideal place for it, but it would've required it to go on the origin route, rather than the destination that cared about the flag, which seemed wrong. Did not think about the router's willTransition though.
app/services/current-user.js
Outdated
| */ | ||
| user: Ember.computed('currentUserId', function() { | ||
| const ObjectPromiseProxy = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin); | ||
| this.get('_setWaflle').perform(); |
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 sort of thing shouldn't go in a computed property, since it might not run if nothing is currently depending on user.
Instead, maybe subscribe to the session's authenticationSucceeded and invalidationSucceeded events in the constructor?
this.get('session').on('authenticationSucceeded', this, function() {
this.get('_setWaffle').perform();
});
app/mixins/waffle-route.ts
Outdated
| features: service(), | ||
| currentUser: service(), | ||
| beforeModel(...args) { | ||
| return this.get('currentUser._setWaflle').perform().then(() => this._super(...args)); |
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.
Calling _super inside a callback makes me wary, since beforeModel will actually return without having called _super.
685bba1 to
1feb3a9
Compare
1feb3a9 to
caa68e5
Compare
app/services/current-user.js
Outdated
|
|
||
| constructor() { | ||
| this._super(...arguments); | ||
| this.get('session').on('authenticationSucceeded', this, function() { |
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.
Could be simplified to .on('authenticationSucceeded', () => {
(correcting myself from when I suggested this, sorry)
app/services/current-user.js
Outdated
| }), | ||
|
|
||
| _setWaffle: task(function* () { | ||
| if (this.get('features').isEnabled('_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.
Seems like we'd want to rewaffle when the user logs in/out. Maybe also add a reload argument (default false) to this task?
(or just delete this check if you do the waffleCheck thing above)
app/router.ts
Outdated
|
|
||
| willTransition(oldInfo, newInfo, transition) { | ||
| const to = transition.targetName; | ||
| const flag = `ember_${to}_page`; |
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 this pattern will hold for all pages. It might be better to map route name (transition.targetName) to flag name in the config, like
featureFlags: {
'dashboard': 'ember_dashboard_page',
'guid-user': 'ember_user_profile_page',
'guid-user.settings': 'ember_user_settings_page',
...
},And we can map new feature-flagged routes as we add them.
app/router.ts
Outdated
| const to = transition.targetName; | ||
| const flag = `ember_${to}_page`; | ||
| if (flag in config.featureFlags) { | ||
| this.get('currentUser._setWaffle').perform().then(() => { |
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.
An issue with this approach is the .then() callback will run sometime later -- ideally, when waffle is already loaded we hit the reload() synchronously, before willTransition returns. Maybe something like
if (currentUser.get('waffleLoaded')) {
// check flag, reload()
} else {
currentUser.doTheWaffle().then(() => {
// check flag, reload()
});
}or you could wrap that in a method on current-user:
// current-user.ts
waffleCheck(flag: string, waffleBack: (enabled: boolean) => void) {
if (this.get('waffleLoaded')) {
waffleBack(this.get('features').isEnabled(flag));
} else {
this.get('_setWaffle').perform().then(() => {
waffleBack(this.get('features').isEnabled(flag));
});
}
}
// router.ts
currentUser.waffleCheck(flag, enabled => {
if (!enabled) {
window.location.reload();
}
});It's also nice that that keeps the task internal. I'm not a fan of exposing tasks if we can avoid it; better to leave them implementation details.
config/environment.js
Outdated
| featureFlags: { // default flags (whether they be switches, flags, or polls) go here with default value. | ||
| ember_support_page: true, | ||
| ember_dashboard_page: true, | ||
| _loaded: false, |
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 makes more sense as a property on current-user, since that's the only place that uses it.
12ac4fb to
8d3203d
Compare
…llow for willTransititon to be triggered
8d3203d to
095bccb
Compare
config/environment.js
Outdated
| featureFlags: { // default flags (whether they be switches, flags, or polls) go here with default value. | ||
| support: 'ember_support_page', | ||
| dashboard: 'ember_dashboard_page', | ||
| _loaded: false, |
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 seems to not be used
config/environment.js
Outdated
| }, | ||
| featureFlags: { // default flags (whether they be switches, flags, or polls) go here with default value. | ||
| support: 'ember_support_page', | ||
| dashboard: 'ember_dashboard_page', |
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.
can we move these under routes?, e.g.
featureFlags: {
routes: {
support: 'ember_support_page',
dashboard: 'ember_dashboard_page',
}
}
app/services/current-user.ts
Outdated
| method: 'GET', | ||
| }); | ||
| for (const flag of data) { | ||
| const { name } = flag.attributes; |
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 not destructure active as well?
app/router.ts
Outdated
| rootURL: config.rootURL, | ||
|
|
||
| willTransition(oldInfo, newInfo, transition) { | ||
| const to = transition.targetName; |
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.
Can we call this targetRouteName (or just use transition.targetName directly) to make it clear that this is a route name?
|
|
||
| getWaffle(this: CurrentUserService, feature: string) { | ||
| if (this.get('waffleLoaded')) { | ||
| return Promise.resolve(this.get('features').isEnabled(feature)); |
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 like this in general, it's a better interface than I proposed, but I think Promise.resolve().then(...) will still be async. willTransition will return and the transition will continue a bit before reload is called.
It's probably not a big problem in practice, but I worry about having visual cues of a transition before loading the legacy page, and want to minimize the delay to the extent we can. I'm not sure, does that seem like a reasonable worry?
| await this._super(transition); | ||
|
|
||
| if (!this.get('session.isAuthenticated')) { | ||
| transition.abort(); |
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.
Out of curiosity, do you have a sense of how much time it adds to start a whole new transition?
| } | ||
|
|
||
| getWaffle(this: CurrentUserService, feature: string) { | ||
| if (this.get('waffleLoaded')) { |
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.
Might be worth checking setWaffle.isRunning, and return setWaffle.last.then(...) if so. Don't want to restart the task if we don't need to.
jamescdavis
left a comment
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.
Nice work!
Purpose
Allow ember-osf-web to consume waffle switches, polls, and flags for client-side feature-flagging.
Summary of Changes
Add
ember-feature-flagsfor feature flag management, and attach a _setWaffle method to populate flags based on the currentUser (or lack of a user), since some can be based on the user session.Side Effects / Testing Notes
No QA able to be done here. For developers, future uses would look exactly like the documentation on ember-feature-flag suggests. The
featuresservice is now included, and we can toggle template or controller logic by injecting it or using the helperfeature-flags.Ticket
https://openscience.atlassian.net/browse/EMB-110
Reviewer Checklist
CHANGELOG.md