Skip to content

Conversation

@hmoco
Copy link
Contributor

@hmoco hmoco commented Mar 13, 2018

Purpose

Allow ember-osf-web to consume waffle switches, polls, and flags for client-side feature-flagging.

Summary of Changes

Add ember-feature-flags for 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 features service is now included, and we can toggle template or controller logic by injecting it or using the helper feature-flags.

Ticket

https://openscience.atlassian.net/browse/EMB-110

Reviewer Checklist

  • meets requirements
  • easy to understand
  • DRY
  • testable and includes test(s)
  • changes described in CHANGELOG.md

Copy link
Contributor

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

@coveralls
Copy link

coveralls commented Mar 13, 2018

Coverage Status

Coverage remained the same at 21.122% when pulling df2e5e1 on hmoco:feature/EMB-110_waffle into 0460519 on CenterForOpenScience:develop.


export default Mixin.create({
features: service(),
currentUser: service(),
Copy link
Contributor

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)

});
}),

_setWaflle: task(function* () {
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: Waflle


_setWaflle: task(function* () {
const url = `${config.OSF.apiUrl}/v2/_waffle/`;
const { data } = yield $.ajax(url, 'GET');
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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.

return this.get('currentUser._setWaflle').perform().then(() => this._super(...args));
},
actions: {
didTransition(...args) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

*/
user: Ember.computed('currentUserId', function() {
const ObjectPromiseProxy = Ember.ObjectProxy.extend(Ember.PromiseProxyMixin);
this.get('_setWaflle').perform();
Copy link
Contributor

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

features: service(),
currentUser: service(),
beforeModel(...args) {
return this.get('currentUser._setWaflle').perform().then(() => this._super(...args));
Copy link
Contributor

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.

@hmoco hmoco force-pushed the feature/EMB-110_waffle branch 2 times, most recently from 685bba1 to 1feb3a9 Compare March 20, 2018 18:35
@hmoco hmoco force-pushed the feature/EMB-110_waffle branch from 1feb3a9 to caa68e5 Compare March 20, 2018 18:38
aaxelb
aaxelb previously requested changes Mar 21, 2018

constructor() {
this._super(...arguments);
this.get('session').on('authenticationSucceeded', this, function() {
Copy link
Contributor

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)

}),

_setWaffle: task(function* () {
if (this.get('features').isEnabled('_loaded')) {
Copy link
Contributor

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`;
Copy link
Contributor

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(() => {
Copy link
Contributor

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.

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,
Copy link
Contributor

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.

@hmoco hmoco force-pushed the feature/EMB-110_waffle branch from 12ac4fb to 8d3203d Compare March 22, 2018 17:54
@hmoco hmoco force-pushed the feature/EMB-110_waffle branch from 8d3203d to 095bccb Compare March 22, 2018 18:40
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,
Copy link
Member

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

},
featureFlags: { // default flags (whether they be switches, flags, or polls) go here with default value.
support: 'ember_support_page',
dashboard: 'ember_dashboard_page',
Copy link
Member

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',
    }
}

method: 'GET',
});
for (const flag of data) {
const { name } = flag.attributes;
Copy link
Member

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;
Copy link
Member

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));
Copy link
Contributor

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();
Copy link
Contributor

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')) {
Copy link
Contributor

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.

Copy link
Member

@jamescdavis jamescdavis left a comment

Choose a reason for hiding this comment

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

Nice work!

@jamescdavis jamescdavis dismissed stale reviews from brianjgeiger and aaxelb March 26, 2018 17:15

Out of date

@jamescdavis jamescdavis merged commit 6bc1d63 into CenterForOpenScience:develop Mar 26, 2018
@jamescdavis jamescdavis added this to the 0.3.0 milestone May 7, 2019
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.

5 participants