From b925b356ab8b143f7682c09bf08daf1608802f4b Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 20 Sep 2023 18:10:11 -0300 Subject: [PATCH] [SDKS-7557/7554] getTreatment/s byFlagset withConfig/s --- .../__tests__/evaluate-features.spec.ts | 57 ++++++++- src/evaluator/index.ts | 25 ++++ .../clientAttributesDecoration.spec.ts | 104 ++++++++++++++++ src/sdkClient/__tests__/testUtils.ts | 2 +- src/sdkClient/client.ts | 42 ++++++- src/sdkClient/clientAttributesDecoration.ts | 24 ++++ src/sdkClient/clientCS.ts | 4 + src/sdkClient/clientInputValidation.ts | 54 ++++++++- src/storages/AbstractSplitsCacheAsync.ts | 2 + src/storages/AbstractSplitsCacheSync.ts | 6 + src/storages/inRedis/SplitsCacheInRedis.ts | 12 ++ .../pluggable/SplitsCachePluggable.ts | 12 ++ src/storages/types.ts | 10 +- src/types.ts | 112 ++++++++++++++++++ 14 files changed, 458 insertions(+), 8 deletions(-) diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index c400c983..c37b0ffe 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -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' }] }, @@ -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) { @@ -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; } } }; @@ -105,3 +122,41 @@ 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 flagsets / 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 multipleEvaluationAtOnce = await evaluateFeaturesByFlagSets( + loggerMock, + 'fake-key', + ['reg_and_config', 'arch_and_killed'], + null, + mockStorage, + ); + + // assert evaluationWithConfig + expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config. + // @todo assert flagset not found - for input validations + + // assert regular + expect(multipleEvaluationAtOnce['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(multipleEvaluationAtOnce['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(multipleEvaluationAtOnce['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 flagsets + expect(multipleEvaluationAtOnce['not_existent_split']).toEqual(undefined); + +}); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index dad4c9e4..10d880cb 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -7,6 +7,7 @@ import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluationResult } from './types'; import { SplitIO } from '../types'; import { ILogger } from '../logger/types'; +import { setToArray } from '../utils/lang/sets'; const treatmentException = { treatment: CONTROL, @@ -87,6 +88,30 @@ 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> { + let storedSplitNames; + + // get ff by flagsets + try { + storedSplitNames = storage.splits.getNamesByFlagsets(flagsets); + } catch (e) { + // Exception on sync `getSplits` storage. Not possible ATM with InMemory and InLocal storages. + // @todo - review exception + return treatmentsException(flagsets); + } + + return thenable(storedSplitNames) ? + storedSplitNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) : + evaluateFeatures(log, key, setToArray(storedSplitNames), attributes, storage); + +} + function getEvaluation( log: ILogger, splitJSON: ISplit | null, diff --git a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts index 45fe70ff..e007dc54 100644 --- a/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts +++ b/src/sdkClient/__tests__/clientAttributesDecoration.spec.ts @@ -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 @@ -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(); + + }); + }); diff --git a/src/sdkClient/__tests__/testUtils.ts b/src/sdkClient/__tests__/testUtils.ts index 30ee5966..901897e3 100644 --- a/src/sdkClient/__tests__/testUtils.ts +++ b/src/sdkClient/__tests__/testUtils.ts @@ -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) { diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 11036519..7fed2261 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -1,4 +1,4 @@ -import { evaluateFeature, evaluateFeatures } from '../evaluator'; +import { evaluateFeature, evaluateFeatures, evaluateFeaturesByFlagSets } from '../evaluator'; import { thenable } from '../utils/promise/thenable'; import { getMatching, getBucketing } from '../utils/key'; import { validateSplitExistance } from '../utils/inputValidation/splitExistance'; @@ -81,6 +81,42 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return getTreatments(key, featureFlagNames, attributes, true); } + function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false) { + const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS); + + const wrapUp = (evaluationResults: Record) => { + const queue: ImpressionDTO[] = []; + const treatments: Record = {}; + Object.keys(evaluationResults).forEach(featureFlagName => { + treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, `getTreatmentsByFlagSets${withConfig ? 'WithConfig' : ''}`, queue); + }); + impressionsTracker.track(queue, attributes); + + stopTelemetryTracker(queue[0] && queue[0].label); + return treatments; + }; + + const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? + evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage) : + isStorageSync(settings) ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected + treatmentsNotReady([]) : + Promise.resolve(treatmentsNotReady([])); // Promisify if async + + return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); + } + + function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, featureFlagNames: string[], attributes: SplitIO.Attributes | undefined) { + return getTreatmentsByFlagSets(key, featureFlagNames, attributes, true); + } + + function getTreatmentsByFlagSet(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) { + return getTreatmentsByFlagSets(key, [featureFlagName], attributes); + } + + function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) { + return getTreatmentsByFlagSets(key, [featureFlagName], attributes, true); + } + // Internal function function processEvaluation( evaluation: IEvaluationResult, @@ -155,6 +191,10 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl getTreatmentWithConfig, getTreatments, getTreatmentsWithConfig, + getTreatmentsByFlagSets, + getTreatmentsWithConfigByFlagSets, + getTreatmentsByFlagSet, + getTreatmentsWithConfigByFlagSet, track, isClientSide: false } as SplitIO.IClient | SplitIO.IAsyncClient; diff --git a/src/sdkClient/clientAttributesDecoration.ts b/src/sdkClient/clientAttributesDecoration.ts index 6db04e3e..57413ad9 100644 --- a/src/sdkClient/clientAttributesDecoration.ts +++ b/src/sdkClient/clientAttributesDecoration.ts @@ -16,6 +16,10 @@ export function clientAttributesDecoration abstract getAll(): Promise abstract getSplitNames(): Promise + abstract getNamesByFlagsets(flagsets: string[]): Promise> abstract trafficTypeExists(trafficType: string): Promise abstract clear(): Promise diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 5bc76665..74b33482 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -1,6 +1,7 @@ import { ISplitsCacheSync } from './types'; import { ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; +import { ISet, _Set } from '../utils/lang/sets'; /** * This class provides a skeletal implementation of the ISplitsCacheSync interface @@ -77,6 +78,11 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { } return false; } + /** NO-OP */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getNamesByFlagsets(flagsets: string[]): ISet { + return new _Set([]); + } } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 43f86481..cf92fa1b 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -5,6 +5,7 @@ import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISplit } from '../../dtos/types'; import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync'; +import { ISet, _Set } from '../../utils/lang/sets'; /** * Discard errors for an answer of multiple operations. @@ -188,6 +189,17 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { ); } + /** + * Get list of split names related to a given flagset names list. + * The returned promise is resolved with the list of split names, + * or rejected if wrapper operation fails. + * @todo this is a no-op method to be implemented + */ + getNamesByFlagsets(): Promise> { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return new Promise(flagSets => new _Set([])); + } + /** * Check traffic type existence. * The returned promise is resolved with a boolean indicating whether the TT exist or not. diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 47421987..1e1bb889 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -5,6 +5,7 @@ import { ILogger } from '../../logger/types'; import { ISplit } from '../../dtos/types'; import { LOG_PREFIX } from './constants'; import { AbstractSplitsCacheAsync } from '../AbstractSplitsCacheAsync'; +import { ISet, _Set } from '../../utils/lang/sets'; /** * ISplitsCacheAsync implementation for pluggable storages. @@ -154,6 +155,17 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { ); } + /** + * Get list of split names related to a given flagset names list. + * The returned promise is resolved with the list of split names, + * or rejected if wrapper operation fails. + * @todo this is a no-op method to be implemented + */ + getNamesByFlagsets(): Promise> { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return new Promise(flagSets => new _Set([])); + } + /** * Check traffic type existence. * The returned promise is resolved with a boolean indicating whether the TT exist or not. diff --git a/src/storages/types.ts b/src/storages/types.ts index dc2dfbc2..5406f1be 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,6 +1,7 @@ import { MaybeThenable, ISplit } from '../dtos/types'; import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types'; import { SplitIO, ImpressionDTO, ISettings } from '../types'; +import { ISet } from '../utils/lang/sets'; /** * Interface of a pluggable storage wrapper. @@ -208,7 +209,8 @@ export interface ISplitsCacheBase { clear(): MaybeThenable, // should never reject or throw an exception. Instead return false by default, to avoid emitting SDK_READY_FROM_CACHE. checkCache(): MaybeThenable, - killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable + killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable, + getNamesByFlagsets(flagsets: string[]): MaybeThenable> } export interface ISplitsCacheSync extends ISplitsCacheBase { @@ -224,7 +226,8 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { usesSegments(): boolean, clear(): void, checkCache(): boolean, - killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean + killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean, + getNamesByFlagsets(flagsets: string[]): ISet } export interface ISplitsCacheAsync extends ISplitsCacheBase { @@ -240,7 +243,8 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { usesSegments(): Promise, clear(): Promise, checkCache(): Promise, - killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise + killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise, + getNamesByFlagsets(flagsets: string[]): Promise> } /** Segments cache */ diff --git a/src/types.ts b/src/types.ts index 1ed3100d..3fce8add 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1064,6 +1064,42 @@ export namespace SplitIO { * @returns {TreatmentsWithConfig} The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): TreatmentsWithConfig, + /** + * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flagSet. + * @function getTreatmentsByFlagSet + * @param {string} key - The string key representing the consumer. + * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): Treatments, + /** + * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flagSets. + * @function getTreatmentsWithConfigByFlagSet + * @param {string} key - The string key representing the consumer. + * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): TreatmentsWithConfig, + /** + * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flagSets. + * @function getTreatmentsByFlagSets + * @param {string} key - The string key representing the consumer. + * @param {Array} flagSets - An array of the flagSet names we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): Treatments, + /** + * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flagSets. + * @function getTreatmentsWithConfigByFlagSets + * @param {string} key - The string key representing the consumer. + * @param {Array} flagSets - An array of the flagSet names we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): TreatmentsWithConfig, /** * Tracks an event to be fed to the results product on Split user interface. * @function track @@ -1124,6 +1160,46 @@ export namespace SplitIO { * @returns {AsyncTreatmentsWithConfig} TreatmentsWithConfig promise that resolves to the map of TreatmentsWithConfig objects. */ getTreatmentsWithConfig(key: SplitKey, featureFlagNames: string[], attributes?: Attributes): AsyncTreatmentsWithConfig, + /** + * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the features related to the given flagset. + * For usage on NodeJS as we don't have only one key. + * @function getTreatmentsByFlagSet + * @param {string} key - The string key representing the consumer. + * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): AsyncTreatments, + /** + * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for features related to the given flagset. + * For usage on NodeJS as we don't have only one key. + * @function getTreatmentsWithConfigByFlagSet + * @param {string} key - The string key representing the consumer. + * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): AsyncTreatmentsWithConfig, + /** + * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the feature flags related to the given flagSets. + * For usage on NodeJS as we don't have only one key. + * @function getTreatmentsByFlagSets + * @param {string} key - The string key representing the consumer. + * @param {Array} flagSets - An array of the flagSet names we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): AsyncTreatments, + /** + * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for the feature flags related to the given flagSets. + * For usage on NodeJS as we don't have only one key. + * @function getTreatmentsWithConfigByFlagSets + * @param {string} key - The string key representing the consumer. + * @param {Array} flagSets - An array of the flagSet names we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): AsyncTreatmentsWithConfig, /** * Tracks an event to be fed to the results product on Split user interface, and returns a promise to signal when the event was successfully queued (or not). * @function track @@ -1174,6 +1250,42 @@ export namespace SplitIO { * @returns {TreatmentsWithConfig} The map with all the TreatmentWithConfig objects */ getTreatmentsWithConfig(featureFlagNames: string[], attributes?: Attributes): TreatmentsWithConfig, + /** + * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flagSet. + * @function getTreatmentsByFlagSet + * @param {string} key - The string key representing the consumer. + * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): Treatments, + /** + * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flagSet. + * @function getTreatmentsWithConfigByFlagSet + * @param {string} key - The string key representing the consumer. + * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsWithConfigByFlagSet(key: SplitKey, flagSet: string, attributes?: Attributes): TreatmentsWithConfig, + /** + * Returns a Returns a Treatments value, which is an object with both treatment and config string for to the feature flags related to the given flagSets. + * @function getTreatmentsByFlagSets + * @param {string} key - The string key representing the consumer. + * @param {Array} flagSets - An array of the flagSet names we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): Treatments, + /** + * Returns a TreatmentsWithConfig value, which is an object map with the TreatmentWithConfig (an object with both treatment and config string) for the feature flags related to the given flagSets. + * @function getTreatmentsWithConfigByFlagSets + * @param {string} key - The string key representing the consumer. + * @param {Array} flagSets - An array of the flagSet names we want to get the treatments. + * @param {Attributes=} attributes - An object of type Attributes defining the attributes for the given key. + * @returns {Treatments} The map with all the TreatmentWithConfig objects + */ + getTreatmentsWithConfigByFlagSets(key: SplitKey, flagSets: string[], attributes?: Attributes): TreatmentsWithConfig, /** * Tracks an event to be fed to the results product on Split user interface. * @function track