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

Bump planout.js major version to v4.0.0 #58

Merged
merged 23 commits into from
Mar 16, 2017
Merged

Bump planout.js major version to v4.0.0 #58

merged 23 commits into from
Mar 16, 2017

Conversation

mattrheault
Copy link
Contributor

@mattrheault mattrheault commented Mar 12, 2017

This PR consists of changes from the following PR's:

V4.0 Changes:

  • Core compatible bundle fixes:
    • Core compatible namespace allocations now match core reference namespace allocations from python version of planout.
    • Core compatible interpreted experiment enrollment now matches core reference interpreted experiment enrollment from python version of planout.
  • Separated the concerns of planout core random operations, and the planout API. The planout.js API (experiment, assignment, namespace, etc) are now composed with the random operations they are passed.
  • Added planoutAPIFactory.js to keep planout bundles consistent.
  • Made experiment & namespace names required to fix Rethink namespace / experiment name as part of assignment algorithm in JS port only #57
  • Fixes WeightedChoice with false-y choices.
  • Fixes tests on windows + running the travis tests on windows as well if this is possible.

@rawls238 @geoffdaigle @jcwilson @eytan

Josh Wilson and others added 20 commits March 5, 2017 18:18
This resolves an issue where the core-compatible Namespace segment
allocation would erroneously use the non-compatible implementation
instead.
This removes cases where we would require() src code in the unit tests
and then try to intertwine those objects with built library code. We
should only be using built library code in the unit tests.

Effects of this commit:
* PlanOut.Ops.Core is no longer undefined
* Exposed the following additional modules on PlanOut:
	* PlanOut.Lib.Utils
	* PlanOut.Ops.Base
	* PlanOut.Ops.Utils
* Unit tests now pass on Windows machines

One trade-off here is that we now expose some previously internal
modules to clients of the library. In some cases, it seems like the
right thing to do. For instance, the unit tests demonstrate how to
register a custom operator, but without PlanOut.Ops.Base one cannot
replicate that in application code. In other cases, like
PlanOut.Lib.Utils, it seems like we might be exposing too much. I think
it may still be ok to do this, as anyone using this library will have
better alternatives for the internal functionality (lodash, underscore),
but I can understand if we want to find an alternative solution.
This resolves an issue that would prevent interpreted Experiments from
working correctly in core-compatibility mode.

The problem was that the Interpreter class uses the operations table
defined in Ops.Utils and that table is initialized with the
non-compatible operation code.

This code allows the build/index_core_compatible.js module to apply the
correct overrides for a core-compatible build.

This also corrects two unit tests that should have caught this issue.
This centralizes the Random code differences to just Ops.Random and
Ops.Utils. We make Namespace look up these operations from the same
place as the Interpreter and other classes, rather than being imported
directly.
For modules that export more than one item, we now use the
`export {...}` syntax. The corresponding imports are updated to the
`import * as <something> from ...`  syntax. This makes it more clear
and consistent that each module is exporting a collection of items
rather than a single item with a collection of properties.
It turns out we don't need to risk exposing these yet. They were
previously exposed for unit test purposes, but an alternative solution
has been found - use requireActual() instead of require() to load the
Lib.Utils code for usage during the unit tests.
It doesn't make sense to create a new instance of BigNumber() for each
operator instance (in compat code), or to waste memory on what's
essentially a constant (in both).
This keeps our index.js and index_core_compatible.js files as similar as
possible while still allowing them to specialize their behavior by
selecting the appropriate Random library to use.
…names

Requiren name to be set from within experiment & namespace setup()
@jcwilson
Copy link
Contributor

jcwilson commented Mar 12, 2017

It occurs to me that Experiment.getDefaultParamNames() will suffer from the same problems as the removed getDefaultNamespaceName() and getDefaultExperimentName() in #57. It tries to examine the assign() function's code and does a lexical search for set( in order to extract param names. The set() function will potentially get munged during minification.

EDIT: actually, we may be safe here. It wouldn't make sense for a minification step to alter the public interface of an object. I still think that function is a kind of trap since it depends on the client writing code in a particular way so as to enable the discovery. At least it must be explicitly invoked by the client and it's not a liability in the only place the library calls it internally. Still it feels weird to still have code that tries to introspect javascript function bodies to accomplish its task.

@mattrheault
Copy link
Contributor Author

@jcwilson I think we'll be safe there since things like object properties / methods shouldn't be munged in a minification process (doing so would break any runtime code that depends on specific property / method names). Though I do want to find a way to make the implementation of getDefaultParamNames() less magical - #56

@jcwilson
Copy link
Contributor

I guess the question is whether or not we hold up the v4 release over it. I'd be ok leaving it as-is for now.

