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

FF-1749: JS SDK Common UFC update #45

Merged
merged 42 commits into from
Apr 19, 2024
Merged

FF-1749: JS SDK Common UFC update #45

merged 42 commits into from
Apr 19, 2024

Conversation

schmit
Copy link
Contributor

@schmit schmit commented Mar 28, 2024

Fixes: #FF-1749

Motivation and Context

Migrating the JS SDK common lib to the UFC to enable more flexible feature flag evaluation and advanced use cases.

Description

  • Leverage UFC to evaluate flags
  • Create explicit integer and numeric variation types
  • Remove the JSONString assignment (can but it back, but is anyone using it?), and renamed getJSONParsedAssignment to getJSONAssignment

To do:

  • obfuscation
  • remove assignment hooks
  • enforce specifying default value

How has this been tested?

  • Updated unit tests
  • Updated cross-SDK UFC test cases

@schmit schmit marked this pull request as ready for review March 29, 2024 06:25
assignmentHooks?: IAssignmentHooks,
): number | null;

getJSONStringAssignment(
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 figured no one would use this -- hence removing it. Let me know if you disagree.

Copy link
Contributor

Choose a reason for hiding this comment

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

We provided it to support the transition of customers who were using getAssignment() with JSON before we added typed values, so it would be an easier transition for them.

I agree its odd, and I'm fine ripping it out and only adding it back if somebody complains.

Comment on lines +474 to +475
allocationKey: result.allocationKey ?? '__eppo_no_allocation',
variationKey: result.variation?.key ?? '__eppo_no_variation',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

using placeholder values here to populate the cache

@@ -7,7 +7,7 @@ describe('EppoValue toHashedString function', () => {
});

it('is JsonType', () => {
const myInstance = EppoValue.JSON('{"hello":"world"}', { hello: 'world' });
const myInstance = EppoValue.JSON({ hello: 'world' });
Copy link
Contributor Author

Choose a reason for hiding this comment

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

no more string version of JSON blob

Copy link
Contributor

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

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

🎉 This is great stuff! I especially love the tests! 👏 Very excited for how it all cleans things up as well and simplifies things. 📈

I'm requesting changes for three(ish) things:

  1. The current implementation which uses falseyness checks in a terinary for passing through default values breaks things in unexpected ways
  2. I think we need more strictness around the newly added Integer type within the internals of EppoValue, such as when we check if the type is valid.
  3. (this one may be controversial) I'd argue that our tests should have the actual, hard-coded values to decouple our obfuscating/unobfuscating. That being said, it would be a huge pain in the ass and make tests less readable. So maybe we could just rely on tests of our hashing and encoding in isolation.

@@ -28,7 +29,7 @@ export interface IAssignmentEvent {
/**
* The assigned variation
*/
variation: string;
variation: string | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

When would a (logged) assigned variation be null?

Copy link
Collaborator

Choose a reason for hiding this comment

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

When no allocation matches (or when the flag is disabled) and we return noneResult. Is the question that you don't expect such occurrenced to be logged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this is to enable logging the lack of assignment, which seems valuable especially because you probably want to figure out why assignments are not working

Copy link
Contributor

Choose a reason for hiding this comment

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

This makes sense if its a change we want. Note that currently, when no assignment is given, we do not log the null assignment.

@@ -74,6 +47,7 @@ export interface IEppoClient {
flagKey: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes?: Record<string, any>,
defaultValue?: string | null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that this pattern does not force the user to provide a default value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, I left this as optional for now pending on a decision on the FF UX project

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated: this is no longer optional

assignmentHooks?: IAssignmentHooks,
): number | null;

getJSONStringAssignment(
Copy link
Contributor

Choose a reason for hiding this comment

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

We provided it to support the transition of customers who were using getAssignment() with JSON before we added typed values, so it would be an easier transition for them.

I agree its odd, and I'm fine ripping it out and only adding it back if somebody complains.

src/client/eppo-client.ts Show resolved Hide resolved

constructor(
evaluator: Evaluator,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use a more specific name? configurationEvaluator? Would help if in the future we have other evaluators, and parallels the other argument names.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be used internally only -- I think it's fine to change this in the future if we do add other evaluators

src/rules.spec.ts Show resolved Hide resolved
src/rules.spec.ts Outdated Show resolved Hide resolved
src/rules.spec.ts Outdated Show resolved Hide resolved
src/rules.spec.ts Outdated Show resolved Hide resolved
src/rules.ts Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/rules.spec.ts Outdated Show resolved Hide resolved
src/rules.spec.ts Show resolved Hide resolved
src/eval.ts Outdated Show resolved Hide resolved
@@ -28,7 +29,7 @@ export interface IAssignmentEvent {
/**
* The assigned variation
*/
variation: string;
variation: string | null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

When no allocation matches (or when the flag is disabled) and we return noneResult. Is the question that you don't expect such occurrenced to be logged?

src/assignment-logger.ts Show resolved Hide resolved
src/eval.spec.ts Outdated Show resolved Hide resolved
src/client/eppo-client.spec.ts Outdated Show resolved Hide resolved
@giorgiomartini0
Copy link
Collaborator

Ugh, I always get tricked when reviewing in editor mode that changing the review type also shares the review... What I wanted to add: this is a true tour the force! Left a few minor comments and requests. Aaron's review already hit the (few) larger issues, and I didn't duplicate his comments.

src/interfaces.ts Outdated Show resolved Hide resolved
src/client/eppo-client.ts Outdated Show resolved Hide resolved
}
}

function getTestAssignments(
Copy link
Contributor

Choose a reason for hiding this comment

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

@schmit Could we export these two functions somewhere? That way we don't need to duplicate them in both JS and Node SDKs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great callout: 9915183

@schmit schmit requested a review from aarsilv April 15, 2024 20:28
src/index.ts Show resolved Hide resolved
Copy link
Contributor

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

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

Thanks for iterating on all the suggested changes! 🎉

I threw in some more minor comments and cleanup suggestions that shouldn't be too big of a lift.

Requesting further changes so we don't accidentally merge before updating to handle our latest obfuscation plan. 📆 You can see the (currently draft) PR for updating the test data here, and the UFC-generation changes to obfuscate in this PR.

I know it's annoying to be chasing a moving target 😞

Comment on lines +228 to +239
const typeAssignmentFunctions = {
[VariationType.BOOLEAN]: client.getBoolAssignment.bind(client),
[VariationType.NUMERIC]: client.getNumericAssignment.bind(client),
[VariationType.INTEGER]: client.getIntegerAssignment.bind(client),
[VariationType.STRING]: client.getStringAssignment.bind(client),
[VariationType.JSON]: client.getJSONAssignment.bind(client),
};

const assignmentFn = typeAssignmentFunctions[variationType];
if (!assignmentFn) {
throw new Error(`Unknown variation type: ${variationType}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
const client = new EppoClient(storage, undefined, true);

const typeAssignmentFunctions = {
Copy link
Contributor

Choose a reason for hiding this comment

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

This through line 290 is all duplicated code and could be consolidated into a helper function

Comment on lines 44 to 49
getStringAssignment(
subjectKey: string,
flagKey: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes?: Record<string, any>,
assignmentHooks?: IAssignmentHooks,
): string | null;
defaultValue: string,
subjectAttributes?: Record<string, AttributeType>,
): string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that Optimizely and LaunchDarkly have flag keys first

this.configurationStore = configurationStore;
this.configurationRequestParameters = configurationRequestParameters;
this.isObfuscated = obfuscated;
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

obfuscated ? getMD5Hash(flagKey) : flagKey,
);
const allowListOverride = this.getSubjectVariationOverride(
const result = this.evaluator.evaluateFlag(
Copy link
Contributor

Choose a reason for hiding this comment

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

Like our pattern on the server side, I think it may be best to have an optional step before this one where we unobfuscated the flag if it's obfuscated and then pass that to the evaluator, who only needs to worry about unobfuscated flags.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nevermind, we can't do this with hashes

@@ -0,0 +1 @@
export const LIB_VERSION = '3.0.0';
Copy link
Contributor

Choose a reason for hiding this comment

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

If we do want to log the version of this common module, we shouldn't hard code this and instead pull it from package.json

metaData: {
obfuscated: this.isObfuscated,
sdkLanguage: 'javascript',
sdkLibVersion: LIB_VERSION,
Copy link
Contributor

Choose a reason for hiding this comment

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

This technically won't be the SDK version because this is just a dependency of that SDK, which will have its own version number. That version number is still pulled as it's used as a URL parameter. So if we want to include this we should make it a parameter in the constructor and have the upstream SDKs pass it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right but because this library contains all the logic, it seems more relevant to log this libary's function than the "proper" SDK one.

return nullAssignment;
if (
result?.variation &&
!this.checkValueTypeMatch(expectedVariationType, result.variation.value)
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that result.variation.value is now encoded and needs to be de-encoded

return defaultValue;
}

return EppoValue.valueOf(result.variation.value, expectedVariationType);
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that now result.variation.value will now be encoded and needs to be decoded

Comment on lines +475 to +478
allocation: result.allocationKey ?? null,
experiment: result.allocationKey ? `${result.flagKey}-${result.allocationKey}` : null,
featureFlag: result.flagKey,
variation: result.variation?.key ?? null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that result.allocationKey, result.flagKey, and result.variation.key will all be obfuscated and should now be deobfuscated upstream.

@aarsilv aarsilv assigned schmit and unassigned aarsilv Apr 17, 2024
Copy link
Contributor

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

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

Approving now as signatures match and the obfuscation moving target has been caught up with! 🏎️

@schmit schmit merged commit 7fd5a4a into main Apr 19, 2024
2 checks passed
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.

6 participants