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

feat: obfuscated precomputed assignments #164

Conversation

sameerank
Copy link
Contributor

@sameerank sameerank commented Dec 9, 2024


labels: mergeable

Fixes: #issue

Motivation and Context

Description

Updates de-obfuscation code to use a salt in hashing the flag key for finding precomputed flags. We expect this to work for both the regular init-ed client and offline init, since both will be obfuscated.

How has this been tested?

Makefile Show resolved Hide resolved
@sameerank sameerank marked this pull request as ready for review December 15, 2024 21:56
src/obfuscation.ts Outdated Show resolved Hide resolved
Copy link
Member

@leoromanovsky leoromanovsky left a comment

Choose a reason for hiding this comment

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

Will take a thorough read tomorrow 👍

src/client/eppo-precomputed-client.spec.ts Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.spec.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
Comment on lines 96 to 101
// Convenience method that combines setters we need to make assignments work
public setSubjectAndPrecomputedFlagsRequestParameters(
parameters: PrecomputedFlagsRequestParameters,
) {
this.setPrecomputedFlagsRequestParameters(parameters);
this.setSubjectData(parameters.precompute.subjectKey, parameters.precompute.subjectAttributes);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should not be public.

It also seems to be doing a bad thing — allows reinterpreting precomputed configuration from another subject. It also does not reset the poller, so it would continue polling with the previous subject key/attributes

Copy link
Collaborator

Choose a reason for hiding this comment

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

made not public.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is an alternative approach to setting these instance variables in the client if this method is no longer public?

https://github.com/Eppo-exp/js-client-sdk/blob/ee3409f5ce9af803687769b56052cc468977c353/src/index.ts#L602

Re: the concern of a mismatch arising between a previously running poller and newly provided request parameters + subject data: I'm thinking that adding a this.requestPoller?.stop(); method at the top of this method would prevent that from happening

Copy link
Collaborator

Choose a reason for hiding this comment

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

Re: the concern of a mismatch arising between a previously running poller and newly provided request parameters + subject data: I'm thinking that adding a this.requestPoller?.stop(); method at the top of this method would prevent that from happening

It will not, because stop() does not interrupt an in-progress request, so what could happen is:

  1. Client is initialized with one subject.
  2. Poller sends a request.
  3. Client is re-initialized with another subject.
  4. Previous request completes, and assigns data from previous subject to the store.

What is an alternative approach to setting these instance variables in the client if this method is no longer public?

Constructor. We shall prefer creating a new instance of the client in initialization instead of resetting the existing one. Resetting is problematic as it makes it impossible to use multiple clients simultaneously, and some data may still linger and intermix as in poller example above. (e.g., this is backfiring in our e2e testing effort, and afaik it is problematic for evaluation reasons on eppo portal)

Alternatively, JS/TS don't have much protection against calling typescript's private methods. If we really need to call a method, it's easy to disable typescript protection (although not recommended).

}

// Convenience method that combines setters we need to make assignments work
public setSubjectSaltAndPrecomputedFlagStore(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shall not be public

Copy link
Collaborator

Choose a reason for hiding this comment

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

done

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If setSubjectAndPrecomputedFlagStore is to be made private and setPrecomputedFlagStore and setSubjectData are also private -- can you provide a suggestion for how the client should set and persist this information in the offlinePrecomputedInit method in js-client-sdk?

https://github.com/Eppo-exp/js-client-sdk/blob/ee3409f5ce9af803687769b56052cc468977c353/src/index.ts#L669-L675

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would it be preferable to delete setSubjectAndPrecomputedFlagStore completely and make both setPrecomputedFlagStore and setSubjectData public? In my mind, I thought of these private individual setters as implementation details and the public setter as a convenient grouping of everything you need to get offline init to work

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would rather have less public methods as all public methods are exposed to our users and become part of our API.

src/precomputed-requestor.ts Outdated Show resolved Hide resolved
src/precomputed-requestor.ts Outdated Show resolved Hide resolved
src/precomputed-requestor.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
Comment on lines 58 to 59
precomputedFlagStore: IConfigurationStore<PrecomputedFlag>;
isObfuscated?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would also expect EppoPrecomputedClient to receive sdk key, subject key, attributes, assignment logger, optional initial configuration, etc in constructor

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've added the request params object to the init function as I don't want to work the PrecomputedFlagsRequestParameters and EppoPrecomputedClientOptions types in this PR. We will have a chance to iterate on the API before releasing to the general public and have a need to punt some of this work in the very short term.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The request parameters could be included in EppoPrecomputedClientOptions. This would be consistent with how the regular client's constructor uses configurationRequestParameters

configurationRequestParameters?: FlagConfigurationRequestParameters;
isObfuscated?: boolean;
}) {
this.eventDispatcher = eventDispatcher;
this.flagConfigurationStore = flagConfigurationStore;
this.banditVariationConfigurationStore = banditVariationConfigurationStore;
this.banditModelConfigurationStore = banditModelConfigurationStore;
this.configurationRequestParameters = configurationRequestParameters;
this.isObfuscated = isObfuscated;
}