@mattrheault
Copy link
Contributor Author

I think we'll be okay to move forward with the v4 release before tackling that issue. The toString() matching is gross, but I don't think it's likely to affect a lot of people. And if it does affect people, then it will only hinder autoexposure logging (enrollment & assignment would be unaffected). For context, I left a followup comment in #56 explaining how we ran into this problem.

@jcwilson
Copy link
Contributor

Cool, sounds good.

And yeah, that example you posted in #56 was exactly why I called it a "trap" :)

@rawls238
Copy link
Owner

Yes it is in fact immune to minification

@rawls238
Copy link
Owner

@mattrheault is this ready for review or are you planning on adding more to this?

BernoulliTrial: BernoulliTrialBuilder(PlanOutOpRandomCoreCompatible),
RandomInteger: RandomIntegerCoreCompatible,
RandomFloat: RandomFloatBuilder(PlanOutOpRandomCoreCompatible)
var WeightedChoiceCoreCompatible = WeightedChoiceBuilder(PlanOutOpRandomCoreCompatible);
Copy link
Owner

Choose a reason for hiding this comment

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

moving this comment from the other PR: should this be const or let instead? I know this project is littered with var but we may as well start moving towards more es6 style

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch

@rawls238
Copy link
Owner

rawls238 commented Mar 15, 2017

copying this in here since I wanted to respond to this but it seems to have been lost since it was an inline-comment

Unfortunately I think we're wrestling a bit with the current structure of the project to force it to work the way that we want. In an ideal world, we shouldn't have to apply any "overrides" to our random operations at runtime.

The direction that I think PlanOut.js should move in is one where we totally separate the concerns of API & random operations. PlanOut.js's experiments, namespaces, interpreter, etc should be capable of being composed from the top with whatever random operations we choose. Right now we're in an awkward middle ground where some stuff is composable, but other stuff directly imports random operations.

Here's a peak into what I'm imagining:

/
/build
index.js
index_core_compatible.js
/es6
/api
...
/ops
...
I'm not married to this exact directory structure, but the important detail is that we should have a hard rule that no module in /api will ever import directly from /ops. Anything that an api module needs from /ops should be passed down from the build's entry point.

With this restructuring, I think that PlanOut.js will be in a good place to scale without having to worry about future changes causing a shift in enrollment for either the compat or non-compat builds.

Taking a step back to the problem at hand...

The fact is that things are broken, and they need to be fixed. The question I'm interested in answering right now is - do we apply a bandaid to fix core-compat, or do we prioritize a more involved project restructure?

@jcwilson do you have any thoughts on the matter? How high priority is fixing core-compat to you and your team?

@rawls238 do you think that what I'm proposing sounds like a reasonable direction to head in?

/cc @geoffdaigle

I think it's best to not overcomplicate things with core-compat.

First, I don't think that many people (if any) require compatibility and the non-compat version is substantially faster than the compat version. I'd be interested to hear about @jcwilson's use-case with respect to this.

