Skip to content

Commit

Permalink
Merge pull request #250 from splitio/sdks-7557-b
Browse files Browse the repository at this point in the history
[SDKS-7557/7554] getTreatment/s byFlagset withConfig/s
  • Loading branch information
emmaz90 authored Sep 27, 2023
2 parents 856f8b7 + fb7e99f commit 85f3498
Show file tree
Hide file tree
Showing 13 changed files with 451 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export interface ISplit {
configurations?: {
[treatmentName: string]: string
},
sets: string[]
sets?: string[]
}

// Split definition used in offline mode
Expand Down
73 changes: 72 additions & 1 deletion src/evaluator/__tests__/evaluate-features.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @ts-nocheck
import { evaluateFeatures } from '../index';
import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index';
import * as LabelsConstants from '../../utils/labels';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
import { _Set } from '../../utils/lang/sets';
import { returnSetsUnion } from '../../utils/lang/sets';

const splitsMock = {
regular: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': 1667452163, 'trafficAllocation': 100, 'trafficTypeName': 'user', 'name': 'always-on', 'seed': 1684183541, 'configurations': {}, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': '', 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': '', 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] },
Expand All @@ -14,6 +16,11 @@ const splitsMock = {
trafficAlocation1WithConfig: { 'changeNumber': 1487277320548, 'trafficAllocationSeed': -1667452163, 'trafficAllocation': 1, 'trafficTypeName': 'user', 'name': 'always-on6', 'seed': 1684183541, 'configurations': { 'off': "{color:'black'}" }, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': '' }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': { 'segmentName': '' }, 'unaryNumericMatcherData': { 'dataType': '', 'value': 0 }, 'whitelistMatcherData': { 'whitelist': null }, 'betweenMatcherData': { 'dataType': '', 'start': 0, 'end': 0 } }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] }
};

const flagSetsMock = {
reg_and_config: new _Set(['regular', 'config']),
arch_and_killed: new _Set(['killed', 'archived']),
};

const mockStorage = {
splits: {
getSplit(name) {
Expand All @@ -29,6 +36,16 @@ const mockStorage = {
});

return splits;
},
getNamesByFlagSets(flagSets) {
let toReturn = new _Set([]);
flagSets.forEach(flagset => {
const featureFlagNames = flagSetsMock[flagset];
if (featureFlagNames) {
toReturn = returnSetsUnion(toReturn, featureFlagNames);
}
});
return toReturn;
}
}
};
Expand Down Expand Up @@ -105,3 +122,57 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre
// If the split is retrieved but is not in split (out of Traffic Allocation), we should get the right evaluation result, label and config.

});

test('EVALUATOR - Multiple evaluations at once by flag sets / should return right labels, treatments and configs if storage returns without errors.', async function () {

const expectedOutput = {
config: {
treatment: 'on', label: 'in segment all',
config: '{color:\'black\'}', changeNumber: 1487277320548
},
not_existent_split: {
treatment: 'control', label: LabelsConstants.SPLIT_NOT_FOUND, config: null
},
};

const getResultsByFlagsets = (flagSets: string[]) => {
return evaluateFeaturesByFlagSets(
loggerMock,
'fake-key',
flagSets,
null,
mockStorage,
);
};



let multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config', 'arch_and_killed']);

// assert evaluationWithConfig
expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config.
// @todo assert flag set not found - for input validations

// assert regular
expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], config: null }); // If the split is retrieved successfully we should get the right evaluation result, label and config. If Split has no config it should have config equal null.
// assert killed
expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual({ ...expectedOutput['config'], treatment: 'off', config: null, label: LabelsConstants.SPLIT_KILLED });
// 'If the split is retrieved but is killed, we should get the right evaluation result, label and config.

// assert archived
expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: LabelsConstants.SPLIT_ARCHIVED, config: null });
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.

// assert not_existent_split not in evaluation if it is not related to defined flag sets
expect(multipleEvaluationAtOnceByFlagSets['not_existent_split']).toEqual(undefined);

multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets([]);
expect(multipleEvaluationAtOnceByFlagSets).toEqual({});

multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config']);
expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']);
expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], config: null });
expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual(undefined);
expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual(undefined);

});
24 changes: 24 additions & 0 deletions src/evaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IStorageAsync, IStorageSync } from '../storages/types';
import { IEvaluationResult } from './types';
import { SplitIO } from '../types';
import { ILogger } from '../logger/types';
import { ISet, setToArray } from '../utils/lang/sets';

const treatmentException = {
treatment: CONTROL,
Expand Down Expand Up @@ -87,6 +88,29 @@ export function evaluateFeatures(
getEvaluations(log, splitNames, parsedSplits, key, attributes, storage);
}

export function evaluateFeaturesByFlagSets(
log: ILogger,
key: SplitIO.SplitKey,
flagSets: string[],
attributes: SplitIO.Attributes | undefined,
storage: IStorageSync | IStorageAsync,
): MaybeThenable<Record<string,IEvaluationResult>> {
let storedFlagNames: MaybeThenable<ISet<string>>;

// get features by flag sets
try {
storedFlagNames = storage.splits.getNamesByFlagSets(flagSets);
} catch (e) {
// return empty evaluations
return {};
}

// evaluate related features
return thenable(storedFlagNames) ?
storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) :
evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage);
}

