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

feat (audiences): Audience combinations #175

Merged
merged 9 commits into from Oct 19, 2018

Conversation

mjc1283
Copy link
Contributor

@mjc1283 mjc1283 commented Oct 16, 2018

This adds support for audience combinations on experiments. If experiment.audienceConditions is present, it will be used as a condition tree where the leaf conditions are audience Ids.

  • Condition tree evaluation and custom attribute condition evaluation are split into separate modules. Condition tree evaluation accepts a leaf evaluator function argument.
  • The audience evaluator uses the condition tree evaluator on both the experiment audience conditions and the individual audience conditions.
  • Project config methods are updated to support audience combinations. getExperimentAudienceConditions returns audienceConditions if it exists, otherwise it falls back to audienceIds.
  • The audience evaluator treats conditions trees and flat lists of audience ids (the old format) the same way, because a flat list of audience ids is a valid condition tree with an implicit OR condition.

Matt Carroll added 5 commits October 15, 2018 15:14
condition evaluator generic to any kind of leaf evaulator. Update
audience evaluator to use the generic condition evaluator, providing a leaf
evaluator that uses the custom attribute condition evaluator module.
Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

I haven't looked at the tests yet, but the code under test looks really good! My only important suggestion relates to a JSDoc.

I'll scan through the tests after lunch!

* determining if the audience conditions are met. If not
* provided, defaults to an empty object.
* @return {Boolean} True if the user attributes match the given audience conditions
* @param {Array} audienceConditions Audience conditions to match the user attributes against - can be an array
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we declare something like {Array|Object|null|undefined}? Since we support a single leaf condition without any operators and we accept empty conditions too.

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

};

if (conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience)) {
return true;
}

return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe

return conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience) || false;

? It's a little more concise and highlights the fact that we're converting falsy values (null) to false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

}
return null;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: for symmetry with customAttributeConditionEvaluator.evaluate, I wonder if we should export this functionality as audienceEvaluator.evaluate, and hoist all the other code up to DecisionService.prototype.__checkIfUserIsInAudience?

🔭:architecture_👨‍🚀:🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting. This does feel cleaner. I could see the other code fitting into __checkIfUserIsInAudience. @mikeng13 thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With this approach, audience_evaluator ends up being pretty uninteresting, to the extent that it maybe shouldn't be a module.

Copy link
Contributor

Choose a reason for hiding this comment

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

Up to you. I was just thinking out loud, especially if there was interest in using the _.partial pattern as we do throughout client-js. Doesn't really make a difference

}
return null;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nchilada @mikeng13 Any thoughts on returning null here? Maybe this should throw an error, and project config should reject a datafile as invalid if an experiment has invalid audience ids.

Copy link
Contributor

@nchilada nchilada Oct 17, 2018

Choose a reason for hiding this comment

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

I believe returning null is correct here - this function is being called at activate time so it's probably too late to decide that the datafile is invalid and to stop handling API calls. That said, we could add an additional check for invalid audience references during initialization. If we do so, we reduce this return null to a mere sanity check.

In the design doc I'd written:

If an audience can't be found in [audiences or typedAudiences], the SDK may fail gracefully or spectacularly, during initialization or during experiment activation—whatever it was doing before.

but I don't mind if someone want to propose that we consistently check for invalid audience references during initialization. @mikeng13 @thomaszurkan-optimizely what do you think?

@coveralls
Copy link

Coverage Status

Coverage increased (+0.05%) to 97.38% when pulling f1dcb2d on mcarroll/audience-combinations into 5c18367 on master.

@coveralls
Copy link

coveralls commented Oct 18, 2018

Coverage Status

Coverage increased (+0.09%) to 97.42% when pulling 34cc8ad on mcarroll/audience-combinations into 5c18367 on master.

Copy link
Contributor

@nchilada nchilada left a comment

Choose a reason for hiding this comment

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

Tests look pretty good - just had some minor suggestions. Nice work @mjc1283!

it('should return false when operands evaluate to falses and nulls', function() {
var leafEvaluator = sinon.stub();
leafEvaluator.onCall(0).returns(false);
leafEvaluator.onCall(1).returns(null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe flip the order of these two return values, or add another test case, to prove that the evaluator doesn't short-circuit with null when it sees a null result?

it('should return null when operands evaluate to falses and nulls', function() {
var leafEvaluator = sinon.stub();
leafEvaluator.onCall(0).returns(null);
leafEvaluator.onCall(1).returns(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe flip the order of these two return values, or add another test case, to prove that the evaluator doesn't short-circuit with false when it sees a false result?

it('should return null when operand evaluates to null', function() {
assert.isNull(conditionEvaluator.evaluate(['not', conditionA], function() { return null; }));
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Other possible test cases, partly inspired by today's offline discussions:

  • no operands
  • additional operands

assert.isTrue(customAttributeEvaluator.evaluate(doubleCondition, userAttributes));
});

it('should return null when condition has an invalid type property', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Another potential test case: 'missing type property'

});
});

describe('exact match type', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also test the implicit "exact" match in cases where there's no match property at all. I believe this might be covered by the very first test case ('should return true there is a match') but a test case with a more specific description might be good.

assert.isFalse(result);
});

it('should return null if the user-provided value is not a number', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also test this with the user-provided value being a number-like string

assert.isFalse(result);
});

it('should return null if the user-provided value is not a number', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

var result = projectConfig.getExperimentAudienceConditions(configObj, 'audience_combinations_experiment');
assert.deepEqual(result, ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']]);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Possible additional test case: 'should return null [or undefined, I guess] if experiment has not audienceConditions or audienceIds'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure this is worth testing.

Copy link
Contributor

Choose a reason for hiding this comment

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

No worries at all!


it('should return experiment audienceConditions if experiment has audienceConditions', function() {
configObj = projectConfig.createProjectConfig(testDatafile.getTypedAudiencesConfig());
var result = projectConfig.getExperimentAudienceConditions(configObj, 'audience_combinations_experiment');
Copy link
Contributor

Choose a reason for hiding this comment

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

We should confirm that we prefer audienceConditions to audienceIds? Does this experiment also have audienceIds? A comment or some kind of easier-to-digest stub might help make that clear.

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 add a comment - this experiment does have both audienceIds and audienceConditions

});
assert.strictEqual(variableValue, 10);
});
});
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 I'm fine with the spareness of these test cases are, but can we at least use a sinon.spy to confirm that conditionTreeEvaluator.evaluate is called? (And FWIW, I think we can even assert that it's called once with the audience-ID leaf evaluator if we export that leaf evaluator function somewhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think it'd satisfy the same need if we assert that audience_evaluator's evaluate was called? audience_evaluator has its own tests verifying the integration between evaluate and condition_tree_evaluator / custom_attribute_condition_evaluator.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, that's right. Sure, that'd probably be even better (given that it's so hard to differentiate between the two places that make direct calls to conditionTreeEvaluator)

Copy link
Contributor

@mikeproeng37 mikeproeng37 left a comment

Choose a reason for hiding this comment

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

Looks good! Just a few small things.

@@ -0,0 +1,125 @@
/****************************************************************************
* Copyright 2016, 2018, Optimizely, Inc. and contributors *
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Remove 2016

@@ -0,0 +1,201 @@
/****************************************************************************
* Copyright 2016, 2018, Optimizely, Inc. and contributors *
Copy link
Contributor

Choose a reason for hiding this comment

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

remove 2016

@mjc1283 mjc1283 merged commit cb9d500 into master Oct 19, 2018
@mjc1283 mjc1283 deleted the mcarroll/audience-combinations branch October 19, 2018 20:59
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