Second, I don't believe it would be desirable to untangle random operations from everything since namespaces directly rely on random ops to do namespace enrollment (as they should) and the assignment class executes the random operators so I don't see how this would be a significant improvement. It's not clear to me how it would be possible to divorce the two - if there are any issues with randomization they necessarily will always impact enrollment based on the current implementation and it isn't clear to me how changing around directory structure or how random ops are injected into the build would make any difference (I have a feeling I'm missing something here, so if I am let me know).

I think that @jcwilson's changes are sufficient for fixing the issue at hand and present a good framework for moving forward and I don't think a project overhaul is necessary. It is also worth pointing out that these kinds of things should be incredibly infrequent since the reference implementation rarely, if ever, will change these core aspects of the project so it seems like we would be overengineering to completely refactor the project to accommodate the slim chance that we need to deal with this issue again.

@jcwilson
Copy link
Contributor

First, I don't think that many people (if any) require compatibility and the non-compat version is substantially faster than the compat version. I'd be interested to hear about @jcwilson's use-case with respect to this.

One of the core benefits of Planout is its ability for clients and servers to have consistent hashing without having to communicate the namespace/experiment data at runtime, say via an api call or server-side rendered template. In our case, we have also have tooling built around the Python implementation that dev and analytics can use to quickly identify if a particular user is selected for an experiment. In our case, we value that consistency over the speed benefits provided by the default distribution of PlanOut.js (though we do appreciate that effort).

My understanding was that this PR accomplishes @mattrheault's goals with regards to any refactoring, so I think we're good there.

@mattrheault
Copy link
Contributor Author

@rawls238

First, I don't think that many people (if any) require compatibility and the non-compat version is substantially faster than the compat version. I'd be interested to hear about @jcwilson's use-case with respect to this.

I think the core compat version is more likely to be used in a node.js environment where the performance loss can be mitigated.

Second, I don't believe it would be desirable to untangle random operations from everything since namespaces directly rely on random ops to do namespace enrollment (as they should) and the assignment class executes the random operators so I don't see how this would be a significant improvement. It's not clear to me how it would be possible to divorce the two - if there are any issues with randomization they necessarily will always impact enrollment based on the current implementation and it isn't clear to me how changing around directory structure or how random ops are injected into the build would make any difference (I have a feeling I'm missing something here, so if I am let me know).

Namespace classes directly importing random operations is fine until we run into a situation where we have multiple bundles where random operations differ. Now that this is the case, things like Namespaces & Experiments should be handed the random operations they need to do their job, rather than importing random operations directly. The restructuring to make this happen isn't too heavy handed IMO, and I don't anticipate any negative side effects. The only thing this effects is the developer experience when contributing to planout.js.

We are running into real breaking problems here because of these imports. I disagree in that what we're doing here is a major overhaul. It's more of a minor refactor around where dependencies come from, which will alter enrollment, and thus will require a major version bump.

Does this clear things up?

constructor(experimentSalt, overrides) {
if (!overrides) {
overrides = {};
export default function provideAssignement(Random) {
Copy link
Owner

Choose a reason for hiding this comment

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

typo here, this should be provideAssignment

@rawls238
Copy link
Owner

One of the core benefits of Planout is its ability for clients and servers to have consistent hashing without having to communicate the namespace/experiment data at runtime, say via an api call or server-side rendered template. In our case, we have also have tooling built around the Python implementation that dev and analytics can use to quickly identify if a particular user is selected for an experiment. In our case, we value that consistency over the speed benefits provided by the default distribution of PlanOut.js (though we do appreciate that effort).

Cool to hear you're doing this! Theoretically this is one of the benefits of using PlanOut but in practice virtually all of the devs that I've found are using it are using a single implementation of it. Glad to see you guys are using multiple implementations!

I think the core compat version is more likely to be used in a node.js environment where the performance loss can be mitigated.

This is not recommended practice. Nobody should be using core compat mode unless they need compatibility, whether on the client or server the performance differences are substantial.

Namespace classes directly importing random operations is fine until we run into a situation where we have multiple bundles where random operations differ. Now that this is the case, things like Namespaces & Experiments should be handed the random operations they need to do their job, rather than importing random operations directly. The restructuring to make this happen isn't too heavy handed IMO, and I don't anticipate any negative side effects. The only thing this effects is the developer experience when contributing to planout.js.

We are running into real breaking problems here because of these imports. I disagree in that what we're doing here is a major overhaul. It's more of a minor refactor around where dependencies come from, which will alter enrollment, and thus will require a major version bump.

I see I see, I was envisioning that you were saying something else (which in my mind made no sense) but this makes a lot of sense. I think in the short-term we should stick to these changes as I don't think they're a band-aid fix but rather a sensible restructuring. In the long-term I'd be interested to see the implementation of something like this and see if it makes things easier on the dev side than the current implementation. The only worry I have is doing something like that and breaking things for no performance or external usability reasons but I think this project has good enough test coverage where it should be OK. Up to you if you want to attempt at doing so, I'm not sure how big the gains would actually be but they seem positive to me.

Doing this after merging this in shouldn't require a major version bump since enrollment won't be affected if we do this kind of refactoring after this fix has already been applied.

Copy link
Owner

@rawls238 rawls238 left a comment

Choose a reason for hiding this comment

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

Besides the few nit comments, I think this looks good to go. Thanks for doing this @jcwilson and @mattrheault for wrapping it up.

I would also add in the CHANGELOG:

  1. Fixes WeightedChoice with false-y choices
  2. Fixes tests on windows + running the travis tests on windows as well if this is possible

constructor(experimentSalt, overrides) {
if (!overrides) {
overrides = {};
export default function provideAssignement(Random) {
Copy link
Owner

Choose a reason for hiding this comment

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

typo here, this should be provideAssignment

Copy link
Contributor

@jcwilson jcwilson Mar 15, 2017

Choose a reason for hiding this comment

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

weird. not sure how that lets anything work unless the import/export stuff doesn't care about the name here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's because the named export is the default export, which you can import as whatever name you choose. The main advantage of naming a default export function is that it'll make stack traces a bit easier to follow when the code is compiled & minified.

@jcwilson
Copy link
Contributor

Thanks for everyone's help on this. I'm excited to start using v4.0! 💯

@rawls238
Copy link
Owner

@jcwilson would you prefer that we put the compat planout implementation as its own package on npm?

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.

Rethink namespace / experiment name as part of assignment algorithm in JS port only
3 participants