@eppo/js-client-sdk-common
Advanced tools
Comparing version 4.6.3 to 4.7.0-alpha.0
@@ -9,3 +9,3 @@ import { IAssignmentLogger } from '../assignment-logger'; | ||
import { BanditParameters, BanditVariation, Flag, ObfuscatedFlag, Variation, VariationType } from '../interfaces'; | ||
import { Attributes, AttributeType, BanditActions, BanditSubjectAttributes, ValueType } from '../types'; | ||
import { Attributes, AttributeType, BanditActions, BanditSubjectAttributes, ContextAttributes, ValueType } from '../types'; | ||
export interface IAssignmentDetails<T extends Variation['value'] | object> { | ||
@@ -218,3 +218,12 @@ variation: T; | ||
private rethrowIfNotGraceful; | ||
private getAllAssignments; | ||
/** | ||
* Computes and returns assignments for a subject from all loaded flags. | ||
* | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional attributes associated with the subject, for example name and email. | ||
* @param obfuscated optional whether to obfuscate the results. | ||
*/ | ||
getPrecomputedAssignments(subjectKey: string, subjectAttributes?: Attributes | ContextAttributes, obfuscated?: boolean): string; | ||
/** | ||
* [Experimental] Get a detailed return of assignment for a particular subject and flag. | ||
@@ -221,0 +230,0 @@ * |
@@ -12,2 +12,3 @@ "use strict"; | ||
const tlru_in_memory_assignment_cache_1 = require("../cache/tlru-in-memory-assignment-cache"); | ||
const configuration_1 = require("../configuration"); | ||
const configuration_requestor_1 = require("../configuration-requestor"); | ||
@@ -544,3 +545,52 @@ const constants_1 = require("../constants"); | ||
} | ||
getAllAssignments(subjectKey, subjectAttributes = {}) { | ||
const configDetails = this.getConfigDetails(); | ||
const flagKeys = this.getFlagKeys(); | ||
const flags = {}; | ||
// Evaluate all the enabled flags for the user | ||
flagKeys.forEach((flagKey) => { | ||
const flag = this.getFlag(flagKey); | ||
if (!flag) { | ||
application_logger_1.logger.debug(`[Eppo SDK] No assigned variation. Flag does not exist.`); | ||
return; | ||
} | ||
// Evaluate the flag for this subject. | ||
const evaluation = this.evaluator.evaluateFlag(flag, configDetails, subjectKey, subjectAttributes, this.isObfuscated); | ||
// allocationKey is set along with variation when there is a result. this check appeases typescript below | ||
if (!evaluation.variation || !evaluation.allocationKey) { | ||
application_logger_1.logger.debug(`[Eppo SDK] No assigned variation: ${flagKey}`); | ||
return; | ||
} | ||
// Transform into a PrecomputedFlag | ||
flags[flagKey] = { | ||
flagKey, | ||
allocationKey: evaluation.allocationKey, | ||
doLog: evaluation.doLog, | ||
extraLogging: evaluation.extraLogging, | ||
variationKey: evaluation.variation.key, | ||
variationType: flag.variationType, | ||
variationValue: evaluation.variation.value.toString(), | ||
}; | ||
}); | ||
return flags; | ||
} | ||
/** | ||
* Computes and returns assignments for a subject from all loaded flags. | ||
* | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional attributes associated with the subject, for example name and email. | ||
* @param obfuscated optional whether to obfuscate the results. | ||
*/ | ||
getPrecomputedAssignments(subjectKey, subjectAttributes = {}, obfuscated = false) { | ||
const configDetails = this.getConfigDetails(); | ||
const subjectContextualAttributes = this.ensureContextualSubjectAttributes(subjectAttributes); | ||
const subjectFlatAttributes = this.ensureNonContextualSubjectAttributes(subjectAttributes); | ||
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes); | ||
const precomputedConfig = obfuscated | ||
? new configuration_1.ObfuscatedPrecomputedConfiguration(subjectKey, flags, subjectContextualAttributes, configDetails.configEnvironment) | ||
: new configuration_1.PrecomputedConfiguration(subjectKey, flags, subjectContextualAttributes, configDetails.configEnvironment); | ||
const configWire = new configuration_1.ConfigurationWireV1(precomputedConfig); | ||
return JSON.stringify(configWire); | ||
} | ||
/** | ||
* [Experimental] Get a detailed return of assignment for a particular subject and flag. | ||
@@ -547,0 +597,0 @@ * |
@@ -5,3 +5,3 @@ import { IAssignmentLogger } from '../assignment-logger'; | ||
import { PrecomputedFlag } from '../interfaces'; | ||
import { Attributes } from '../types'; | ||
import { Attributes, ContextAttributes } from '../types'; | ||
export type PrecomputedFlagsRequestParameters = { | ||
@@ -25,5 +25,8 @@ apiKey: string; | ||
}; | ||
export declare function convertContextAttributesToSubjectAttributes(contextAttributes: ContextAttributes): Attributes; | ||
interface EppoPrecomputedClientOptions { | ||
precomputedFlagStore: IConfigurationStore<PrecomputedFlag>; | ||
isObfuscated?: boolean; | ||
} | ||
export default class EppoPrecomputedClient { | ||
private precomputedFlagStore; | ||
private isObfuscated; | ||
private readonly queuedAssignmentEvents; | ||
@@ -36,10 +39,15 @@ private assignmentLogger?; | ||
private subjectAttributes?; | ||
constructor(precomputedFlagStore: IConfigurationStore<PrecomputedFlag>, isObfuscated?: boolean); | ||
setPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters: PrecomputedFlagsRequestParameters): void; | ||
setSubjectAndPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters: PrecomputedFlagsRequestParameters): void; | ||
setPrecomputedFlagStore(precomputedFlagStore: IConfigurationStore<PrecomputedFlag>): void; | ||
private decodedFlagKeySalt; | ||
private precomputedFlagStore; | ||
private isObfuscated; | ||
constructor(options: EppoPrecomputedClientOptions); | ||
setIsObfuscated(isObfuscated: boolean): void; | ||
private setDecodedFlagKeySalt; | ||
private setPrecomputedFlagsRequestParameters; | ||
private setSubjectData; | ||
setSubjectAndPrecomputedFlagsRequestParameters(parameters: PrecomputedFlagsRequestParameters): void; | ||
fetchPrecomputedFlags(): Promise<void>; | ||
stopPolling(): void; | ||
setSubjectAndPrecomputedFlagStore(subjectKey: string, subjectAttributes: Attributes, precomputedFlagStore: IConfigurationStore<PrecomputedFlag>): void; | ||
private setPrecomputedFlagStore; | ||
setSubjectSaltAndPrecomputedFlagStore(subjectKey: string, subjectAttributes: Attributes, salt: string, precomputedFlagStore: IConfigurationStore<PrecomputedFlag>): void; | ||
private getPrecomputedAssignment; | ||
@@ -106,2 +114,3 @@ /** | ||
} | ||
export {}; | ||
//# sourceMappingURL=eppo-precomputed-client.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.convertContextAttributesToSubjectAttributes = convertContextAttributesToSubjectAttributes; | ||
const api_endpoints_1 = require("../api-endpoints"); | ||
@@ -16,22 +17,34 @@ const application_logger_1 = require("../application-logger"); | ||
const version_1 = require("../version"); | ||
const eppo_client_1 = require("./eppo-client"); | ||
function convertContextAttributesToSubjectAttributes(contextAttributes) { | ||
return { | ||
...(contextAttributes.numericAttributes || {}), | ||
...(contextAttributes.categoricalAttributes || {}), | ||
}; | ||
} | ||
class EppoPrecomputedClient { | ||
constructor(precomputedFlagStore, isObfuscated = false) { | ||
this.precomputedFlagStore = precomputedFlagStore; | ||
this.isObfuscated = isObfuscated; | ||
constructor(options) { | ||
this.queuedAssignmentEvents = []; | ||
this.decodedFlagKeySalt = ''; | ||
this.precomputedFlagStore = options.precomputedFlagStore; | ||
this.isObfuscated = options.isObfuscated ?? true; // Default to true if not provided | ||
} | ||
setPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters) { | ||
this.precomputedFlagsRequestParameters = precomputedFlagsRequestParameters; | ||
setIsObfuscated(isObfuscated) { | ||
this.isObfuscated = isObfuscated; | ||
} | ||
setSubjectAndPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters) { | ||
this.setPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters); | ||
this.subjectKey = precomputedFlagsRequestParameters.precompute.subjectKey; | ||
this.subjectAttributes = precomputedFlagsRequestParameters.precompute.subjectAttributes; | ||
setDecodedFlagKeySalt(salt) { | ||
this.decodedFlagKeySalt = salt; | ||
} | ||
setPrecomputedFlagStore(precomputedFlagStore) { | ||
this.precomputedFlagStore = precomputedFlagStore; | ||
setPrecomputedFlagsRequestParameters(parameters) { | ||
this.precomputedFlagsRequestParameters = parameters; | ||
} | ||
setIsObfuscated(isObfuscated) { | ||
this.isObfuscated = isObfuscated; | ||
setSubjectData(subjectKey, subjectAttributes) { | ||
this.subjectKey = subjectKey; | ||
this.subjectAttributes = subjectAttributes; | ||
} | ||
// Convenience method that combines setters we need to make assignments work | ||
setSubjectAndPrecomputedFlagsRequestParameters(parameters) { | ||
this.setPrecomputedFlagsRequestParameters(parameters); | ||
this.setSubjectData(parameters.precompute.subjectKey, parameters.precompute.subjectAttributes); | ||
} | ||
async fetchPrecomputedFlags() { | ||
@@ -57,2 +70,7 @@ if (!this.precomputedFlagsRequestParameters) { | ||
const precomputedRequestor = new precomputed_requestor_1.default(httpClient, this.precomputedFlagStore, subjectKey, subjectAttributes); | ||
// A callback to capture the salt and subject information | ||
precomputedRequestor.onPrecomputedResponse = (responseData) => { | ||
this.setDecodedFlagKeySalt((0, obfuscation_1.decodeBase64)(responseData.salt)); | ||
this.setSubjectData(responseData.subjectKey, responseData.subjectAttributes); | ||
}; | ||
const pollingCallback = async () => { | ||
@@ -78,20 +96,23 @@ if (await this.precomputedFlagStore.isExpired()) { | ||
} | ||
setSubjectAndPrecomputedFlagStore(subjectKey, subjectAttributes, precomputedFlagStore) { | ||
// Save the new subject data and precomputed flag store together because they are related | ||
// Stop any polling process if it exists from previous subject data to protect consistency | ||
setPrecomputedFlagStore(store) { | ||
this.requestPoller?.stop(); | ||
this.precomputedFlagStore = store; | ||
} | ||
// Convenience method that combines setters we need to make assignments work | ||
setSubjectSaltAndPrecomputedFlagStore(subjectKey, subjectAttributes, salt, precomputedFlagStore) { | ||
this.setPrecomputedFlagStore(precomputedFlagStore); | ||
this.subjectKey = subjectKey; | ||
this.subjectAttributes = subjectAttributes; | ||
this.setSubjectData(subjectKey, subjectAttributes); | ||
this.setDecodedFlagKeySalt((0, obfuscation_1.decodeBase64)(salt)); | ||
} | ||
getPrecomputedAssignment(flagKey, defaultValue, expectedType, valueTransformer = (v) => v) { | ||
(0, validation_1.validateNotBlank)(flagKey, 'Invalid argument: flagKey cannot be blank'); | ||
const preComputedFlag = this.getPrecomputedFlag(flagKey); | ||
if (preComputedFlag == null) { | ||
const precomputedFlag = this.getPrecomputedFlag(flagKey); | ||
if (precomputedFlag == null) { | ||
application_logger_1.logger.warn(`[Eppo SDK] No assigned variation. Flag not found: ${flagKey}`); | ||
return defaultValue; | ||
} | ||
// Check variation type | ||
if (preComputedFlag.variationType !== expectedType) { | ||
application_logger_1.logger.error(`[Eppo SDK] Type mismatch: expected ${expectedType} but flag ${flagKey} has type ${preComputedFlag.variationType}`); | ||
// Add type checking before proceeding | ||
if (!(0, eppo_client_1.checkTypeMatch)(expectedType, precomputedFlag.variationType)) { | ||
const errorMessage = `[Eppo SDK] Type mismatch: expected ${expectedType} but flag ${flagKey} has type ${precomputedFlag.variationType}`; | ||
application_logger_1.logger.error(errorMessage); | ||
return defaultValue; | ||
@@ -105,8 +126,8 @@ } | ||
variation: { | ||
key: preComputedFlag.variationKey, | ||
value: preComputedFlag.variationValue, | ||
key: precomputedFlag.variationKey, | ||
value: precomputedFlag.variationValue, | ||
}, | ||
allocationKey: preComputedFlag.allocationKey, | ||
extraLogging: preComputedFlag.extraLogging, | ||
doLog: preComputedFlag.doLog, | ||
allocationKey: precomputedFlag.allocationKey, | ||
extraLogging: precomputedFlag.extraLogging, | ||
doLog: precomputedFlag.doLog, | ||
}; | ||
@@ -192,3 +213,4 @@ try { | ||
getObfuscatedFlag(flagKey) { | ||
const precomputedFlag = this.precomputedFlagStore.get((0, obfuscation_1.getMD5Hash)(flagKey)); | ||
const saltedAndHashedFlagKey = (0, obfuscation_1.getMD5Hash)(flagKey, this.decodedFlagKeySalt); | ||
const precomputedFlag = this.precomputedFlagStore.get(saltedAndHashedFlagKey); | ||
return precomputedFlag ? (0, decoding_1.decodePrecomputedFlag)(precomputedFlag) : null; | ||
@@ -195,0 +217,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { ObfuscatedFlag, Flag, ObfuscatedVariation, VariationType, Variation, ObfuscatedAllocation, Allocation, Split, Shard, ObfuscatedSplit, PrecomputedFlag } from './interfaces'; | ||
import { ObfuscatedFlag, Flag, ObfuscatedVariation, VariationType, Variation, ObfuscatedAllocation, Allocation, Split, Shard, ObfuscatedSplit, PrecomputedFlag, DecodedPrecomputedFlag } from './interfaces'; | ||
export declare function decodeFlag(flag: ObfuscatedFlag): Flag; | ||
@@ -9,3 +9,3 @@ export declare function decodeVariations(variations: Record<string, ObfuscatedVariation>, variationType: VariationType): Record<string, Variation>; | ||
export declare function decodeObject(obj: Record<string, string>): Record<string, string>; | ||
export declare function decodePrecomputedFlag(precomputedFlag: PrecomputedFlag): PrecomputedFlag; | ||
export declare function decodePrecomputedFlag(precomputedFlag: PrecomputedFlag): DecodedPrecomputedFlag; | ||
//# sourceMappingURL=decoding.d.ts.map |
@@ -69,3 +69,3 @@ "use strict"; | ||
variationKey: (0, obfuscation_1.decodeBase64)(precomputedFlag.variationKey), | ||
variationValue: (0, obfuscation_1.decodeBase64)(precomputedFlag.variationValue), | ||
variationValue: decodeValue(precomputedFlag.variationValue, precomputedFlag.variationType), | ||
extraLogging: decodeObject(precomputedFlag.extraLogging), | ||
@@ -72,0 +72,0 @@ }; |
@@ -1,2 +0,2 @@ | ||
import { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import NamedEventQueue from './named-event-queue'; | ||
@@ -3,0 +3,0 @@ export default class BatchEventProcessor { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const MIN_BATCH_SIZE = 100; | ||
const MAX_BATCH_SIZE = 10000; | ||
class BatchEventProcessor { | ||
constructor(eventQueue, batchSize) { | ||
this.eventQueue = eventQueue; | ||
this.batchSize = batchSize; | ||
// clamp batch size between min and max | ||
this.batchSize = Math.max(MIN_BATCH_SIZE, Math.min(MAX_BATCH_SIZE, batchSize)); | ||
} | ||
@@ -8,0 +11,0 @@ nextBatch() { |
@@ -1,3 +0,6 @@ | ||
import EventDelivery from './event-delivery'; | ||
import { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import { EventDeliveryResult } from './event-delivery'; | ||
export type IEventDelivery = { | ||
deliver(batch: Event[]): Promise<EventDeliveryResult>; | ||
}; | ||
/** | ||
@@ -15,3 +18,3 @@ * Attempts to retry delivering a batch of events to the ingestionUrl up to `maxRetries` times | ||
*/ | ||
constructor(delivery: EventDelivery, config: { | ||
constructor(delivery: IEventDelivery, config: { | ||
retryIntervalMs: number; | ||
@@ -21,5 +24,5 @@ maxRetryDelayMs: number; | ||
}); | ||
/** Re-attempts delivery of the provided batch, returns whether the retry succeeded. */ | ||
retry(batch: Event[], attempt?: number): Promise<boolean>; | ||
/** Re-attempts delivery of the provided batch, returns the UUIDs of events that failed retry. */ | ||
retry(batch: Event[], attempt?: number): Promise<Event[]>; | ||
} | ||
//# sourceMappingURL=batch-retry-manager.d.ts.map |
@@ -18,20 +18,20 @@ "use strict"; | ||
} | ||
/** Re-attempts delivery of the provided batch, returns whether the retry succeeded. */ | ||
/** Re-attempts delivery of the provided batch, returns the UUIDs of events that failed retry. */ | ||
async retry(batch, attempt = 0) { | ||
const { retryIntervalMs, maxRetryDelayMs, maxRetries } = this.config; | ||
const delay = Math.min(retryIntervalMs * Math.pow(2, attempt), maxRetryDelayMs); | ||
application_logger_1.logger.info(`[BatchRetryManager] Retrying batch delivery in ${delay}ms...`); | ||
application_logger_1.logger.info(`[BatchRetryManager] Retrying batch delivery of ${batch.length} events in ${delay}ms...`); | ||
await new Promise((resolve) => setTimeout(resolve, delay)); | ||
const success = await this.delivery.deliver(batch); | ||
if (success) { | ||
application_logger_1.logger.info(`[BatchRetryManager] Batch delivery successfully after ${attempt} retries.`); | ||
return true; | ||
const { failedEvents } = await this.delivery.deliver(batch); | ||
if (failedEvents.length === 0) { | ||
application_logger_1.logger.info(`[BatchRetryManager] Batch delivery successfully after ${attempt + 1} tries.`); | ||
return []; | ||
} | ||
// attempts are zero-indexed while maxRetries is not | ||
if (attempt < maxRetries - 1) { | ||
return this.retry(batch, attempt + 1); | ||
return this.retry(failedEvents, attempt + 1); | ||
} | ||
else { | ||
application_logger_1.logger.warn(`[BatchRetryManager] Failed to deliver batch after ${maxRetries} retries, bailing`); | ||
return false; | ||
application_logger_1.logger.warn(`[BatchRetryManager] Failed to deliver batch after ${maxRetries} tries, bailing`); | ||
return batch; | ||
} | ||
@@ -38,0 +38,0 @@ } |
import BatchEventProcessor from './batch-event-processor'; | ||
import EventDispatcher, { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import EventDispatcher from './event-dispatcher'; | ||
import NamedEventQueue from './named-event-queue'; | ||
import NetworkStatusListener from './network-status-listener'; | ||
export type EventDispatcherConfig = { | ||
sdkKey: string; | ||
ingestionUrl: string; | ||
@@ -12,4 +14,4 @@ deliveryIntervalMs: number; | ||
}; | ||
export declare const DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 100; | ||
export declare const DEFAULT_EVENT_DISPATCHER_CONFIG: Omit<EventDispatcherConfig, 'ingestionUrl'>; | ||
export declare const DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 1000; | ||
export declare const DEFAULT_EVENT_DISPATCHER_CONFIG: Omit<EventDispatcherConfig, 'ingestionUrl' | 'sdkKey'>; | ||
/** | ||
@@ -36,3 +38,3 @@ * @internal | ||
/** Creates a new {@link DefaultEventDispatcher} with the provided configuration. */ | ||
export declare function newDefaultEventDispatcher(eventQueue: NamedEventQueue<Event>, networkStatusListener: NetworkStatusListener, sdkKey: string, batchSize?: number, config?: Omit<EventDispatcherConfig, 'ingestionUrl'>): EventDispatcher; | ||
export declare function newDefaultEventDispatcher(eventQueue: NamedEventQueue<Event>, networkStatusListener: NetworkStatusListener, sdkKey: string, batchSize?: number, config?: Omit<EventDispatcherConfig, 'ingestionUrl' | 'sdkKey'>): EventDispatcher; | ||
//# sourceMappingURL=default-event-dispatcher.d.ts.map |
@@ -11,5 +11,3 @@ "use strict"; | ||
const sdk_key_decoder_1 = require("./sdk-key-decoder"); | ||
// TODO: Have more realistic default batch size based on average event payload size once we have | ||
// more concrete data. | ||
exports.DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 100; | ||
exports.DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 1000; | ||
exports.DEFAULT_EVENT_DISPATCHER_CONFIG = { | ||
@@ -34,7 +32,8 @@ deliveryIntervalMs: 10000, | ||
this.ensureConfigFields(config); | ||
this.eventDelivery = new event_delivery_1.default(config.ingestionUrl); | ||
const { sdkKey, ingestionUrl, retryIntervalMs, maxRetryDelayMs, maxRetries = 3 } = config; | ||
this.eventDelivery = new event_delivery_1.default(sdkKey, ingestionUrl); | ||
this.retryManager = new batch_retry_manager_1.default(this.eventDelivery, { | ||
retryIntervalMs: config.retryIntervalMs, | ||
maxRetryDelayMs: config.maxRetryDelayMs, | ||
maxRetries: config.maxRetries || 3, | ||
retryIntervalMs, | ||
maxRetryDelayMs, | ||
maxRetries, | ||
}); | ||
@@ -68,9 +67,9 @@ this.deliveryIntervalMs = config.deliveryIntervalMs; | ||
} | ||
const success = await this.eventDelivery.deliver(batch); | ||
if (!success) { | ||
application_logger_1.logger.warn('[EventDispatcher] Failed to deliver batch, retrying...'); | ||
const retrySucceeded = await this.retryManager.retry(batch); | ||
if (!retrySucceeded) { | ||
const { failedEvents } = await this.eventDelivery.deliver(batch); | ||
if (failedEvents.length > 0) { | ||
application_logger_1.logger.warn('[EventDispatcher] Failed to deliver some events from batch, retrying...'); | ||
const failedRetry = await this.retryManager.retry(failedEvents); | ||
if (failedRetry.length > 0) { | ||
// re-enqueue events that failed to retry | ||
this.batchProcessor.push(...batch); | ||
this.batchProcessor.push(...failedRetry); | ||
} | ||
@@ -116,4 +115,4 @@ } | ||
} | ||
return new DefaultEventDispatcher(new batch_event_processor_1.default(eventQueue, batchSize), networkStatusListener, { ...config, ingestionUrl }); | ||
return new DefaultEventDispatcher(new batch_event_processor_1.default(eventQueue, batchSize), networkStatusListener, { ...config, ingestionUrl, sdkKey }); | ||
} | ||
//# sourceMappingURL=default-event-dispatcher.js.map |
@@ -1,7 +0,16 @@ | ||
import { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
export type EventDeliveryResult = { | ||
failedEvents: Event[]; | ||
}; | ||
export default class EventDelivery { | ||
private readonly sdkKey; | ||
private readonly ingestionUrl; | ||
constructor(ingestionUrl: string); | ||
deliver(batch: Event[]): Promise<boolean>; | ||
constructor(sdkKey: string, ingestionUrl: string); | ||
/** | ||
* Delivers a batch of events to the ingestion URL endpoint. Returns the UUIDs of any events from | ||
* the batch that failed ingestion. | ||
*/ | ||
deliver(batch: Event[]): Promise<EventDeliveryResult>; | ||
private parseFailedEvents; | ||
} | ||
//# sourceMappingURL=event-delivery.d.ts.map |
@@ -5,5 +5,10 @@ "use strict"; | ||
class EventDelivery { | ||
constructor(ingestionUrl) { | ||
constructor(sdkKey, ingestionUrl) { | ||
this.sdkKey = sdkKey; | ||
this.ingestionUrl = ingestionUrl; | ||
} | ||
/** | ||
* Delivers a batch of events to the ingestion URL endpoint. Returns the UUIDs of any events from | ||
* the batch that failed ingestion. | ||
*/ | ||
async deliver(batch) { | ||
@@ -14,16 +19,34 @@ try { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
// TODO: Figure out proper request body encoding format for batch, using JSON for now | ||
body: JSON.stringify(batch), | ||
headers: { 'Content-Type': 'application/json', 'x-eppo-token': this.sdkKey }, | ||
body: JSON.stringify({ eppo_events: batch }), | ||
}); | ||
// TODO: Parse response to check `failed_event_uploads` for any failed event ingestions in the batch | ||
return response.ok; | ||
if (response.ok) { | ||
return await this.parseFailedEvents(response, batch); | ||
} | ||
else { | ||
return { failedEvents: batch }; | ||
} | ||
} | ||
catch { | ||
application_logger_1.logger.warn('Failed to upload event batch'); | ||
return false; | ||
catch (e) { | ||
application_logger_1.logger.warn(`Failed to upload event batch`, e); | ||
return { failedEvents: batch }; | ||
} | ||
} | ||
async parseFailedEvents(response, batch) { | ||
application_logger_1.logger.info('[EventDispatcher] Batch delivered successfully.'); | ||
const responseBody = (await response.json()); | ||
const failedEvents = new Set(responseBody?.failed_events || []); | ||
if (failedEvents.size > 0) { | ||
application_logger_1.logger.warn(`[EventDispatcher] ${failedEvents.size}/${batch.length} events failed ingestion.`); | ||
// even though some events may have failed to successfully deliver, we'll still consider | ||
// the batch as a whole to have been delivered successfully and just re-enqueue the failed | ||
// events for retry later | ||
return { failedEvents: batch.filter(({ uuid }) => failedEvents.has(uuid)) }; | ||
} | ||
else { | ||
return { failedEvents: [] }; | ||
} | ||
} | ||
} | ||
exports.default = EventDelivery; | ||
//# sourceMappingURL=event-delivery.js.map |
@@ -1,7 +0,2 @@ | ||
export type Event = { | ||
uuid: string; | ||
timestamp: number; | ||
type: string; | ||
payload: Record<string, unknown>; | ||
}; | ||
import Event from './event'; | ||
export default interface EventDispatcher { | ||
@@ -8,0 +3,0 @@ /** Dispatches (enqueues) an event for eventual delivery. */ |
@@ -1,2 +0,3 @@ | ||
import EventDispatcher, { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import EventDispatcher from './event-dispatcher'; | ||
export default class NoOpEventDispatcher implements EventDispatcher { | ||
@@ -3,0 +4,0 @@ dispatch(_: Event): void; |
import ApiEndpoints from './api-endpoints'; | ||
import { BanditParameters, BanditVariation, Environment, Flag, FormatEnum, PrecomputedFlag, PrecomputedFlagsPayload } from './interfaces'; | ||
import { IPrecomputedConfigurationResponse } from './configuration'; | ||
import { BanditParameters, BanditVariation, Environment, Flag, FormatEnum, PrecomputedFlagsPayload } from './interfaces'; | ||
import { Attributes } from './types'; | ||
@@ -29,12 +30,6 @@ export interface IQueryParams { | ||
} | ||
export interface IPrecomputedFlagsResponse { | ||
createdAt: string; | ||
format: FormatEnum; | ||
environment: Environment; | ||
flags: Record<string, PrecomputedFlag>; | ||
} | ||
export interface IHttpClient { | ||
getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>; | ||
getBanditParameters(): Promise<IBanditParametersResponse | undefined>; | ||
getPrecomputedFlags(payload: PrecomputedFlagsPayload): Promise<IPrecomputedFlagsResponse | undefined>; | ||
getPrecomputedFlags(payload: PrecomputedFlagsPayload): Promise<IPrecomputedConfigurationResponse | undefined>; | ||
rawGet<T>(url: URL): Promise<T | undefined>; | ||
@@ -49,3 +44,3 @@ rawPost<T, P>(url: URL, payload: P): Promise<T | undefined>; | ||
getBanditParameters(): Promise<IBanditParametersResponse | undefined>; | ||
getPrecomputedFlags(payload: PrecomputedFlagsPayload): Promise<IPrecomputedFlagsResponse | undefined>; | ||
getPrecomputedFlags(payload: PrecomputedFlagsPayload): Promise<IPrecomputedConfigurationResponse | undefined>; | ||
rawGet<T>(url: URL): Promise<T | undefined>; | ||
@@ -52,0 +47,0 @@ rawPost<T, P>(url: URL, payload: P): Promise<T | undefined>; |
@@ -10,3 +10,4 @@ import ApiEndpoints from './api-endpoints'; | ||
import EppoClient, { FlagConfigurationRequestParameters, IAssignmentDetails, IContainerExperiment } from './client/eppo-client'; | ||
import EppoPrecomputedClient, { PrecomputedFlagsRequestParameters } from './client/eppo-precomputed-client'; | ||
import EppoPrecomputedClient, { convertContextAttributesToSubjectAttributes, PrecomputedFlagsRequestParameters } from './client/eppo-precomputed-client'; | ||
import { IConfigurationWire, IPrecomputedConfigurationResponse } from './configuration'; | ||
import FlagConfigRequestor from './configuration-requestor'; | ||
@@ -20,3 +21,4 @@ import { IConfigurationStore, IAsyncStore, ISyncStore } from './configuration-store/configuration-store'; | ||
import DefaultEventDispatcher, { DEFAULT_EVENT_DISPATCHER_CONFIG, DEFAULT_EVENT_DISPATCHER_BATCH_SIZE, newDefaultEventDispatcher } from './events/default-event-dispatcher'; | ||
import EventDispatcher, { Event } from './events/event-dispatcher'; | ||
import Event from './events/event'; | ||
import EventDispatcher from './events/event-dispatcher'; | ||
import NamedEventQueue from './events/named-event-queue'; | ||
@@ -28,3 +30,3 @@ import NetworkStatusListener from './events/network-status-listener'; | ||
import * as validation from './validation'; | ||
export { applicationLogger, AbstractAssignmentCache, IAssignmentDetails, IAssignmentHooks, IAssignmentLogger, IAssignmentEvent, IBanditLogger, IBanditEvent, IContainerExperiment, PrecomputedFlagsRequestParameters, EppoClient, constants, ApiEndpoints, FlagConfigRequestor, HttpClient, validation, EppoPrecomputedClient, IConfigurationStore, IAsyncStore, ISyncStore, MemoryStore, HybridConfigurationStore, MemoryOnlyConfigurationStore, AssignmentCacheKey, AssignmentCacheValue, AssignmentCacheEntry, AssignmentCache, AsyncMap, NonExpiringInMemoryAssignmentCache, LRUInMemoryAssignmentCache, assignmentCacheKeyToString, assignmentCacheValueToString, FlagConfigurationRequestParameters, Flag, ObfuscatedFlag, PrecomputedFlag, VariationType, AttributeType, Attributes, ContextAttributes, BanditSubjectAttributes, BanditActions, NamedEventQueue, EventDispatcher, BoundedEventQueue, DEFAULT_EVENT_DISPATCHER_CONFIG, DEFAULT_EVENT_DISPATCHER_BATCH_SIZE, newDefaultEventDispatcher, BatchEventProcessor, NetworkStatusListener, DefaultEventDispatcher, Event, }; | ||
export { applicationLogger, AbstractAssignmentCache, IAssignmentDetails, IAssignmentHooks, IAssignmentLogger, IAssignmentEvent, IBanditLogger, IBanditEvent, IContainerExperiment, EppoClient, constants, ApiEndpoints, FlagConfigRequestor, HttpClient, validation, PrecomputedFlagsRequestParameters, EppoPrecomputedClient, convertContextAttributesToSubjectAttributes, IConfigurationStore, IAsyncStore, ISyncStore, MemoryStore, HybridConfigurationStore, MemoryOnlyConfigurationStore, AssignmentCacheKey, AssignmentCacheValue, AssignmentCacheEntry, AssignmentCache, AsyncMap, NonExpiringInMemoryAssignmentCache, LRUInMemoryAssignmentCache, assignmentCacheKeyToString, assignmentCacheValueToString, FlagConfigurationRequestParameters, Flag, ObfuscatedFlag, VariationType, AttributeType, Attributes, ContextAttributes, BanditSubjectAttributes, BanditActions, NamedEventQueue, EventDispatcher, BoundedEventQueue, DEFAULT_EVENT_DISPATCHER_CONFIG, DEFAULT_EVENT_DISPATCHER_BATCH_SIZE, newDefaultEventDispatcher, BatchEventProcessor, NetworkStatusListener, DefaultEventDispatcher, Event, IConfigurationWire, IPrecomputedConfigurationResponse, PrecomputedFlag, }; | ||
//# sourceMappingURL=index.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.DefaultEventDispatcher = exports.BatchEventProcessor = exports.newDefaultEventDispatcher = exports.DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = exports.DEFAULT_EVENT_DISPATCHER_CONFIG = exports.BoundedEventQueue = exports.VariationType = exports.assignmentCacheValueToString = exports.assignmentCacheKeyToString = exports.LRUInMemoryAssignmentCache = exports.NonExpiringInMemoryAssignmentCache = exports.MemoryOnlyConfigurationStore = exports.HybridConfigurationStore = exports.MemoryStore = exports.EppoPrecomputedClient = exports.validation = exports.HttpClient = exports.FlagConfigRequestor = exports.ApiEndpoints = exports.constants = exports.EppoClient = exports.AbstractAssignmentCache = exports.applicationLogger = void 0; | ||
exports.DefaultEventDispatcher = exports.BatchEventProcessor = exports.newDefaultEventDispatcher = exports.DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = exports.DEFAULT_EVENT_DISPATCHER_CONFIG = exports.BoundedEventQueue = exports.VariationType = exports.assignmentCacheValueToString = exports.assignmentCacheKeyToString = exports.LRUInMemoryAssignmentCache = exports.NonExpiringInMemoryAssignmentCache = exports.MemoryOnlyConfigurationStore = exports.HybridConfigurationStore = exports.MemoryStore = exports.convertContextAttributesToSubjectAttributes = exports.EppoPrecomputedClient = exports.validation = exports.HttpClient = exports.FlagConfigRequestor = exports.ApiEndpoints = exports.constants = exports.EppoClient = exports.AbstractAssignmentCache = exports.applicationLogger = void 0; | ||
const api_endpoints_1 = require("./api-endpoints"); | ||
@@ -20,2 +20,3 @@ exports.ApiEndpoints = api_endpoints_1.default; | ||
exports.EppoPrecomputedClient = eppo_precomputed_client_1.default; | ||
Object.defineProperty(exports, "convertContextAttributesToSubjectAttributes", { enumerable: true, get: function () { return eppo_precomputed_client_1.convertContextAttributesToSubjectAttributes; } }); | ||
const configuration_requestor_1 = require("./configuration-requestor"); | ||
@@ -22,0 +23,0 @@ exports.FlagConfigRequestor = configuration_requestor_1.default; |
@@ -38,2 +38,3 @@ import { Rule } from './rules'; | ||
} | ||
export declare const UNKNOWN_ENVIRONMENT_NAME = "UNKNOWN"; | ||
export interface ConfigDetails { | ||
@@ -124,10 +125,16 @@ configFetchedAt: string; | ||
} | ||
export interface PrecomputedFlag { | ||
export type BasePrecomputedFlag = { | ||
flagKey?: string; | ||
allocationKey: string; | ||
variationKey: string; | ||
variationType: VariationType; | ||
variationValue: string; | ||
extraLogging: Record<string, string>; | ||
doLog: boolean; | ||
}; | ||
export interface PrecomputedFlag extends BasePrecomputedFlag { | ||
variationValue: string; | ||
} | ||
export interface DecodedPrecomputedFlag extends BasePrecomputedFlag { | ||
variationValue: Variation['value']; | ||
} | ||
export interface PrecomputedFlagsDetails { | ||
@@ -134,0 +141,0 @@ precomputedFlagsFetchedAt: string; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.FormatEnum = exports.VariationType = void 0; | ||
exports.FormatEnum = exports.UNKNOWN_ENVIRONMENT_NAME = exports.VariationType = void 0; | ||
var VariationType; | ||
@@ -12,2 +12,3 @@ (function (VariationType) { | ||
})(VariationType || (exports.VariationType = VariationType = {})); | ||
exports.UNKNOWN_ENVIRONMENT_NAME = 'UNKNOWN'; | ||
var FormatEnum; | ||
@@ -14,0 +15,0 @@ (function (FormatEnum) { |
@@ -1,4 +0,8 @@ | ||
export declare function getMD5Hash(input: string): string; | ||
import { PrecomputedFlag } from './interfaces'; | ||
export declare function getMD5Hash(input: string, salt?: string): string; | ||
export declare function encodeBase64(input: string): string; | ||
export declare function decodeBase64(input: string): string; | ||
export declare function obfuscatePrecomputedFlags(salt: string, precomputedFlags: Record<string, PrecomputedFlag>): Record<string, PrecomputedFlag>; | ||
export declare function setSaltOverrideForTests(salt: Uint8Array | null): void; | ||
export declare function generateSalt(length?: number): string; | ||
//# sourceMappingURL=obfuscation.d.ts.map |
@@ -6,6 +6,9 @@ "use strict"; | ||
exports.decodeBase64 = decodeBase64; | ||
exports.obfuscatePrecomputedFlags = obfuscatePrecomputedFlags; | ||
exports.setSaltOverrideForTests = setSaltOverrideForTests; | ||
exports.generateSalt = generateSalt; | ||
const base64 = require("js-base64"); | ||
const SparkMD5 = require("spark-md5"); | ||
function getMD5Hash(input) { | ||
return SparkMD5.hash(input); | ||
function getMD5Hash(input, salt = '') { | ||
return new SparkMD5().appendBinary(salt).append(input).end(); | ||
} | ||
@@ -18,2 +21,28 @@ function encodeBase64(input) { | ||
} | ||
function obfuscatePrecomputedFlags(salt, precomputedFlags) { | ||
const response = {}; | ||
Object.keys(precomputedFlags).map((flagKey) => { | ||
const assignment = precomputedFlags[flagKey]; | ||
// Encode extraLogging keys and values. | ||
const encodedExtraLogging = Object.fromEntries(Object.entries(assignment.extraLogging).map((kvArr) => kvArr.map(encodeBase64))); | ||
const hashedKey = getMD5Hash(flagKey, salt); | ||
response[hashedKey] = { | ||
flagKey: hashedKey, | ||
variationType: assignment.variationType, | ||
extraLogging: encodedExtraLogging, | ||
doLog: assignment.doLog, | ||
allocationKey: encodeBase64(assignment.allocationKey), | ||
variationKey: encodeBase64(assignment.variationKey), | ||
variationValue: encodeBase64(assignment.variationValue), | ||
}; | ||
}); | ||
return response; | ||
} | ||
let saltOverrideBytes; | ||
function setSaltOverrideForTests(salt) { | ||
saltOverrideBytes = salt ? salt : null; | ||
} | ||
function generateSalt(length = 16) { | ||
return base64.fromUint8Array(saltOverrideBytes ? saltOverrideBytes : crypto.getRandomValues(new Uint8Array(length))); | ||
} | ||
//# sourceMappingURL=obfuscation.js.map |
@@ -5,2 +5,7 @@ import { IConfigurationStore } from './configuration-store/configuration-store'; | ||
import { Attributes } from './types'; | ||
export interface PrecomputedResponseData { | ||
salt: string; | ||
subjectKey: string; | ||
subjectAttributes: Attributes; | ||
} | ||
export default class PrecomputedFlagRequestor { | ||
@@ -11,2 +16,3 @@ private readonly httpClient; | ||
private readonly subjectAttributes; | ||
onPrecomputedResponse?: (response: PrecomputedResponseData) => void; | ||
constructor(httpClient: IHttpClient, precomputedFlagStore: IConfigurationStore<PrecomputedFlag>, subjectKey: string, subjectAttributes: Attributes); | ||
@@ -13,0 +19,0 @@ fetchAndStorePrecomputedFlags(): Promise<void>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const configuration_store_utils_1 = require("./configuration-store/configuration-store-utils"); | ||
const interfaces_1 = require("./interfaces"); | ||
// Requests AND stores precomputed flags, reuses the configuration store | ||
@@ -17,2 +18,9 @@ class PrecomputedFlagRequestor { | ||
}); | ||
if (this.onPrecomputedResponse && precomputedResponse) { | ||
this.onPrecomputedResponse({ | ||
salt: precomputedResponse.salt, | ||
subjectKey: this.subjectKey, | ||
subjectAttributes: this.subjectAttributes, | ||
}); | ||
} | ||
if (!precomputedResponse?.flags) { | ||
@@ -23,3 +31,3 @@ return; | ||
entries: precomputedResponse.flags, | ||
environment: precomputedResponse.environment, | ||
environment: precomputedResponse.environment ?? { name: interfaces_1.UNKNOWN_ENVIRONMENT_NAME }, | ||
createdAt: precomputedResponse.createdAt, | ||
@@ -26,0 +34,0 @@ format: precomputedResponse.format, |
{ | ||
"name": "@eppo/js-client-sdk-common", | ||
"version": "4.6.3", | ||
"version": "4.7.0-alpha.0", | ||
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -12,2 +12,9 @@ import { v4 as randomUUID } from 'uuid'; | ||
import { TLRUInMemoryAssignmentCache } from '../cache/tlru-in-memory-assignment-cache'; | ||
import { | ||
IConfigurationWire, | ||
ConfigurationWireV1, | ||
IPrecomputedConfiguration, | ||
ObfuscatedPrecomputedConfiguration, | ||
PrecomputedConfiguration, | ||
} from '../configuration'; | ||
import ConfigurationRequestor from '../configuration-requestor'; | ||
@@ -39,2 +46,3 @@ import { IConfigurationStore } from '../configuration-store/configuration-store'; | ||
ObfuscatedFlag, | ||
PrecomputedFlag, | ||
Variation, | ||
@@ -881,3 +889,85 @@ VariationType, | ||
private getAllAssignments( | ||
subjectKey: string, | ||
subjectAttributes: Attributes = {}, | ||
): Record<string, PrecomputedFlag> { | ||
const configDetails = this.getConfigDetails(); | ||
const flagKeys = this.getFlagKeys(); | ||
const flags: Record<string, PrecomputedFlag> = {}; | ||
// Evaluate all the enabled flags for the user | ||
flagKeys.forEach((flagKey) => { | ||
const flag = this.getFlag(flagKey); | ||
if (!flag) { | ||
logger.debug(`[Eppo SDK] No assigned variation. Flag does not exist.`); | ||
return; | ||
} | ||
// Evaluate the flag for this subject. | ||
const evaluation = this.evaluator.evaluateFlag( | ||
flag, | ||
configDetails, | ||
subjectKey, | ||
subjectAttributes, | ||
this.isObfuscated, | ||
); | ||
// allocationKey is set along with variation when there is a result. this check appeases typescript below | ||
if (!evaluation.variation || !evaluation.allocationKey) { | ||
logger.debug(`[Eppo SDK] No assigned variation: ${flagKey}`); | ||
return; | ||
} | ||
// Transform into a PrecomputedFlag | ||
flags[flagKey] = { | ||
flagKey, | ||
allocationKey: evaluation.allocationKey, | ||
doLog: evaluation.doLog, | ||
extraLogging: evaluation.extraLogging, | ||
variationKey: evaluation.variation.key, | ||
variationType: flag.variationType, | ||
variationValue: evaluation.variation.value.toString(), | ||
}; | ||
}); | ||
return flags; | ||
} | ||
/** | ||
* Computes and returns assignments for a subject from all loaded flags. | ||
* | ||
* @param subjectKey an identifier of the experiment subject, for example a user ID. | ||
* @param subjectAttributes optional attributes associated with the subject, for example name and email. | ||
* @param obfuscated optional whether to obfuscate the results. | ||
*/ | ||
getPrecomputedAssignments( | ||
subjectKey: string, | ||
subjectAttributes: Attributes | ContextAttributes = {}, | ||
obfuscated = false, | ||
): string { | ||
const configDetails = this.getConfigDetails(); | ||
const subjectContextualAttributes = this.ensureContextualSubjectAttributes(subjectAttributes); | ||
const subjectFlatAttributes = this.ensureNonContextualSubjectAttributes(subjectAttributes); | ||
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes); | ||
const precomputedConfig: IPrecomputedConfiguration = obfuscated | ||
? new ObfuscatedPrecomputedConfiguration( | ||
subjectKey, | ||
flags, | ||
subjectContextualAttributes, | ||
configDetails.configEnvironment, | ||
) | ||
: new PrecomputedConfiguration( | ||
subjectKey, | ||
flags, | ||
subjectContextualAttributes, | ||
configDetails.configEnvironment, | ||
); | ||
const configWire: IConfigurationWire = new ConfigurationWireV1(precomputedConfig); | ||
return JSON.stringify(configWire); | ||
} | ||
/** | ||
* [Experimental] Get a detailed return of assignment for a particular subject and flag. | ||
@@ -884,0 +974,0 @@ * |
@@ -19,10 +19,12 @@ import ApiEndpoints from '../api-endpoints'; | ||
import FetchHttpClient from '../http-client'; | ||
import { PrecomputedFlag, VariationType } from '../interfaces'; | ||
import { getMD5Hash } from '../obfuscation'; | ||
import { DecodedPrecomputedFlag, PrecomputedFlag, VariationType } from '../interfaces'; | ||
import { decodeBase64, getMD5Hash } from '../obfuscation'; | ||
import initPoller, { IPoller } from '../poller'; | ||
import PrecomputedRequestor from '../precomputed-requestor'; | ||
import { Attributes } from '../types'; | ||
import { Attributes, ContextAttributes } from '../types'; | ||
import { validateNotBlank } from '../validation'; | ||
import { LIB_VERSION } from '../version'; | ||
import { checkTypeMatch } from './eppo-client'; | ||
export type PrecomputedFlagsRequestParameters = { | ||
@@ -47,2 +49,16 @@ apiKey: string; | ||
export function convertContextAttributesToSubjectAttributes( | ||
contextAttributes: ContextAttributes, | ||
): Attributes { | ||
return { | ||
...(contextAttributes.numericAttributes || {}), | ||
...(contextAttributes.categoricalAttributes || {}), | ||
}; | ||
} | ||
interface EppoPrecomputedClientOptions { | ||
precomputedFlagStore: IConfigurationStore<PrecomputedFlag>; | ||
isObfuscated?: boolean; | ||
} | ||
export default class EppoPrecomputedClient { | ||
@@ -56,30 +72,36 @@ private readonly queuedAssignmentEvents: IAssignmentEvent[] = []; | ||
private subjectAttributes?: Attributes; | ||
private decodedFlagKeySalt = ''; | ||
private precomputedFlagStore: IConfigurationStore<PrecomputedFlag>; | ||
private isObfuscated: boolean; | ||
constructor( | ||
private precomputedFlagStore: IConfigurationStore<PrecomputedFlag>, | ||
private isObfuscated = false, | ||
) {} | ||
constructor(options: EppoPrecomputedClientOptions) { | ||
this.precomputedFlagStore = options.precomputedFlagStore; | ||
this.isObfuscated = options.isObfuscated ?? true; // Default to true if not provided | ||
} | ||
public setPrecomputedFlagsRequestParameters( | ||
precomputedFlagsRequestParameters: PrecomputedFlagsRequestParameters, | ||
) { | ||
this.precomputedFlagsRequestParameters = precomputedFlagsRequestParameters; | ||
public setIsObfuscated(isObfuscated: boolean) { | ||
this.isObfuscated = isObfuscated; | ||
} | ||
public setSubjectAndPrecomputedFlagsRequestParameters( | ||
precomputedFlagsRequestParameters: PrecomputedFlagsRequestParameters, | ||
) { | ||
this.setPrecomputedFlagsRequestParameters(precomputedFlagsRequestParameters); | ||
this.subjectKey = precomputedFlagsRequestParameters.precompute.subjectKey; | ||
this.subjectAttributes = precomputedFlagsRequestParameters.precompute.subjectAttributes; | ||
private setDecodedFlagKeySalt(salt: string) { | ||
this.decodedFlagKeySalt = salt; | ||
} | ||
public setPrecomputedFlagStore(precomputedFlagStore: IConfigurationStore<PrecomputedFlag>) { | ||
this.precomputedFlagStore = precomputedFlagStore; | ||
private setPrecomputedFlagsRequestParameters(parameters: PrecomputedFlagsRequestParameters) { | ||
this.precomputedFlagsRequestParameters = parameters; | ||
} | ||
public setIsObfuscated(isObfuscated: boolean) { | ||
this.isObfuscated = isObfuscated; | ||
private setSubjectData(subjectKey: string, subjectAttributes: Attributes) { | ||
this.subjectKey = subjectKey; | ||
this.subjectAttributes = subjectAttributes; | ||
} | ||
// 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); | ||
} | ||
public async fetchPrecomputedFlags() { | ||
@@ -126,2 +148,8 @@ if (!this.precomputedFlagsRequestParameters) { | ||
// A callback to capture the salt and subject information | ||
precomputedRequestor.onPrecomputedResponse = (responseData) => { | ||
this.setDecodedFlagKeySalt(decodeBase64(responseData.salt)); | ||
this.setSubjectData(responseData.subjectKey, responseData.subjectAttributes); | ||
}; | ||
const pollingCallback = async () => { | ||
@@ -151,13 +179,17 @@ if (await this.precomputedFlagStore.isExpired()) { | ||
public setSubjectAndPrecomputedFlagStore( | ||
private setPrecomputedFlagStore(store: IConfigurationStore<PrecomputedFlag>) { | ||
this.requestPoller?.stop(); | ||
this.precomputedFlagStore = store; | ||
} | ||
// Convenience method that combines setters we need to make assignments work | ||
public setSubjectSaltAndPrecomputedFlagStore( | ||
subjectKey: string, | ||
subjectAttributes: Attributes, | ||
salt: string, | ||
precomputedFlagStore: IConfigurationStore<PrecomputedFlag>, | ||
) { | ||
// Save the new subject data and precomputed flag store together because they are related | ||
// Stop any polling process if it exists from previous subject data to protect consistency | ||
this.requestPoller?.stop(); | ||
this.setPrecomputedFlagStore(precomputedFlagStore); | ||
this.subjectKey = subjectKey; | ||
this.subjectAttributes = subjectAttributes; | ||
this.setSubjectData(subjectKey, subjectAttributes); | ||
this.setDecodedFlagKeySalt(decodeBase64(salt)); | ||
} | ||
@@ -173,5 +205,5 @@ | ||
const preComputedFlag = this.getPrecomputedFlag(flagKey); | ||
const precomputedFlag = this.getPrecomputedFlag(flagKey); | ||
if (preComputedFlag == null) { | ||
if (precomputedFlag == null) { | ||
logger.warn(`[Eppo SDK] No assigned variation. Flag not found: ${flagKey}`); | ||
@@ -181,7 +213,6 @@ return defaultValue; | ||
// Check variation type | ||
if (preComputedFlag.variationType !== expectedType) { | ||
logger.error( | ||
`[Eppo SDK] Type mismatch: expected ${expectedType} but flag ${flagKey} has type ${preComputedFlag.variationType}`, | ||
); | ||
// Add type checking before proceeding | ||
if (!checkTypeMatch(expectedType, precomputedFlag.variationType)) { | ||
const errorMessage = `[Eppo SDK] Type mismatch: expected ${expectedType} but flag ${flagKey} has type ${precomputedFlag.variationType}`; | ||
logger.error(errorMessage); | ||
return defaultValue; | ||
@@ -196,8 +227,8 @@ } | ||
variation: { | ||
key: preComputedFlag.variationKey, | ||
value: preComputedFlag.variationValue, | ||
key: precomputedFlag.variationKey, | ||
value: precomputedFlag.variationValue, | ||
}, | ||
allocationKey: preComputedFlag.allocationKey, | ||
extraLogging: preComputedFlag.extraLogging, | ||
doLog: preComputedFlag.doLog, | ||
allocationKey: precomputedFlag.allocationKey, | ||
extraLogging: precomputedFlag.extraLogging, | ||
doLog: precomputedFlag.doLog, | ||
}; | ||
@@ -285,3 +316,3 @@ | ||
private getPrecomputedFlag(flagKey: string): PrecomputedFlag | null { | ||
private getPrecomputedFlag(flagKey: string): DecodedPrecomputedFlag | null { | ||
return this.isObfuscated | ||
@@ -292,5 +323,6 @@ ? this.getObfuscatedFlag(flagKey) | ||
private getObfuscatedFlag(flagKey: string): PrecomputedFlag | null { | ||
private getObfuscatedFlag(flagKey: string): DecodedPrecomputedFlag | null { | ||
const saltedAndHashedFlagKey = getMD5Hash(flagKey, this.decodedFlagKeySalt); | ||
const precomputedFlag: PrecomputedFlag | null = this.precomputedFlagStore.get( | ||
getMD5Hash(flagKey), | ||
saltedAndHashedFlagKey, | ||
) as PrecomputedFlag; | ||
@@ -297,0 +329,0 @@ return precomputedFlag ? decodePrecomputedFlag(precomputedFlag) : null; |
@@ -13,2 +13,3 @@ import { | ||
PrecomputedFlag, | ||
DecodedPrecomputedFlag, | ||
} from './interfaces'; | ||
@@ -82,3 +83,3 @@ import { decodeBase64 } from './obfuscation'; | ||
export function decodePrecomputedFlag(precomputedFlag: PrecomputedFlag): PrecomputedFlag { | ||
export function decodePrecomputedFlag(precomputedFlag: PrecomputedFlag): DecodedPrecomputedFlag { | ||
return { | ||
@@ -88,5 +89,5 @@ ...precomputedFlag, | ||
variationKey: decodeBase64(precomputedFlag.variationKey), | ||
variationValue: decodeBase64(precomputedFlag.variationValue), | ||
variationValue: decodeValue(precomputedFlag.variationValue, precomputedFlag.variationType), | ||
extraLogging: decodeObject(precomputedFlag.extraLogging), | ||
}; | ||
} |
@@ -1,10 +0,15 @@ | ||
import { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import NamedEventQueue from './named-event-queue'; | ||
const MIN_BATCH_SIZE = 100; | ||
const MAX_BATCH_SIZE = 10_000; | ||
export default class BatchEventProcessor { | ||
constructor( | ||
private readonly eventQueue: NamedEventQueue<Event>, | ||
private readonly batchSize: number, | ||
) {} | ||
private readonly batchSize: number; | ||
constructor(private readonly eventQueue: NamedEventQueue<Event>, batchSize: number) { | ||
// clamp batch size between min and max | ||
this.batchSize = Math.max(MIN_BATCH_SIZE, Math.min(MAX_BATCH_SIZE, batchSize)); | ||
} | ||
nextBatch(): Event[] { | ||
@@ -11,0 +16,0 @@ return this.eventQueue.splice(this.batchSize); |
import { logger } from '../application-logger'; | ||
import EventDelivery from './event-delivery'; | ||
import { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import { EventDeliveryResult } from './event-delivery'; | ||
export type IEventDelivery = { | ||
deliver(batch: Event[]): Promise<EventDeliveryResult>; | ||
}; | ||
/** | ||
@@ -17,3 +21,3 @@ * Attempts to retry delivering a batch of events to the ingestionUrl up to `maxRetries` times | ||
constructor( | ||
private readonly delivery: EventDelivery, | ||
private readonly delivery: IEventDelivery, | ||
private readonly config: { | ||
@@ -26,24 +30,24 @@ retryIntervalMs: number; | ||
/** Re-attempts delivery of the provided batch, returns whether the retry succeeded. */ | ||
async retry(batch: Event[], attempt = 0): Promise<boolean> { | ||
/** Re-attempts delivery of the provided batch, returns the UUIDs of events that failed retry. */ | ||
async retry(batch: Event[], attempt = 0): Promise<Event[]> { | ||
const { retryIntervalMs, maxRetryDelayMs, maxRetries } = this.config; | ||
const delay = Math.min(retryIntervalMs * Math.pow(2, attempt), maxRetryDelayMs); | ||
logger.info(`[BatchRetryManager] Retrying batch delivery in ${delay}ms...`); | ||
logger.info( | ||
`[BatchRetryManager] Retrying batch delivery of ${batch.length} events in ${delay}ms...`, | ||
); | ||
await new Promise((resolve) => setTimeout(resolve, delay)); | ||
const success = await this.delivery.deliver(batch); | ||
if (success) { | ||
logger.info(`[BatchRetryManager] Batch delivery successfully after ${attempt} retries.`); | ||
return true; | ||
const { failedEvents } = await this.delivery.deliver(batch); | ||
if (failedEvents.length === 0) { | ||
logger.info(`[BatchRetryManager] Batch delivery successfully after ${attempt + 1} tries.`); | ||
return []; | ||
} | ||
// attempts are zero-indexed while maxRetries is not | ||
if (attempt < maxRetries - 1) { | ||
return this.retry(batch, attempt + 1); | ||
return this.retry(failedEvents, attempt + 1); | ||
} else { | ||
logger.warn( | ||
`[BatchRetryManager] Failed to deliver batch after ${maxRetries} retries, bailing`, | ||
); | ||
return false; | ||
logger.warn(`[BatchRetryManager] Failed to deliver batch after ${maxRetries} tries, bailing`); | ||
return batch; | ||
} | ||
} | ||
} |
@@ -5,4 +5,5 @@ import { logger } from '../application-logger'; | ||
import BatchRetryManager from './batch-retry-manager'; | ||
import Event from './event'; | ||
import EventDelivery from './event-delivery'; | ||
import EventDispatcher, { Event } from './event-dispatcher'; | ||
import EventDispatcher from './event-dispatcher'; | ||
import NamedEventQueue from './named-event-queue'; | ||
@@ -14,2 +15,4 @@ import NetworkStatusListener from './network-status-listener'; | ||
export type EventDispatcherConfig = { | ||
// The Eppo SDK key | ||
sdkKey: string; | ||
// target url to deliver events to | ||
@@ -27,6 +30,7 @@ ingestionUrl: string; | ||
// TODO: Have more realistic default batch size based on average event payload size once we have | ||
// more concrete data. | ||
export const DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 100; | ||
export const DEFAULT_EVENT_DISPATCHER_CONFIG: Omit<EventDispatcherConfig, 'ingestionUrl'> = { | ||
export const DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 1_000; | ||
export const DEFAULT_EVENT_DISPATCHER_CONFIG: Omit< | ||
EventDispatcherConfig, | ||
'ingestionUrl' | 'sdkKey' | ||
> = { | ||
deliveryIntervalMs: 10_000, | ||
@@ -57,7 +61,8 @@ retryIntervalMs: 5_000, | ||
this.ensureConfigFields(config); | ||
this.eventDelivery = new EventDelivery(config.ingestionUrl); | ||
const { sdkKey, ingestionUrl, retryIntervalMs, maxRetryDelayMs, maxRetries = 3 } = config; | ||
this.eventDelivery = new EventDelivery(sdkKey, ingestionUrl); | ||
this.retryManager = new BatchRetryManager(this.eventDelivery, { | ||
retryIntervalMs: config.retryIntervalMs, | ||
maxRetryDelayMs: config.maxRetryDelayMs, | ||
maxRetries: config.maxRetries || 3, | ||
retryIntervalMs, | ||
maxRetryDelayMs, | ||
maxRetries, | ||
}); | ||
@@ -94,9 +99,9 @@ this.deliveryIntervalMs = config.deliveryIntervalMs; | ||
const success = await this.eventDelivery.deliver(batch); | ||
if (!success) { | ||
logger.warn('[EventDispatcher] Failed to deliver batch, retrying...'); | ||
const retrySucceeded = await this.retryManager.retry(batch); | ||
if (!retrySucceeded) { | ||
const { failedEvents } = await this.eventDelivery.deliver(batch); | ||
if (failedEvents.length > 0) { | ||
logger.warn('[EventDispatcher] Failed to deliver some events from batch, retrying...'); | ||
const failedRetry = await this.retryManager.retry(failedEvents); | ||
if (failedRetry.length > 0) { | ||
// re-enqueue events that failed to retry | ||
this.batchProcessor.push(...batch); | ||
this.batchProcessor.push(...failedRetry); | ||
} | ||
@@ -142,3 +147,3 @@ } | ||
batchSize: number = DEFAULT_EVENT_DISPATCHER_BATCH_SIZE, | ||
config: Omit<EventDispatcherConfig, 'ingestionUrl'> = DEFAULT_EVENT_DISPATCHER_CONFIG, | ||
config: Omit<EventDispatcherConfig, 'ingestionUrl' | 'sdkKey'> = DEFAULT_EVENT_DISPATCHER_CONFIG, | ||
): EventDispatcher { | ||
@@ -156,4 +161,4 @@ const sdkKeyDecoder = new SdkKeyDecoder(); | ||
networkStatusListener, | ||
{ ...config, ingestionUrl }, | ||
{ ...config, ingestionUrl, sdkKey }, | ||
); | ||
} |
import { logger } from '../application-logger'; | ||
import { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
export type EventDeliveryResult = { | ||
failedEvents: Event[]; | ||
}; | ||
export default class EventDelivery { | ||
constructor(private readonly ingestionUrl: string) {} | ||
constructor(private readonly sdkKey: string, private readonly ingestionUrl: string) {} | ||
async deliver(batch: Event[]): Promise<boolean> { | ||
/** | ||
* Delivers a batch of events to the ingestion URL endpoint. Returns the UUIDs of any events from | ||
* the batch that failed ingestion. | ||
*/ | ||
async deliver(batch: Event[]): Promise<EventDeliveryResult> { | ||
try { | ||
@@ -15,13 +23,35 @@ logger.info( | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
// TODO: Figure out proper request body encoding format for batch, using JSON for now | ||
body: JSON.stringify(batch), | ||
headers: { 'Content-Type': 'application/json', 'x-eppo-token': this.sdkKey }, | ||
body: JSON.stringify({ eppo_events: batch }), | ||
}); | ||
// TODO: Parse response to check `failed_event_uploads` for any failed event ingestions in the batch | ||
return response.ok; | ||
} catch { | ||
logger.warn('Failed to upload event batch'); | ||
return false; | ||
if (response.ok) { | ||
return await this.parseFailedEvents(response, batch); | ||
} else { | ||
return { failedEvents: batch }; | ||
} | ||
} catch (e: any) { | ||
logger.warn(`Failed to upload event batch`, e); | ||
return { failedEvents: batch }; | ||
} | ||
} | ||
private async parseFailedEvents( | ||
response: Response, | ||
batch: Event[], | ||
): Promise<EventDeliveryResult> { | ||
logger.info('[EventDispatcher] Batch delivered successfully.'); | ||
const responseBody = (await response.json()) as { failed_events?: string[] }; | ||
const failedEvents = new Set(responseBody?.failed_events || []); | ||
if (failedEvents.size > 0) { | ||
logger.warn( | ||
`[EventDispatcher] ${failedEvents.size}/${batch.length} events failed ingestion.`, | ||
); | ||
// even though some events may have failed to successfully deliver, we'll still consider | ||
// the batch as a whole to have been delivered successfully and just re-enqueue the failed | ||
// events for retry later | ||
return { failedEvents: batch.filter(({ uuid }) => failedEvents.has(uuid)) }; | ||
} else { | ||
return { failedEvents: [] }; | ||
} | ||
} | ||
} |
@@ -1,7 +0,2 @@ | ||
export type Event = { | ||
uuid: string; | ||
timestamp: number; | ||
type: string; | ||
payload: Record<string, unknown>; | ||
}; | ||
import Event from './event'; | ||
@@ -8,0 +3,0 @@ export default interface EventDispatcher { |
@@ -1,2 +0,3 @@ | ||
import EventDispatcher, { Event } from './event-dispatcher'; | ||
import Event from './event'; | ||
import EventDispatcher from './event-dispatcher'; | ||
@@ -3,0 +4,0 @@ export default class NoOpEventDispatcher implements EventDispatcher { |
import ApiEndpoints from './api-endpoints'; | ||
import { IPrecomputedConfigurationResponse } from './configuration'; | ||
import { | ||
@@ -8,3 +9,2 @@ BanditParameters, | ||
FormatEnum, | ||
PrecomputedFlag, | ||
PrecomputedFlagsPayload, | ||
@@ -46,9 +46,2 @@ } from './interfaces'; | ||
export interface IPrecomputedFlagsResponse { | ||
createdAt: string; | ||
format: FormatEnum; | ||
environment: Environment; | ||
flags: Record<string, PrecomputedFlag>; | ||
} | ||
export interface IHttpClient { | ||
@@ -59,3 +52,3 @@ getUniversalFlagConfiguration(): Promise<IUniversalFlagConfigResponse | undefined>; | ||
payload: PrecomputedFlagsPayload, | ||
): Promise<IPrecomputedFlagsResponse | undefined>; | ||
): Promise<IPrecomputedConfigurationResponse | undefined>; | ||
rawGet<T>(url: URL): Promise<T | undefined>; | ||
@@ -80,5 +73,8 @@ rawPost<T, P>(url: URL, payload: P): Promise<T | undefined>; | ||
payload: PrecomputedFlagsPayload, | ||
): Promise<IPrecomputedFlagsResponse | undefined> { | ||
): Promise<IPrecomputedConfigurationResponse | undefined> { | ||
const url = this.apiEndpoints.precomputedFlagsEndpoint(); | ||
return await this.rawPost<IPrecomputedFlagsResponse, PrecomputedFlagsPayload>(url, payload); | ||
return await this.rawPost<IPrecomputedConfigurationResponse, PrecomputedFlagsPayload>( | ||
url, | ||
payload, | ||
); | ||
} | ||
@@ -85,0 +81,0 @@ |
@@ -24,4 +24,6 @@ import ApiEndpoints from './api-endpoints'; | ||
import EppoPrecomputedClient, { | ||
convertContextAttributesToSubjectAttributes, | ||
PrecomputedFlagsRequestParameters, | ||
} from './client/eppo-precomputed-client'; | ||
import { IConfigurationWire, IPrecomputedConfigurationResponse } from './configuration'; | ||
import FlagConfigRequestor from './configuration-requestor'; | ||
@@ -43,3 +45,4 @@ import { | ||
} from './events/default-event-dispatcher'; | ||
import EventDispatcher, { Event } from './events/event-dispatcher'; | ||
import Event from './events/event'; | ||
import EventDispatcher from './events/event-dispatcher'; | ||
import NamedEventQueue from './events/named-event-queue'; | ||
@@ -68,3 +71,2 @@ import NetworkStatusListener from './events/network-status-listener'; | ||
IContainerExperiment, | ||
PrecomputedFlagsRequestParameters, | ||
EppoClient, | ||
@@ -76,3 +78,7 @@ constants, | ||
validation, | ||
// Precomputed Client | ||
PrecomputedFlagsRequestParameters, | ||
EppoPrecomputedClient, | ||
convertContextAttributesToSubjectAttributes, | ||
@@ -102,3 +108,2 @@ // Configuration store | ||
ObfuscatedFlag, | ||
PrecomputedFlag, | ||
VariationType, | ||
@@ -122,2 +127,7 @@ AttributeType, | ||
Event, | ||
// Configuration interchange. | ||
IConfigurationWire, | ||
IPrecomputedConfigurationResponse, | ||
PrecomputedFlag, | ||
}; |
@@ -45,2 +45,3 @@ import { Rule } from './rules'; | ||
} | ||
export const UNKNOWN_ENVIRONMENT_NAME = 'UNKNOWN'; | ||
@@ -146,11 +147,19 @@ export interface ConfigDetails { | ||
export interface PrecomputedFlag { | ||
export type BasePrecomputedFlag = { | ||
flagKey?: string; | ||
allocationKey: string; | ||
variationKey: string; | ||
variationType: VariationType; | ||
variationValue: string; | ||
extraLogging: Record<string, string>; | ||
doLog: boolean; | ||
}; | ||
export interface PrecomputedFlag extends BasePrecomputedFlag { | ||
variationValue: string; | ||
} | ||
export interface DecodedPrecomputedFlag extends BasePrecomputedFlag { | ||
variationValue: Variation['value']; | ||
} | ||
export interface PrecomputedFlagsDetails { | ||
@@ -157,0 +166,0 @@ precomputedFlagsFetchedAt: string; |
import base64 = require('js-base64'); | ||
import * as SparkMD5 from 'spark-md5'; | ||
export function getMD5Hash(input: string): string { | ||
return SparkMD5.hash(input); | ||
import { PrecomputedFlag } from './interfaces'; | ||
export function getMD5Hash(input: string, salt = ''): string { | ||
return new SparkMD5().appendBinary(salt).append(input).end(); | ||
} | ||
@@ -15,1 +17,40 @@ | ||
} | ||
export function obfuscatePrecomputedFlags( | ||
salt: string, | ||
precomputedFlags: Record<string, PrecomputedFlag>, | ||
): Record<string, PrecomputedFlag> { | ||
const response: Record<string, PrecomputedFlag> = {}; | ||
Object.keys(precomputedFlags).map((flagKey) => { | ||
const assignment = precomputedFlags[flagKey]; | ||
// Encode extraLogging keys and values. | ||
const encodedExtraLogging = Object.fromEntries( | ||
Object.entries(assignment.extraLogging).map((kvArr) => kvArr.map(encodeBase64)), | ||
); | ||
const hashedKey = getMD5Hash(flagKey, salt); | ||
response[hashedKey] = { | ||
flagKey: hashedKey, | ||
variationType: assignment.variationType, | ||
extraLogging: encodedExtraLogging, | ||
doLog: assignment.doLog, | ||
allocationKey: encodeBase64(assignment.allocationKey), | ||
variationKey: encodeBase64(assignment.variationKey), | ||
variationValue: encodeBase64(assignment.variationValue), | ||
}; | ||
}); | ||
return response; | ||
} | ||
let saltOverrideBytes: Uint8Array | null; | ||
export function setSaltOverrideForTests(salt: Uint8Array | null) { | ||
saltOverrideBytes = salt ? salt : null; | ||
} | ||
export function generateSalt(length = 16): string { | ||
return base64.fromUint8Array( | ||
saltOverrideBytes ? saltOverrideBytes : crypto.getRandomValues(new Uint8Array(length)), | ||
); | ||
} |
import { IConfigurationStore } from './configuration-store/configuration-store'; | ||
import { hydrateConfigurationStore } from './configuration-store/configuration-store-utils'; | ||
import { IHttpClient } from './http-client'; | ||
import { PrecomputedFlag } from './interfaces'; | ||
import { PrecomputedFlag, UNKNOWN_ENVIRONMENT_NAME } from './interfaces'; | ||
import { Attributes } from './types'; | ||
export interface PrecomputedResponseData { | ||
salt: string; | ||
subjectKey: string; | ||
subjectAttributes: Attributes; | ||
} | ||
// Requests AND stores precomputed flags, reuses the configuration store | ||
export default class PrecomputedFlagRequestor { | ||
public onPrecomputedResponse?: (response: PrecomputedResponseData) => void; | ||
constructor( | ||
@@ -22,2 +30,10 @@ private readonly httpClient: IHttpClient, | ||
if (this.onPrecomputedResponse && precomputedResponse) { | ||
this.onPrecomputedResponse({ | ||
salt: precomputedResponse.salt, | ||
subjectKey: this.subjectKey, | ||
subjectAttributes: this.subjectAttributes, | ||
}); | ||
} | ||
if (!precomputedResponse?.flags) { | ||
@@ -29,3 +45,3 @@ return; | ||
entries: precomputedResponse.flags, | ||
environment: precomputedResponse.environment, | ||
environment: precomputedResponse.environment ?? { name: UNKNOWN_ENVIRONMENT_NAME }, | ||
createdAt: precomputedResponse.createdAt, | ||
@@ -32,0 +48,0 @@ format: precomputedResponse.format, |
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
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
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
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
1127858
265
9968
1
1