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
Conversation
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.
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 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 |
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 declare something like {Array|Object|null|undefined}
? Since we support a single leaf condition without any operators and we accept empty conditions too.
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 do this.
}; | ||
|
||
if (conditionTreeEvaluator.evaluate(audienceConditions, evaluateAudience)) { | ||
return true; | ||
} | ||
|
||
return 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.
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
.
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.
👍
} | ||
return null; | ||
}; |
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.
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_👨🚀:🤔
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.
Interesting. This does feel cleaner. I could see the other code fitting into __checkIfUserIsInAudience
. @mikeng13 thoughts?
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.
With this approach, audience_evaluator
ends up being pretty uninteresting, to the extent that it maybe shouldn't be a module.
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.
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; |
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.
@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.
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 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
ortypedAudiences
], 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?
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.
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); |
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.
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); |
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.
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; })); | ||
}); | ||
}); |
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.
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() { |
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.
Another potential test case: 'missing type property'
}); | ||
}); | ||
|
||
describe('exact match type', 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.
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() { |
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.
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() { |
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.
Ditto
var result = projectConfig.getExperimentAudienceConditions(configObj, 'audience_combinations_experiment'); | ||
assert.deepEqual(result, ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643']]); | ||
}); | ||
}); |
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.
Possible additional test case: 'should return null [or undefined, I guess] if experiment has not audienceConditions or audienceIds'
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'm not sure this is worth testing.
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.
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'); |
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.
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.
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 add a comment - this experiment does have both audienceIds
and audienceConditions
}); | ||
assert.strictEqual(variableValue, 10); | ||
}); | ||
}); |
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 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)
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.
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
.
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.
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
)
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.
Looks good! Just a few small things.
@@ -0,0 +1,125 @@ | |||
/**************************************************************************** | |||
* Copyright 2016, 2018, Optimizely, Inc. and contributors * |
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.
nit: Remove 2016
@@ -0,0 +1,201 @@ | |||
/**************************************************************************** | |||
* Copyright 2016, 2018, Optimizely, Inc. and contributors * |
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.
remove 2016
API tests for audience combinations.
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.getExperimentAudienceConditions
returnsaudienceConditions
if it exists, otherwise it falls back toaudienceIds
.