@eppo/js-client-sdk-common
Advanced tools
Comparing version 3.4.0 to 3.5.0
@@ -12,4 +12,5 @@ import { IQueryParams } from './http-client'; | ||
ufcEndpoint(): URL; | ||
banditParametersEndpoint(): URL; | ||
} | ||
export {}; | ||
//# sourceMappingURL=api-endpoints.d.ts.map |
@@ -20,4 +20,7 @@ "use strict"; | ||
} | ||
banditParametersEndpoint() { | ||
return this.endpoint(constants_1.BANDIT_ENDPOINT); | ||
} | ||
} | ||
exports.default = ApiEndpoints; | ||
//# sourceMappingURL=api-endpoints.js.map |
import { IAssignmentLogger } from '../assignment-logger'; | ||
import { IBanditLogger } from '../bandit-logger'; | ||
import { AssignmentCache } from '../cache/abstract-assignment-cache'; | ||
import { IConfigurationStore } from '../configuration-store/configuration-store'; | ||
import { FlagEvaluation } from '../evaluator'; | ||
import { Flag, ObfuscatedFlag, VariationType } from '../interfaces'; | ||
import { AttributeType, ValueType } from '../types'; | ||
import { BanditVariation, BanditParameters, Flag, ObfuscatedFlag, VariationType } from '../interfaces'; | ||
import { AttributeType, BanditActions, BanditSubjectAttributes, ValueType } from '../types'; | ||
/** | ||
@@ -68,7 +69,28 @@ * Client for assigning experiment variations. | ||
getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: object): object; | ||
/** | ||
* Maps a subject to a string assignment for a given experiment. | ||
* This variation may be a bandit-selected action. | ||
* | ||
* @param flagKey feature flag identifier | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional (can be empty) attributes associated with the subject, for example name and email. | ||
* @param actions possible attributes and their optional (can be empty) attributes to be evaluated by a contextual, | ||
* multi-armed bandit--if one is assigned to the subject. | ||
* @param defaultValue default value to return if the subject is not part of the experiment sample, | ||
* there are no bandit actions, or an error is countered evaluating the feature flag or bandit action */ | ||
getBanditAction(flagKey: string, subjectKey: string, subjectAttributes: BanditSubjectAttributes, actions: BanditActions, defaultValue: string): { | ||
variation: string; | ||
action: string | null; | ||
}; | ||
/** @Deprecated Renamed to setAssignmentLogger for clarity */ | ||
setLogger(logger: IAssignmentLogger): void; | ||
setAssignmentLogger(assignmentLogger: IAssignmentLogger): void; | ||
setBanditLogger(banditLogger: IBanditLogger): void; | ||
useLRUInMemoryAssignmentCache(maxSize: number): void; | ||
useCustomAssignmentCache(cache: AssignmentCache): void; | ||
setConfigurationRequestParameters(configurationRequestParameters: FlagConfigurationRequestParameters): void; | ||
setConfigurationStore(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>): void; | ||
setFlagConfigurationStore(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>): void; | ||
setBanditVariationConfigurationStore(banditVariationConfigurationStore: IConfigurationStore<BanditVariation[]>): void; | ||
setBanditModelConfigurationStore(banditModelConfigurationStore: IConfigurationStore<BanditParameters>): void; | ||
setIsObfuscated(isObfuscated: boolean): void; | ||
fetchFlagConfigurations(): void; | ||
@@ -95,14 +117,21 @@ stopPolling(): void; | ||
export default class EppoClient implements IEppoClient { | ||
private configurationStore; | ||
private flagConfigurationStore; | ||
private banditVariationConfigurationStore?; | ||
private banditModelConfigurationStore?; | ||
private configurationRequestParameters?; | ||
private isObfuscated; | ||
private queuedEvents; | ||
private readonly queuedAssignmentEvents; | ||
private assignmentLogger?; | ||
private readonly queuedBanditEvents; | ||
private banditLogger?; | ||
private isGracefulFailureMode; | ||
private assignmentCache?; | ||
private requestPoller?; | ||
private evaluator; | ||
constructor(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>, configurationRequestParameters?: FlagConfigurationRequestParameters | undefined, isObfuscated?: boolean); | ||
private readonly evaluator; | ||
private readonly banditEvaluator; | ||
constructor(flagConfigurationStore: IConfigurationStore<Flag | ObfuscatedFlag>, banditVariationConfigurationStore?: IConfigurationStore<BanditVariation[]> | undefined, banditModelConfigurationStore?: IConfigurationStore<BanditParameters> | undefined, configurationRequestParameters?: FlagConfigurationRequestParameters | undefined, isObfuscated?: boolean); | ||
setConfigurationRequestParameters(configurationRequestParameters: FlagConfigurationRequestParameters): void; | ||
setConfigurationStore(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>): void; | ||
setFlagConfigurationStore(flagConfigurationStore: IConfigurationStore<Flag | ObfuscatedFlag>): void; | ||
setBanditVariationConfigurationStore(banditVariationConfigurationStore: IConfigurationStore<BanditVariation[]>): void; | ||
setBanditModelConfigurationStore(banditModelConfigurationStore: IConfigurationStore<BanditParameters>): void; | ||
setIsObfuscated(isObfuscated: boolean): void; | ||
@@ -117,2 +146,12 @@ fetchFlagConfigurations(): Promise<void>; | ||
getJSONAssignment(flagKey: string, subjectKey: string, subjectAttributes: Record<string, AttributeType>, defaultValue: object): object; | ||
getBanditAction(flagKey: string, subjectKey: string, subjectAttributes: BanditSubjectAttributes, actions: BanditActions, defaultValue: string): { | ||
variation: string; | ||
action: string | null; | ||
}; | ||
private ensureNonContextualSubjectAttributes; | ||
private ensureContextualSubjectAttributes; | ||
private ensureActionsWithContextualAttributes; | ||
private isInstanceOfContextualAttributes; | ||
private deduceAttributeContext; | ||
private logBanditAction; | ||
private getAssignmentVariation; | ||
@@ -137,3 +176,6 @@ private rethrowIfNotGraceful; | ||
isInitialized(): boolean; | ||
/** @deprecated Renamed to setAssignmentLogger */ | ||
setLogger(logger: IAssignmentLogger): void; | ||
setAssignmentLogger(logger: IAssignmentLogger): void; | ||
setBanditLogger(logger: IBanditLogger): void; | ||
/** | ||
@@ -150,2 +192,3 @@ * Assignment cache methods. | ||
private logAssignment; | ||
private buildLoggerMetadata; | ||
} | ||
@@ -152,0 +195,0 @@ export declare function checkTypeMatch(expectedType?: VariationType, actualType?: VariationType): boolean; |
@@ -6,3 +6,5 @@ "use strict"; | ||
const application_logger_1 = require("../application-logger"); | ||
const bandit_evaluator_1 = require("../bandit-evaluator"); | ||
const abstract_assignment_cache_1 = require("../cache/abstract-assignment-cache"); | ||
const configuration_requestor_1 = require("../configuration-requestor"); | ||
const constants_1 = require("../constants"); | ||
@@ -12,3 +14,2 @@ const decoding_1 = require("../decoding"); | ||
const evaluator_1 = require("../evaluator"); | ||
const flag_configuration_requestor_1 = require("../flag-configuration-requestor"); | ||
const http_client_1 = require("../http-client"); | ||
@@ -21,9 +22,13 @@ const interfaces_1 = require("../interfaces"); | ||
class EppoClient { | ||
constructor(configurationStore, configurationRequestParameters, isObfuscated = false) { | ||
this.configurationStore = configurationStore; | ||
constructor(flagConfigurationStore, banditVariationConfigurationStore, banditModelConfigurationStore, configurationRequestParameters, isObfuscated = false) { | ||
this.flagConfigurationStore = flagConfigurationStore; | ||
this.banditVariationConfigurationStore = banditVariationConfigurationStore; | ||
this.banditModelConfigurationStore = banditModelConfigurationStore; | ||
this.configurationRequestParameters = configurationRequestParameters; | ||
this.isObfuscated = isObfuscated; | ||
this.queuedEvents = []; | ||
this.queuedAssignmentEvents = []; | ||
this.queuedBanditEvents = []; | ||
this.isGracefulFailureMode = true; | ||
this.evaluator = new evaluator_1.Evaluator(); | ||
this.banditEvaluator = new bandit_evaluator_1.BanditEvaluator(); | ||
} | ||
@@ -33,5 +38,11 @@ setConfigurationRequestParameters(configurationRequestParameters) { | ||
} | ||
setConfigurationStore(configurationStore) { | ||
this.configurationStore = configurationStore; | ||
setFlagConfigurationStore(flagConfigurationStore) { | ||
this.flagConfigurationStore = flagConfigurationStore; | ||
} | ||
setBanditVariationConfigurationStore(banditVariationConfigurationStore) { | ||
this.banditVariationConfigurationStore = banditVariationConfigurationStore; | ||
} | ||
setBanditModelConfigurationStore(banditModelConfigurationStore) { | ||
this.banditModelConfigurationStore = banditModelConfigurationStore; | ||
} | ||
setIsObfuscated(isObfuscated) { | ||
@@ -41,2 +52,3 @@ this.isObfuscated = isObfuscated; | ||
async fetchFlagConfigurations() { | ||
var _a, _b; | ||
if (!this.configurationRequestParameters) { | ||
@@ -49,3 +61,3 @@ throw new Error('Eppo SDK unable to fetch flag configurations without configuration request parameters'); | ||
} | ||
const isExpired = await this.configurationStore.isExpired(); | ||
const isExpired = await this.flagConfigurationStore.isExpired(); | ||
if (!isExpired) { | ||
@@ -63,3 +75,3 @@ application_logger_1.logger.info('[Eppo SDK] Configuration store is not expired. Skipping fetching flag configurations'); | ||
const httpClient = new http_client_1.default(apiEndpoints, requestTimeoutMs); | ||
const configurationRequestor = new flag_configuration_requestor_1.default(this.configurationStore, httpClient); | ||
const configurationRequestor = new configuration_requestor_1.default(httpClient, this.flagConfigurationStore, (_a = this.banditVariationConfigurationStore) !== null && _a !== void 0 ? _a : null, (_b = this.banditModelConfigurationStore) !== null && _b !== void 0 ? _b : null); | ||
this.requestPoller = (0, poller_1.default)(constants_1.POLL_INTERVAL_MS, configurationRequestor.fetchAndStoreConfigurations.bind(configurationRequestor), { | ||
@@ -103,2 +115,140 @@ maxStartRetries: numInitialRequestRetries, | ||
} | ||
getBanditAction(flagKey, subjectKey, subjectAttributes, actions, defaultValue) { | ||
var _a, _b, _c; | ||
const defaultResult = { variation: defaultValue, action: null }; | ||
let variation = defaultValue; | ||
let action = null; | ||
try { | ||
const banditVariations = (_a = this.banditVariationConfigurationStore) === null || _a === void 0 ? void 0 : _a.get(flagKey); | ||
if (banditVariations && !Object.keys(actions).length) { | ||
// No actions passed for a flag known to have an active bandit, so we just return the default values so that | ||
// we don't log a variation or bandit assignment | ||
return defaultResult; | ||
} | ||
// Get the assigned variation for the flag with a possible bandit | ||
// Note for getting assignments, we don't care about context | ||
const nonContextualSubjectAttributes = this.ensureNonContextualSubjectAttributes(subjectAttributes); | ||
variation = this.getStringAssignment(flagKey, subjectKey, nonContextualSubjectAttributes, defaultValue); | ||
// 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 banditKey = (_b = banditVariations === null || banditVariations === void 0 ? void 0 : banditVariations.find((banditVariation) => banditVariation.variationValue === variation)) === null || _b === void 0 ? void 0 : _b.key; | ||
if (banditKey) { | ||
// Retrieve the model parameters for the bandit | ||
const banditParameters = (_c = this.banditModelConfigurationStore) === null || _c === void 0 ? void 0 : _c.get(banditKey); | ||
if (!banditParameters) { | ||
throw new Error('No model parameters for bandit ' + banditKey); | ||
} | ||
const banditModelData = banditParameters.modelData; | ||
const contextualSubjectAttributes = this.ensureContextualSubjectAttributes(subjectAttributes); | ||
const actionsWithContextualAttributes = this.ensureActionsWithContextualAttributes(actions); | ||
const banditEvaluation = this.banditEvaluator.evaluateBandit(flagKey, subjectKey, contextualSubjectAttributes, actionsWithContextualAttributes, banditModelData); | ||
action = banditEvaluation.actionKey; | ||
const banditEvent = { | ||
timestamp: new Date().toISOString(), | ||
featureFlag: flagKey, | ||
bandit: banditKey, | ||
subject: subjectKey, | ||
action, | ||
actionProbability: banditEvaluation.actionWeight, | ||
optimalityGap: banditEvaluation.optimalityGap, | ||
modelVersion: banditParameters.modelVersion, | ||
subjectNumericAttributes: contextualSubjectAttributes.numericAttributes, | ||
subjectCategoricalAttributes: contextualSubjectAttributes.categoricalAttributes, | ||
actionNumericAttributes: actionsWithContextualAttributes[action].numericAttributes, | ||
actionCategoricalAttributes: actionsWithContextualAttributes[action].categoricalAttributes, | ||
metaData: this.buildLoggerMetadata(), | ||
}; | ||
this.logBanditAction(banditEvent); | ||
} | ||
} | ||
catch (err) { | ||
application_logger_1.logger.error('Error evaluating bandit action', err); | ||
if (!this.isGracefulFailureMode) { | ||
throw err; | ||
} | ||
return defaultResult; | ||
} | ||
return { variation, action }; | ||
} | ||
ensureNonContextualSubjectAttributes(subjectAttributes) { | ||
let result; | ||
if (this.isInstanceOfContextualAttributes(subjectAttributes)) { | ||
const contextualSubjectAttributes = subjectAttributes; | ||
result = Object.assign(Object.assign({}, contextualSubjectAttributes.numericAttributes), contextualSubjectAttributes.categoricalAttributes); | ||
} | ||
else { | ||
// Attributes are non-contextual | ||
result = subjectAttributes; | ||
} | ||
return result; | ||
} | ||
ensureContextualSubjectAttributes(subjectAttributes) { | ||
let result; | ||
if (this.isInstanceOfContextualAttributes(subjectAttributes)) { | ||
result = subjectAttributes; | ||
} | ||
else { | ||
result = this.deduceAttributeContext(subjectAttributes); | ||
} | ||
return result; | ||
} | ||
ensureActionsWithContextualAttributes(actions) { | ||
let result = {}; | ||
if (Array.isArray(actions)) { | ||
// no context | ||
actions.forEach((action) => { | ||
result[action] = { numericAttributes: {}, categoricalAttributes: {} }; | ||
}); | ||
} | ||
else if (!Object.values(actions).every(this.isInstanceOfContextualAttributes)) { | ||
// Actions have non-contextual attributes; bucket based on number or not | ||
Object.entries(actions).forEach(([action, attributes]) => { | ||
result[action] = this.deduceAttributeContext(attributes); | ||
}); | ||
} | ||
else { | ||
// Actions already have contextual attributes | ||
result = actions; | ||
} | ||
return result; | ||
} | ||
isInstanceOfContextualAttributes(attributes) { | ||
return Boolean(typeof attributes === 'object' && | ||
attributes && // exclude null | ||
'numericAttributes' in attributes && | ||
'categoricalAttributes' in attributes); | ||
} | ||
deduceAttributeContext(attributes) { | ||
const contextualAttributes = { | ||
numericAttributes: {}, | ||
categoricalAttributes: {}, | ||
}; | ||
Object.entries(attributes).forEach(([attribute, value]) => { | ||
const isNumeric = typeof value === 'number' && isFinite(value); | ||
if (isNumeric) { | ||
contextualAttributes.numericAttributes[attribute] = value; | ||
} | ||
else { | ||
contextualAttributes.categoricalAttributes[attribute] = value; | ||
} | ||
}); | ||
return contextualAttributes; | ||
} | ||
logBanditAction(banditEvent) { | ||
if (!this.banditLogger) { | ||
// No bandit logger set; enqueue the event in case a logger is later set | ||
if (this.queuedBanditEvents.length < constants_1.MAX_EVENT_QUEUE_SIZE) { | ||
this.queuedBanditEvents.push(banditEvent); | ||
} | ||
return; | ||
} | ||
// If here, we have a logger | ||
try { | ||
this.banditLogger.logBanditAction(banditEvent); | ||
} | ||
catch (err) { | ||
application_logger_1.logger.warn('Error encountered logging bandit action', err); | ||
} | ||
} | ||
getAssignmentVariation(flagKey, subjectKey, subjectAttributes, defaultValue, expectedVariationType) { | ||
@@ -174,6 +324,6 @@ try { | ||
} | ||
return this.configurationStore.get(flagKey); | ||
return this.flagConfigurationStore.get(flagKey); | ||
} | ||
getObfuscatedFlag(flagKey) { | ||
const flag = this.configurationStore.get((0, obfuscation_1.getMD5Hash)(flagKey)); | ||
const flag = this.flagConfigurationStore.get((0, obfuscation_1.getMD5Hash)(flagKey)); | ||
return flag ? (0, decoding_1.decodeFlag)(flag) : null; | ||
@@ -186,13 +336,28 @@ } | ||
* | ||
* Note that it is generally not a good idea to pre-load all flag configurations. | ||
* Note that it is generally not a good idea to preload all flag configurations. | ||
*/ | ||
return this.configurationStore.getKeys(); | ||
return this.flagConfigurationStore.getKeys(); | ||
} | ||
isInitialized() { | ||
return this.configurationStore.isInitialized(); | ||
return (this.flagConfigurationStore.isInitialized() && | ||
(!this.banditVariationConfigurationStore || | ||
this.banditVariationConfigurationStore.isInitialized()) && | ||
(!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized())); | ||
} | ||
/** @deprecated Renamed to setAssignmentLogger */ | ||
setLogger(logger) { | ||
this.setAssignmentLogger(logger); | ||
} | ||
setAssignmentLogger(logger) { | ||
var _a; | ||
this.assignmentLogger = logger; | ||
this.flushQueuedEvents(); // log any events that may have been queued while initializing | ||
// log any assignment events that may have been queued while initializing | ||
this.flushQueuedEvents(this.queuedAssignmentEvents, (_a = this.assignmentLogger) === null || _a === void 0 ? void 0 : _a.logAssignment); | ||
} | ||
setBanditLogger(logger) { | ||
var _a; | ||
this.banditLogger = logger; | ||
// log any bandit events that may have been queued while initializing | ||
this.flushQueuedEvents(this.queuedBanditEvents, (_a = this.banditLogger) === null || _a === void 0 ? void 0 : _a.logBanditAction); | ||
} | ||
/** | ||
@@ -217,16 +382,18 @@ * Assignment cache methods. | ||
getFlagConfigurations() { | ||
return this.configurationStore.entries(); | ||
return this.flagConfigurationStore.entries(); | ||
} | ||
flushQueuedEvents() { | ||
var _a; | ||
const eventsToFlush = this.queuedEvents; | ||
this.queuedEvents = []; | ||
try { | ||
for (const event of eventsToFlush) { | ||
(_a = this.assignmentLogger) === null || _a === void 0 ? void 0 : _a.logAssignment(event); | ||
flushQueuedEvents(eventQueue, logFunction) { | ||
const eventsToFlush = [...eventQueue]; // defensive copy | ||
eventQueue.length = 0; // Truncate the array | ||
if (!logFunction) { | ||
return; | ||
} | ||
eventsToFlush.forEach((event) => { | ||
try { | ||
logFunction(event); | ||
} | ||
} | ||
catch (error) { | ||
application_logger_1.logger.error(`[Eppo SDK] Error flushing assignment events: ${error.message}`); | ||
} | ||
catch (error) { | ||
application_logger_1.logger.error(`[Eppo SDK] Error flushing event to logger: ${error.message}`); | ||
} | ||
}); | ||
} | ||
@@ -236,7 +403,3 @@ logAssignment(result) { | ||
const { flagKey, subjectKey, allocationKey, subjectAttributes, variation } = result; | ||
const event = Object.assign(Object.assign({}, ((_a = result.extraLogging) !== null && _a !== void 0 ? _a : {})), { allocation: allocationKey !== null && allocationKey !== void 0 ? allocationKey : null, experiment: allocationKey ? `${flagKey}-${allocationKey}` : null, featureFlag: flagKey, variation: (_b = variation === null || variation === void 0 ? void 0 : variation.key) !== null && _b !== void 0 ? _b : null, subject: subjectKey, timestamp: new Date().toISOString(), subjectAttributes, metaData: { | ||
obfuscated: this.isObfuscated, | ||
sdkLanguage: 'javascript', | ||
sdkLibVersion: version_1.LIB_VERSION, | ||
} }); | ||
const event = Object.assign(Object.assign({}, ((_a = result.extraLogging) !== null && _a !== void 0 ? _a : {})), { allocation: allocationKey !== null && allocationKey !== void 0 ? allocationKey : null, experiment: allocationKey ? `${flagKey}-${allocationKey}` : null, featureFlag: flagKey, variation: (_b = variation === null || variation === void 0 ? void 0 : variation.key) !== null && _b !== void 0 ? _b : null, subject: subjectKey, timestamp: new Date().toISOString(), subjectAttributes, metaData: this.buildLoggerMetadata() }); | ||
if (variation && allocationKey) { | ||
@@ -255,3 +418,4 @@ const hasLoggedAssignment = (_c = this.assignmentCache) === null || _c === void 0 ? void 0 : _c.has({ | ||
if (!this.assignmentLogger) { | ||
this.queuedEvents.length < constants_1.MAX_EVENT_QUEUE_SIZE && this.queuedEvents.push(event); | ||
this.queuedAssignmentEvents.length < constants_1.MAX_EVENT_QUEUE_SIZE && | ||
this.queuedAssignmentEvents.push(event); | ||
return; | ||
@@ -272,2 +436,9 @@ } | ||
} | ||
buildLoggerMetadata() { | ||
return { | ||
obfuscated: this.isObfuscated, | ||
sdkLanguage: 'javascript', | ||
sdkLibVersion: version_1.LIB_VERSION, | ||
}; | ||
} | ||
} | ||
@@ -274,0 +445,0 @@ exports.default = EppoClient; |
@@ -9,5 +9,7 @@ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 5000; | ||
export declare const UFC_ENDPOINT = "/flag-config/v1/config"; | ||
export declare const BANDIT_ENDPOINT = "/flag-config/v1/bandits"; | ||
export declare const SESSION_ASSIGNMENT_CONFIG_LOADED = "eppo-session-assignment-config-loaded"; | ||
export declare const NULL_SENTINEL = "EPPO_NULL"; | ||
export declare const MAX_EVENT_QUEUE_SIZE = 100; | ||
export declare const BANDIT_ASSIGNMENT_SHARDS = 10000; | ||
//# sourceMappingURL=constants.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.MAX_EVENT_QUEUE_SIZE = exports.NULL_SENTINEL = exports.SESSION_ASSIGNMENT_CONFIG_LOADED = exports.UFC_ENDPOINT = exports.BASE_URL = exports.DEFAULT_POLL_CONFIG_REQUEST_RETRIES = exports.DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES = exports.POLL_JITTER_PCT = exports.POLL_INTERVAL_MS = exports.REQUEST_TIMEOUT_MILLIS = exports.DEFAULT_REQUEST_TIMEOUT_MS = void 0; | ||
exports.BANDIT_ASSIGNMENT_SHARDS = exports.MAX_EVENT_QUEUE_SIZE = exports.NULL_SENTINEL = exports.SESSION_ASSIGNMENT_CONFIG_LOADED = exports.BANDIT_ENDPOINT = exports.UFC_ENDPOINT = exports.BASE_URL = exports.DEFAULT_POLL_CONFIG_REQUEST_RETRIES = exports.DEFAULT_INITIAL_CONFIG_REQUEST_RETRIES = exports.POLL_JITTER_PCT = exports.POLL_INTERVAL_MS = exports.REQUEST_TIMEOUT_MILLIS = exports.DEFAULT_REQUEST_TIMEOUT_MS = void 0; | ||
exports.DEFAULT_REQUEST_TIMEOUT_MS = 5000; | ||
@@ -12,2 +12,3 @@ exports.REQUEST_TIMEOUT_MILLIS = exports.DEFAULT_REQUEST_TIMEOUT_MS; // for backwards compatibility | ||
exports.UFC_ENDPOINT = '/flag-config/v1/config'; | ||
exports.BANDIT_ENDPOINT = '/flag-config/v1/bandits'; | ||
exports.SESSION_ASSIGNMENT_CONFIG_LOADED = 'eppo-session-assignment-config-loaded'; | ||
@@ -17,2 +18,3 @@ exports.NULL_SENTINEL = 'EPPO_NULL'; | ||
exports.MAX_EVENT_QUEUE_SIZE = 100; | ||
exports.BANDIT_ASSIGNMENT_SHARDS = 10000; | ||
//# sourceMappingURL=constants.js.map |
import { Flag, Shard, Range, Variation } from './interfaces'; | ||
import { Rule } from './rules'; | ||
import { Sharder } from './sharders'; | ||
import { SubjectAttributes } from './types'; | ||
import { Attributes } from './types'; | ||
export interface FlagEvaluation { | ||
flagKey: string; | ||
subjectKey: string; | ||
subjectAttributes: SubjectAttributes; | ||
subjectAttributes: Attributes; | ||
allocationKey: string | null; | ||
@@ -17,3 +17,3 @@ variation: Variation | null; | ||
constructor(sharder?: Sharder); | ||
evaluateFlag(flag: Flag, subjectKey: string, subjectAttributes: SubjectAttributes, obfuscated: boolean): FlagEvaluation; | ||
evaluateFlag(flag: Flag, subjectKey: string, subjectAttributes: Attributes, obfuscated: boolean): FlagEvaluation; | ||
matchesShard(shard: Shard, subjectKey: string, totalShards: number): boolean; | ||
@@ -23,4 +23,4 @@ } | ||
export declare function hashKey(salt: string, subjectKey: string): string; | ||
export declare function noneResult(flagKey: string, subjectKey: string, subjectAttributes: SubjectAttributes): FlagEvaluation; | ||
export declare function matchesRules(rules: Rule[], subjectAttributes: SubjectAttributes, obfuscated: boolean): boolean; | ||
export declare function noneResult(flagKey: string, subjectKey: string, subjectAttributes: Attributes): FlagEvaluation; | ||
export declare function matchesRules(rules: Rule[], subjectAttributes: Attributes, obfuscated: boolean): boolean; | ||
//# sourceMappingURL=evaluator.d.ts.map |
import ApiEndpoints from './api-endpoints'; | ||
import { Flag } from './interfaces'; | ||
import { BanditVariation, BanditParameters, Flag } from './interfaces'; | ||
export interface IQueryParams { | ||
@@ -14,7 +14,12 @@ apiKey: string; | ||
} | ||
export interface IUniversalFlagConfig { | ||
export interface IUniversalFlagConfigResponse { | ||
flags: Record<string, Flag>; | ||
bandits: Record<string, BanditVariation[]>; | ||
} | ||
export interface IBanditParametersResponse { | ||
bandits: Record<string, BanditParameters>; | ||
} | ||
export interface IHttpClient { | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfig | undefined>; | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>; | ||
getBanditParameters(): Promise<IBanditParametersResponse | undefined>; | ||
rawGet<T>(url: URL): Promise<T | undefined>; | ||
@@ -26,5 +31,6 @@ } | ||
constructor(apiEndpoints: ApiEndpoints, timeout: number); | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfig | undefined>; | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>; | ||
getBanditParameters(): Promise<IBanditParametersResponse | undefined>; | ||
rawGet<T>(url: URL): Promise<T | undefined>; | ||
} | ||
//# sourceMappingURL=http-client.d.ts.map |
@@ -25,2 +25,6 @@ "use strict"; | ||
} | ||
async getBanditParameters() { | ||
const url = this.apiEndpoints.banditParametersEndpoint(); | ||
return await this.rawGet(url); | ||
} | ||
async rawGet(url) { | ||
@@ -36,4 +40,4 @@ try { | ||
clearTimeout(timeoutId); | ||
if (!response.ok) { | ||
throw new HttpRequestError('Failed to fetch data', response.status); | ||
if (!(response === null || response === void 0 ? void 0 : response.ok)) { | ||
throw new HttpRequestError('Failed to fetch data', response === null || response === void 0 ? void 0 : response.status); | ||
} | ||
@@ -40,0 +44,0 @@ return await response.json(); |
@@ -5,4 +5,6 @@ import ApiEndpoints from './api-endpoints'; | ||
import { IAssignmentLogger, IAssignmentEvent } from './assignment-logger'; | ||
import { IBanditLogger, IBanditEvent } from './bandit-logger'; | ||
import { AbstractAssignmentCache, AssignmentCache, NonExpiringInMemoryAssignmentCache, LRUInMemoryAssignmentCache, AsyncMap, AssignmentCacheKey, AssignmentCacheValue, AssignmentCacheEntry, assignmentCacheKeyToString, assignmentCacheValueToString } from './cache/abstract-assignment-cache'; | ||
import EppoClient, { FlagConfigurationRequestParameters, IEppoClient } from './client/eppo-client'; | ||
import FlagConfigRequestor from './configuration-requestor'; | ||
import { IConfigurationStore, IAsyncStore, ISyncStore } from './configuration-store/configuration-store'; | ||
@@ -12,8 +14,7 @@ import { HybridConfigurationStore } from './configuration-store/hybrid.store'; | ||
import * as constants from './constants'; | ||
import FlagConfigRequestor from './flag-configuration-requestor'; | ||
import HttpClient from './http-client'; | ||
import { Flag, ObfuscatedFlag, VariationType } from './interfaces'; | ||
import { AttributeType, SubjectAttributes } from './types'; | ||
import { AttributeType, Attributes } from './types'; | ||
import * as validation from './validation'; | ||
export { logger as applicationLogger, AbstractAssignmentCache, IAssignmentHooks, IAssignmentLogger, IAssignmentEvent, EppoClient, IEppoClient, constants, ApiEndpoints, FlagConfigRequestor, HttpClient, validation, IConfigurationStore, IAsyncStore, ISyncStore, MemoryStore, HybridConfigurationStore, MemoryOnlyConfigurationStore, AssignmentCacheKey, AssignmentCacheValue, AssignmentCacheEntry, AssignmentCache, AsyncMap, NonExpiringInMemoryAssignmentCache, LRUInMemoryAssignmentCache, assignmentCacheKeyToString, assignmentCacheValueToString, FlagConfigurationRequestParameters, Flag, ObfuscatedFlag, VariationType, AttributeType, SubjectAttributes, }; | ||
export { logger as applicationLogger, AbstractAssignmentCache, IAssignmentHooks, IAssignmentLogger, IAssignmentEvent, IBanditLogger, IBanditEvent, EppoClient, IEppoClient, constants, ApiEndpoints, FlagConfigRequestor, HttpClient, validation, IConfigurationStore, IAsyncStore, ISyncStore, MemoryStore, HybridConfigurationStore, MemoryOnlyConfigurationStore, AssignmentCacheKey, AssignmentCacheValue, AssignmentCacheEntry, AssignmentCache, AsyncMap, NonExpiringInMemoryAssignmentCache, LRUInMemoryAssignmentCache, assignmentCacheKeyToString, assignmentCacheValueToString, FlagConfigurationRequestParameters, Flag, ObfuscatedFlag, VariationType, AttributeType, Attributes, }; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -16,2 +16,4 @@ "use strict"; | ||
exports.EppoClient = eppo_client_1.default; | ||
const configuration_requestor_1 = require("./configuration-requestor"); | ||
exports.FlagConfigRequestor = configuration_requestor_1.default; | ||
const hybrid_store_1 = require("./configuration-store/hybrid.store"); | ||
@@ -24,4 +26,2 @@ Object.defineProperty(exports, "HybridConfigurationStore", { enumerable: true, get: function () { return hybrid_store_1.HybridConfigurationStore; } }); | ||
exports.constants = constants; | ||
const flag_configuration_requestor_1 = require("./flag-configuration-requestor"); | ||
exports.FlagConfigRequestor = flag_configuration_requestor_1.default; | ||
const http_client_1 = require("./http-client"); | ||
@@ -28,0 +28,0 @@ exports.HttpClient = http_client_1.default; |
@@ -71,2 +71,38 @@ import { Rule } from './rules'; | ||
} | ||
export interface BanditVariation { | ||
key: string; | ||
flagKey: string; | ||
variationKey: string; | ||
variationValue: string; | ||
} | ||
export interface BanditParameters { | ||
banditKey: string; | ||
modelName: string; | ||
modelVersion: string; | ||
modelData: BanditModelData; | ||
} | ||
export interface BanditModelData { | ||
gamma: number; | ||
defaultActionScore: number; | ||
actionProbabilityFloor: number; | ||
coefficients: Record<string, BanditCoefficients>; | ||
} | ||
export interface BanditCoefficients { | ||
actionKey: string; | ||
intercept: number; | ||
subjectNumericCoefficients: BanditNumericAttributeCoefficients[]; | ||
subjectCategoricalCoefficients: BanditCategoricalAttributeCoefficients[]; | ||
actionNumericCoefficients: BanditNumericAttributeCoefficients[]; | ||
actionCategoricalCoefficients: BanditCategoricalAttributeCoefficients[]; | ||
} | ||
export interface BanditNumericAttributeCoefficients { | ||
attributeKey: string; | ||
coefficient: number; | ||
missingValueCoefficient: number; | ||
} | ||
export interface BanditCategoricalAttributeCoefficients { | ||
attributeKey: string; | ||
valueCoefficients: Record<string, number>; | ||
missingValueCoefficient: number; | ||
} | ||
//# sourceMappingURL=interfaces.d.ts.map |
export declare type ValueType = string | number | boolean | JSON; | ||
export declare type AttributeType = string | number | boolean; | ||
export declare type ConditionValueType = AttributeType | AttributeType[]; | ||
export declare type SubjectAttributes = { | ||
export declare type Attributes = { | ||
[key: string]: AttributeType; | ||
}; | ||
export declare type ContextAttributes = { | ||
numericAttributes: Attributes; | ||
categoricalAttributes: Attributes; | ||
}; | ||
export declare type BanditSubjectAttributes = Attributes | ContextAttributes; | ||
export declare type BanditActions = string[] | Record<string, Attributes> | Record<string, ContextAttributes>; | ||
//# sourceMappingURL=types.d.ts.map |
{ | ||
"name": "@eppo/js-client-sdk-common", | ||
"version": "3.4.0", | ||
"version": "3.5.0", | ||
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)", | ||
@@ -76,2 +76,2 @@ "main": "dist/index.js", | ||
} | ||
} | ||
} |
@@ -1,2 +0,2 @@ | ||
import { BASE_URL as DEFAULT_BASE_URL, UFC_ENDPOINT } from './constants'; | ||
import { BASE_URL as DEFAULT_BASE_URL, UFC_ENDPOINT, BANDIT_ENDPOINT } from './constants'; | ||
import { IQueryParams } from './http-client'; | ||
@@ -26,2 +26,6 @@ | ||
} | ||
banditParametersEndpoint(): URL { | ||
return this.endpoint(BANDIT_ENDPOINT); | ||
} | ||
} |
import ApiEndpoints from '../api-endpoints'; | ||
import { logger } from '../application-logger'; | ||
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger'; | ||
import { BanditEvaluator } from '../bandit-evaluator'; | ||
import { IBanditEvent, IBanditLogger } from '../bandit-logger'; | ||
import { | ||
@@ -9,2 +11,3 @@ AssignmentCache, | ||
} from '../cache/abstract-assignment-cache'; | ||
import ConfigurationRequestor from '../configuration-requestor'; | ||
import { IConfigurationStore } from '../configuration-store/configuration-store'; | ||
@@ -21,8 +24,20 @@ import { | ||
import { Evaluator, FlagEvaluation, noneResult } from '../evaluator'; | ||
import FlagConfigurationRequestor from '../flag-configuration-requestor'; | ||
import FetchHttpClient from '../http-client'; | ||
import { Flag, ObfuscatedFlag, VariationType } from '../interfaces'; | ||
import { | ||
BanditVariation, | ||
BanditParameters, | ||
Flag, | ||
ObfuscatedFlag, | ||
VariationType, | ||
} from '../interfaces'; | ||
import { getMD5Hash } from '../obfuscation'; | ||
import initPoller, { IPoller } from '../poller'; | ||
import { AttributeType, ValueType } from '../types'; | ||
import { | ||
Attributes, | ||
AttributeType, | ||
BanditActions, | ||
BanditSubjectAttributes, | ||
ContextAttributes, | ||
ValueType, | ||
} from '../types'; | ||
import { validateNotBlank } from '../validation'; | ||
@@ -128,4 +143,28 @@ import { LIB_VERSION } from '../version'; | ||
/** | ||
* Maps a subject to a string assignment for a given experiment. | ||
* This variation may be a bandit-selected action. | ||
* | ||
* @param flagKey feature flag identifier | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional (can be empty) attributes associated with the subject, for example name and email. | ||
* @param actions possible attributes and their optional (can be empty) attributes to be evaluated by a contextual, | ||
* multi-armed bandit--if one is assigned to the subject. | ||
* @param defaultValue default value to return if the subject is not part of the experiment sample, | ||
* there are no bandit actions, or an error is countered evaluating the feature flag or bandit action */ | ||
getBanditAction( | ||
flagKey: string, | ||
subjectKey: string, | ||
subjectAttributes: BanditSubjectAttributes, | ||
actions: BanditActions, | ||
defaultValue: string, | ||
): { variation: string; action: string | null }; | ||
/** @Deprecated Renamed to setAssignmentLogger for clarity */ | ||
setLogger(logger: IAssignmentLogger): void; | ||
setAssignmentLogger(assignmentLogger: IAssignmentLogger): void; | ||
setBanditLogger(banditLogger: IBanditLogger): void; | ||
useLRUInMemoryAssignmentCache(maxSize: number): void; | ||
@@ -139,4 +178,14 @@ | ||
setConfigurationStore(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>): void; | ||
setFlagConfigurationStore(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>): void; | ||
setBanditVariationConfigurationStore( | ||
banditVariationConfigurationStore: IConfigurationStore<BanditVariation[]>, | ||
): void; | ||
setBanditModelConfigurationStore( | ||
banditModelConfigurationStore: IConfigurationStore<BanditParameters>, | ||
): void; | ||
setIsObfuscated(isObfuscated: boolean): void; | ||
fetchFlagConfigurations(): void; | ||
@@ -170,11 +219,16 @@ | ||
export default class EppoClient implements IEppoClient { | ||
private queuedEvents: IAssignmentEvent[] = []; | ||
private readonly queuedAssignmentEvents: IAssignmentEvent[] = []; | ||
private assignmentLogger?: IAssignmentLogger; | ||
private readonly queuedBanditEvents: IBanditEvent[] = []; | ||
private banditLogger?: IBanditLogger; | ||
private isGracefulFailureMode = true; | ||
private assignmentCache?: AssignmentCache; | ||
private requestPoller?: IPoller; | ||
private evaluator = new Evaluator(); | ||
private readonly evaluator = new Evaluator(); | ||
private readonly banditEvaluator = new BanditEvaluator(); | ||
constructor( | ||
private configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>, | ||
private flagConfigurationStore: IConfigurationStore<Flag | ObfuscatedFlag>, | ||
private banditVariationConfigurationStore?: IConfigurationStore<BanditVariation[]>, | ||
private banditModelConfigurationStore?: IConfigurationStore<BanditParameters>, | ||
private configurationRequestParameters?: FlagConfigurationRequestParameters, | ||
@@ -190,6 +244,20 @@ private isObfuscated = false, | ||
public setConfigurationStore(configurationStore: IConfigurationStore<Flag | ObfuscatedFlag>) { | ||
this.configurationStore = configurationStore; | ||
public setFlagConfigurationStore( | ||
flagConfigurationStore: IConfigurationStore<Flag | ObfuscatedFlag>, | ||
) { | ||
this.flagConfigurationStore = flagConfigurationStore; | ||
} | ||
public setBanditVariationConfigurationStore( | ||
banditVariationConfigurationStore: IConfigurationStore<BanditVariation[]>, | ||
) { | ||
this.banditVariationConfigurationStore = banditVariationConfigurationStore; | ||
} | ||
public setBanditModelConfigurationStore( | ||
banditModelConfigurationStore: IConfigurationStore<BanditParameters>, | ||
) { | ||
this.banditModelConfigurationStore = banditModelConfigurationStore; | ||
} | ||
public setIsObfuscated(isObfuscated: boolean) { | ||
@@ -211,3 +279,3 @@ this.isObfuscated = isObfuscated; | ||
const isExpired = await this.configurationStore.isExpired(); | ||
const isExpired = await this.flagConfigurationStore.isExpired(); | ||
if (!isExpired) { | ||
@@ -238,5 +306,7 @@ logger.info( | ||
const httpClient = new FetchHttpClient(apiEndpoints, requestTimeoutMs); | ||
const configurationRequestor = new FlagConfigurationRequestor( | ||
this.configurationStore, | ||
const configurationRequestor = new ConfigurationRequestor( | ||
httpClient, | ||
this.flagConfigurationStore, | ||
this.banditVariationConfigurationStore ?? null, | ||
this.banditModelConfigurationStore ?? null, | ||
); | ||
@@ -360,2 +430,179 @@ | ||
public getBanditAction( | ||
flagKey: string, | ||
subjectKey: string, | ||
subjectAttributes: BanditSubjectAttributes, | ||
actions: BanditActions, | ||
defaultValue: string, | ||
): { variation: string; action: string | null } { | ||
const defaultResult = { variation: defaultValue, action: null }; | ||
let variation = defaultValue; | ||
let action: string | null = null; | ||
try { | ||
const banditVariations = this.banditVariationConfigurationStore?.get(flagKey); | ||
if (banditVariations && !Object.keys(actions).length) { | ||
// No actions passed for a flag known to have an active bandit, so we just return the default values so that | ||
// we don't log a variation or bandit assignment | ||
return defaultResult; | ||
} | ||
// Get the assigned variation for the flag with a possible bandit | ||
// Note for getting assignments, we don't care about context | ||
const nonContextualSubjectAttributes = | ||
this.ensureNonContextualSubjectAttributes(subjectAttributes); | ||
variation = this.getStringAssignment( | ||
flagKey, | ||
subjectKey, | ||
nonContextualSubjectAttributes, | ||
defaultValue, | ||
); | ||
// 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 banditKey = banditVariations?.find( | ||
(banditVariation) => banditVariation.variationValue === variation, | ||
)?.key; | ||
if (banditKey) { | ||
// Retrieve the model parameters for the bandit | ||
const banditParameters = this.banditModelConfigurationStore?.get(banditKey); | ||
if (!banditParameters) { | ||
throw new Error('No model parameters for bandit ' + banditKey); | ||
} | ||
const banditModelData = banditParameters.modelData; | ||
const contextualSubjectAttributes = | ||
this.ensureContextualSubjectAttributes(subjectAttributes); | ||
const actionsWithContextualAttributes = this.ensureActionsWithContextualAttributes(actions); | ||
const banditEvaluation = this.banditEvaluator.evaluateBandit( | ||
flagKey, | ||
subjectKey, | ||
contextualSubjectAttributes, | ||
actionsWithContextualAttributes, | ||
banditModelData, | ||
); | ||
action = banditEvaluation.actionKey; | ||
const banditEvent: IBanditEvent = { | ||
timestamp: new Date().toISOString(), | ||
featureFlag: flagKey, | ||
bandit: banditKey, | ||
subject: subjectKey, | ||
action, | ||
actionProbability: banditEvaluation.actionWeight, | ||
optimalityGap: banditEvaluation.optimalityGap, | ||
modelVersion: banditParameters.modelVersion, | ||
subjectNumericAttributes: contextualSubjectAttributes.numericAttributes, | ||
subjectCategoricalAttributes: contextualSubjectAttributes.categoricalAttributes, | ||
actionNumericAttributes: actionsWithContextualAttributes[action].numericAttributes, | ||
actionCategoricalAttributes: | ||
actionsWithContextualAttributes[action].categoricalAttributes, | ||
metaData: this.buildLoggerMetadata(), | ||
}; | ||
this.logBanditAction(banditEvent); | ||
} | ||
} catch (err) { | ||
logger.error('Error evaluating bandit action', err); | ||
if (!this.isGracefulFailureMode) { | ||
throw err; | ||
} | ||
return defaultResult; | ||
} | ||
return { variation, action }; | ||
} | ||
private ensureNonContextualSubjectAttributes( | ||
subjectAttributes: BanditSubjectAttributes, | ||
): Attributes { | ||
let result: Attributes; | ||
if (this.isInstanceOfContextualAttributes(subjectAttributes)) { | ||
const contextualSubjectAttributes = subjectAttributes as ContextAttributes; | ||
result = { | ||
...contextualSubjectAttributes.numericAttributes, | ||
...contextualSubjectAttributes.categoricalAttributes, | ||
}; | ||
} else { | ||
// Attributes are non-contextual | ||
result = subjectAttributes as Attributes; | ||
} | ||
return result; | ||
} | ||
private ensureContextualSubjectAttributes( | ||
subjectAttributes: BanditSubjectAttributes, | ||
): ContextAttributes { | ||
let result: ContextAttributes; | ||
if (this.isInstanceOfContextualAttributes(subjectAttributes)) { | ||
result = subjectAttributes as ContextAttributes; | ||
} else { | ||
result = this.deduceAttributeContext(subjectAttributes as Attributes); | ||
} | ||
return result; | ||
} | ||
private ensureActionsWithContextualAttributes( | ||
actions: BanditActions, | ||
): Record<string, ContextAttributes> { | ||
let result: Record<string, ContextAttributes> = {}; | ||
if (Array.isArray(actions)) { | ||
// no context | ||
actions.forEach((action) => { | ||
result[action] = { numericAttributes: {}, categoricalAttributes: {} }; | ||
}); | ||
} else if (!Object.values(actions).every(this.isInstanceOfContextualAttributes)) { | ||
// Actions have non-contextual attributes; bucket based on number or not | ||
Object.entries(actions).forEach(([action, attributes]) => { | ||
result[action] = this.deduceAttributeContext(attributes); | ||
}); | ||
} else { | ||
// Actions already have contextual attributes | ||
result = actions as Record<string, ContextAttributes>; | ||
} | ||
return result; | ||
} | ||
private isInstanceOfContextualAttributes(attributes: unknown): boolean { | ||
return Boolean( | ||
typeof attributes === 'object' && | ||
attributes && // exclude null | ||
'numericAttributes' in attributes && | ||
'categoricalAttributes' in attributes, | ||
); | ||
} | ||
private deduceAttributeContext(attributes: Attributes): ContextAttributes { | ||
const contextualAttributes: ContextAttributes = { | ||
numericAttributes: {}, | ||
categoricalAttributes: {}, | ||
}; | ||
Object.entries(attributes).forEach(([attribute, value]) => { | ||
const isNumeric = typeof value === 'number' && isFinite(value); | ||
if (isNumeric) { | ||
contextualAttributes.numericAttributes[attribute] = value; | ||
} else { | ||
contextualAttributes.categoricalAttributes[attribute] = value as AttributeType; | ||
} | ||
}); | ||
return contextualAttributes; | ||
} | ||
private logBanditAction(banditEvent: IBanditEvent): void { | ||
if (!this.banditLogger) { | ||
// No bandit logger set; enqueue the event in case a logger is later set | ||
if (this.queuedBanditEvents.length < MAX_EVENT_QUEUE_SIZE) { | ||
this.queuedBanditEvents.push(banditEvent); | ||
} | ||
return; | ||
} | ||
// If here, we have a logger | ||
try { | ||
this.banditLogger.logBanditAction(banditEvent); | ||
} catch (err) { | ||
logger.warn('Error encountered logging bandit action', err); | ||
} | ||
} | ||
private getAssignmentVariation( | ||
@@ -465,7 +712,7 @@ flagKey: string, | ||
} | ||
return this.configurationStore.get(flagKey); | ||
return this.flagConfigurationStore.get(flagKey); | ||
} | ||
private getObfuscatedFlag(flagKey: string): Flag | null { | ||
const flag: ObfuscatedFlag | null = this.configurationStore.get( | ||
const flag: ObfuscatedFlag | null = this.flagConfigurationStore.get( | ||
getMD5Hash(flagKey), | ||
@@ -481,16 +728,33 @@ ) as ObfuscatedFlag; | ||
* | ||
* Note that it is generally not a good idea to pre-load all flag configurations. | ||
* Note that it is generally not a good idea to preload all flag configurations. | ||
*/ | ||
return this.configurationStore.getKeys(); | ||
return this.flagConfigurationStore.getKeys(); | ||
} | ||
public isInitialized() { | ||
return this.configurationStore.isInitialized(); | ||
return ( | ||
this.flagConfigurationStore.isInitialized() && | ||
(!this.banditVariationConfigurationStore || | ||
this.banditVariationConfigurationStore.isInitialized()) && | ||
(!this.banditModelConfigurationStore || this.banditModelConfigurationStore.isInitialized()) | ||
); | ||
} | ||
/** @deprecated Renamed to setAssignmentLogger */ | ||
public setLogger(logger: IAssignmentLogger) { | ||
this.setAssignmentLogger(logger); | ||
} | ||
public setAssignmentLogger(logger: IAssignmentLogger) { | ||
this.assignmentLogger = logger; | ||
this.flushQueuedEvents(); // log any events that may have been queued while initializing | ||
// log any assignment events that may have been queued while initializing | ||
this.flushQueuedEvents(this.queuedAssignmentEvents, this.assignmentLogger?.logAssignment); | ||
} | ||
public setBanditLogger(logger: IBanditLogger) { | ||
this.banditLogger = logger; | ||
// log any bandit events that may have been queued while initializing | ||
this.flushQueuedEvents(this.queuedBanditEvents, this.banditLogger?.logBanditAction); | ||
} | ||
/** | ||
@@ -520,15 +784,20 @@ * Assignment cache methods. | ||
public getFlagConfigurations(): Record<string, Flag> { | ||
return this.configurationStore.entries(); | ||
return this.flagConfigurationStore.entries(); | ||
} | ||
private flushQueuedEvents() { | ||
const eventsToFlush = this.queuedEvents; | ||
this.queuedEvents = []; | ||
try { | ||
for (const event of eventsToFlush) { | ||
this.assignmentLogger?.logAssignment(event); | ||
private flushQueuedEvents<T>(eventQueue: T[], logFunction?: (event: T) => void) { | ||
const eventsToFlush = [...eventQueue]; // defensive copy | ||
eventQueue.length = 0; // Truncate the array | ||
if (!logFunction) { | ||
return; | ||
} | ||
eventsToFlush.forEach((event) => { | ||
try { | ||
logFunction(event); | ||
} catch (error) { | ||
logger.error(`[Eppo SDK] Error flushing event to logger: ${error.message}`); | ||
} | ||
} catch (error) { | ||
logger.error(`[Eppo SDK] Error flushing assignment events: ${error.message}`); | ||
} | ||
}); | ||
} | ||
@@ -547,7 +816,3 @@ | ||
subjectAttributes, | ||
metaData: { | ||
obfuscated: this.isObfuscated, | ||
sdkLanguage: 'javascript', | ||
sdkLibVersion: LIB_VERSION, | ||
}, | ||
metaData: this.buildLoggerMetadata(), | ||
}; | ||
@@ -569,3 +834,4 @@ | ||
if (!this.assignmentLogger) { | ||
this.queuedEvents.length < MAX_EVENT_QUEUE_SIZE && this.queuedEvents.push(event); | ||
this.queuedAssignmentEvents.length < MAX_EVENT_QUEUE_SIZE && | ||
this.queuedAssignmentEvents.push(event); | ||
return; | ||
@@ -585,2 +851,10 @@ } | ||
} | ||
private buildLoggerMetadata(): Record<string, unknown> { | ||
return { | ||
obfuscated: this.isObfuscated, | ||
sdkLanguage: 'javascript', | ||
sdkLibVersion: LIB_VERSION, | ||
}; | ||
} | ||
} | ||
@@ -587,0 +861,0 @@ |
@@ -9,2 +9,3 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 5000; | ||
export const UFC_ENDPOINT = '/flag-config/v1/config'; | ||
export const BANDIT_ENDPOINT = '/flag-config/v1/bandits'; | ||
export const SESSION_ASSIGNMENT_CONFIG_LOADED = 'eppo-session-assignment-config-loaded'; | ||
@@ -14,1 +15,2 @@ export const NULL_SENTINEL = 'EPPO_NULL'; | ||
export const MAX_EVENT_QUEUE_SIZE = 100; | ||
export const BANDIT_ASSIGNMENT_SHARDS = 10000; |
import { Flag, Shard, Range, Variation } from './interfaces'; | ||
import { Rule, matchesRule } from './rules'; | ||
import { MD5Sharder, Sharder } from './sharders'; | ||
import { SubjectAttributes } from './types'; | ||
import { Attributes } from './types'; | ||
@@ -9,3 +9,3 @@ export interface FlagEvaluation { | ||
subjectKey: string; | ||
subjectAttributes: SubjectAttributes; | ||
subjectAttributes: Attributes; | ||
allocationKey: string | null; | ||
@@ -27,3 +27,3 @@ variation: Variation | null; | ||
subjectKey: string, | ||
subjectAttributes: SubjectAttributes, | ||
subjectAttributes: Attributes, | ||
obfuscated: boolean, | ||
@@ -81,3 +81,3 @@ ): FlagEvaluation { | ||
subjectKey: string, | ||
subjectAttributes: SubjectAttributes, | ||
subjectAttributes: Attributes, | ||
): FlagEvaluation { | ||
@@ -97,3 +97,3 @@ return { | ||
rules: Rule[], | ||
subjectAttributes: SubjectAttributes, | ||
subjectAttributes: Attributes, | ||
obfuscated: boolean, | ||
@@ -100,0 +100,0 @@ ): boolean { |
import ApiEndpoints from './api-endpoints'; | ||
import { Flag } from './interfaces'; | ||
import { BanditVariation, BanditParameters, Flag } from './interfaces'; | ||
@@ -19,8 +19,14 @@ export interface IQueryParams { | ||
export interface IUniversalFlagConfig { | ||
export interface IUniversalFlagConfigResponse { | ||
flags: Record<string, Flag>; | ||
bandits: Record<string, BanditVariation[]>; | ||
} | ||
export interface IBanditParametersResponse { | ||
bandits: Record<string, BanditParameters>; | ||
} | ||
export interface IHttpClient { | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfig | undefined>; | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>; | ||
getBanditParameters(): Promise<IBanditParametersResponse | undefined>; | ||
rawGet<T>(url: URL): Promise<T | undefined>; | ||
@@ -32,7 +38,12 @@ } | ||
async getUniversalFlagConfiguration(): Promise<IUniversalFlagConfig | undefined> { | ||
async getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined> { | ||
const url = this.apiEndpoints.ufcEndpoint(); | ||
return await this.rawGet<IUniversalFlagConfig>(url); | ||
return await this.rawGet<IUniversalFlagConfigResponse>(url); | ||
} | ||
async getBanditParameters(): Promise<IBanditParametersResponse | undefined> { | ||
const url = this.apiEndpoints.banditParametersEndpoint(); | ||
return await this.rawGet<IBanditParametersResponse>(url); | ||
} | ||
async rawGet<T>(url: URL): Promise<T | undefined> { | ||
@@ -49,4 +60,4 @@ try { | ||
if (!response.ok) { | ||
throw new HttpRequestError('Failed to fetch data', response.status); | ||
if (!response?.ok) { | ||
throw new HttpRequestError('Failed to fetch data', response?.status); | ||
} | ||
@@ -53,0 +64,0 @@ return await response.json(); |
@@ -5,2 +5,3 @@ import ApiEndpoints from './api-endpoints'; | ||
import { IAssignmentLogger, IAssignmentEvent } from './assignment-logger'; | ||
import { IBanditLogger, IBanditEvent } from './bandit-logger'; | ||
import { | ||
@@ -19,2 +20,3 @@ AbstractAssignmentCache, | ||
import EppoClient, { FlagConfigurationRequestParameters, IEppoClient } from './client/eppo-client'; | ||
import FlagConfigRequestor from './configuration-requestor'; | ||
import { | ||
@@ -28,6 +30,5 @@ IConfigurationStore, | ||
import * as constants from './constants'; | ||
import FlagConfigRequestor from './flag-configuration-requestor'; | ||
import HttpClient from './http-client'; | ||
import { Flag, ObfuscatedFlag, VariationType } from './interfaces'; | ||
import { AttributeType, SubjectAttributes } from './types'; | ||
import { AttributeType, Attributes } from './types'; | ||
import * as validation from './validation'; | ||
@@ -41,2 +42,4 @@ | ||
IAssignmentEvent, | ||
IBanditLogger, | ||
IBanditEvent, | ||
EppoClient, | ||
@@ -75,3 +78,3 @@ IEppoClient, | ||
AttributeType, | ||
SubjectAttributes, | ||
Attributes, | ||
}; |
@@ -83,1 +83,43 @@ import { Rule } from './rules'; | ||
} | ||
export interface BanditVariation { | ||
key: string; | ||
flagKey: string; | ||
variationKey: string; | ||
variationValue: string; | ||
} | ||
export interface BanditParameters { | ||
banditKey: string; | ||
modelName: string; | ||
modelVersion: string; | ||
modelData: BanditModelData; | ||
} | ||
export interface BanditModelData { | ||
gamma: number; | ||
defaultActionScore: number; | ||
actionProbabilityFloor: number; | ||
coefficients: Record<string, BanditCoefficients>; | ||
} | ||
export interface BanditCoefficients { | ||
actionKey: string; | ||
intercept: number; | ||
subjectNumericCoefficients: BanditNumericAttributeCoefficients[]; | ||
subjectCategoricalCoefficients: BanditCategoricalAttributeCoefficients[]; | ||
actionNumericCoefficients: BanditNumericAttributeCoefficients[]; | ||
actionCategoricalCoefficients: BanditCategoricalAttributeCoefficients[]; | ||
} | ||
export interface BanditNumericAttributeCoefficients { | ||
attributeKey: string; | ||
coefficient: number; | ||
missingValueCoefficient: number; | ||
} | ||
export interface BanditCategoricalAttributeCoefficients { | ||
attributeKey: string; | ||
valueCoefficients: Record<string, number>; | ||
missingValueCoefficient: number; | ||
} |
export type ValueType = string | number | boolean | JSON; | ||
export type AttributeType = string | number | boolean; | ||
export type ConditionValueType = AttributeType | AttributeType[]; | ||
export type SubjectAttributes = { [key: string]: AttributeType }; | ||
export type Attributes = { [key: string]: AttributeType }; | ||
export type ContextAttributes = { | ||
numericAttributes: Attributes; | ||
categoricalAttributes: Attributes; | ||
}; | ||
export type BanditSubjectAttributes = Attributes | ContextAttributes; | ||
export type BanditActions = | ||
| string[] | ||
| Record<string, Attributes> | ||
| Record<string, ContextAttributes>; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
695715
146
5328