function getEvaluation(
log: ILogger,
splitJSON: ISplit | null,
Expand Down
104 changes: 104 additions & 0 deletions src/sdkClient/__tests__/clientAttributesDecoration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ const clientMock = {
},
getTreatmentsWithConfig(maybeKey: any, maybeFeatureFlagNames: string[], maybeAttributes?: any) {
return maybeAttributes;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getTreatmentsByFlagSets(maybeKey: any, maybeFeatureFlagNames: string[], maybeAttributes?: any, maybeFlagSetNames?: string[] | undefined) {
return maybeAttributes;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getTreatmentsWithConfigByFlagSets(maybeKey: any, maybeFeatureFlagNames: string[], maybeAttributes?: any, maybeFlagSetNames?: string[] | undefined) {
return maybeAttributes;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getTreatmentsByFlagSet(maybeKey: any, maybeFeatureFlagNames: string[], maybeAttributes?: any, maybeFlagSetName?: string | undefined) {
return maybeAttributes;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getTreatmentsWithConfigByFlagSet(maybeKey: any, maybeFeatureFlagNames: string[], maybeAttributes?: any, maybeFlagSetName?: string | undefined) {
return maybeAttributes;
}
};
// @ts-expect-error
Expand Down Expand Up @@ -225,4 +241,92 @@ describe('ATTRIBUTES DECORATION / evaluation', () => {

});

test('Evaluation attributes logic and precedence / getTreatmentsByFlagSets', () => {

// If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence.
expect(client.getTreatmentsByFlagSets('key', ['set'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api
expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api
expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty
client.setAttribute('func_attr_bool', false);
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute
expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations
expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations
client.setAttributes({ func_attr_str: 'false' });
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes
expect(client.getTreatmentsByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
expect(client.getTreatmentsByFlagSets('key', ['set'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
client.clearAttributes();

});

test('Evaluation attributes logic and precedence / getTreatmentsWithConfigByFlagSets', () => {

// If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence.
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'])).toEqual(undefined); // Nothing changes if no attributes were provided using the new api
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api
expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty
client.setAttribute('func_attr_bool', false);
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations
client.setAttributes({ func_attr_str: 'false' });
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'], null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
expect(client.getTreatmentsWithConfigByFlagSets('key', ['set'])).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
client.clearAttributes();

});

test('Evaluation attributes logic and precedence / getTreatmentsByFlagSet', () => {

// If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence.
expect(client.getTreatmentsByFlagSet('key', 'set')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api
expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api
expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty
client.setAttribute('func_attr_bool', false);
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute
expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations
expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations
client.setAttributes({ func_attr_str: 'false' });
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes
expect(client.getTreatmentsByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
expect(client.getTreatmentsByFlagSet('key', 'set')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
client.clearAttributes();

});

test('Evaluation attributes logic and precedence / getTreatmentsWithConfigByFlagSet', () => {

// If the same attribute is “cached” and provided on the function, the value received on the function call takes precedence.
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set')).toEqual(undefined); // Nothing changes if no attributes were provided using the new api
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Nothing changes if no attributes were provided using the new api
expect(client.getAttributes()).toEqual({}); // Attributes in memory storage must be empty
client.setAttribute('func_attr_bool', false);
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false }); // In memory attribute storage must have the unique stored attribute
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true' })).toEqual({ func_attr_bool: true, func_attr_str: 'true' }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false }); // API attributes should be kept in memory and use for evaluations
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_str: 'true' })).toEqual({ func_attr_bool: false, func_attr_str: 'true' }); // API attributes should be kept in memory and use for evaluations
client.setAttributes({ func_attr_str: 'false' });
expect(client.getAttributes()).toEqual({ 'func_attr_bool': false, 'func_attr_str': 'false' }); // In memory attribute storage must have two stored attributes
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', { func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 })).toEqual({ func_attr_bool: true, func_attr_str: 'true', func_attr_number: 1 }); // Function attributes has precedence against api ones
// @ts-ignore
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set', null)).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
expect(client.getTreatmentsWithConfigByFlagSet('key', 'set')).toEqual({ func_attr_bool: false, func_attr_str: 'false' }); // If the getTreatment function is called without attributes, stored attributes will be used to evaluate.
client.clearAttributes();

});

});
2 changes: 1 addition & 1 deletion src/sdkClient/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const clientApiMethods = ['getTreatment', 'getTreatments', 'getTreatmentWithConfig', 'getTreatmentsWithConfig', 'track', 'destroy'];
const clientApiMethods = ['getTreatment', 'getTreatments', 'getTreatmentWithConfig', 'getTreatmentsWithConfig', 'getTreatmentsByFlagSets', 'getTreatmentsWithConfigByFlagSets', 'getTreatmentsByFlagSet', 'getTreatmentsWithConfigByFlagSet', 'track', 'destroy'];

export function assertClientApi(client: any, sdkStatus?: object) {

Expand Down
Loading

0 comments on commit 85f3498

Please sign in to comment.