I opted against this pattern because the js-client-sdk doesn't actually use this field in the constructor and explicitly calls a instance.setConfigurationRequestParameters(requestConfiguration);, which is the same approach I'd taken with using setSubjectAndPrecomputedFlagsRequestParameters. If you don't intend to use an argument in a constructor, why even have the argument in the first place?

@typotter typotter changed the title feat: use salt in flag key hash feat: obfuscated precomputed assignments Dec 18, 2024
Copy link
Member

@leoromanovsky leoromanovsky left a comment

Choose a reason for hiding this comment

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

approved but version bump needed before merging

Comment on lines -78 to -79
public setIsObfuscated(isObfuscated: boolean) {
this.isObfuscated = isObfuscated;
Copy link
Member

Choose a reason for hiding this comment

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

before we added the ability to parse whether a configuration is obfuscated from the configuration, when users needed to bootstrap the SDK in offline mode they need to be able to set this. the canonical example is the JS browser client (which by default is in obfuscated mode) getting a configuration from a nodejs server (which is not obfuscated).

that being said, I know there are customers using this method. removing this public method will require a major version bump please.

overall this is a more elegant solution to receive the obfuscation status from the configuration - users will need to upgrade both their nodejs and js browser SDKs to make sure that metadata is included.

Copy link
Collaborator

Choose a reason for hiding this comment

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

that being said, I know there are customers using this method. removing this public method will require a major version bump please.

This is a method on a PrecomputedEppoClient, not the regular one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rasendubi What is the purpose of providing this obfuscated boolean field in the response (https://github.com/Eppo-exp/eppo-multiplatform/blob/5aadb2ed0572a96fbb999066c0191e949abd20f5/eppo_core/src/precomputed.rs#L83) if we're designing the SDKs to always assume that is it true?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if we're designing the SDKs to always assume that is it true?

That may change in the future. The flag is here to facilitate that change

package.json Outdated
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "4.7.0",
"version": "4.7.0-alpha.1",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"version": "4.7.0-alpha.1",
"version": "5.0.0-alpha.1",

removing some public methods that are used by customers (see below)

Copy link
Collaborator

Choose a reason for hiding this comment

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

See my other comment — we are not removing used methods. All changes are for the new precomputed client, not the regular one

src/attributes.ts Outdated Show resolved Hide resolved
src/attributes.ts Outdated Show resolved Hide resolved
src/attributes.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
src/client/eppo-precomputed-client.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
test/testHelpers.ts Outdated Show resolved Hide resolved
Comment on lines +83 to +92
if (!this.precomputedFlagStore.isInitialized()) {
logger.error(
'[Eppo SDK] EppoPrecomputedClient requires an initialized precomputedFlagStore if requestParameters are not provided',
);
}
if (!this.precomputedFlagStore.salt) {
logger.error(
'[Eppo SDK] EppoPrecomputedClient requires a precomputedFlagStore with a salt if requestParameters are not provided',
);
}
Copy link
Contributor Author

@sameerank sameerank Jan 8, 2025

Choose a reason for hiding this comment

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

Printing instead of throwing errors if the precomputed flag store is misconfigured because the SDK shouldn't disrupt the user's code, but also these scenarios should never happen, and if they do, assignments will always return the user-provided default

@sameerank sameerank merged commit 65986fe into main Jan 8, 2025
8 checks passed
@sameerank sameerank deleted the sameeran/ff-3682-use-salt-in-flag-key-hash-to-getobfuscatedprecomputedflag branch January 8, 2025 21:45
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.

4 participants