-
Notifications
You must be signed in to change notification settings - Fork 31
[sc-154361] #1 Implement target evaluation and 'OFF' evaluation. #12
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
Conversation
|
This pull request has been linked to Shortcut Story #154361: Implement evaluation: Scaffold, Operators, Targets. |
| }); | ||
|
|
||
| it('should get the same values', () => { | ||
| expect(context?.valueForKind('user', new AttributeReference('cat'))).toEqual('calico'); |
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 changed the order of these params to allow for the kind to be optional and simplify evaluation logic.
| @@ -0,0 +1,127 @@ | |||
| import { Context, LDContext } from '@launchdarkly/js-sdk-common'; | |||
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.
Currently there are very limited tests for just the couple things the evaluator supports.
| @@ -0,0 +1,38 @@ | |||
| import { LDEvaluationDetail, LDEvaluationReason } from '../api'; | |||
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.
In the node implementation it was very difficult to get errors to propagate out of the evaluation. This was due to a combination of methods that had a return value independent of their callback value. In hopes to reduce the complexity some, aside from using async/away, I am trying to make sure any layer can easily propagate an error result.
| return EvalResult.ForError(ErrorKinds.FlagNotFound, 'Temporary'); | ||
| } | ||
|
|
||
| // private async ruleMatchContext( |
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 started working on this and then realized I need to work on the reviver/replacer for JSON first. Currently node doesn't have anything like that, so the eval code has to check every possible combination of null/undefined for every single JSON field.
Additionally it has no way to pre-compute AttributeReferences.
So, I am going to pause this and implement that functionality and then come back.
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 you say more about what you mean by "the eval code has to check every possible combination of null/undefined for every single JSON field"?
| /** | ||
| * Allows checking if a specific operator is defined and allows execution of an operator on data. | ||
| * | ||
| * @internal |
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 may be missing some of these, but in the end I will audit what appears in the public interface to make sure nothing has leaked out.
| @@ -0,0 +1,46 @@ | |||
| /** | |||
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.
In the node implementation we use the async package to handle all kinds of callback hell. If we are using async/await, then we don't need it, but we do need some mechanism for some common iteration strategies.
| import { Versioned } from './Versioned'; | ||
|
|
||
| type VariationOrRollout = number | Rollout; | ||
| type VariationOrRollout = { variation: number; } | { rollout: Rollout }; |
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.
Oops
| @@ -0,0 +1,47 @@ | |||
| import { Context } from '@launchdarkly/js-sdk-common'; | |||
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.
Target evaluation doesn't use any queries, so I have done it as free functions instead of incorporating it into the evaluator. Prerequisites and rule evaluation are going to require queries, so will either be encapsulated objects for those purposes, or directly integrated into evaluator.
| operator: (val: T) => U | undefined, | ||
| ): U | undefined { | ||
| let res; | ||
| collection?.some((item) => { |
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 dislike JS naming of things, but 'some' is equivalent to the more sensible language of 'any'. In that it stops iteration once any return value is truthy.
So it is a common way to iterate until a condition is true. In JS you general want to use prefer for loops, or collection methods like some versus using for in/of. This is because there is a substantial performance difference because range based for loops use generators and iterators and have much more runtime cost.
So, this just gives us an easy way to get the first result that is defined.
some by itself just uses the return value for truthiness, so we could not get the actual result.
| expect(context?.valueForKind('user', new AttributeReference('cat'))).toEqual('calico'); | ||
| expect(context?.valueForKind('user', new AttributeReference('name'))).toEqual('context name'); | ||
| expect(context?.valueForKind(new AttributeReference('cat'), 'user')).toEqual('calico'); | ||
| expect(context?.valueForKind(new AttributeReference('name'), 'user')).toEqual('context name'); |
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.
Not a big deal, but: I'm unclear on why you're using context? in all the tests like this. I mean, if line 63 fails somehow and context ends up being null/undefined, then the "should create a context" test will fail with a clear error. And I would think that if these other tests failed by throwing a null reference exception, that would also be fairly clear. Using ?. in all these expectations means, if I understand correctly, that it will instead be telling us "expected for valueForKind to return 'calico', but it returned undefined", etc.— which is a bit misleading since the problem has nothing to do with valueForKind. Plus, if we got past the first line, we know context is not null/undefined, so what's the point of repeating ?. every time?
| // @ts-ignore | ||
| it(`produces the expected evaluation result for context: ${context.key} ${context.kind} targets: ${flag.targets?.map((t) => `${t.values}, ${t.variation}`)} context targets: ${flag.contextTargets?.map((t) => `${t.contextKind}: ${t.values}, ${t.variation}`)}`, async () => { | ||
| const result = await evaluator.evaluate(flag, Context.FromLDContext(context)!); | ||
| expect(result?.isError).toEqual(expected?.isError); |
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.
Similar to my earlier comment, I think I would prefer a more specific failure here if result is null/undefined (which it shouldn't ever be, right?).
| * | ||
| * @internal | ||
| */ | ||
| export default class Reasons { |
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.
Is there a particular reason to make this a separate non-public class, instead of having these be static methods of LDEvaluationReason? We do the latter in other SDKs, mainly because it facilitates testing in other projects that use the SDK.
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.
The main reason at the moment is all the external interface is composed of interfaces. Which I cannot add static methods to.
It is possible we could change some of them to classes and depend on the generated interfaces, but I have not yet considered doing this.
|
|
||
| function evalTarget(flag: Flag, target: Target, context: Context): EvalResult | undefined { | ||
| const contextKey = context.key(target.contextKind); | ||
| if (contextKey !== undefined) { |
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.
Would one potential solution to the "always having to check specifically for null or undefined" issue be to ensure that context.key can never return any potentially truthy type other than string? I mean, if (contextKey) is only a problem if we think it could be a boolean/number/etc., right?
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.
On the flag data from LD we now transform to undefined if there are nulls. I can similarly ensure that for the context as well, assuming I am not already. Most of the checks I am using type guard now instead of direct comparisons. Aside from a few cases like these.
This PR adds support for evaluating targets/context targets in flags as well as handling the evaluation of flags that are off.