-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
assignmentHooks?: IAssignmentHooks, | ||
): number | null; | ||
|
||
getJSONStringAssignment( |
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 figured no one would use this -- hence removing it. Let me know if you disagree.
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 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.
allocationKey: result.allocationKey ?? '__eppo_no_allocation', | ||
variationKey: result.variation?.key ?? '__eppo_no_variation', |
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.
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' }); |
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 more string version of JSON blob
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.
🎉 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:
- The current implementation which uses falseyness checks in a terinary for passing through default values breaks things in unexpected ways
- 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.
- (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; |
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.
When would a (logged) assigned variation be 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.
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?
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.
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
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.
This makes sense if its a change we want. Note that currently, when no assignment is given, we do not log the null assignment.
src/client/eppo-client.ts
Outdated
@@ -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, |
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.
Note that this pattern does not force the user to provide a default value.
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.
True, I left this as optional for now pending on a decision on the FF UX project
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.
Updated: this is no longer optional
assignmentHooks?: IAssignmentHooks, | ||
): number | null; | ||
|
||
getJSONStringAssignment( |
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 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
Outdated
|
||
constructor( | ||
evaluator: 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.
Should we use a more specific name? configurationEvaluator
? Would help if in the future we have other evaluators, and parallels the other argument names.
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.
This should be used internally only -- I think it's fine to change this in the future if we do add other evaluators
@@ -28,7 +29,7 @@ export interface IAssignmentEvent { | |||
/** | |||
* The assigned variation | |||
*/ | |||
variation: string; | |||
variation: string | 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.
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?
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/client/eppo-client.spec.ts
Outdated
} | ||
} | ||
|
||
function getTestAssignments( |
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.
@schmit Could we export these two functions somewhere? That way we don't need to duplicate them in both JS and Node SDKs.
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.
Great callout: 9915183
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.
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 😞
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}`); | ||
} |
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.
❤️
async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => { | ||
const client = new EppoClient(storage, undefined, true); | ||
|
||
const typeAssignmentFunctions = { |
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.
This through line 290 is all duplicated code and could be consolidated into a helper function
src/client/eppo-client.ts
Outdated
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; |
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.
Note that Optimizely and LaunchDarkly have flag keys first
this.configurationStore = configurationStore; | ||
this.configurationRequestParameters = configurationRequestParameters; | ||
this.isObfuscated = obfuscated; |
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.
❤️
obfuscated ? getMD5Hash(flagKey) : flagKey, | ||
); | ||
const allowListOverride = this.getSubjectVariationOverride( | ||
const result = this.evaluator.evaluateFlag( |
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.
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.
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.
Nevermind, we can't do this with hashes
@@ -0,0 +1 @@ | |||
export const LIB_VERSION = '3.0.0'; |
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.
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, |
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.
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.
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.
Right but because this library contains all the logic, it seems more relevant to log this libary's function than the "proper" SDK one.
src/client/eppo-client.ts
Outdated
return nullAssignment; | ||
if ( | ||
result?.variation && | ||
!this.checkValueTypeMatch(expectedVariationType, result.variation.value) |
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.
Note that result.variation.value
is now encoded and needs to be de-encoded
return defaultValue; | ||
} | ||
|
||
return EppoValue.valueOf(result.variation.value, expectedVariationType); |
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.
Note that now result.variation.value
will now be encoded and needs to be decoded
allocation: result.allocationKey ?? null, | ||
experiment: result.allocationKey ? `${result.flagKey}-${result.allocationKey}` : null, | ||
featureFlag: result.flagKey, | ||
variation: result.variation?.key ?? 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.
Note that result.allocationKey
, result.flagKey
, and result.variation.key
will all be obfuscated and should now be deobfuscated upstream.
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.
Approving now as signatures match and the obfuscation moving target has been caught up with! 🏎️
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
To do:
How has this been tested?