Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
2.12.0 (February 24, 2026)
- Added support for ioredis v5.
- Added support for ioredis v5 (Related to issue https://github.com/splitio/javascript-commons/issues/471).

2.11.0 (January 28, 2026)
- Added functionality to provide metadata alongside SDK update and READY events. Read more in our docs.
Expand Down
2 changes: 1 addition & 1 deletion src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface IBetweenStringMatcherData {
}

export interface IWhitelistMatcherData {
whitelist: string[]
whitelist?: string[] | null
}

export interface IInSegmentMatcherData {
Expand Down
47 changes: 24 additions & 23 deletions src/evaluator/__tests__/evaluate-feature.spec.ts

Large diffs are not rendered by default.

53 changes: 26 additions & 27 deletions src/evaluator/__tests__/evaluate-features.spec.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/evaluator/condition/engineUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export function getTreatment(log: ILogger, key: string, seed: number | undefined
/**
* Evaluates the traffic allocation to see if we should apply rollout conditions or not.
*/
export function shouldApplyRollout(trafficAllocation: number, key: string, trafficAllocationSeed: number) {
export function shouldApplyRollout(trafficAllocation: number, bucketingKey: string, trafficAllocationSeed: number) {
// For rollout, if traffic allocation for splits is 100%, we don't need to filter it because everything should evaluate the rollout.
if (trafficAllocation < 100) {
const _bucket = bucket(key, trafficAllocationSeed);
const _bucket = bucket(bucketingKey, trafficAllocationSeed);

if (_bucket > trafficAllocation) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ describe('FallbackTreatmentsCalculator' , () => {
'featureA': { treatment: 'TREATMENT_A', config: '{ value: 1 }' },
},
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('featureA', 'label by flag');
const calculator = FallbackTreatmentsCalculator(config);
const result = calculator('featureA', 'label by flag');

expect(result).toEqual({
treatment: 'TREATMENT_A',
Expand All @@ -24,8 +24,8 @@ describe('FallbackTreatmentsCalculator' , () => {
byFlag: {},
global: { treatment: 'GLOBAL_TREATMENT', config: '{ global: true }' },
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('missingFlag', 'label by global');
const calculator = FallbackTreatmentsCalculator(config);
const result = calculator('missingFlag', 'label by global');

expect(result).toEqual({
treatment: 'GLOBAL_TREATMENT',
Expand All @@ -38,8 +38,8 @@ describe('FallbackTreatmentsCalculator' , () => {
const config: FallbackTreatmentConfiguration = {
byFlag: {},
};
const calculator = new FallbackTreatmentsCalculator(config);
const result = calculator.resolve('missingFlag', 'label by noFallback');
const calculator = FallbackTreatmentsCalculator(config);
const result = calculator('missingFlag', 'label by noFallback');

expect(result).toEqual({
treatment: CONTROL,
Expand Down
64 changes: 23 additions & 41 deletions src/evaluator/fallbackTreatmentsCalculator/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,32 @@
import { FallbackTreatmentConfiguration, Treatment, TreatmentWithConfig } from '../../../types/splitio';
import { FallbackTreatmentConfiguration, TreatmentWithConfig } from '../../../types/splitio';
import { CONTROL } from '../../utils/constants';
import { isString } from '../../utils/lang';

export type IFallbackTreatmentsCalculator = {
resolve(flagName: string, label: string): TreatmentWithConfig & { label: string };
}
export type IFallbackTreatmentsCalculator = (flagName: string, label?: string) => TreatmentWithConfig & { label: string };

export const FALLBACK_PREFIX = 'fallback - ';

export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator {
private readonly fallbacks: FallbackTreatmentConfiguration;

constructor(fallbacks: FallbackTreatmentConfiguration = {}) {
this.fallbacks = fallbacks;
}

resolve(flagName: string, label: string): TreatmentWithConfig & { label: string } {
const treatment = this.fallbacks.byFlag?.[flagName];
if (treatment) {
return this.copyWithLabel(treatment, label);
}

if (this.fallbacks.global) {
return this.copyWithLabel(this.fallbacks.global, label);
}

return {
treatment: CONTROL,
config: null,
label,
};
}

private copyWithLabel(fallback: Treatment | TreatmentWithConfig, label: string): TreatmentWithConfig & { label: string } {
if (isString(fallback)) {
return {
treatment: fallback,
export function FallbackTreatmentsCalculator(fallbacks: FallbackTreatmentConfiguration = {}): IFallbackTreatmentsCalculator {

return (flagName: string, label = '') => {
const fallback = fallbacks.byFlag?.[flagName] || fallbacks.global;

return fallback ?
isString(fallback) ?
{
treatment: fallback,
config: null,
label: `${FALLBACK_PREFIX}${label}`,
} :
{
treatment: fallback.treatment,
config: fallback.config,
label: `${FALLBACK_PREFIX}${label}`,
} :
{
treatment: CONTROL,
config: null,
label: `${FALLBACK_PREFIX}${label}`,
label,
};
}

return {
treatment: fallback.treatment,
config: fallback.config,
label: `${FALLBACK_PREFIX}${label}`,
};
}
};
}
4 changes: 2 additions & 2 deletions src/evaluator/matchers/whitelist.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export function whitelistMatcherContext(ruleAttr: string[]) {
const whitelistSet = new Set(ruleAttr);
export function whitelistMatcherContext(ruleAttr?: string[] | null) {
const whitelistSet = new Set(ruleAttr || []);

return function whitelistMatcher(runtimeAttr: string): boolean {
const isInWhitelist = whitelistSet.has(runtimeAttr);
Expand Down
1 change: 0 additions & 1 deletion src/logger/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export const ENGINE_DEFAULT = 41;

export const CLIENT_READY_FROM_CACHE = 100;
export const CLIENT_READY = 101;
export const IMPRESSION = 102;
export const IMPRESSION_QUEUEING = 103;
export const NEW_SHARED_CLIENT = 104;
export const NEW_FACTORY = 105;
Expand Down
3 changes: 1 addition & 2 deletions src/logger/messages/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([
[c.CLIENT_READY_FROM_CACHE, READY_MSG + ' from cache'],
[c.CLIENT_READY, READY_MSG],
// SDK
[c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Feature flag: %s. Key: %s. Evaluation: %s. Label: %s'],
[c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression.'],
[c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing impression. Feature flag: %s. Key: %s. Evaluation: %s. Label: %s'],
[c.NEW_SHARED_CLIENT, 'New shared client instance created.'],
[c.NEW_FACTORY, 'New Split SDK instance created. %s'],
[c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully queued %s'],
Expand Down
2 changes: 1 addition & 1 deletion src/sdkClient/__tests__/clientInputValidation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const settings: any = {
const EVALUATION_RESULT = 'on';
const client: any = createClientMock(EVALUATION_RESULT);

const fallbackTreatmentsCalculator: IFallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings);
const fallbackTreatmentsCalculator: IFallbackTreatmentsCalculator = FallbackTreatmentsCalculator();

const readinessManager: any = {
isReadyFromCache: () => true,
Expand Down
4 changes: 2 additions & 2 deletions src/sdkClient/__tests__/sdkClientMethod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const paramMocks = [
telemetryTracker: telemetryTrackerFactory(),
clients: {},
impressionsTracker: { start: jest.fn(), stop: jest.fn(), track: jest.fn() },
fallbackTreatmentsCalculator: new FallbackTreatmentsCalculator({})
fallbackTreatmentsCalculator: FallbackTreatmentsCalculator({})
},
// SyncManager (i.e., Sync SDK) and Signal listener
{
Expand All @@ -31,7 +31,7 @@ const paramMocks = [
telemetryTracker: telemetryTrackerFactory(),
clients: {},
impressionsTracker: { start: jest.fn(), stop: jest.fn(), track: jest.fn() },
fallbackTreatmentsCalculator: new FallbackTreatmentsCalculator({})
fallbackTreatmentsCalculator: FallbackTreatmentsCalculator({})
}
];

Expand Down
8 changes: 3 additions & 5 deletions src/sdkClient/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SDK_NOT_READY } from '../utils/labels';
import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, 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, TRACK_FN_LABEL } from '../utils/constants';
import { IEvaluationResult } from '../evaluator/types';
import SplitIO from '../../types/splitio';
import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants';
import { IMPRESSION_QUEUEING } from '../logger/constants';
import { ISdkFactoryContext } from '../sdkFactory/types';
import { isConsumerMode } from '../utils/settingsValidation/mode';
import { Method } from '../sync/submitters/types';
Expand Down Expand Up @@ -147,16 +147,14 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
let { treatment, label, config = null } = evaluation;

if (treatment === CONTROL) {
const fallbackTreatment = fallbackTreatmentsCalculator.resolve(featureFlagName, label);
const fallbackTreatment = fallbackTreatmentsCalculator(featureFlagName, label);
treatment = fallbackTreatment.treatment;
label = fallbackTreatment.label;
config = fallbackTreatment.config;
}

log.info(IMPRESSION, [featureFlagName, matchingKey, treatment, label]);

if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) {
log.info(IMPRESSION_QUEUEING);
log.info(IMPRESSION_QUEUEING, [featureFlagName, matchingKey, treatment, label]);
queue.push({
imp: {
feature: featureFlagName,
Expand Down
99 changes: 38 additions & 61 deletions src/sdkClient/clientInputValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,20 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
};
}

function evaluateFallBackTreatment(featureFlagName: string, withConfig: boolean): SplitIO.Treatment | SplitIO.TreatmentWithConfig {
const { treatment, config } = fallbackTreatmentsCalculator.resolve(featureFlagName, '');
function evaluateFallBackTreatment(featureFlagName: string | false | string[], withConfig: boolean) {
if (Array.isArray(featureFlagName)) {
const res: SplitIO.Treatments = {};
featureFlagName.forEach((split: string) => res[split] = evaluateFallBackTreatment(split, withConfig) as SplitIO.Treatment);
return res;
}

const { treatment, config } = fallbackTreatmentsCalculator(featureFlagName as string);

if (withConfig) {
return {
return withConfig ?
{
treatment,
config
};
}

return treatment;
} : treatment;
}

function wrapResult<T>(value: T): MaybeThenable<T> {
Expand All @@ -79,89 +82,65 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
function getTreatment(maybeKey: SplitIO.SplitKey, maybeFeatureFlagName: string, maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENT, maybeKey, maybeFeatureFlagName, maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatment(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
const result = evaluateFallBackTreatment(params.nameOrNames as string, false);
return wrapResult(result);
}
return params.valid ?
client.getTreatment(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult(evaluateFallBackTreatment(params.nameOrNames, false));
}

function getTreatmentWithConfig(maybeKey: SplitIO.SplitKey, maybeFeatureFlagName: string, maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENT_WITH_CONFIG, maybeKey, maybeFeatureFlagName, maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatmentWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
const result = evaluateFallBackTreatment(params.nameOrNames as string, true);
return wrapResult(result);
}
return params.valid ?
client.getTreatmentWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult(evaluateFallBackTreatment(params.nameOrNames, true));
}

function getTreatments(maybeKey: SplitIO.SplitKey, maybeFeatureFlagNames: string[], maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENTS, maybeKey, maybeFeatureFlagNames, maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatments(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
const res: SplitIO.Treatments = {};
if (params.nameOrNames) (params.nameOrNames as string[]).forEach((split: string) => res[split] = evaluateFallBackTreatment(split, false) as SplitIO.Treatment);

return wrapResult(res);
}
return params.valid ?
client.getTreatments(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult(evaluateFallBackTreatment(params.nameOrNames || [], false));
}

function getTreatmentsWithConfig(maybeKey: SplitIO.SplitKey, maybeFeatureFlagNames: string[], maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENTS_WITH_CONFIG, maybeKey, maybeFeatureFlagNames, maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatmentsWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
const res: SplitIO.TreatmentsWithConfig = {};
if (params.nameOrNames) (params.nameOrNames as string[]).forEach(split => res[split] = evaluateFallBackTreatment(split, true) as SplitIO.TreatmentWithConfig);

return wrapResult(res);
}
return params.valid ?
client.getTreatmentsWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult(evaluateFallBackTreatment(params.nameOrNames || [], true));
}

function getTreatmentsByFlagSets(maybeKey: SplitIO.SplitKey, maybeFlagSets: string[], maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENTS_BY_FLAG_SETS, maybeKey, maybeFlagSets, maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatmentsByFlagSets(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
return wrapResult({});
}
return params.valid ?
client.getTreatmentsByFlagSets(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult({});
}

function getTreatmentsWithConfigByFlagSets(maybeKey: SplitIO.SplitKey, maybeFlagSets: string[], maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, maybeKey, maybeFlagSets, maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatmentsWithConfigByFlagSets(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
return wrapResult({});
}
return params.valid ?
client.getTreatmentsWithConfigByFlagSets(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult({});
}

function getTreatmentsByFlagSet(maybeKey: SplitIO.SplitKey, maybeFlagSet: string, maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENTS_BY_FLAG_SET, maybeKey, [maybeFlagSet], maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatmentsByFlagSet(params.key as SplitIO.SplitKey, (params.nameOrNames as string[])[0], params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
return wrapResult({});
}
return params.valid ?
client.getTreatmentsByFlagSet(params.key as SplitIO.SplitKey, (params.nameOrNames as string[])[0], params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult({});
}

function getTreatmentsWithConfigByFlagSet(maybeKey: SplitIO.SplitKey, maybeFlagSet: string, maybeAttributes?: SplitIO.Attributes, maybeOptions?: SplitIO.EvaluationOptions) {
const params = validateEvaluationParams(GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, maybeKey, [maybeFlagSet], maybeAttributes, maybeOptions);

if (params.valid) {
return client.getTreatmentsWithConfigByFlagSet(params.key as SplitIO.SplitKey, (params.nameOrNames as string[])[0], params.attributes as SplitIO.Attributes | undefined, params.options);
} else {
return wrapResult({});
}
return params.valid ?
client.getTreatmentsWithConfigByFlagSet(params.key as SplitIO.SplitKey, (params.nameOrNames as string[])[0], params.attributes as SplitIO.Attributes | undefined, params.options) :
wrapResult({});
}

function track(maybeKey: SplitIO.SplitKey, maybeTT: string, maybeEvent: string, maybeEventValue?: number, maybeProperties?: SplitIO.Properties) {
Expand All @@ -172,11 +151,9 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
const { properties, size } = validateEventProperties(log, maybeProperties, TRACK_FN_LABEL);
const isNotDestroyed = validateIfNotDestroyed(log, readinessManager, TRACK_FN_LABEL);

if (isNotDestroyed && key && tt && event && eventValue !== false && properties !== false) { // @ts-expect-error
return client.track(key, tt, event, eventValue, properties, size);
} else {
return isAsync ? Promise.resolve(false) : false;
}
return isNotDestroyed && key && tt && event && eventValue !== false && properties !== false ? // @ts-expect-error
client.track(key, tt, event, eventValue, properties, size) :
wrapResult(false);
}

return {
Expand Down
2 changes: 1 addition & 1 deletion src/sdkFactory/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const paramsForAsyncSDK = {
platform: {
EventEmitter
},
fallbackTreatmentsCalculator: new FallbackTreatmentsCalculator()
fallbackTreatmentsCalculator: FallbackTreatmentsCalculator()
};

// IBrowserSDK, full params
Expand Down
2 changes: 1 addition & 1 deletion src/sdkFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
}
});

const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.fallbackTreatments);
const fallbackTreatmentsCalculator = FallbackTreatmentsCalculator(settings.fallbackTreatments);

if (initialRolloutPlan) {
setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key));
Expand Down