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

add bandits configuration to offline bootstrap in precomputed client … #195

Merged
merged 12 commits into from
Jan 15, 2025

Conversation

leoromanovsky
Copy link
Member

@leoromanovsky leoromanovsky commented Jan 10, 2025

…(FF-3796)


labels: mergeable

Fixes: #issue

Motivation and Context

The precomputed clients needs to have two bandit evaluation functionalities:

  • request bandit evaluation from the server
  • receive offline init configuration from an external source (ex: nodejs server)

In both cases the SDK initializes in offline mode and is able to perform:

  • bandit precomputed evaluation
  • sends bandit logging events if a logger is defined

Not all bandit APIs are at parity with the local-evaluation client: specifically this PR only addeds getBanditAction, not details since those are not available in the API. Nor does it add getBestAction, which was previously developed for coinbase, since that method is not supported in the server.

Description

How has this been tested?

Our test sample data (https://github.com/Eppo-exp/sdk-test-data/blob/main/configuration-wire/precomputed-v1.json) needs precomputed bandits to be able to support testing against the shared data suite. For now adding just a simple case and in-parallel adding these.

if (options.requestParameters) {
// Online-mode
this.requestParameters = options.requestParameters;
} else {
// Offline-mode

// Offline mode depends on pre-populated IConfigurationStores (flags and bandits) to source configuration.
if (!this.precomputedFlagStore.isInitialized()) {
logger.error(
'[Eppo SDK] EppoPrecomputedClient requires an initialized precomputedFlagStore if requestParameters are not provided',
Copy link
Member Author

Choose a reason for hiding this comment

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

use logger constant [Eppo SDK]

Comment on lines +33 to +53
const promises: Promise<void>[] = [];
promises.push(
hydrateConfigurationStore(this.precomputedFlagStore, {
entries: precomputedResponse.flags,
environment: precomputedResponse.environment ?? { name: UNKNOWN_ENVIRONMENT_NAME },
createdAt: precomputedResponse.createdAt,
format: precomputedResponse.format,
salt: precomputedResponse.salt,
}),
);
if (this.precomputedBanditsStore) {
promises.push(
hydrateConfigurationStore(this.precomputedBanditsStore, {
entries: precomputedResponse.bandits,
environment: precomputedResponse.environment ?? { name: UNKNOWN_ENVIRONMENT_NAME },
createdAt: precomputedResponse.createdAt,
format: precomputedResponse.format,
salt: precomputedResponse.salt,
}),
);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

when a bandit store is present - hydrate it

Copy link
Contributor

Choose a reason for hiding this comment

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

Confirming both stores use the same salt

this.salt = salt;
this.bandits = obfuscatePrecomputedBanditMap(this.salt, bandits);
this.flags = obfuscatePrecomputedFlags(this.salt, flags);
}

export interface IPrecomputedBandit {
banditKey: string;
action: string;
export type BasePrecomputedBandit = {
Copy link
Member Author

Choose a reason for hiding this comment

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

since we haven't published these interfaces publically, i took a moment to rewrite them to more explicitly mention which are obfuscated.

@@ -382,6 +488,40 @@ export default class EppoPrecomputedClient {
}
}

private logBanditAction(banditEvent: IBanditEvent): void {
Copy link
Member Author

Choose a reason for hiding this comment

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

directly copied from local client

Comment on lines 324 to 339
// An evaluation details object is needed for the bandit event, but it is not used for anything else.
const evaluationDetails: IFlagEvaluationDetails = {
environmentName: '',
flagEvaluationCode: 'MATCH',
flagEvaluationDescription: 'Bandit action assigned',
variationKey: '',
variationValue: '',
banditKey: banditEvaluation.banditKey,
banditAction: banditEvaluation.action,
configFetchedAt: '',
configPublishedAt: '',
matchedRule: null,
matchedAllocation: null,
unmatchedAllocations: [],
unevaluatedAllocations: [],
};
Copy link
Member Author

Choose a reason for hiding this comment

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

unfortunately an evaluation detail is required for the callback even though we don't support details at this time

Copy link
Contributor

Choose a reason for hiding this comment

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

Unless there's some dependency preventing this, it would be more elegant to make evaluationDetails optional in IBanditEvent

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for mentioning that - the assignment callback allows it to be null so I will normalize this.

export interface IAssignmentEvent {
  /**
   * The flag evaluation details. Null if the flag was precomputed.
   */
  evaluationDetails: IFlagEvaluationDetails | null;
}

Copy link
Contributor

@sameerank sameerank left a comment

Choose a reason for hiding this comment

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

Lgtm. Please take a pass at the linter warnings before merging

Comment on lines 101 to +103
// Offline-mode

// Offline mode depends on pre-populated IConfigurationStores (flags and bandits) to source configuration.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can combine these comments

Suggested change
// Offline-mode
// Offline mode depends on pre-populated IConfigurationStores (flags and bandits) to source configuration.
// Offline-mode: depends on pre-populated IConfigurationStores (flags and bandits) to source configuration.

Comment on lines 324 to 339
// An evaluation details object is needed for the bandit event, but it is not used for anything else.
const evaluationDetails: IFlagEvaluationDetails = {
environmentName: '',
flagEvaluationCode: 'MATCH',
flagEvaluationDescription: 'Bandit action assigned',
variationKey: '',
variationValue: '',
banditKey: banditEvaluation.banditKey,
banditAction: banditEvaluation.action,
configFetchedAt: '',
configPublishedAt: '',
matchedRule: null,
matchedAllocation: null,
unmatchedAllocations: [],
unevaluatedAllocations: [],
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless there's some dependency preventing this, it would be more elegant to make evaluationDetails optional in IBanditEvent

Comment on lines +33 to +53
const promises: Promise<void>[] = [];
promises.push(
hydrateConfigurationStore(this.precomputedFlagStore, {
entries: precomputedResponse.flags,
environment: precomputedResponse.environment ?? { name: UNKNOWN_ENVIRONMENT_NAME },
createdAt: precomputedResponse.createdAt,
format: precomputedResponse.format,
salt: precomputedResponse.salt,
}),
);
if (this.precomputedBanditsStore) {
promises.push(
hydrateConfigurationStore(this.precomputedBanditsStore, {
entries: precomputedResponse.bandits,
environment: precomputedResponse.environment ?? { name: UNKNOWN_ENVIRONMENT_NAME },
createdAt: precomputedResponse.createdAt,
format: precomputedResponse.format,
salt: precomputedResponse.salt,
}),
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Confirming both stores use the same salt

this.salt = salt;
this.bandits = obfuscatePrecomputedBanditMap(this.salt, bandits);
this.flags = obfuscatePrecomputedFlags(this.salt, flags);
}

},
});

const client = new EppoPrecomputedClient({
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused variable

@leoromanovsky leoromanovsky merged commit 3f84c6b into main Jan 15, 2025
8 checks passed
@leoromanovsky leoromanovsky deleted the lr/ff-3796 branch January 15, 2025 04:13
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.

2 participants