Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@eppo/js-client-sdk-common

Package Overview
Dependencies
Maintainers
8
Versions
86
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@eppo/js-client-sdk-common - npm Package Compare versions

Comparing version 4.6.3 to 4.7.0-alpha.0

dist/configuration.d.ts

11

dist/client/eppo-client.d.ts

@@ -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 @@ *

25

dist/client/eppo-precomputed-client.d.ts

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc