Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f45b39c
chore: rename configuration.ts to configuration-wire-types.ts
typotter Feb 19, 2025
704d2e5
chore: deprecate fetchFlagConfigurations in favour of startPolling
typotter Feb 19, 2025
0e9c2f5
feat: Wrap configuration store access in new Configuration object
typotter Feb 19, 2025
8b11bb4
feat: ConfigurationRequestor.getConfiguration
typotter Feb 19, 2025
84857ad
merge main
typotter Feb 19, 2025
688b200
chore: delegate isObfuscated check to i-config
typotter Feb 19, 2025
40a953d
chore: delegate getFlagKeys to i-config
typotter Feb 19, 2025
3786b78
chore: delegate getFlagConfigurations to i-config
typotter Feb 19, 2025
2ab3dda
chore: delegate getFlag->getNormalizedFlag and evalDetails method to …
typotter Feb 19, 2025
06fc3e5
fix: point default config at other config stores
typotter Feb 19, 2025
1cc8cea
chore: remove findBanditByVariation and delegate to i-config
typotter Feb 19, 2025
f7e539e
chore: remove getBandit and delegate to i-config
typotter Feb 19, 2025
39aed59
feat: i-config.isInitialized
typotter Feb 19, 2025
421470d
remove getConfigDetails and delegate to i-config
typotter Feb 19, 2025
5b34fc7
feat: getters for other config stores and some renaming
typotter Feb 19, 2025
15e3262
chore: delegate banditVariations.get to i-config
typotter Feb 19, 2025
bf412f8
move flag config expired check to config requestor
typotter Feb 19, 2025
1ff4f35
v4.11.0
typotter Feb 19, 2025
5fc7ef3
chore: drop unimplemented and add specific string types
typotter Feb 19, 2025
3ab4301
chore: rename new config file to make a nicer diff
typotter Feb 19, 2025
3533fa5
chore: save startPolling for later
typotter Feb 19, 2025
b4b44b9
chore:lint
typotter Feb 19, 2025
4a1231b
chore: take TODO
typotter Feb 19, 2025
0f1874e
chore: don't duplicate code
typotter Feb 19, 2025
3f86324
Merge branch 'main' into tp/i-configuration
typotter Feb 21, 2025
94e7f3e
comments
typotter Feb 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "4.10.0",
"version": "4.11.0",
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
"main": "dist/index.js",
"files": [
Expand Down
6 changes: 3 additions & 3 deletions src/client/eppo-client-with-bandits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import ApiEndpoints from '../api-endpoints';
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
import { BanditEvaluation, BanditEvaluator } from '../bandit-evaluator';
import { IBanditEvent, IBanditLogger } from '../bandit-logger';
import ConfigurationRequestor from '../configuration-requestor';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
import {
IConfigurationWire,
IPrecomputedConfiguration,
IObfuscatedPrecomputedConfigurationResponse,
} from '../configuration';
import ConfigurationRequestor from '../configuration-requestor';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
} from '../configuration-wire-types';
import { Evaluator, FlagEvaluation } from '../evaluator';
import {
AllocationEvaluationCode,
Expand Down
8 changes: 4 additions & 4 deletions src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {
} from '../../test/testHelpers';
import { IAssignmentLogger } from '../assignment-logger';
import { AssignmentCache } from '../cache/abstract-assignment-cache';
import { IConfigurationStore } from '../configuration-store/configuration-store';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
import {
IConfigurationWire,
IObfuscatedPrecomputedConfigurationResponse,
ObfuscatedPrecomputedConfigurationResponse,
} from '../configuration';
import { IConfigurationStore } from '../configuration-store/configuration-store';
import { MemoryOnlyConfigurationStore } from '../configuration-store/memory.store';
import { DEFAULT_POLL_INTERVAL_MS, MAX_EVENT_QUEUE_SIZE, POLL_JITTER_PCT } from '../constants';
} from '../configuration-wire-types';
import { MAX_EVENT_QUEUE_SIZE, DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
import { decodePrecomputedFlag } from '../decoding';
import { Flag, ObfuscatedFlag, VariationType, FormatEnum, Variation } from '../interfaces';
import { getMD5Hash } from '../obfuscation';
Expand Down
133 changes: 61 additions & 72 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,19 @@ import { AssignmentCache } from '../cache/abstract-assignment-cache';
import { LRUInMemoryAssignmentCache } from '../cache/lru-in-memory-assignment-cache';
import { NonExpiringInMemoryAssignmentCache } from '../cache/non-expiring-in-memory-cache-assignment';
import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache';
import ConfigurationRequestor from '../configuration-requestor';
import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store';
import {
ConfigurationWireV1,
IConfigurationWire,
IPrecomputedConfiguration,
PrecomputedConfiguration,
} from '../configuration';
import ConfigurationRequestor from '../configuration-requestor';
import { IConfigurationStore, ISyncStore } from '../configuration-store/configuration-store';
} from '../configuration-wire-types';
import {
DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES,
DEFAULT_POLL_CONFIG_REQUEST_RETRIES,
DEFAULT_POLL_INTERVAL_MS,
DEFAULT_REQUEST_TIMEOUT_MS,
OBFUSCATED_FORMATS,
} from '../constants';
import { decodeFlag } from '../decoding';
import { EppoValue } from '../eppo_value';
Expand All @@ -41,13 +40,12 @@ import {
} from '../flag-evaluation-details-builder';
import { FlagEvaluationError } from '../flag-evaluation-error';
import FetchHttpClient from '../http-client';
import { IConfiguration, StoreBackedConfiguration } from '../i-configuration';
import {
BanditModelData,
BanditParameters,
BanditVariation,
ConfigDetails,
Flag,
FormatEnum,
IPrecomputedBandit,
ObfuscatedFlag,
PrecomputedFlag,
Expand Down Expand Up @@ -135,6 +133,7 @@ export default class EppoClient {
private configObfuscatedCache?: boolean;
private requestPoller?: IPoller;
private readonly evaluator = new Evaluator();
private configurationRequestor?: ConfigurationRequestor;

constructor({
eventDispatcher = new NoOpEventDispatcher(),
Expand Down Expand Up @@ -164,6 +163,16 @@ export default class EppoClient {
this.expectObfuscated = isObfuscated;
}

private getConfiguration(): IConfiguration {
return this.configurationRequestor
? this.configurationRequestor.getConfiguration()
: new StoreBackedConfiguration(
this.flagConfigurationStore,
this.banditVariationConfigurationStore,
this.banditModelConfigurationStore,
);
}

private maybeWarnAboutObfuscationMismatch(configObfuscated: boolean) {
// Don't warn again if we did on the last check.
if (configObfuscated !== this.expectObfuscated && !this.obfuscationMismatchWarningIssued) {
Expand All @@ -177,11 +186,14 @@ export default class EppoClient {
}
}

private isObfuscated() {
/**
* This method delegates to the configuration to determine whether it is obfuscated, then caches the actual
* obfuscation state and issues a warning if it hasn't already.
* This method can be removed with the next major update when the @deprecated setIsObfuscated is removed
*/
private isObfuscated(config: IConfiguration) {
if (this.configObfuscatedCache === undefined) {
this.configObfuscatedCache = OBFUSCATED_FORMATS.includes(
this.flagConfigurationStore.getFormat() ?? FormatEnum.SERVER,
);
this.configObfuscatedCache = config.isObfuscated();
}
this.maybeWarnAboutObfuscationMismatch(this.configObfuscatedCache);
return this.configObfuscatedCache;
Expand Down Expand Up @@ -307,7 +319,7 @@ export default class EppoClient {
);

const pollingCallback = async () => {
if (await this.flagConfigurationStore.isExpired()) {
if (await configurationRequestor.isFlagConfigExpired()) {
this.configObfuscatedCache = undefined;
return configurationRequestor.fetchAndStoreConfigurations();
}
Expand Down Expand Up @@ -624,13 +636,14 @@ export default class EppoClient {
actions: BanditActions,
defaultAction: string,
): string {
const config = this.getConfiguration();
let result: string | null = null;

const flagBanditVariations = this.banditVariationConfigurationStore?.get(flagKey);
const flagBanditVariations = config.getFlagBanditVariations(flagKey);
const banditKey = flagBanditVariations?.at(0)?.key;

if (banditKey) {
const banditParameters = this.getBandit(banditKey);
const banditParameters = config.getBandit(banditKey);
if (banditParameters) {
const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes);
const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions);
Expand All @@ -653,11 +666,13 @@ export default class EppoClient {
actions: BanditActions,
defaultValue: string,
): IAssignmentDetails<string> {
const config = this.getConfiguration();
let variation = defaultValue;
let action: string | null = null;

// Initialize with a generic evaluation details. This will mutate as the function progresses.
let evaluationDetails: IFlagEvaluationDetails = this.newFlagEvaluationDetailsBuilder(
config,
flagKey,
).buildForNoneResult(
'ASSIGNMENT_ERROR',
Expand All @@ -681,7 +696,7 @@ export default class EppoClient {
// Check if the assigned variation is an active bandit
// Note: the reason for non-bandit assignments include the subject being bucketed into a non-bandit variation or
// a rollout having been done.
const bandit = this.findBanditByVariation(flagKey, variation);
const bandit = config.getFlagVariationBandit(flagKey, variation);

if (!bandit) {
return { variation, action: null, evaluationDetails };
Expand Down Expand Up @@ -909,13 +924,14 @@ export default class EppoClient {
subjectKey: string,
subjectAttributes: Attributes = {},
): Record<FlagKey, PrecomputedFlag> {
const configDetails = this.getConfigDetails();
const config = this.getConfiguration();
const configDetails = config.getFlagConfigDetails();
const flagKeys = this.getFlagKeys();
const flags: Record<FlagKey, PrecomputedFlag> = {};

// Evaluate all the enabled flags for the user
flagKeys.forEach((flagKey) => {
const flag = this.getFlag(flagKey);
const flag = this.getNormalizedFlag(config, flagKey);
if (!flag) {
logger.debug(`${loggerPrefix} No assigned variation. Flag does not exist.`);
return;
Expand All @@ -927,7 +943,7 @@ export default class EppoClient {
configDetails,
subjectKey,
subjectAttributes,
this.isObfuscated(),
this.isObfuscated(config),
);

// allocationKey is set along with variation when there is a result. this check appeases typescript below
Expand Down Expand Up @@ -965,13 +981,15 @@ export default class EppoClient {
banditActions: Record<FlagKey, BanditActions> = {},
salt?: string,
): string {
const configDetails = this.getConfigDetails();
const config = this.getConfiguration();
const configDetails = config.getFlagConfigDetails();

const subjectContextualAttributes = ensureContextualSubjectAttributes(subjectAttributes);
const subjectFlatAttributes = ensureNonContextualSubjectAttributes(subjectAttributes);
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes);

const bandits = this.computeBanditsForFlags(
config,
subjectKey,
subjectContextualAttributes,
banditActions,
Expand Down Expand Up @@ -1011,8 +1029,9 @@ export default class EppoClient {
): FlagEvaluation {
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
const config = this.getConfiguration();

const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(flagKey);
const flagEvaluationDetailsBuilder = this.newFlagEvaluationDetailsBuilder(config, flagKey);
const overrideVariation = this.overrideStore?.get(flagKey);
if (overrideVariation) {
return overrideResult(
Expand All @@ -1024,8 +1043,8 @@ export default class EppoClient {
);
}

const configDetails = this.getConfigDetails();
const flag = this.getFlag(flagKey);
const configDetails = config.getFlagConfigDetails();
const flag = this.getNormalizedFlag(config, flagKey);

if (flag === null) {
logger.warn(`${loggerPrefix} No assigned variation. Flag not found: ${flagKey}`);
Expand Down Expand Up @@ -1077,7 +1096,7 @@ export default class EppoClient {
);
}

const isObfuscated = this.isObfuscated();
const isObfuscated = this.isObfuscated(config);
const result = this.evaluator.evaluateFlag(
flag,
configDetails,
Expand Down Expand Up @@ -1114,9 +1133,12 @@ export default class EppoClient {
});
}

private newFlagEvaluationDetailsBuilder(flagKey: string): FlagEvaluationDetailsBuilder {
const flag = this.getFlag(flagKey);
const configDetails = this.getConfigDetails();
private newFlagEvaluationDetailsBuilder(
config: IConfiguration,
flagKey: string,
): FlagEvaluationDetailsBuilder {
const flag = this.getNormalizedFlag(config, flagKey);
const configDetails = config.getFlagConfigDetails();
return new FlagEvaluationDetailsBuilder(
configDetails.configEnvironment.name,
flag?.allocations ?? [],
Expand All @@ -1125,35 +1147,17 @@ export default class EppoClient {
);
}

private getConfigDetails(): ConfigDetails {
return {
configFetchedAt: this.flagConfigurationStore.getConfigFetchedAt() ?? '',
configPublishedAt: this.flagConfigurationStore.getConfigPublishedAt() ?? '',
configEnvironment: this.flagConfigurationStore.getEnvironment() ?? {
name: '',
},
configFormat: this.flagConfigurationStore.getFormat() ?? '',
};
}

private getFlag(flagKey: string): Flag | null {
return this.isObfuscated()
? this.getObfuscatedFlag(flagKey)
: this.flagConfigurationStore.get(flagKey);
private getNormalizedFlag(config: IConfiguration, flagKey: string): Flag | null {
return this.isObfuscated(config)
? this.getObfuscatedFlag(config, flagKey)
: config.getFlag(flagKey);
}

private getObfuscatedFlag(flagKey: string): Flag | null {
const flag: ObfuscatedFlag | null = this.flagConfigurationStore.get(
getMD5Hash(flagKey),
) as ObfuscatedFlag;
private getObfuscatedFlag(config: IConfiguration, flagKey: string): Flag | null {
const flag: ObfuscatedFlag | null = config.getFlag(getMD5Hash(flagKey)) as ObfuscatedFlag;
return flag ? decodeFlag(flag) : null;
}

private getBandit(banditKey: string): BanditParameters | null {
// Upstreams for this SDK do not yet support obfuscating bandits, so no `isObfuscated` check here.
return this.banditModelConfigurationStore?.get(banditKey) ?? null;
}

// noinspection JSUnusedGlobalSymbols
getFlagKeys() {
/**
Expand All @@ -1162,16 +1166,11 @@ export default class EppoClient {
*
* Note that it is generally not a good idea to preload all flag configurations.
*/
return this.flagConfigurationStore.getKeys();
return this.getConfiguration().getFlagKeys();
}

isInitialized() {
return (
this.flagConfigurationStore.isInitialized() &&
(!this.banditVariationConfigurationStore ||
this.banditVariationConfigurationStore.isInitialized()) &&
(!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized())
);
return this.getConfiguration().isInitialized();
}

/** @deprecated Use `setAssignmentLogger` */
Expand Down Expand Up @@ -1237,7 +1236,7 @@ export default class EppoClient {
}

getFlagConfigurations(): Record<string, Flag> {
return this.flagConfigurationStore.entries();
return this.getConfiguration().getFlags();
}

private flushQueuedEvents<T>(eventQueue: BoundedEventQueue<T>, logFunction?: (event: T) => void) {
Expand Down Expand Up @@ -1305,13 +1304,14 @@ export default class EppoClient {

private buildLoggerMetadata(): Record<string, unknown> {
return {
obfuscated: this.isObfuscated(),
obfuscated: this.isObfuscated(this.getConfiguration()),
sdkLanguage: 'javascript',
sdkLibVersion: LIB_VERSION,
};
}

private computeBanditsForFlags(
config: IConfiguration,
subjectKey: string,
subjectAttributes: ContextAttributes,
banditActions: Record<FlagKey, BanditActions>,
Expand All @@ -1325,6 +1325,7 @@ export default class EppoClient {
if (flagVariation) {
// Precompute a bandit, if there is one matching this variation.
const precomputedResult = this.getPrecomputedBandit(
config,
flagKey,
flagVariation.variationValue,
subjectKey,
Expand All @@ -1339,27 +1340,15 @@ export default class EppoClient {
return banditResults;
}

private findBanditByVariation(flagKey: string, variationValue: string): BanditParameters | null {
const banditVariations = this.banditVariationConfigurationStore?.get(flagKey);
const banditKey = banditVariations?.find(
(banditVariation) => banditVariation.variationValue === variationValue,
)?.key;

if (banditKey) {
// Retrieve the model parameters for the bandit
return this.getBandit(banditKey);
}
return null;
}

private getPrecomputedBandit(
config: IConfiguration,
flagKey: string,
variationValue: string,
subjectKey: string,
subjectAttributes: ContextAttributes,
banditActions: BanditActions,
): IPrecomputedBandit | null {
const bandit = this.findBanditByVariation(flagKey, variationValue);
const bandit = config.getFlagVariationBandit(flagKey, variationValue);
if (!bandit) {
return null;
}
Expand Down
Loading
Loading