Skip to content

Conversation

@kinyoklion
Copy link
Member

This PR adds support for evaluating targets/context targets in flags as well as handling the evaluation of flags that are off.

@shortcut-integration
Copy link

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

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

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

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(
Copy link
Member Author

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.

Copy link
Contributor

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

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 @@
/**
Copy link
Member Author

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

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

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.

@kinyoklion kinyoklion marked this pull request as ready for review June 3, 2022 18:05
operator: (val: T) => U | undefined,
): U | undefined {
let res;
collection?.some((item) => {
Copy link
Member Author

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.

@kinyoklion kinyoklion changed the title [sc-154361] Implement target evaluation and 'OFF' evaluation. [sc-154361] #1 Implement target evaluation and 'OFF' evaluation. Jun 8, 2022
@kinyoklion kinyoklion requested a review from eli-darkly June 22, 2022 16:20
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');
Copy link
Contributor

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

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

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.

Copy link
Member Author

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

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?

Copy link
Member Author

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.

@kinyoklion kinyoklion merged commit 34f38c6 into main Jul 5, 2022
@kinyoklion kinyoklion deleted the rlamb/sc-154361/target-evaluation branch July 5, 2022 15:56
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.

3 participants