@splitsoftware/splitio-commons
Version:
Split JavaScript SDK common components
197 lines (164 loc) • 9.37 kB
text/typescript
import { evaluateFeature, evaluateFeatures, evaluateFeaturesByFlagSets } from '../evaluator';
import { thenable } from '../utils/promise/thenable';
import { getMatching, getBucketing } from '../utils/key';
import { validateDefinitionExistence } from '../utils/inputValidation/definitionExistence';
import { SDK_NOT_READY } from '../utils/labels';
import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, TREATMENTS_BY_FLAGSETS, TREATMENTS_BY_FLAGSET, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENT_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS } from '../utils/constants';
import { IEvaluationResult } from '../evaluator/types';
import SplitIO from '../../types/splitio';
import { IMPRESSION_QUEUEING } from '../logger/constants';
import { ISdkFactoryContext } from '../sdkFactory/types';
import { isConsumerMode } from '../utils/settingsValidation/mode';
import { Method } from '../sync/submitters/types';
import { ImpressionDecorated } from '../trackers/types';
import { trackMethodFactory } from './trackMethod';
const treatmentNotReady = { treatment: CONTROL, label: SDK_NOT_READY };
function treatmentsNotReady(featureFlagNames: string[]) {
const evaluations: Record<string, IEvaluationResult> = {};
featureFlagNames.forEach(featureFlagName => {
evaluations[featureFlagName] = treatmentNotReady;
});
return evaluations;
}
export function stringify(options?: SplitIO.EvaluationOptions) {
if (options && options.properties) {
try {
return JSON.stringify(options.properties);
} catch { /* JSON.stringify should never throw with validated options, but handling just in case */ }
}
}
/**
* Creator of base client with getTreatments and track methods.
*/
export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient {
const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, telemetryTracker, fallbackCalculator } = params;
const { log, mode } = settings;
const isAsync = isConsumerMode(mode);
function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) {
const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT);
const wrapUp = (evaluationResult: IEvaluationResult) => {
const queue: ImpressionDecorated[] = [];
const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue);
impressionsTracker.track(queue, attributes);
stopTelemetryTracker(queue[0] && queue[0].imp.label);
return treatment;
};
const evaluation = readinessManager.isReadyFromCache() ?
evaluateFeature(log, key, featureFlagName, attributes, storage, options) :
isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected
Promise.resolve(treatmentNotReady) :
treatmentNotReady;
return thenable(evaluation) ? evaluation.then((res) => wrapUp(res)) : wrapUp(evaluation);
}
function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatment(key, featureFlagName, attributes, options, true, GET_TREATMENT_WITH_CONFIG);
}
function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) {
const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS);
const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
const queue: ImpressionDecorated[] = [];
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
const properties = stringify(options);
Object.keys(evaluationResults).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
});
impressionsTracker.track(queue, attributes);
stopTelemetryTracker(queue[0] && queue[0].imp.label);
return treatments;
};
const evaluations = readinessManager.isReadyFromCache() ?
evaluateFeatures(log, key, featureFlagNames, attributes, storage, options) :
isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected
Promise.resolve(treatmentsNotReady(featureFlagNames)) :
treatmentsNotReady(featureFlagNames);
return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations);
}
function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG);
}
function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) {
const stopTelemetryTracker = telemetryTracker.trackEval(method);
const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
const queue: ImpressionDecorated[] = [];
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
const properties = stringify(options);
Object.keys(evaluationResults).forEach(featureFlagName => {
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
});
impressionsTracker.track(queue, attributes);
stopTelemetryTracker(queue[0] && queue[0].imp.label);
return treatments;
};
const evaluations = readinessManager.isReadyFromCache() ?
evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName, options) :
isAsync ?
Promise.resolve({}) :
{};
return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations);
}
function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatmentsByFlagSets(key, flagSetNames, attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
}
function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET);
}
function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
}
// Internal function
function processEvaluation(
evaluation: IEvaluationResult,
featureFlagName: string,
key: SplitIO.SplitKey,
properties: string | undefined,
withConfig: boolean,
invokingMethodName: string,
queue: ImpressionDecorated[]
): SplitIO.Treatment | SplitIO.TreatmentWithConfig {
const matchingKey = getMatching(key);
const bucketingKey = getBucketing(key);
const { changeNumber, impressionsDisabled } = evaluation;
let { treatment, label, config = null } = evaluation;
if (treatment === CONTROL) {
const fallbackTreatment = fallbackCalculator(featureFlagName, label);
treatment = fallbackTreatment.treatment;
label = fallbackTreatment.label;
config = fallbackTreatment.config;
}
if (validateDefinitionExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) {
log.info(IMPRESSION_QUEUEING, ['Feature flag', featureFlagName, matchingKey, treatment, label]);
queue.push({
imp: {
feature: featureFlagName,
keyName: matchingKey,
treatment,
time: Date.now(),
bucketingKey,
label,
changeNumber: changeNumber as number,
properties
},
disabled: impressionsDisabled
});
}
if (withConfig) {
return {
treatment,
config: config as string | null
};
}
return treatment;
}
const track = trackMethodFactory(params);
return {
getTreatment,
getTreatmentWithConfig,
getTreatments,
getTreatmentsWithConfig,
getTreatmentsByFlagSets,
getTreatmentsWithConfigByFlagSets,
getTreatmentsByFlagSet,
getTreatmentsWithConfigByFlagSet,
track,
} as SplitIO.IClient | SplitIO.IAsyncClient;
}