From 7eaaac8fb272c5ddf95c79c203face6999381830 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 24 Aug 2023 21:21:15 -0300 Subject: [PATCH 01/43] [SDKS-7463] Add new filter --- package-lock.json | 4 +- package.json | 2 +- src/__tests__/mocks/fetchSpecificSplits.ts | 82 +++++++++++++++---- src/logger/constants.ts | 3 + src/logger/messages/warn.ts | 5 +- .../inLocalStorage/SplitsCacheInLocal.ts | 2 +- src/types.ts | 2 +- .../__tests__/settings.mocks.ts | 2 +- .../__tests__/splitFilters.spec.ts | 59 +++++++++++-- src/utils/settingsValidation/splitFilters.ts | 64 ++++++++++++++- 10 files changed, 194 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b4a82b5..b345dd22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.9.0", + "version": "1.9.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.9.0", + "version": "1.9.1-rc.0", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 1e0e5d5a..543c6582 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.9.0", + "version": "1.9.1-rc.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/__tests__/mocks/fetchSpecificSplits.ts b/src/__tests__/mocks/fetchSpecificSplits.ts index 8f9098c3..852f768e 100644 --- a/src/__tests__/mocks/fetchSpecificSplits.ts +++ b/src/__tests__/mocks/fetchSpecificSplits.ts @@ -10,6 +10,13 @@ const valuesExamples = [ ['p0', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9', 'p10', 'p11', 'p12', 'p13', 'p14', 'p15', 'p16', 'p17', 'p18', 'p19', 'p20', 'p21', 'p22', 'p23', 'p24', 'p25', 'p26', 'p27', 'p28', 'p29', 'p30', 'p31', 'p32', 'p33', 'p34', 'p35', 'p36', 'p37', 'p38', 'p39', 'p40', 'p41', 'p42', 'p43', 'p44', 'p45', 'p46', 'p47', 'p48', 'p49', 'p50'], ['__ш', '__a', '%', '%25', ' __ш ', '% '], // to test that we order before encoding: '__a' < '__ш' but encodeURIComponent('__a') > encodeURIComponent('__ш') ['%', '%25', '__a', '__ш'], // [7] ordered and deduplicated + // flagSets examples + [' set_1','set_3 ',' set_a ','set_2','set_c','set_b'], // [9] trim + ['set_1','set_2','set_3','set_a','set_b','set_c'], // [10] sanitized [9] + ['set_ 1','set _3','3set_a','_set_2','seT_c','set_B','set_1234567890_1234567890_234567890_1234567890_1234567890','set_a','set_2'], // [11] lowercase & regexp + ['set_2','set_a','set_b','set_c'], // [12] sanitized [11] + ['set_2','set_a','SET_2','set_a','set_b','set_B','set_1','set_3!'], // [13] dedupe, dedupe with case sensitive + ['set_1','set_2','set_a','set_b'], // [14] sanitized [13] ]; export const splitFilters: SplitIO.SplitFilter[][] = [ @@ -41,39 +48,86 @@ export const splitFilters: SplitIO.SplitFilter[][] = [ ], [ { type: 'byName', values: valuesExamples[7] } - ] + ], + // FlagSet filters + [ // [6] + { type: 'byPrefix', values: valuesExamples[1] }, + { type: 'bySet', values: valuesExamples[9] }, + { type: 'byName', values: valuesExamples[1] } + ], + [ // [7] + { type: 'bySet', values: valuesExamples[11] }, + { type: 'byPrefix', values: [] }, + { type: 'byName', values: valuesExamples[6] } + ], + [ // [8] + { type: 'byPrefix', values: [] }, + { type: 'byName', values: valuesExamples[6] }, + { type: 'bySet', values: valuesExamples[13] } + ], ]; // each entry corresponds to the queryString or exception message of each splitFilters entry export const queryStrings = [ - '&names=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc', - '&prefixes=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc', - '&names=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc&prefixes=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc', - "400 unique values can be specified at most for 'byName' filter. You passed 401. Please consider reducing the amount or using other filter.", - "50 unique values can be specified at most for 'byPrefix' filter. You passed 51. Please consider reducing the amount or using other filter.", - '&names=%25,%2525,__a,__%D1%88', + '&names=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc', // [0] + '&prefixes=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc', // [1] + '&names=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc&prefixes=abc%C8%A3,abc%C8%A3asd,ausgef%C3%BCllt,%C8%A3abc', // [2] + "400 unique values can be specified at most for 'byName' filter. You passed 401. Please consider reducing the amount or using other filter.", // [3] + "50 unique values can be specified at most for 'byPrefix' filter. You passed 51. Please consider reducing the amount or using other filter.", // [4] + '&names=%25,%2525,__a,__%D1%88', // [5] + // FlagSet filters + '&sets=set_1,set_2,set_3,set_a,set_b,set_c', // [6] + '&sets=set_2,set_a,set_b,set_c', // [7] + '&sets=set_1,set_2,set_a,set_b', // [8] ]; // each entry corresponds to a `groupedFilter` object returned by `validateSplitFilter` for each `splitFilters` input. // `groupedFilter` contains valid, unique and ordered values per filter type. // An `undefined` value means that `validateSplitFilter` throws an exception which message value is at `queryStrings`. export const groupedFilters = [ - { + { // [0] + bySet: [], byName: valuesExamples[2], byPrefix: [] }, - { + { // [1] + bySet: [], byName: [], byPrefix: valuesExamples[2] }, - { + { // [2] + bySet: [], byName: valuesExamples[2], byPrefix: valuesExamples[2] }, - undefined, - undefined, - { + undefined, // [3] + undefined, // [4] + { // [5] + bySet: [], byName: valuesExamples[8], byPrefix: [] - } + }, + // FlagSet filters + { // [6] + byName: [], + bySet: valuesExamples[10], + byPrefix: [] + }, + { // [7] + byName: [], + bySet: valuesExamples[12], + byPrefix: [] + }, + { // [8] + byName: [], + bySet: valuesExamples[14], + byPrefix: [] + }, +]; + +export const flagSetValidFilters = [ + undefined, undefined, undefined, undefined, undefined, undefined, + [{ type: 'bySet', values: valuesExamples[9] }], + [{ type: 'bySet', values: valuesExamples[11] }], + [{ type: 'bySet', values: valuesExamples[13] }], ]; diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 75f5fe1c..fcebb0e5 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -97,6 +97,9 @@ export const WARN_SPLITS_FILTER_EMPTY = 221; export const WARN_SDK_KEY = 222; export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223; export const STREAMING_PARSING_SPLIT_UPDATE = 224; +export const WARN_SPLITS_FILTER_NAME_AND_SET = 225; +export const WARN_SPLITS_FILTER_INVALID_SET = 226; +export const WARN_SPLITS_FILTER_LOWERCASE_SET = 227; export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; export const ERROR_LOGLEVEL_INVALID = 301; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 9917f862..69aa40ca 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -27,10 +27,13 @@ export const codesWarn: [number, string][] = codesError.concat([ // initialization / settings validation [c.WARN_INTEGRATION_INVALID, c.LOG_PREFIX_SETTINGS+': %s integration item(s) at settings is invalid. %s'], [c.WARN_SPLITS_FILTER_IGNORED, c.LOG_PREFIX_SETTINGS+': feature flag filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], - [c.WARN_SPLITS_FILTER_INVALID, c.LOG_PREFIX_SETTINGS+': feature flag filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'], + [c.WARN_SPLITS_FILTER_INVALID, c.LOG_PREFIX_SETTINGS+': feature flag filter at position %s is invalid. It must be an object with a valid filter type ("bySet", "byName" or "byPrefix") and a list of "values".'], [c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS+': feature flag filter configuration must be a non-empty array of filter objects.'], [c.WARN_SDK_KEY, c.LOG_PREFIX_SETTINGS+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'], [c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'], [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'], + [c.WARN_SPLITS_FILTER_NAME_AND_SET, c.LOG_PREFIX_SETTINGS+': names and sets filter cannot be used at the same time. The sdk will proceed using sets filter.'], + [c.WARN_SPLITS_FILTER_INVALID_SET, c.LOG_PREFIX_SETTINGS+': you passed %s, Flag Set must adhere to the regular expressions %s. This means a Flag Set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.'], + [c.WARN_SPLITS_FILTER_LOWERCASE_SET, c.LOG_PREFIX_SETTINGS+': Flag Set name %s should be all lowercase - converting string to lowercase.'], ]); diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 44f4664c..5d827177 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -21,7 +21,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * @param {number | undefined} expirationTimestamp * @param {ISplitFiltersValidation} splitFiltersValidation */ - constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { byName: [], byPrefix: [] }, validFilters: [] }) { + constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }) { super(); this.keys = keys; this.splitFiltersValidation = splitFiltersValidation; diff --git a/src/types.ts b/src/types.ts index 4c215404..61daaab5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -709,7 +709,7 @@ export namespace SplitIO { * SplitFilter type. * @typedef {string} SplitFilterType */ - export type SplitFilterType = 'byName' | 'byPrefix'; + export type SplitFilterType = 'byName' | 'byPrefix' | 'bySet'; /** * Defines a feature flag filter, described by a type and list of values. */ diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index ad8d812a..6a1f1c82 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -77,7 +77,7 @@ export const fullSettings: ISettings = { __splitFiltersValidation: { validFilters: [], queryString: null, - groupedFilters: { byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] } }, enabled: true }, diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index 1e23b752..a5a66387 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -3,18 +3,18 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { STANDALONE_MODE, CONSUMER_MODE } from '../../constants'; // Split filter and QueryStrings examples -import { splitFilters, queryStrings, groupedFilters } from '../../../__tests__/mocks/fetchSpecificSplits'; +import { splitFilters, queryStrings, groupedFilters, flagSetValidFilters } from '../../../__tests__/mocks/fetchSpecificSplits'; // Test target import { validateSplitFilters } from '../splitFilters'; -import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY } from '../../../logger/constants'; +import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, WARN_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET } from '../../../logger/constants'; describe('validateSplitFilters', () => { const defaultOutput = { validFilters: [], queryString: null, - groupedFilters: { byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] } }; afterEach(() => { loggerMock.mockClear(); }); @@ -39,13 +39,14 @@ describe('validateSplitFilters', () => { test('Returns object with null queryString, if `splitFilters` contains invalid filters or contains filters with no values or invalid values', () => { const splitFilters: any[] = [ + { type: 'bySet', values: [] }, { type: 'byName', values: [] }, { type: 'byName', values: [] }, { type: 'byPrefix', values: [] }]; const output = { validFilters: [...splitFilters], queryString: null, - groupedFilters: { byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] } }; expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // filters without values expect(loggerMock.debug).toBeCalledWith(SETTINGS_SPLITS_FILTER, [null]); @@ -60,9 +61,9 @@ describe('validateSplitFilters', () => { expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // some filters are invalid expect(loggerMock.debug.mock.calls).toEqual([[SETTINGS_SPLITS_FILTER, [null]]]); expect(loggerMock.warn.mock.calls).toEqual([ - [WARN_SPLITS_FILTER_INVALID, [3]], // invalid value of `type` property - [WARN_SPLITS_FILTER_INVALID, [4]], // invalid type of `values` property - [WARN_SPLITS_FILTER_INVALID, [5]] // invalid type of `type` property + [WARN_SPLITS_FILTER_INVALID, [4]], // invalid value of `type` property + [WARN_SPLITS_FILTER_INVALID, [5]], // invalid type of `values` property + [WARN_SPLITS_FILTER_INVALID, [6]] // invalid type of `type` property ]); expect(loggerMock.error.mock.calls).toEqual([ @@ -73,7 +74,7 @@ describe('validateSplitFilters', () => { test('Returns object with a queryString, if `splitFilters` contains at least a valid `byName` or `byPrefix` filter with at least a valid value', () => { - for (let i = 0; i < splitFilters.length; i++) { + for (let i = 0; i < 6; i++) { if (groupedFilters[i]) { // tests where validateSplitFilters executes normally const output = { @@ -91,3 +92,45 @@ describe('validateSplitFilters', () => { }); }); + + +describe('validateSetFilter', () => { + + const getOutput = (testIndex: number) => { + return { + // @ts-ignore + validFilters: [...flagSetValidFilters[testIndex]], + queryString: queryStrings[testIndex], + groupedFilters: groupedFilters[testIndex] + }; + }; + + const regexp = /^[a-z][_a-z0-9]{0,49}$/; + + test('Config validations', () => { + // extra spaces trimmed and sorted query output + expect(validateSplitFilters(loggerMock, splitFilters[6], STANDALONE_MODE)).toEqual(getOutput(6)); // trim & sort + expect(loggerMock.warn.mock.calls[0]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_1']]); + expect(loggerMock.warn.mock.calls[1]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', 'set_3 ']]); + expect(loggerMock.warn.mock.calls[2]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_a ']]); + expect(loggerMock.warn.mock.calls[3]).toEqual([WARN_SPLITS_FILTER_NAME_AND_SET]); + + expect(validateSplitFilters(loggerMock, splitFilters[7], STANDALONE_MODE)).toEqual(getOutput(7)); // lowercase and regexp + expect(loggerMock.warn.mock.calls[4]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['seT_c']]); // lowercase + expect(loggerMock.warn.mock.calls[5]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase + expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_ 1', regexp, 'set_ 1']]); // empty spaces + expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set _3', regexp, 'set _3']]); // empty spaces + expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['3set_a', regexp, '3set_a']]); // start with a letter + expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_2', regexp, '_set_2']]); // start with a letter + expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters + expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_NAME_AND_SET]); + + expect(validateSplitFilters(loggerMock, splitFilters[8], STANDALONE_MODE)).toEqual(getOutput(8)); // lowercase and dedupe + expect(loggerMock.warn.mock.calls[12]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase + expect(loggerMock.warn.mock.calls[13]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase + expect(loggerMock.warn.mock.calls[14]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character + expect(loggerMock.warn.mock.calls[15]).toEqual([WARN_SPLITS_FILTER_NAME_AND_SET]); + + expect(loggerMock.warn.mock.calls.length).toEqual(16); + }); +}); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index eb7b0371..c7f1e957 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -3,11 +3,18 @@ import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, WARN_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET } from '../../logger/constants'; +import { objectAssign } from '../lang/objectAssign'; +import { uniq } from '../lang'; // Split filters metadata. // Ordered according to their precedency when forming the filter query string: `&names=&prefixes=` const FILTERS_METADATA = [ + { + type: 'bySet' as SplitIO.SplitFilterType, + maxLength: 50, + queryParam: 'sets=' + }, { type: 'byName' as SplitIO.SplitFilterType, maxLength: 400, @@ -20,6 +27,9 @@ const FILTERS_METADATA = [ } ]; +const VALID_FLAGSET_REGEX = /^[a-z][_a-z0-9]{0,49}$/; +const CAPITAL_LETTERS_REGEX = /[A-Z]/; + /** * Validates that the given value is a valid filter type */ @@ -42,6 +52,11 @@ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values let result = validateSplits(log, values, LOG_PREFIX_SETTINGS, `${type} filter`, `${type} filter value`); if (result) { + + if (type === 'bySet') { + result = sanitizeFlagSets(log, result); + } + // check max length if (result.length > maxLength) throw new Error(`${maxLength} unique values can be specified at most for '${type}' filter. You passed ${result.length}. Please consider reducing the amount or using other filter.`); @@ -72,6 +87,43 @@ function queryStringBuilder(groupedFilters: Record 0 ? '&' + queryParams.join('&') : null; } +/** + * Sanitizes set names list taking in account: + * - It should be lowercase + * - Must adhere the following regular expression /^[a-z][_a-z0-9]{0,49}$/ that means + * - must start with a letter + * - Be in lowercase + * - Be alphanumeric + * - have a max length of 50 characteres + * + * @param {ILogger} log + * @param {string[]} flagsets + * @returns sanitized list of set names + */ +function sanitizeFlagSets(log: ILogger, flagsets: string[]) { + let sanitizedSets = flagsets + .map(flagSet => { + if (CAPITAL_LETTERS_REGEX.test(flagSet)){ + log.warn(WARN_SPLITS_FILTER_LOWERCASE_SET,[flagSet]); + flagSet = flagSet.toLowerCase(); + } + return flagSet; + }) + .filter(flagSet => { + if (!VALID_FLAGSET_REGEX.test(flagSet)){ + log.warn(WARN_SPLITS_FILTER_INVALID_SET, [flagSet,VALID_FLAGSET_REGEX,flagSet]); + return false; + } + if (typeof flagSet !== 'string') return false; + return true; + }); + return uniq(sanitizedSets); +} + +function configuredFilter(validFilters: SplitIO.SplitFilter[], filterType: SplitIO.SplitFilterType) { + return validFilters.find(filter => filter.type === filterType && filter.values.length > 0); +} + /** * Validates `splitFilters` configuration object and parses it into a query string for filtering splits on `/splitChanges` fetch. * @@ -90,7 +142,7 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: const res = { validFilters: [], queryString: null, - groupedFilters: { byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] } } as ISplitFiltersValidation; // do nothing if `splitFilters` param is not a non-empty array or mode is not STANDALONE @@ -122,6 +174,14 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: if (res.groupedFilters[type].length > 0) res.groupedFilters[type] = validateSplitFilter(log, type, res.groupedFilters[type], maxLength); }); + const setFilter = configuredFilter(res.validFilters, 'bySet'); + // Clean all filters if set filter is present + if (setFilter) { + if (configuredFilter(res.validFilters, 'byName')) log.warn(WARN_SPLITS_FILTER_NAME_AND_SET); + objectAssign(res.groupedFilters, { byName: [], byPrefix: [] }); + res.validFilters = [setFilter]; + } + // build query string res.queryString = queryStringBuilder(res.groupedFilters); log.debug(SETTINGS_SPLITS_FILTER, [res.queryString]); From a42693cd1742d83b2291c23557c78bc1295c6696 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 30 Aug 2023 12:12:40 -0300 Subject: [PATCH 02/43] Fix typo and styles --- src/logger/messages/warn.ts | 4 +-- .../__tests__/splitFilters.spec.ts | 29 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 69aa40ca..1a29dd66 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -34,6 +34,6 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'], [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'], [c.WARN_SPLITS_FILTER_NAME_AND_SET, c.LOG_PREFIX_SETTINGS+': names and sets filter cannot be used at the same time. The sdk will proceed using sets filter.'], - [c.WARN_SPLITS_FILTER_INVALID_SET, c.LOG_PREFIX_SETTINGS+': you passed %s, Flag Set must adhere to the regular expressions %s. This means a Flag Set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.'], - [c.WARN_SPLITS_FILTER_LOWERCASE_SET, c.LOG_PREFIX_SETTINGS+': Flag Set name %s should be all lowercase - converting string to lowercase.'], + [c.WARN_SPLITS_FILTER_INVALID_SET, c.LOG_PREFIX_SETTINGS+': you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.'], + [c.WARN_SPLITS_FILTER_LOWERCASE_SET, c.LOG_PREFIX_SETTINGS+': flag set %s should be all lowercase - converting string to lowercase.'], ]); diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index a5a66387..73938186 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -17,6 +17,17 @@ describe('validateSplitFilters', () => { groupedFilters: { bySet: [], byName: [], byPrefix: [] } }; + const getOutput = (testIndex: number) => { + return { + // @ts-ignore + validFilters: [...flagSetValidFilters[testIndex]], + queryString: queryStrings[testIndex], + groupedFilters: groupedFilters[testIndex] + }; + }; + + const regexp = /^[a-z][_a-z0-9]{0,49}$/; + afterEach(() => { loggerMock.mockClear(); }); test('Returns default output with empty values if `splitFilters` is an invalid object or `mode` is not \'standalone\'', () => { @@ -91,23 +102,7 @@ describe('validateSplitFilters', () => { } }); -}); - - -describe('validateSetFilter', () => { - - const getOutput = (testIndex: number) => { - return { - // @ts-ignore - validFilters: [...flagSetValidFilters[testIndex]], - queryString: queryStrings[testIndex], - groupedFilters: groupedFilters[testIndex] - }; - }; - - const regexp = /^[a-z][_a-z0-9]{0,49}$/; - - test('Config validations', () => { + test('Validates flag set filters', () => { // extra spaces trimmed and sorted query output expect(validateSplitFilters(loggerMock, splitFilters[6], STANDALONE_MODE)).toEqual(getOutput(6)); // trim & sort expect(loggerMock.warn.mock.calls[0]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_1']]); From e4ecaf295461efdacdfdc9c53e014dbc822f952f Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 1 Sep 2023 17:34:44 -0300 Subject: [PATCH 03/43] [SDKS-7465] flagset synchronization --- src/dtos/types.ts | 3 +- .../matchers/__tests__/dependency.spec.ts | 4 +- src/sync/polling/syncTasks/splitsSyncTask.ts | 1 + .../__tests__/splitChangesUpdater.spec.ts | 106 +++++++++++++++++- .../polling/updaters/splitChangesUpdater.ts | 23 +++- 5 files changed, 125 insertions(+), 12 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index a61e3777..a4995344 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -163,7 +163,8 @@ export interface ISplit { trafficAllocationSeed?: number configurations?: { [treatmentName: string]: string - } + }, + sets: string[] } // Split definition used in offline mode diff --git a/src/evaluator/matchers/__tests__/dependency.spec.ts b/src/evaluator/matchers/__tests__/dependency.spec.ts index 6ec2b0dc..926750ae 100644 --- a/src/evaluator/matchers/__tests__/dependency.spec.ts +++ b/src/evaluator/matchers/__tests__/dependency.spec.ts @@ -6,8 +6,8 @@ import { IStorageSync } from '../../../storages/types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { ISplit } from '../../../dtos/types'; -const ALWAYS_ON_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }] } as ISplit; -const ALWAYS_OFF_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-off', 'trafficAllocation': 100, 'trafficAllocationSeed': -331690370, 'seed': 403891040, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'on', 'changeNumber': 1494365020316, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 0 }, { 'treatment': 'off', 'size': 100 }], 'label': 'in segment all' }] } as ISplit; +const ALWAYS_ON_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets':[] } as ISplit; +const ALWAYS_OFF_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-off', 'trafficAllocation': 100, 'trafficAllocationSeed': -331690370, 'seed': 403891040, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'on', 'changeNumber': 1494365020316, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 0 }, { 'treatment': 'off', 'size': 100 }], 'label': 'in segment all' }], 'sets':[] } as ISplit; const STORED_SPLITS: Record = { 'always-on': ALWAYS_ON_SPLIT, diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index a4568d6d..baef383c 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -24,6 +24,7 @@ export function splitsSyncTaskFactory( splitChangesFetcherFactory(fetchSplitChanges), storage.splits, storage.segments, + settings.sync.__splitFiltersValidation, readiness.splits, settings.startup.requestTimeoutBeforeReady, settings.startup.retriesOnFailureBeforeReady, diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index c38e569d..3729eded 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -13,6 +13,8 @@ import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; import { splitNotifications } from '../../../streaming/__tests__/dataMocks'; +const ARCHIVED_FF = 'ARCHIVED'; + const activeSplitWithSegments = { name: 'Split1', status: 'ACTIVE', @@ -38,6 +40,48 @@ const archivedSplit = { name: 'Split2', status: 'ARCHIVED' }; +// @ts-ignore +const testFFSetsAB: ISplit = +{ + name: 'test', + status: 'ACTIVE', + conditions: [], + killed: false, + sets: ['set_a', 'set_b'] +}; +// @ts-ignore +const test2FFSetsX: ISplit = +{ + name: 'test2', + status: 'ACTIVE', + conditions: [], + killed: false, + sets: ['set_x'] +}; +// @ts-ignore +const testFFRemoveSetB: ISplit = +{ + name: 'test', + status: 'ACTIVE', + conditions: [], + sets: ['set_a'] +}; +// @ts-ignore +const testFFRemoveSetA: ISplit = +{ + name: 'test', + status: 'ACTIVE', + conditions: [], + sets: ['set_x'] +}; +// @ts-ignore +const testFFEmptySet: ISplit = +{ + name: 'test', + status: 'ACTIVE', + conditions: [], + sets: [] +}; test('splitChangesUpdater / segments parser', () => { @@ -48,12 +92,63 @@ test('splitChangesUpdater / segments parser', () => { }); test('splitChangesUpdater / compute splits mutation', () => { + const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - const splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[]); + let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); expect(splitsMutation.added).toEqual([[activeSplitWithSegments.name, activeSplitWithSegments]]); expect(splitsMutation.removed).toEqual([archivedSplit.name]); expect(splitsMutation.segments).toEqual(['A', 'B']); + + // SDK initialization without sets + // should process all the notifications + splitsMutation = computeSplitsMutation([testFFSetsAB, test2FFSetsX] as ISplit[], splitFiltersValidation); + + expect(splitsMutation.added).toEqual([[testFFSetsAB.name, testFFSetsAB],[test2FFSetsX.name, test2FFSetsX]]); + expect(splitsMutation.removed).toEqual([]); + expect(splitsMutation.segments).toEqual([]); +}); + +test('splitChangesUpdater / compute splits mutation with filters', () => { + // SDK initialization with sets: [set_a, set_b] + let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; + + // fetching new feature flag in sets A & B + let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); + + // should add it to mutations + expect(splitsMutation.added).toEqual([[testFFSetsAB.name, testFFSetsAB]]); + expect(splitsMutation.removed).toEqual([]); + + // fetching existing test feature flag removed from set B + splitsMutation = computeSplitsMutation([testFFRemoveSetB], splitFiltersValidation); + + expect(splitsMutation.added).toEqual([[testFFRemoveSetB.name, testFFRemoveSetB]]); + expect(splitsMutation.removed).toEqual([]); + + // fetching existing test feature flag removed from set B + splitsMutation = computeSplitsMutation([testFFRemoveSetA], splitFiltersValidation); + + expect(splitsMutation.added).toEqual([]); + expect(splitsMutation.removed).toEqual([testFFRemoveSetA.name]); + + // fetching existing test feature flag removed from set B + splitsMutation = computeSplitsMutation([testFFEmptySet], splitFiltersValidation); + + expect(splitsMutation.added).toEqual([]); + expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); + + // SDK initialization with names: ['test2'] + splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; + splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); + + expect(splitsMutation.added).toEqual([]); + expect(splitsMutation.removed).toEqual([testFFSetsAB.name]); + + splitsMutation = computeSplitsMutation([test2FFSetsX, testFFEmptySet], splitFiltersValidation); + + expect(splitsMutation.added).toEqual([[test2FFSetsX.name, test2FFSetsX],]); + expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); }); describe('splitChangesUpdater', () => { @@ -73,7 +168,9 @@ describe('splitChangesUpdater', () => { const readinessManager = readinessManagerFactory(EventEmitter); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); - const splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, readinessManager.splits, 1000, 1); + const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; + + const splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); afterEach(() => { jest.clearAllMocks(); @@ -93,13 +190,12 @@ describe('splitChangesUpdater', () => { }); test('test with payload', async () => { - const ARCHIVED_FF = 'ARCHIVED'; let index = 0; for (const notification of splitNotifications) { - const payload = notification.decoded as ISplit; + const payload = notification.decoded as Pick; const changeNumber = payload.changeNumber; - await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets:[]}, changeNumber: changeNumber })).resolves.toBe(true); // fetch not being called expect(fetchSplitChanges).toBeCalledTimes(0); // Change number being updated diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 63ff08f0..78ee91f4 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -1,7 +1,7 @@ import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; import { ISegmentsCacheBase, ISplitsCacheBase } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; -import { ISplit, ISplitChangesResponse } from '../../../dtos/types'; +import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; @@ -45,15 +45,29 @@ interface ISplitMutations { segments: string[] } +/** + * If there are defined filters and one feature flag doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it + * If there are set filter defined, names filter is ignored + * + * @param featureFlag feature flag to be evaluated + * @param filters splitFiltersValidation bySet | byName + */ +function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { + const { bySet: setsFilter, byName: namesFilter } = filters.groupedFilters; + if (setsFilter.length > 0) return featureFlag.sets && featureFlag.sets.find((featureFlagSet: string) => setsFilter.indexOf(featureFlagSet) > -1); + if (namesFilter.length > 0) return namesFilter.indexOf(featureFlag.name) > -1; + return true; +} + /** * Given the list of splits from /splitChanges endpoint, it returns the mutations, * i.e., an object with added splits, removed splits and used segments. * Exported for testing purposes. */ -export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { +export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersValidation): ISplitMutations { const segments = new _Set(); const computed = entries.reduce((accum, split) => { - if (split.status === 'ACTIVE') { + if (split.status === 'ACTIVE' && matchFilters(split, filters)) { accum.added.push([split.name, split]); parseSegments(split).forEach((segmentName: string) => { @@ -90,6 +104,7 @@ export function splitChangesUpdaterFactory( splitChangesFetcher: ISplitChangesFetcher, splits: ISplitsCacheBase, segments: ISegmentsCacheBase, + splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, @@ -126,7 +141,7 @@ export function splitChangesUpdaterFactory( .then((splitChanges: ISplitChangesResponse) => { startingUp = false; - const mutation = computeSplitsMutation(splitChanges.splits); + const mutation = computeSplitsMutation(splitChanges.splits, splitFiltersValidation); log.debug(SYNC_SPLITS_NEW, [mutation.added.length]); log.debug(SYNC_SPLITS_REMOVED, [mutation.removed.length]); From 76f7b6d2647dae3fa627728922a72bad96e4bb66 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 4 Sep 2023 14:25:10 -0300 Subject: [PATCH 04/43] [SDKS-7464] Handle 414 uri too long --- src/logger/constants.ts | 1 + src/logger/messages/error.ts | 1 + src/services/splitApi.ts | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index fcebb0e5..6ce066da 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -128,6 +128,7 @@ export const ERROR_LOCALHOST_MODULE_REQUIRED = 323; export const ERROR_STORAGE_INVALID = 324; export const ERROR_NOT_BOOLEAN = 325; export const ERROR_MIN_CONFIG_PARAM = 326; +export const ERROR_TOO_MANY_SETS = 327; // Log prefixes (a.k.a. tags or categories) export const LOG_PREFIX_SETTINGS = 'settings'; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 62691a72..f5bd61d5 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -34,4 +34,5 @@ export const codesError: [number, string][] = [ [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'], [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Falling back into default MEMORY storage'], [c.ERROR_MIN_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': the provided "%s" config param is lower than allowed. Setting to the minimum value %s seconds'], + [c.ERROR_TOO_MANY_SETS, c.LOG_PREFIX_SETTINGS + ': the amount of flag sets provided are big causing uri length error.'], ]; diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index cdc5fff4..f3d21bff 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -5,6 +5,7 @@ import { ISplitApi } from './types'; import { objectAssign } from '../utils/lang/objectAssign'; import { ITelemetryTracker } from '../trackers/types'; import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MY_SEGMENT } from '../utils/constants'; +import { ERROR_TOO_MANY_SETS } from '../logger/constants'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -53,7 +54,12 @@ export function splitApiFactory( fetchSplitChanges(since: number, noCache?: boolean, till?: number) { const url = `${urls.sdk}/splitChanges?since=${since}${till ? '&till=' + till : ''}${filterQueryString || ''}`; - return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)); + return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) + .catch((err) => { + if (err.statusCode === 414) + settings.log.error(ERROR_TOO_MANY_SETS); + return err; + }); }, fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean, till?: number) { From e1aa073d130c9708cf226668b7251db8ab2f2947 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 4 Sep 2023 16:21:29 -0300 Subject: [PATCH 05/43] throw error instead of returning it --- src/services/splitApi.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index f3d21bff..c81918f5 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -56,9 +56,8 @@ export function splitApiFactory( const url = `${urls.sdk}/splitChanges?since=${since}${till ? '&till=' + till : ''}${filterQueryString || ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) .catch((err) => { - if (err.statusCode === 414) - settings.log.error(ERROR_TOO_MANY_SETS); - return err; + if (err.statusCode === 414) settings.log.error(ERROR_TOO_MANY_SETS); + throw err; }); }, From 5d4b7b501920cfad26d0fbcb3c2cff068e5103e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 4 Sep 2023 17:03:19 -0300 Subject: [PATCH 06/43] Replace find array method with utility function in --- src/sync/polling/updaters/splitChangesUpdater.ts | 3 ++- src/utils/settingsValidation/splitFilters.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 78ee91f4..92468362 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -7,6 +7,7 @@ import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; +import { find } from '../../../utils/lang'; type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise @@ -54,7 +55,7 @@ interface ISplitMutations { */ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { const { bySet: setsFilter, byName: namesFilter } = filters.groupedFilters; - if (setsFilter.length > 0) return featureFlag.sets && featureFlag.sets.find((featureFlagSet: string) => setsFilter.indexOf(featureFlagSet) > -1); + if (setsFilter.length > 0) return featureFlag.sets && find(featureFlag.sets, (featureFlagSet: string) => setsFilter.indexOf(featureFlagSet) > -1); if (namesFilter.length > 0) return namesFilter.indexOf(featureFlag.name) > -1; return true; } diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index c7f1e957..fe8ece49 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -5,7 +5,7 @@ import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, WARN_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET } from '../../logger/constants'; import { objectAssign } from '../lang/objectAssign'; -import { uniq } from '../lang'; +import { find, uniq } from '../lang'; // Split filters metadata. // Ordered according to their precedency when forming the filter query string: `&names=&prefixes=` @@ -121,7 +121,7 @@ function sanitizeFlagSets(log: ILogger, flagsets: string[]) { } function configuredFilter(validFilters: SplitIO.SplitFilter[], filterType: SplitIO.SplitFilterType) { - return validFilters.find(filter => filter.type === filterType && filter.values.length > 0); + return find(validFilters, (filter: SplitIO.SplitFilter) => filter.type === filterType && filter.values.length > 0); } /** From 207f519128a402961d0935b10346d2e69686e8e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 4 Sep 2023 19:32:56 -0300 Subject: [PATCH 07/43] [SDKS-7494] flagsets - Manager: add sets property --- src/sdkManager/__tests__/mocks/input.json | 3 ++- src/sdkManager/__tests__/mocks/output.json | 3 ++- src/sdkManager/index.ts | 3 ++- src/types.ts | 5 +++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/sdkManager/__tests__/mocks/input.json b/src/sdkManager/__tests__/mocks/input.json index 2e418b84..7aea3413 100644 --- a/src/sdkManager/__tests__/mocks/input.json +++ b/src/sdkManager/__tests__/mocks/input.json @@ -40,5 +40,6 @@ ], "configurations": { "on": "\"color\": \"green\"" - } + }, + "sets": ["set_a"] } diff --git a/src/sdkManager/__tests__/mocks/output.json b/src/sdkManager/__tests__/mocks/output.json index c96c535f..dedecd40 100644 --- a/src/sdkManager/__tests__/mocks/output.json +++ b/src/sdkManager/__tests__/mocks/output.json @@ -6,5 +6,6 @@ "treatments": ["on", "off"], "configs": { "on": "\"color\": \"green\"" - } + }, + "sets": ["set_a"] } diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 71dc7716..84fce7fb 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -31,7 +31,8 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { killed: splitObject.killed, changeNumber: splitObject.changeNumber || 0, treatments: collectTreatments(splitObject), - configs: splitObject.configurations || {} + configs: splitObject.configurations || {}, + sets: splitObject.sets || [] }; } diff --git a/src/types.ts b/src/types.ts index 61daaab5..1ed3100d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -610,6 +610,11 @@ export namespace SplitIO { configs: { [treatmentName: string]: string } + /** + * list of sets per feature flag + * @property {string[]} sets + */ + sets: string[] }; /** * A promise that resolves to a feature flag view. From 383e0e8fa4f3731a674fa723c2ec8ba01bc5b5a3 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 4 Sep 2023 18:57:53 -0300 Subject: [PATCH 08/43] [SDKS-7493] flagsets - Add telemetry --- src/__tests__/mocks/fetchSpecificSplits.ts | 6 +-- src/dtos/types.ts | 3 +- src/logger/constants.ts | 6 +-- src/logger/messages/error.ts | 1 + src/logger/messages/warn.ts | 1 - .../inLocalStorage/SplitsCacheInLocal.ts | 2 +- .../__tests__/splitChangesUpdater.spec.ts | 8 ++-- .../__tests__/telemetrySubmitter.spec.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 28 ++++++++++- src/sync/submitters/types.ts | 2 + .../__tests__/settings.mocks.ts | 3 +- .../__tests__/splitFilters.spec.ts | 48 +++++++++++-------- src/utils/settingsValidation/splitFilters.ts | 10 ++-- 13 files changed, 80 insertions(+), 40 deletions(-) diff --git a/src/__tests__/mocks/fetchSpecificSplits.ts b/src/__tests__/mocks/fetchSpecificSplits.ts index 852f768e..6bd10189 100644 --- a/src/__tests__/mocks/fetchSpecificSplits.ts +++ b/src/__tests__/mocks/fetchSpecificSplits.ts @@ -127,7 +127,7 @@ export const groupedFilters = [ export const flagSetValidFilters = [ undefined, undefined, undefined, undefined, undefined, undefined, - [{ type: 'bySet', values: valuesExamples[9] }], - [{ type: 'bySet', values: valuesExamples[11] }], - [{ type: 'bySet', values: valuesExamples[13] }], + [{ type: 'bySet', values: valuesExamples[10] }], + [{ type: 'bySet', values: valuesExamples[12] }], + [{ type: 'bySet', values: valuesExamples[14] }], ]; diff --git a/src/dtos/types.ts b/src/dtos/types.ts index a4995344..3cec6804 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -209,5 +209,6 @@ export interface IMetadata { export type ISplitFiltersValidation = { queryString: string | null, groupedFilters: Record, - validFilters: SplitIO.SplitFilter[] + validFilters: SplitIO.SplitFilter[], + originalFilters: SplitIO.SplitFilter[] }; diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 6ce066da..5e0e5591 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -97,9 +97,8 @@ export const WARN_SPLITS_FILTER_EMPTY = 221; export const WARN_SDK_KEY = 222; export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223; export const STREAMING_PARSING_SPLIT_UPDATE = 224; -export const WARN_SPLITS_FILTER_NAME_AND_SET = 225; -export const WARN_SPLITS_FILTER_INVALID_SET = 226; -export const WARN_SPLITS_FILTER_LOWERCASE_SET = 227; +export const WARN_SPLITS_FILTER_INVALID_SET = 225; +export const WARN_SPLITS_FILTER_LOWERCASE_SET = 226; export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; export const ERROR_LOGLEVEL_INVALID = 301; @@ -129,6 +128,7 @@ export const ERROR_STORAGE_INVALID = 324; export const ERROR_NOT_BOOLEAN = 325; export const ERROR_MIN_CONFIG_PARAM = 326; export const ERROR_TOO_MANY_SETS = 327; +export const ERROR_SPLITS_FILTER_NAME_AND_SET = 328; // Log prefixes (a.k.a. tags or categories) export const LOG_PREFIX_SETTINGS = 'settings'; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index f5bd61d5..91f2481f 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -35,4 +35,5 @@ export const codesError: [number, string][] = [ [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Falling back into default MEMORY storage'], [c.ERROR_MIN_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': the provided "%s" config param is lower than allowed. Setting to the minimum value %s seconds'], [c.ERROR_TOO_MANY_SETS, c.LOG_PREFIX_SETTINGS + ': the amount of flag sets provided are big causing uri length error.'], + [c.ERROR_SPLITS_FILTER_NAME_AND_SET, c.LOG_PREFIX_SETTINGS+': names and sets filter cannot be used at the same time. The sdk will proceed using sets filter.'], ]; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 1a29dd66..4dce8590 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -33,7 +33,6 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'], [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'], - [c.WARN_SPLITS_FILTER_NAME_AND_SET, c.LOG_PREFIX_SETTINGS+': names and sets filter cannot be used at the same time. The sdk will proceed using sets filter.'], [c.WARN_SPLITS_FILTER_INVALID_SET, c.LOG_PREFIX_SETTINGS+': you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.'], [c.WARN_SPLITS_FILTER_LOWERCASE_SET, c.LOG_PREFIX_SETTINGS+': flag set %s should be all lowercase - converting string to lowercase.'], ]); diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 5d827177..bfaa6885 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -21,7 +21,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * @param {number | undefined} expirationTimestamp * @param {ISplitFiltersValidation} splitFiltersValidation */ - constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }) { + constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [], originalFilters: [] }) { super(); this.keys = keys; this.splitFiltersValidation = splitFiltersValidation; diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 3729eded..611a5fb9 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -92,7 +92,7 @@ test('splitChangesUpdater / segments parser', () => { }); test('splitChangesUpdater / compute splits mutation', () => { - const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; + const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [], originalFilters: [] }; let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); @@ -111,7 +111,7 @@ test('splitChangesUpdater / compute splits mutation', () => { test('splitChangesUpdater / compute splits mutation with filters', () => { // SDK initialization with sets: [set_a, set_b] - let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; + let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [], originalFilters: [] }; // fetching new feature flag in sets A & B let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); @@ -139,7 +139,7 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); // SDK initialization with names: ['test2'] - splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; + splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [], originalFilters: [] }; splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); @@ -168,7 +168,7 @@ describe('splitChangesUpdater', () => { const readinessManager = readinessManagerFactory(EventEmitter); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); - const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; + const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [], originalFilters: [] }; const splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); diff --git a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts index be7b6542..000dc0a7 100644 --- a/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts +++ b/src/sync/submitters/__tests__/telemetrySubmitter.spec.ts @@ -77,7 +77,7 @@ describe('Telemetry submitter', () => { expect(recordTimeUntilReadySpy).toBeCalledTimes(1); expect(postMetricsConfig).toBeCalledWith(JSON.stringify({ - oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, se: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0 + oM: 0, st: 'memory', aF: 0, rF: 0, sE: true, rR: { sp: 0.001, se: 0.001, im: 0.001, ev: 0.001, te: 0.1 }, uO: { s: true, e: true, a: true, st: true, t: true }, iQ: 1, eQ: 1, iM: 0, iL: false, hP: false, tR: 0, tC: 0, nR: 0, t: [], i: ['NoopIntegration'], uC: 0, fsT: 0, fsI: 0 })); // Stop submitter, to not execute the 1st periodic metrics/usage POST diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index a72d40b6..598281ad 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -3,12 +3,13 @@ import { submitterFactory, firstPushWindowDecorator } from './submitter'; import { TelemetryConfigStatsPayload, TelemetryConfigStats } from './types'; import { CONSUMER_MODE, CONSUMER_ENUM, STANDALONE_MODE, CONSUMER_PARTIAL_MODE, STANDALONE_ENUM, CONSUMER_PARTIAL_ENUM, OPTIMIZED, DEBUG, NONE, DEBUG_ENUM, OPTIMIZED_ENUM, NONE_ENUM, CONSENT_GRANTED, CONSENT_DECLINED, CONSENT_UNKNOWN } from '../../utils/constants'; import { SDK_READY, SDK_READY_FROM_CACHE } from '../../readiness/constants'; -import { ConsentStatus, ISettings, SDKMode } from '../../types'; +import { ConsentStatus, ISettings, SDKMode, SplitIO } from '../../types'; import { base } from '../../utils/settingsValidation'; import { usedKeysMap } from '../../utils/inputValidation/apiKey'; import { timer } from '../../utils/timeTracker/timer'; import { ISdkFactoryContextSync } from '../../sdkFactory/types'; import { objectAssign } from '../../utils/lang/objectAssign'; +import { ISplitFiltersValidation } from '../../dtos/types'; const OPERATION_MODE_MAP = { [STANDALONE_MODE]: STANDALONE_ENUM, @@ -38,6 +39,25 @@ function getRedundantActiveFactories() { }, 0); } +function getTelemetryFlagSetsStats(splitFiltersValidation: ISplitFiltersValidation) { + // Group every configured flagset in an unique array called originalSets + const originalSets: any[] = []; + splitFiltersValidation.originalFilters.forEach((filter: SplitIO.SplitFilter) => { + if (filter.type === 'bySet') { + if (Array.isArray(filter.values) && filter.values.length > 0) { + originalSets.push(...filter.values); + return; + } + else originalSets.push(filter.values); + } + }); + + const flagSetsTotal = originalSets.length; + const flagSetsValid = splitFiltersValidation.groupedFilters.bySet.length; + const flagSetsIgnored = flagSetsTotal - flagSetsValid; + return { flagSetsTotal, flagSetsIgnored }; +} + export function getTelemetryConfigStats(mode: SDKMode, storageType: string): TelemetryConfigStats { return { oM: OPERATION_MODE_MAP[mode], // @ts-ignore lower case of storage type @@ -59,6 +79,8 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett const { urls, scheduler } = settings; const isClientSide = settings.core.key !== undefined; + const { flagSetsTotal, flagSetsIgnored } = getTelemetryFlagSetsStats(settings.sync.__splitFiltersValidation); + return objectAssign(getTelemetryConfigStats(settings.mode, settings.storage.type), { sE: settings.streamingEnabled, rR: { @@ -86,7 +108,9 @@ export function telemetryCacheConfigAdapter(telemetry: ITelemetryCacheSync, sett nR: telemetry.getNonReadyUsage(), t: telemetry.popTags(), i: settings.integrations && settings.integrations.map(int => int.type), - uC: settings.userConsent ? USER_CONSENT_MAP[settings.userConsent] : 0 + uC: settings.userConsent ? USER_CONSENT_MAP[settings.userConsent] : 0, + fsT: flagSetsTotal, + fsI: flagSetsIgnored }); } }; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 95653fec..13b55391 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -234,6 +234,8 @@ export type TelemetryConfigStatsPayload = TelemetryConfigStats & { nR: number, // SDKNotReadyUsage i?: Array, // integrations uC: number, // userConsent + fsT: number, // flagsetsTotal + fsI: number, // flagsetsInvalid } export interface ISubmitterManager extends ISyncTask { diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 6a1f1c82..a47f19ec 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -77,7 +77,8 @@ export const fullSettings: ISettings = { __splitFiltersValidation: { validFilters: [], queryString: null, - groupedFilters: { bySet: [], byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] }, + originalFilters: [], }, enabled: true }, diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index 73938186..fa784808 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -7,14 +7,15 @@ import { splitFilters, queryStrings, groupedFilters, flagSetValidFilters } from // Test target import { validateSplitFilters } from '../splitFilters'; -import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, WARN_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET } from '../../../logger/constants'; +import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET } from '../../../logger/constants'; describe('validateSplitFilters', () => { const defaultOutput = { validFilters: [], queryString: null, - groupedFilters: { bySet: [], byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] }, + originalFilters: [] }; const getOutput = (testIndex: number) => { @@ -22,7 +23,8 @@ describe('validateSplitFilters', () => { // @ts-ignore validFilters: [...flagSetValidFilters[testIndex]], queryString: queryStrings[testIndex], - groupedFilters: groupedFilters[testIndex] + groupedFilters: groupedFilters[testIndex], + originalFilters: splitFilters[testIndex] }; }; @@ -57,7 +59,8 @@ describe('validateSplitFilters', () => { const output = { validFilters: [...splitFilters], queryString: null, - groupedFilters: { bySet: [], byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] }, + originalFilters: [...splitFilters] }; expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // filters without values expect(loggerMock.debug).toBeCalledWith(SETTINGS_SPLITS_FILTER, [null]); @@ -69,6 +72,11 @@ describe('validateSplitFilters', () => { { type: null, values: [] }, { type: 'byName', values: [13] }); output.validFilters.push({ type: 'byName', values: [13] }); + output.originalFilters.push( + { type: 'invalid', values: [] }, + { type: 'byName', values: 'invalid' }, + { type: null, values: [] }, + { type: 'byName', values: [13] }); expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // some filters are invalid expect(loggerMock.debug.mock.calls).toEqual([[SETTINGS_SPLITS_FILTER, [null]]]); expect(loggerMock.warn.mock.calls).toEqual([ @@ -91,7 +99,8 @@ describe('validateSplitFilters', () => { const output = { validFilters: [...splitFilters[i]], queryString: queryStrings[i], - groupedFilters: groupedFilters[i] + groupedFilters: groupedFilters[i], + originalFilters: [...splitFilters[i]] }; expect(validateSplitFilters(loggerMock, splitFilters[i], STANDALONE_MODE)).toEqual(output); // splitFilters #${i} expect(loggerMock.debug).lastCalledWith(SETTINGS_SPLITS_FILTER, [queryStrings[i]]); @@ -108,24 +117,25 @@ describe('validateSplitFilters', () => { expect(loggerMock.warn.mock.calls[0]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_1']]); expect(loggerMock.warn.mock.calls[1]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', 'set_3 ']]); expect(loggerMock.warn.mock.calls[2]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_a ']]); - expect(loggerMock.warn.mock.calls[3]).toEqual([WARN_SPLITS_FILTER_NAME_AND_SET]); + expect(loggerMock.error.mock.calls[0]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); expect(validateSplitFilters(loggerMock, splitFilters[7], STANDALONE_MODE)).toEqual(getOutput(7)); // lowercase and regexp - expect(loggerMock.warn.mock.calls[4]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['seT_c']]); // lowercase - expect(loggerMock.warn.mock.calls[5]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase - expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_ 1', regexp, 'set_ 1']]); // empty spaces - expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set _3', regexp, 'set _3']]); // empty spaces - expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['3set_a', regexp, '3set_a']]); // start with a letter - expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_2', regexp, '_set_2']]); // start with a letter - expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters - expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_NAME_AND_SET]); + expect(loggerMock.warn.mock.calls[3]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['seT_c']]); // lowercase + expect(loggerMock.warn.mock.calls[4]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase + expect(loggerMock.warn.mock.calls[5]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_ 1', regexp, 'set_ 1']]); // empty spaces + expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set _3', regexp, 'set _3']]); // empty spaces + expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['3set_a', regexp, '3set_a']]); // start with a letter + expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_2', regexp, '_set_2']]); // start with a letter + expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters + expect(loggerMock.error.mock.calls[1]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); expect(validateSplitFilters(loggerMock, splitFilters[8], STANDALONE_MODE)).toEqual(getOutput(8)); // lowercase and dedupe - expect(loggerMock.warn.mock.calls[12]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase - expect(loggerMock.warn.mock.calls[13]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase - expect(loggerMock.warn.mock.calls[14]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character - expect(loggerMock.warn.mock.calls[15]).toEqual([WARN_SPLITS_FILTER_NAME_AND_SET]); + expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase + expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase + expect(loggerMock.warn.mock.calls[12]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character + expect(loggerMock.error.mock.calls[2]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); - expect(loggerMock.warn.mock.calls.length).toEqual(16); + expect(loggerMock.warn.mock.calls.length).toEqual(13); + expect(loggerMock.error.mock.calls.length).toEqual(3); }); }); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index fe8ece49..d2049d44 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -3,7 +3,7 @@ import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, WARN_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET } from '../../logger/constants'; import { objectAssign } from '../lang/objectAssign'; import { find, uniq } from '../lang'; @@ -142,7 +142,8 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: const res = { validFilters: [], queryString: null, - groupedFilters: { bySet: [], byName: [], byPrefix: [] } + groupedFilters: { bySet: [], byName: [], byPrefix: [] }, + originalFilters: [] } as ISplitFiltersValidation; // do nothing if `splitFilters` param is not a non-empty array or mode is not STANDALONE @@ -158,6 +159,7 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: return res; } + res.originalFilters = maybeSplitFilters; // Validate filters and group their values by filter type inside `groupedFilters` object res.validFilters = maybeSplitFilters.filter((filter, index) => { if (filter && validateFilterType(filter.type) && Array.isArray(filter.values)) { @@ -177,9 +179,9 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: const setFilter = configuredFilter(res.validFilters, 'bySet'); // Clean all filters if set filter is present if (setFilter) { - if (configuredFilter(res.validFilters, 'byName')) log.warn(WARN_SPLITS_FILTER_NAME_AND_SET); + if (configuredFilter(res.validFilters, 'byName')) log.error(ERROR_SPLITS_FILTER_NAME_AND_SET); objectAssign(res.groupedFilters, { byName: [], byPrefix: [] }); - res.validFilters = [setFilter]; + res.validFilters = [{type: 'bySet', values: res.groupedFilters.bySet}]; } // build query string From e2da3d85770ae30eed26961f20f33ab1d788551a Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 7 Sep 2023 16:56:45 -0300 Subject: [PATCH 09/43] Rollback originalFilters, fix validFilters property --- src/__tests__/mocks/fetchSpecificSplits.ts | 7 ------- src/dtos/types.ts | 1 - src/storages/inLocalStorage/SplitsCacheInLocal.ts | 2 +- .../updaters/__tests__/splitChangesUpdater.spec.ts | 8 ++++---- src/sync/submitters/telemetrySubmitter.ts | 13 +++---------- .../settingsValidation/__tests__/settings.mocks.ts | 1 - .../__tests__/splitFilters.spec.ts | 13 ++----------- src/utils/settingsValidation/splitFilters.ts | 3 --- 8 files changed, 10 insertions(+), 38 deletions(-) diff --git a/src/__tests__/mocks/fetchSpecificSplits.ts b/src/__tests__/mocks/fetchSpecificSplits.ts index 6bd10189..f3199134 100644 --- a/src/__tests__/mocks/fetchSpecificSplits.ts +++ b/src/__tests__/mocks/fetchSpecificSplits.ts @@ -124,10 +124,3 @@ export const groupedFilters = [ byPrefix: [] }, ]; - -export const flagSetValidFilters = [ - undefined, undefined, undefined, undefined, undefined, undefined, - [{ type: 'bySet', values: valuesExamples[10] }], - [{ type: 'bySet', values: valuesExamples[12] }], - [{ type: 'bySet', values: valuesExamples[14] }], -]; diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 3cec6804..4fdf562c 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -210,5 +210,4 @@ export type ISplitFiltersValidation = { queryString: string | null, groupedFilters: Record, validFilters: SplitIO.SplitFilter[], - originalFilters: SplitIO.SplitFilter[] }; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index bfaa6885..5d827177 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -21,7 +21,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * @param {number | undefined} expirationTimestamp * @param {ISplitFiltersValidation} splitFiltersValidation */ - constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [], originalFilters: [] }) { + constructor(private readonly log: ILogger, keys: KeyBuilderCS, expirationTimestamp?: number, splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }) { super(); this.keys = keys; this.splitFiltersValidation = splitFiltersValidation; diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 611a5fb9..3729eded 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -92,7 +92,7 @@ test('splitChangesUpdater / segments parser', () => { }); test('splitChangesUpdater / compute splits mutation', () => { - const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [], originalFilters: [] }; + const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); @@ -111,7 +111,7 @@ test('splitChangesUpdater / compute splits mutation', () => { test('splitChangesUpdater / compute splits mutation with filters', () => { // SDK initialization with sets: [set_a, set_b] - let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [], originalFilters: [] }; + let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a','set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; // fetching new feature flag in sets A & B let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); @@ -139,7 +139,7 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { expect(splitsMutation.removed).toEqual([testFFEmptySet.name]); // SDK initialization with names: ['test2'] - splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [], originalFilters: [] }; + splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); expect(splitsMutation.added).toEqual([]); @@ -168,7 +168,7 @@ describe('splitChangesUpdater', () => { const readinessManager = readinessManagerFactory(EventEmitter); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); - const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [], originalFilters: [] }; + const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; const splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 598281ad..63996f95 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -41,18 +41,11 @@ function getRedundantActiveFactories() { function getTelemetryFlagSetsStats(splitFiltersValidation: ISplitFiltersValidation) { // Group every configured flagset in an unique array called originalSets - const originalSets: any[] = []; - splitFiltersValidation.originalFilters.forEach((filter: SplitIO.SplitFilter) => { - if (filter.type === 'bySet') { - if (Array.isArray(filter.values) && filter.values.length > 0) { - originalSets.push(...filter.values); - return; - } - else originalSets.push(filter.values); - } + let flagSetsTotal = 0; + splitFiltersValidation.validFilters.forEach((filter: SplitIO.SplitFilter) => { + if (filter.type === 'bySet') flagSetsTotal += filter.values.length; }); - const flagSetsTotal = originalSets.length; const flagSetsValid = splitFiltersValidation.groupedFilters.bySet.length; const flagSetsIgnored = flagSetsTotal - flagSetsValid; return { flagSetsTotal, flagSetsIgnored }; diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index a47f19ec..ac2b8293 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -78,7 +78,6 @@ export const fullSettings: ISettings = { validFilters: [], queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, - originalFilters: [], }, enabled: true }, diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index fa784808..dc3ddf6e 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -3,7 +3,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { STANDALONE_MODE, CONSUMER_MODE } from '../../constants'; // Split filter and QueryStrings examples -import { splitFilters, queryStrings, groupedFilters, flagSetValidFilters } from '../../../__tests__/mocks/fetchSpecificSplits'; +import { splitFilters, queryStrings, groupedFilters } from '../../../__tests__/mocks/fetchSpecificSplits'; // Test target import { validateSplitFilters } from '../splitFilters'; @@ -15,16 +15,14 @@ describe('validateSplitFilters', () => { validFilters: [], queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, - originalFilters: [] }; const getOutput = (testIndex: number) => { return { // @ts-ignore - validFilters: [...flagSetValidFilters[testIndex]], + validFilters: splitFilters[testIndex], queryString: queryStrings[testIndex], groupedFilters: groupedFilters[testIndex], - originalFilters: splitFilters[testIndex] }; }; @@ -60,7 +58,6 @@ describe('validateSplitFilters', () => { validFilters: [...splitFilters], queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, - originalFilters: [...splitFilters] }; expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // filters without values expect(loggerMock.debug).toBeCalledWith(SETTINGS_SPLITS_FILTER, [null]); @@ -72,11 +69,6 @@ describe('validateSplitFilters', () => { { type: null, values: [] }, { type: 'byName', values: [13] }); output.validFilters.push({ type: 'byName', values: [13] }); - output.originalFilters.push( - { type: 'invalid', values: [] }, - { type: 'byName', values: 'invalid' }, - { type: null, values: [] }, - { type: 'byName', values: [13] }); expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // some filters are invalid expect(loggerMock.debug.mock.calls).toEqual([[SETTINGS_SPLITS_FILTER, [null]]]); expect(loggerMock.warn.mock.calls).toEqual([ @@ -100,7 +92,6 @@ describe('validateSplitFilters', () => { validFilters: [...splitFilters[i]], queryString: queryStrings[i], groupedFilters: groupedFilters[i], - originalFilters: [...splitFilters[i]] }; expect(validateSplitFilters(loggerMock, splitFilters[i], STANDALONE_MODE)).toEqual(output); // splitFilters #${i} expect(loggerMock.debug).lastCalledWith(SETTINGS_SPLITS_FILTER, [queryStrings[i]]); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index d2049d44..b54b1d5e 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -143,7 +143,6 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: validFilters: [], queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, - originalFilters: [] } as ISplitFiltersValidation; // do nothing if `splitFilters` param is not a non-empty array or mode is not STANDALONE @@ -159,7 +158,6 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: return res; } - res.originalFilters = maybeSplitFilters; // Validate filters and group their values by filter type inside `groupedFilters` object res.validFilters = maybeSplitFilters.filter((filter, index) => { if (filter && validateFilterType(filter.type) && Array.isArray(filter.values)) { @@ -181,7 +179,6 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: if (setFilter) { if (configuredFilter(res.validFilters, 'byName')) log.error(ERROR_SPLITS_FILTER_NAME_AND_SET); objectAssign(res.groupedFilters, { byName: [], byPrefix: [] }); - res.validFilters = [{type: 'bySet', values: res.groupedFilters.bySet}]; } // build query string From a903878835e2304935cdc3cbc16591c96504c364 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 8 Sep 2023 17:24:14 -0300 Subject: [PATCH 10/43] Update regexp to allow to start with a letter or number --- src/__tests__/mocks/fetchSpecificSplits.ts | 4 ++-- .../__tests__/splitFilters.spec.ts | 15 +++++++-------- src/utils/settingsValidation/splitFilters.ts | 6 +++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/__tests__/mocks/fetchSpecificSplits.ts b/src/__tests__/mocks/fetchSpecificSplits.ts index f3199134..4b2f4c90 100644 --- a/src/__tests__/mocks/fetchSpecificSplits.ts +++ b/src/__tests__/mocks/fetchSpecificSplits.ts @@ -14,7 +14,7 @@ const valuesExamples = [ [' set_1','set_3 ',' set_a ','set_2','set_c','set_b'], // [9] trim ['set_1','set_2','set_3','set_a','set_b','set_c'], // [10] sanitized [9] ['set_ 1','set _3','3set_a','_set_2','seT_c','set_B','set_1234567890_1234567890_234567890_1234567890_1234567890','set_a','set_2'], // [11] lowercase & regexp - ['set_2','set_a','set_b','set_c'], // [12] sanitized [11] + ['3set_a','set_2','set_a','set_b','set_c'], // [12] sanitized [11] ['set_2','set_a','SET_2','set_a','set_b','set_B','set_1','set_3!'], // [13] dedupe, dedupe with case sensitive ['set_1','set_2','set_a','set_b'], // [14] sanitized [13] ]; @@ -77,7 +77,7 @@ export const queryStrings = [ '&names=%25,%2525,__a,__%D1%88', // [5] // FlagSet filters '&sets=set_1,set_2,set_3,set_a,set_b,set_c', // [6] - '&sets=set_2,set_a,set_b,set_c', // [7] + '&sets=3set_a,set_2,set_a,set_b,set_c', // [7] '&sets=set_1,set_2,set_a,set_b', // [8] ]; diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index dc3ddf6e..455a385f 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -26,7 +26,7 @@ describe('validateSplitFilters', () => { }; }; - const regexp = /^[a-z][_a-z0-9]{0,49}$/; + const regexp = /^[a-z0-9][_a-z0-9]{0,49}$/; afterEach(() => { loggerMock.mockClear(); }); @@ -115,18 +115,17 @@ describe('validateSplitFilters', () => { expect(loggerMock.warn.mock.calls[4]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase expect(loggerMock.warn.mock.calls[5]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_ 1', regexp, 'set_ 1']]); // empty spaces expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set _3', regexp, 'set _3']]); // empty spaces - expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['3set_a', regexp, '3set_a']]); // start with a letter - expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_2', regexp, '_set_2']]); // start with a letter - expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters + expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_2', regexp, '_set_2']]); // start with a letter + expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters expect(loggerMock.error.mock.calls[1]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); expect(validateSplitFilters(loggerMock, splitFilters[8], STANDALONE_MODE)).toEqual(getOutput(8)); // lowercase and dedupe - expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase - expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase - expect(loggerMock.warn.mock.calls[12]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character + expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase + expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase + expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character expect(loggerMock.error.mock.calls[2]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); - expect(loggerMock.warn.mock.calls.length).toEqual(13); + expect(loggerMock.warn.mock.calls.length).toEqual(12); expect(loggerMock.error.mock.calls.length).toEqual(3); }); }); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index b54b1d5e..4cd10c30 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -27,7 +27,7 @@ const FILTERS_METADATA = [ } ]; -const VALID_FLAGSET_REGEX = /^[a-z][_a-z0-9]{0,49}$/; +const VALID_FLAGSET_REGEX = /^[a-z0-9][_a-z0-9]{0,49}$/; const CAPITAL_LETTERS_REGEX = /[A-Z]/; /** @@ -90,8 +90,8 @@ function queryStringBuilder(groupedFilters: Record Date: Fri, 8 Sep 2023 17:52:38 -0300 Subject: [PATCH 11/43] FIX typo --- src/logger/messages/warn.ts | 2 +- src/utils/settingsValidation/splitFilters.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 4dce8590..b576095a 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -33,6 +33,6 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'], [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'], - [c.WARN_SPLITS_FILTER_INVALID_SET, c.LOG_PREFIX_SETTINGS+': you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter, be in lowercase, alphanumeric and have a max length of 50 characteres. %s was discarded.'], + [c.WARN_SPLITS_FILTER_INVALID_SET, c.LOG_PREFIX_SETTINGS+': you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. %s was discarded.'], [c.WARN_SPLITS_FILTER_LOWERCASE_SET, c.LOG_PREFIX_SETTINGS+': flag set %s should be all lowercase - converting string to lowercase.'], ]); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 4cd10c30..a4869ca4 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -94,7 +94,7 @@ function queryStringBuilder(groupedFilters: Record Date: Mon, 11 Sep 2023 19:10:29 -0300 Subject: [PATCH 12/43] [SDKS-7496] remove all flags when checkFilterQuery --- .../inLocalStorage/SplitsCacheInLocal.ts | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 5d827177..0d05a297 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -13,7 +13,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly keys: KeyBuilderCS; private readonly splitFiltersValidation: ISplitFiltersValidation; private hasSync?: boolean; - private cacheReadyButNeedsToFlush: boolean = false; private updateNewFilter?: boolean; /** @@ -133,11 +132,6 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } setChangeNumber(changeNumber: number): boolean { - // when cache is ready but using a new split query, we must clear all split data - if (this.cacheReadyButNeedsToFlush) { - this.clear(); - this.cacheReadyButNeedsToFlush = false; - } // when using a new split query, we must update it at the store if (this.updateNewFilter) { @@ -220,7 +214,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { * @override */ checkCache(): boolean { - return this.getChangeNumber() > -1 || this.cacheReadyButNeedsToFlush; + return this.getChangeNumber() > -1; } /** @@ -237,7 +231,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } private _checkFilterQuery() { - const { queryString, groupedFilters } = this.splitFiltersValidation; + const { queryString } = this.splitFiltersValidation; const queryKey = this.keys.buildSplitsFilterQueryKey(); const currentQueryString = localStorage.getItem(queryKey); @@ -251,19 +245,8 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // * set change number to -1, to fetch splits with -1 `since` value. localStorage.setItem(this.keys.buildSplitsTillKey(), '-1'); - // * remove from cache splits that doesn't match with the new filters - this.getSplitNames().forEach((splitName) => { - if (queryString && ( - // @TODO consider redefining `groupedFilters` to expose a method like `groupedFilters::filter(splitName): boolean` - groupedFilters.byName.indexOf(splitName) > -1 || - groupedFilters.byPrefix.some((prefix: string) => splitName.startsWith(prefix + '__')) - )) { - // * set `cacheReadyButNeedsToFlush` so that `checkCache` returns true (the storage is ready to be used) and the data is cleared before updating on first successful splits fetch - this.cacheReadyButNeedsToFlush = true; - return; - } - this.removeSplit(splitName); - }); + // * Remove all splits from cache + this.removeSplits(this.getSplitNames()); } } catch (e) { this.log.error(LOG_PREFIX + e); From 5fc6faf25746055e781da44a557ec7dc00ec00a9 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 14 Sep 2023 14:23:36 -0300 Subject: [PATCH 13/43] clear cache instead of cleaning flags only --- src/storages/inLocalStorage/SplitsCacheInLocal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 0d05a297..6809518c 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -245,8 +245,8 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // * set change number to -1, to fetch splits with -1 `since` value. localStorage.setItem(this.keys.buildSplitsTillKey(), '-1'); - // * Remove all splits from cache - this.removeSplits(this.getSplitNames()); + // * clear cache + this.clear(); } } catch (e) { this.log.error(LOG_PREFIX + e); From 186ea2386670003c7cc20728289a4e08b1fdd5d6 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 14 Sep 2023 15:51:12 -0300 Subject: [PATCH 14/43] remove unnecessary line --- src/storages/inLocalStorage/SplitsCacheInLocal.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 6809518c..20646262 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -240,14 +240,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // mark cache to update the new query filter on first successful splits fetch this.updateNewFilter = true; - // if cache is ready: - if (this.checkCache()) { - // * set change number to -1, to fetch splits with -1 `since` value. - localStorage.setItem(this.keys.buildSplitsTillKey(), '-1'); + // if there is cache, clear it + if (this.checkCache()) this.clear(); - // * clear cache - this.clear(); - } } catch (e) { this.log.error(LOG_PREFIX + e); } From cb081df9c49c7388aad044b8c65f075a8f9833b1 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 15 Sep 2023 19:38:57 -0300 Subject: [PATCH 15/43] [SDKS-7498] flagsets cache storage in memory & local storage --- src/storages/KeyBuilder.ts | 4 ++ src/storages/__tests__/KeyBuilder.spec.ts | 11 +++ src/storages/__tests__/testUtils.ts | 13 ++++ .../inLocalStorage/SplitsCacheInLocal.ts | 68 +++++++++++++++++++ .../__tests__/SplitsCacheInLocal.spec.ts | 63 ++++++++++++++++- src/storages/inLocalStorage/index.ts | 4 +- src/storages/inMemory/InMemoryStorage.ts | 4 +- src/storages/inMemory/InMemoryStorageCS.ts | 6 +- src/storages/inMemory/SplitsCacheInMemory.ts | 50 +++++++++++++- .../__tests__/SplitsCacheInMemory.spec.ts | 63 ++++++++++++++++- .../polling/updaters/splitChangesUpdater.ts | 3 +- src/utils/lang/__tests__/sets.spec.ts | 10 ++- src/utils/lang/sets.ts | 8 +++ 13 files changed, 294 insertions(+), 13 deletions(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 3c54d403..262bcf39 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -20,6 +20,10 @@ export class KeyBuilder { return `${this.prefix}.trafficType.${trafficType}`; } + buildFlagsetKey(flagset: string) { + return `${this.prefix}.flagset.${flagset}`; + } + buildSplitKey(splitName: string) { return `${this.prefix}.split.${splitName}`; } diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index 8103ef41..6da01a30 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -67,6 +67,17 @@ test('KEYS / traffic type keys', () => { }); +test('KEYS / flagset keys', () => { + const prefix = 'unit_test.SPLITIO'; + const builder = new KeyBuilder(prefix); + + const flagsetName = 'flagset_x'; + const expectedKey = `${prefix}.flagset.${flagsetName}`; + + expect(builder.buildFlagsetKey(flagsetName)).toBe(expectedKey); + +}); + test('KEYS / impressions', () => { const builder = new KeyBuilderSS(prefix, metadata); diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index 2f8cb245..ff90bc3b 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -32,3 +32,16 @@ export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'acc export const something: ISplit = { name: 'something' }; //@ts-ignore export const somethingElse: ISplit = { name: 'something else' }; + +// - With flagsets + +//@ts-ignore +export const featureFlagWithEmptyFS: ISplit = { name: 'ff_empty', sets: [] }; +//@ts-ignore +export const featureFlagOne: ISplit = { name: 'ff_one', sets: ['o','n','e'] }; +//@ts-ignore +export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t','w','o'] }; +//@ts-ignore +export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t','h','r','e'] }; +//@ts-ignore +export const featureFlagWithoutFS: ISplit = { name: 'ff_four' }; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 20646262..6d702aeb 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -4,6 +4,7 @@ import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang'; import { KeyBuilderCS } from '../KeyBuilderCS'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; +import { ISet, _Set, returnSetsUnion, setToArray } from '../../utils/lang/sets'; /** * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage. @@ -12,6 +13,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly keys: KeyBuilderCS; private readonly splitFiltersValidation: ISplitFiltersValidation; + private readonly flagsetsFilter: string[]; private hasSync?: boolean; private updateNewFilter?: boolean; @@ -24,6 +26,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { super(); this.keys = keys; this.splitFiltersValidation = splitFiltersValidation; + this.flagsetsFilter = this.splitFiltersValidation.groupedFilters.bySet; this._checkExpiration(expirationTimestamp); @@ -105,6 +108,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this._incrementCounts(split); this._decrementCounts(previousSplit); + if (previousSplit && previousSplit.sets) this.removeFromFlagsets(previousSplit.name, previousSplit.sets); + this.addToFlagsets(split); + return true; } catch (e) { this.log.error(LOG_PREFIX + e); @@ -118,6 +124,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { localStorage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); + if (split && split.sets) this.removeFromFlagsets(split.name, split.sets); return true; } catch (e) { @@ -249,4 +256,65 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } // if the filter didn't change, nothing is done } + + getNamesByFlagsets(flagsets: string[]): ISet{ + let toReturn: ISet = new _Set([]); + flagsets.forEach(flagset => { + const flagsetKey = this.keys.buildFlagsetKey(flagset); + let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); + + if (flagsetFromLocalStorage) { + const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage)); + toReturn = returnSetsUnion(toReturn, flagsetCache); + } + }); + return toReturn; + + } + + addToFlagsets(featureFlag: ISplit) { + if (!featureFlag.sets) return; + + featureFlag.sets.forEach(featureFlagset => { + + if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagset => filterFlagset === featureFlagset)) return; + + const flagsetKey = this.keys.buildFlagsetKey(featureFlagset); + + let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); + if (!flagsetFromLocalStorage) flagsetFromLocalStorage = '[]'; + + const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage)); + flagsetCache.add(featureFlag.name); + + localStorage.setItem(flagsetKey, JSON.stringify(setToArray(flagsetCache))); + }); + } + + removeFromFlagsets(featureFlagName: string, flagsets: string[]) { + if (!flagsets) return; + + flagsets.forEach(flagset => { + this.removeNames(flagset, featureFlagName); + }); + } + + removeNames(flagsetName: string, featureFlagName: string) { + const flagsetKey = this.keys.buildFlagsetKey(flagsetName); + + let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); + + if (!flagsetFromLocalStorage) return; + + const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage)); + flagsetCache.delete(featureFlagName); + + if (flagsetCache.size === 0) { + localStorage.removeItem(flagsetKey); + return; + } + + localStorage.setItem(flagsetKey, JSON.stringify(setToArray(flagsetCache))); + } + } diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 782b2f7f..7e9c25c9 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -1,8 +1,9 @@ import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; import { KeyBuilderCS } from '../../KeyBuilderCS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments, something, somethingElse } from '../../__tests__/testUtils'; +import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments, something, somethingElse, featureFlagOne, featureFlagTwo, featureFlagThree, featureFlagWithEmptyFS, featureFlagWithoutFS } from '../../__tests__/testUtils'; import { ISplit } from '../../../dtos/types'; +import { _Set } from '../../../utils/lang/sets'; test('SPLIT CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); @@ -160,3 +161,63 @@ test('SPLIT CACHE / LocalStorage / usesSegments', () => { cache.removeSplit('split4'); expect(cache.usesSegments()).toBe(false); // 0 splits using segments }); + +test('SPLIT CACHE / LocalStorage / flagset cache tests', () => { + // @ts-ignore + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'), undefined, { groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); + const emptySet = new _Set([]); + + cache.addSplits([ + [featureFlagOne.name, featureFlagOne], + [featureFlagTwo.name, featureFlagTwo], + [featureFlagThree.name, featureFlagThree], + ]); + cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + + expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cache.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cache.getNamesByFlagsets(['t'])).toEqual(emptySet); // 't' not in filter + expect(cache.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + + cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['1']}); + + expect(cache.getNamesByFlagsets(['1'])).toEqual(emptySet); // '1' not in filter + expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_two'])); + expect(cache.getNamesByFlagsets(['n'])).toEqual(emptySet); + + cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['x']}); + expect(cache.getNamesByFlagsets(['x'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagsets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + + + cache.removeSplit(featureFlagOne.name); + expect(cache.getNamesByFlagsets(['x'])).toEqual(emptySet); + + cache.removeSplit(featureFlagOne.name); + expect(cache.getNamesByFlagsets(['y'])).toEqual(emptySet); // 'y' not in filter + expect(cache.getNamesByFlagsets([])).toEqual(emptySet); + + cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + expect(cache.getNamesByFlagsets([])).toEqual(emptySet); +}); + +// if FlagSets are not defined, it should store all FlagSets in memory. +test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => { + const cacheWithoutFilters = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); + const emptySet = new _Set([]); + + cacheWithoutFilters.addSplits([ + [featureFlagOne.name, featureFlagOne], + [featureFlagTwo.name, featureFlagTwo], + [featureFlagThree.name, featureFlagThree], + ]); + cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + + expect(cacheWithoutFilters.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['t'])).toEqual(new _Set(['ff_two','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['y'])).toEqual(emptySet); + expect(cacheWithoutFilters.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); +}); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 59eefae5..79bf1946 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -54,7 +54,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn uniqueKeys: impressionsMode === NONE ? new UniqueKeysCacheInMemoryCS() : undefined, destroy() { - this.splits = new SplitsCacheInMemory(); + this.splits = new SplitsCacheInMemory(__splitFiltersValidation); this.segments = new MySegmentsCacheInMemory(); this.impressions.clear(); this.impressionCounts && this.impressionCounts.clear(); @@ -75,7 +75,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn telemetry: this.telemetry, destroy() { - this.splits = new SplitsCacheInMemory(); + this.splits = new SplitsCacheInMemory(__splitFiltersValidation); this.segments = new MySegmentsCacheInMemory(); } }; diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index fbc6711d..ccf3bd6a 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -14,9 +14,9 @@ import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory'; * @param params parameters required by EventsCacheSync */ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageSync { - const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params; + const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params; - const splits = new SplitsCacheInMemory(); + const splits = new SplitsCacheInMemory(__splitFiltersValidation); const segments = new SegmentsCacheInMemory(); const storage = { diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index 2fd31203..84d2351b 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -14,9 +14,9 @@ import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS'; * @param params parameters required by EventsCacheSync */ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorageSync { - const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode } } } = params; + const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { impressionsMode, __splitFiltersValidation } } } = params; - const splits = new SplitsCacheInMemory(); + const splits = new SplitsCacheInMemory(__splitFiltersValidation); const segments = new MySegmentsCacheInMemory(); const storage = { @@ -50,7 +50,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag // Set a new splits cache to clean it for the client without affecting other clients destroy() { - this.splits = new SplitsCacheInMemory(); + this.splits = new SplitsCacheInMemory(__splitFiltersValidation); this.segments.clear(); } }; diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index ef0d9dcf..4c812906 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -1,6 +1,7 @@ -import { ISplit } from '../../dtos/types'; +import { ISplit, ISplitFiltersValidation } from '../../dtos/types'; import { AbstractSplitsCacheSync, usesSegments } from '../AbstractSplitsCacheSync'; import { isFiniteNumber } from '../../utils/lang'; +import { ISet, _Set, returnSetsUnion } from '../../utils/lang/sets'; /** * Default ISplitsCacheSync implementation that stores split definitions in memory. @@ -8,10 +9,17 @@ import { isFiniteNumber } from '../../utils/lang'; */ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { + private flagsetsFilter: string[]; private splitsCache: Record = {}; private ttCache: Record = {}; private changeNumber: number = -1; private splitsWithSegmentsCount: number = 0; + private flagsetsCache: Record> = {}; + + constructor(splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }) { + super(); + this.flagsetsFilter = splitFiltersValidation.groupedFilters.bySet; + } clear() { this.splitsCache = {}; @@ -26,6 +34,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { const previousTtName = previousSplit.trafficTypeName; this.ttCache[previousTtName]--; + this.removeFromFlagsets(previousSplit.name, previousSplit.sets); if (!this.ttCache[previousTtName]) delete this.ttCache[previousTtName]; if (usesSegments(previousSplit)) { // Substract from segments count for the previous version of this Split. @@ -39,6 +48,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { // Update TT cache const ttName = split.trafficTypeName; this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1; + this.addToFlagsets(split); // Add to segments count for the new version of the Split if (usesSegments(split)) this.splitsWithSegmentsCount++; @@ -58,6 +68,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { const ttName = split.trafficTypeName; this.ttCache[ttName]--; // Update tt cache if (!this.ttCache[ttName]) delete this.ttCache[ttName]; + this.removeFromFlagsets(split.name, split.sets); // Update the segments count. if (usesSegments(split)) this.splitsWithSegmentsCount--; @@ -93,4 +104,41 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { return this.getChangeNumber() === -1 || this.splitsWithSegmentsCount > 0; } + getNamesByFlagsets(flagsets: string[]): ISet{ + let toReturn: ISet = new _Set([]); + flagsets.forEach(flagset => { + const featureFlagNames = this.flagsetsCache[flagset]; + if (featureFlagNames) { + toReturn = returnSetsUnion(toReturn, featureFlagNames); + } + }); + return toReturn; + + } + + addToFlagsets(featureFlag: ISplit) { + if (!featureFlag.sets) return; + featureFlag.sets.forEach(featureFlagset => { + + if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagset => filterFlagset === featureFlagset)) return; + + if (!this.flagsetsCache[featureFlagset]) this.flagsetsCache[featureFlagset] = new _Set([]); + + this.flagsetsCache[featureFlagset].add(featureFlag.name); + }); + } + + removeFromFlagsets(featureFlagName :string, flagsets: string[] | undefined) { + if (!flagsets) return; + flagsets.forEach(flagset => { + this.removeNames(flagset, featureFlagName); + }); + } + + removeNames(flagsetName: string, featureFlagName: string) { + if (!this.flagsetsCache[flagsetName]) return; + this.flagsetsCache[flagsetName].delete(featureFlagName); + if (this.flagsetsCache[flagsetName].size === 0) delete this.flagsetsCache[flagsetName]; + } + } diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 979906df..62f4e08f 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -1,6 +1,7 @@ import { SplitsCacheInMemory } from '../SplitsCacheInMemory'; import { ISplit } from '../../../dtos/types'; -import { splitWithUserTT, splitWithAccountTT, something, somethingElse } from '../../__tests__/testUtils'; +import { splitWithUserTT, splitWithAccountTT, something, somethingElse, featureFlagWithEmptyFS, featureFlagWithoutFS, featureFlagOne, featureFlagTwo, featureFlagThree } from '../../__tests__/testUtils'; +import { _Set } from '../../../utils/lang/sets'; test('SPLITS CACHE / In Memory', () => { const cache = new SplitsCacheInMemory(); @@ -113,3 +114,63 @@ test('SPLITS CACHE / In Memory / killLocally', () => { expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older }); + +test('SPLITS CACHE / In Memory / flagset cache tests', () => { + // @ts-ignore + const cache = new SplitsCacheInMemory({ groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); + const emptySet = new _Set([]); + + cache.addSplits([ + [featureFlagOne.name, featureFlagOne], + [featureFlagTwo.name, featureFlagTwo], + [featureFlagThree.name, featureFlagThree], + ]); + cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + + expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cache.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cache.getNamesByFlagsets(['t'])).toEqual(emptySet); // 't' not in filter + expect(cache.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + + cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['1']}); + + expect(cache.getNamesByFlagsets(['1'])).toEqual(emptySet); // '1' not in filter + expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_two'])); + expect(cache.getNamesByFlagsets(['n'])).toEqual(emptySet); + + cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['x']}); + expect(cache.getNamesByFlagsets(['x'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagsets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + + + cache.removeSplit(featureFlagOne.name); + expect(cache.getNamesByFlagsets(['x'])).toEqual(emptySet); + + cache.removeSplit(featureFlagOne.name); + expect(cache.getNamesByFlagsets(['y'])).toEqual(emptySet); // 'y' not in filter + expect(cache.getNamesByFlagsets([])).toEqual(emptySet); + + cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); + expect(cache.getNamesByFlagsets([])).toEqual(emptySet); +}); + +// if FlagSets are not defined, it should store all FlagSets in memory. +test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => { + const cacheWithoutFilters = new SplitsCacheInMemory(); + const emptySet = new _Set([]); + + cacheWithoutFilters.addSplits([ + [featureFlagOne.name, featureFlagOne], + [featureFlagTwo.name, featureFlagTwo], + [featureFlagThree.name, featureFlagThree], + ]); + cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); + + expect(cacheWithoutFilters.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['t'])).toEqual(new _Set(['ff_two','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagsets(['y'])).toEqual(emptySet); + expect(cacheWithoutFilters.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); +}); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 92468362..ccbab176 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -7,7 +7,6 @@ import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; -import { find } from '../../../utils/lang'; type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise @@ -55,7 +54,7 @@ interface ISplitMutations { */ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { const { bySet: setsFilter, byName: namesFilter } = filters.groupedFilters; - if (setsFilter.length > 0) return featureFlag.sets && find(featureFlag.sets, (featureFlagSet: string) => setsFilter.indexOf(featureFlagSet) > -1); + if (setsFilter.length > 0) return featureFlag.sets && featureFlag.sets.some((featureFlagSet: string) => setsFilter.indexOf(featureFlagSet) > -1); if (namesFilter.length > 0) return namesFilter.indexOf(featureFlag.name) > -1; return true; } diff --git a/src/utils/lang/__tests__/sets.spec.ts b/src/utils/lang/__tests__/sets.spec.ts index ae01dded..82d6058e 100644 --- a/src/utils/lang/__tests__/sets.spec.ts +++ b/src/utils/lang/__tests__/sets.spec.ts @@ -1,4 +1,4 @@ -import { __getSetConstructor, SetPoly } from '../sets'; +import { __getSetConstructor, _Set, returnSetsUnion, SetPoly } from '../sets'; test('__getSetConstructor', () => { @@ -14,3 +14,11 @@ test('__getSetConstructor', () => { global.Set = originalSet; // restore original global Set }); + +test('returnSetsUnion', () => { + const set = new _Set(['1','2','3']); + const set2 = new _Set(['4','5','6']); + expect(returnSetsUnion(set, set2)).toEqual(new _Set(['1','2','3','4','5','6'])); + expect(set).toEqual(new _Set(['1','2','3'])); + expect(set2).toEqual(new _Set(['4','5','6'])); +}); diff --git a/src/utils/lang/sets.ts b/src/utils/lang/sets.ts index 6ecb157d..f89fc29f 100644 --- a/src/utils/lang/sets.ts +++ b/src/utils/lang/sets.ts @@ -111,3 +111,11 @@ export function __getSetConstructor(): ISetConstructor { } export const _Set = __getSetConstructor(); + +export function returnSetsUnion(set: ISet, set2: ISet): ISet { + const result = new _Set(setToArray(set)); + set2.forEach( value => { + result.add(value); + }); + return result; +} From 859fc525b5915c72c4fa0d63720715e8cc48d506 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 18 Sep 2023 16:28:56 -0300 Subject: [PATCH 16/43] set methods as private and optimize logic --- src/storages/inLocalStorage/SplitsCacheInLocal.ts | 8 ++++---- src/storages/inMemory/SplitsCacheInMemory.ts | 7 ++++--- src/utils/lang/__tests__/sets.spec.ts | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 6d702aeb..442fe581 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -108,7 +108,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this._incrementCounts(split); this._decrementCounts(previousSplit); - if (previousSplit && previousSplit.sets) this.removeFromFlagsets(previousSplit.name, previousSplit.sets); + if (previousSplit) this.removeFromFlagsets(previousSplit.name, previousSplit.sets); this.addToFlagsets(split); return true; @@ -124,7 +124,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { localStorage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); - if (split && split.sets) this.removeFromFlagsets(split.name, split.sets); + if (split) this.removeFromFlagsets(split.name, split.sets); return true; } catch (e) { @@ -291,7 +291,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { }); } - removeFromFlagsets(featureFlagName: string, flagsets: string[]) { + private removeFromFlagsets(featureFlagName: string, flagsets?: string[]) { if (!flagsets) return; flagsets.forEach(flagset => { @@ -299,7 +299,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { }); } - removeNames(flagsetName: string, featureFlagName: string) { + private removeNames(flagsetName: string, featureFlagName: string) { const flagsetKey = this.keys.buildFlagsetKey(flagsetName); let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 4c812906..5897601f 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -34,9 +34,10 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { const previousTtName = previousSplit.trafficTypeName; this.ttCache[previousTtName]--; - this.removeFromFlagsets(previousSplit.name, previousSplit.sets); if (!this.ttCache[previousTtName]) delete this.ttCache[previousTtName]; + this.removeFromFlagsets(previousSplit.name, previousSplit.sets); + if (usesSegments(previousSplit)) { // Substract from segments count for the previous version of this Split. this.splitsWithSegmentsCount--; } @@ -128,14 +129,14 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { }); } - removeFromFlagsets(featureFlagName :string, flagsets: string[] | undefined) { + private removeFromFlagsets(featureFlagName :string, flagsets: string[] | undefined) { if (!flagsets) return; flagsets.forEach(flagset => { this.removeNames(flagset, featureFlagName); }); } - removeNames(flagsetName: string, featureFlagName: string) { + private removeNames(flagsetName: string, featureFlagName: string) { if (!this.flagsetsCache[flagsetName]) return; this.flagsetsCache[flagsetName].delete(featureFlagName); if (this.flagsetsCache[flagsetName].size === 0) delete this.flagsetsCache[flagsetName]; diff --git a/src/utils/lang/__tests__/sets.spec.ts b/src/utils/lang/__tests__/sets.spec.ts index 82d6058e..1cb99853 100644 --- a/src/utils/lang/__tests__/sets.spec.ts +++ b/src/utils/lang/__tests__/sets.spec.ts @@ -21,4 +21,9 @@ test('returnSetsUnion', () => { expect(returnSetsUnion(set, set2)).toEqual(new _Set(['1','2','3','4','5','6'])); expect(set).toEqual(new _Set(['1','2','3'])); expect(set2).toEqual(new _Set(['4','5','6'])); + + const emptySet = new _Set([]); + expect(returnSetsUnion(emptySet, emptySet)).toEqual(emptySet); + expect(returnSetsUnion(set, emptySet)).toEqual(set); + expect(returnSetsUnion(emptySet, set2)).toEqual(set2); }); From ab75e9d80add85314f8f6626cce88d3ae762df80 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 18 Sep 2023 17:34:22 -0300 Subject: [PATCH 17/43] define addToFlagsets as private method --- src/storages/inLocalStorage/SplitsCacheInLocal.ts | 2 +- src/storages/inMemory/SplitsCacheInMemory.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 442fe581..9916da17 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -272,7 +272,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } - addToFlagsets(featureFlag: ISplit) { + private addToFlagsets(featureFlag: ISplit) { if (!featureFlag.sets) return; featureFlag.sets.forEach(featureFlagset => { diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 5897601f..d239712c 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -117,7 +117,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { } - addToFlagsets(featureFlag: ISplit) { + private addToFlagsets(featureFlag: ISplit) { if (!featureFlag.sets) return; featureFlag.sets.forEach(featureFlagset => { From f906cb95672d148601547477bee07d9a777f9ea6 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 21 Sep 2023 14:41:10 -0300 Subject: [PATCH 18/43] fix flagsets typo in storage --- src/storages/AbstractSplitsCacheAsync.ts | 2 + src/storages/AbstractSplitsCacheSync.ts | 6 +++ src/storages/KeyBuilder.ts | 2 +- src/storages/__tests__/KeyBuilder.spec.ts | 2 +- .../inLocalStorage/SplitsCacheInLocal.ts | 22 +++++----- .../__tests__/SplitsCacheInLocal.spec.ts | 40 +++++++++---------- src/storages/inMemory/SplitsCacheInMemory.ts | 20 +++++----- .../__tests__/SplitsCacheInMemory.spec.ts | 40 +++++++++---------- src/storages/inRedis/SplitsCacheInRedis.ts | 12 ++++++ .../pluggable/SplitsCachePluggable.ts | 12 ++++++ src/storages/types.ts | 10 +++-- 11 files changed, 102 insertions(+), 66 deletions(-) diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index db127a97..ea2c25fc 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -1,6 +1,7 @@ import { ISplitsCacheAsync } from './types'; import { ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; +import { ISet } from '../utils/lang/sets'; /** * This class provides a skeletal implementation of the ISplitsCacheAsync interface @@ -17,6 +18,7 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { abstract getChangeNumber(): Promise 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..26b5f473 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/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 262bcf39..a5f99f82 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -20,7 +20,7 @@ export class KeyBuilder { return `${this.prefix}.trafficType.${trafficType}`; } - buildFlagsetKey(flagset: string) { + buildFlagSetKey(flagset: string) { return `${this.prefix}.flagset.${flagset}`; } diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index 6da01a30..b91714fd 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -74,7 +74,7 @@ test('KEYS / flagset keys', () => { const flagsetName = 'flagset_x'; const expectedKey = `${prefix}.flagset.${flagsetName}`; - expect(builder.buildFlagsetKey(flagsetName)).toBe(expectedKey); + expect(builder.buildFlagSetKey(flagsetName)).toBe(expectedKey); }); diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 9916da17..b959cd2b 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -108,8 +108,8 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { this._incrementCounts(split); this._decrementCounts(previousSplit); - if (previousSplit) this.removeFromFlagsets(previousSplit.name, previousSplit.sets); - this.addToFlagsets(split); + if (previousSplit) this.removeFromFlagSets(previousSplit.name, previousSplit.sets); + this.addToFlagSets(split); return true; } catch (e) { @@ -124,7 +124,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { localStorage.removeItem(this.keys.buildSplitKey(name)); this._decrementCounts(split); - if (split) this.removeFromFlagsets(split.name, split.sets); + if (split) this.removeFromFlagSets(split.name, split.sets); return true; } catch (e) { @@ -257,10 +257,10 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // if the filter didn't change, nothing is done } - getNamesByFlagsets(flagsets: string[]): ISet{ + getNamesByFlagSets(flagsets: string[]): ISet{ let toReturn: ISet = new _Set([]); flagsets.forEach(flagset => { - const flagsetKey = this.keys.buildFlagsetKey(flagset); + const flagsetKey = this.keys.buildFlagSetKey(flagset); let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); if (flagsetFromLocalStorage) { @@ -272,14 +272,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } - private addToFlagsets(featureFlag: ISplit) { + private addToFlagSets(featureFlag: ISplit) { if (!featureFlag.sets) return; - featureFlag.sets.forEach(featureFlagset => { + featureFlag.sets.forEach(featureFlagSet => { - if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagset => filterFlagset === featureFlagset)) return; + if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagSet => filterFlagSet === featureFlagSet)) return; - const flagsetKey = this.keys.buildFlagsetKey(featureFlagset); + const flagsetKey = this.keys.buildFlagSetKey(featureFlagSet); let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); if (!flagsetFromLocalStorage) flagsetFromLocalStorage = '[]'; @@ -291,7 +291,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { }); } - private removeFromFlagsets(featureFlagName: string, flagsets?: string[]) { + private removeFromFlagSets(featureFlagName: string, flagsets?: string[]) { if (!flagsets) return; flagsets.forEach(flagset => { @@ -300,7 +300,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { } private removeNames(flagsetName: string, featureFlagName: string) { - const flagsetKey = this.keys.buildFlagsetKey(flagsetName); + const flagsetKey = this.keys.buildFlagSetKey(flagsetName); let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 7e9c25c9..e7c0df11 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -174,32 +174,32 @@ test('SPLIT CACHE / LocalStorage / flagset cache tests', () => { ]); cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); - expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); - expect(cache.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); - expect(cache.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); - expect(cache.getNamesByFlagsets(['t'])).toEqual(emptySet); // 't' not in filter - expect(cache.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + expect(cache.getNamesByFlagSets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cache.getNamesByFlagSets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagSets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cache.getNamesByFlagSets(['t'])).toEqual(emptySet); // 't' not in filter + expect(cache.getNamesByFlagSets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['1']}); - expect(cache.getNamesByFlagsets(['1'])).toEqual(emptySet); // '1' not in filter - expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_two'])); - expect(cache.getNamesByFlagsets(['n'])).toEqual(emptySet); + expect(cache.getNamesByFlagSets(['1'])).toEqual(emptySet); // '1' not in filter + expect(cache.getNamesByFlagSets(['o'])).toEqual(new _Set(['ff_two'])); + expect(cache.getNamesByFlagSets(['n'])).toEqual(emptySet); cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['x']}); - expect(cache.getNamesByFlagsets(['x'])).toEqual(new _Set(['ff_one'])); - expect(cache.getNamesByFlagsets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + expect(cache.getNamesByFlagSets(['x'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagSets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); cache.removeSplit(featureFlagOne.name); - expect(cache.getNamesByFlagsets(['x'])).toEqual(emptySet); + expect(cache.getNamesByFlagSets(['x'])).toEqual(emptySet); cache.removeSplit(featureFlagOne.name); - expect(cache.getNamesByFlagsets(['y'])).toEqual(emptySet); // 'y' not in filter - expect(cache.getNamesByFlagsets([])).toEqual(emptySet); + expect(cache.getNamesByFlagSets(['y'])).toEqual(emptySet); // 'y' not in filter + expect(cache.getNamesByFlagSets([])).toEqual(emptySet); cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); - expect(cache.getNamesByFlagsets([])).toEqual(emptySet); + expect(cache.getNamesByFlagSets([])).toEqual(emptySet); }); // if FlagSets are not defined, it should store all FlagSets in memory. @@ -214,10 +214,10 @@ test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => { ]); cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); - expect(cacheWithoutFilters.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['t'])).toEqual(new _Set(['ff_two','ff_three'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['y'])).toEqual(emptySet); - expect(cacheWithoutFilters.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual(new _Set(['ff_two','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual(emptySet); + expect(cacheWithoutFilters.getNamesByFlagSets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); }); diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index d239712c..66a72dcc 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -36,7 +36,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { this.ttCache[previousTtName]--; if (!this.ttCache[previousTtName]) delete this.ttCache[previousTtName]; - this.removeFromFlagsets(previousSplit.name, previousSplit.sets); + this.removeFromFlagSets(previousSplit.name, previousSplit.sets); if (usesSegments(previousSplit)) { // Substract from segments count for the previous version of this Split. this.splitsWithSegmentsCount--; @@ -49,7 +49,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { // Update TT cache const ttName = split.trafficTypeName; this.ttCache[ttName] = (this.ttCache[ttName] || 0) + 1; - this.addToFlagsets(split); + this.addToFlagSets(split); // Add to segments count for the new version of the Split if (usesSegments(split)) this.splitsWithSegmentsCount++; @@ -69,7 +69,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { const ttName = split.trafficTypeName; this.ttCache[ttName]--; // Update tt cache if (!this.ttCache[ttName]) delete this.ttCache[ttName]; - this.removeFromFlagsets(split.name, split.sets); + this.removeFromFlagSets(split.name, split.sets); // Update the segments count. if (usesSegments(split)) this.splitsWithSegmentsCount--; @@ -105,7 +105,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { return this.getChangeNumber() === -1 || this.splitsWithSegmentsCount > 0; } - getNamesByFlagsets(flagsets: string[]): ISet{ + getNamesByFlagSets(flagsets: string[]): ISet{ let toReturn: ISet = new _Set([]); flagsets.forEach(flagset => { const featureFlagNames = this.flagsetsCache[flagset]; @@ -117,19 +117,19 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { } - private addToFlagsets(featureFlag: ISplit) { + private addToFlagSets(featureFlag: ISplit) { if (!featureFlag.sets) return; - featureFlag.sets.forEach(featureFlagset => { + featureFlag.sets.forEach(featureFlagSet => { - if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagset => filterFlagset === featureFlagset)) return; + if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagSet => filterFlagSet === featureFlagSet)) return; - if (!this.flagsetsCache[featureFlagset]) this.flagsetsCache[featureFlagset] = new _Set([]); + if (!this.flagsetsCache[featureFlagSet]) this.flagsetsCache[featureFlagSet] = new _Set([]); - this.flagsetsCache[featureFlagset].add(featureFlag.name); + this.flagsetsCache[featureFlagSet].add(featureFlag.name); }); } - private removeFromFlagsets(featureFlagName :string, flagsets: string[] | undefined) { + private removeFromFlagSets(featureFlagName :string, flagsets: string[] | undefined) { if (!flagsets) return; flagsets.forEach(flagset => { this.removeNames(flagset, featureFlagName); diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 62f4e08f..1752d000 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -127,32 +127,32 @@ test('SPLITS CACHE / In Memory / flagset cache tests', () => { ]); cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); - expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); - expect(cache.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); - expect(cache.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); - expect(cache.getNamesByFlagsets(['t'])).toEqual(emptySet); // 't' not in filter - expect(cache.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + expect(cache.getNamesByFlagSets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cache.getNamesByFlagSets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagSets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cache.getNamesByFlagSets(['t'])).toEqual(emptySet); // 't' not in filter + expect(cache.getNamesByFlagSets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['1']}); - expect(cache.getNamesByFlagsets(['1'])).toEqual(emptySet); // '1' not in filter - expect(cache.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_two'])); - expect(cache.getNamesByFlagsets(['n'])).toEqual(emptySet); + expect(cache.getNamesByFlagSets(['1'])).toEqual(emptySet); // '1' not in filter + expect(cache.getNamesByFlagSets(['o'])).toEqual(new _Set(['ff_two'])); + expect(cache.getNamesByFlagSets(['n'])).toEqual(emptySet); cache.addSplit(featureFlagOne.name, {...featureFlagOne, sets: ['x']}); - expect(cache.getNamesByFlagsets(['x'])).toEqual(new _Set(['ff_one'])); - expect(cache.getNamesByFlagsets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + expect(cache.getNamesByFlagSets(['x'])).toEqual(new _Set(['ff_one'])); + expect(cache.getNamesByFlagSets(['o','e','x'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); cache.removeSplit(featureFlagOne.name); - expect(cache.getNamesByFlagsets(['x'])).toEqual(emptySet); + expect(cache.getNamesByFlagSets(['x'])).toEqual(emptySet); cache.removeSplit(featureFlagOne.name); - expect(cache.getNamesByFlagsets(['y'])).toEqual(emptySet); // 'y' not in filter - expect(cache.getNamesByFlagsets([])).toEqual(emptySet); + expect(cache.getNamesByFlagSets(['y'])).toEqual(emptySet); // 'y' not in filter + expect(cache.getNamesByFlagSets([])).toEqual(emptySet); cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithoutFS); - expect(cache.getNamesByFlagsets([])).toEqual(emptySet); + expect(cache.getNamesByFlagSets([])).toEqual(emptySet); }); // if FlagSets are not defined, it should store all FlagSets in memory. @@ -167,10 +167,10 @@ test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => { ]); cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS); - expect(cacheWithoutFilters.getNamesByFlagsets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['n'])).toEqual(new _Set(['ff_one'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['t'])).toEqual(new _Set(['ff_two','ff_three'])); - expect(cacheWithoutFilters.getNamesByFlagsets(['y'])).toEqual(emptySet); - expect(cacheWithoutFilters.getNamesByFlagsets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual(new _Set(['ff_one', 'ff_two'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual(new _Set(['ff_one'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual(new _Set(['ff_one','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual(new _Set(['ff_two','ff_three'])); + expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual(emptySet); + expect(cacheWithoutFilters.getNamesByFlagSets(['o','n','e'])).toEqual(new _Set(['ff_one','ff_two','ff_three'])); }); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 43f86481..ac6004b0 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..4eba04e0 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..ab24a479 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 */ From 3bbf16b1a94fc6c18f58453034623585d5b34171 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 21 Sep 2023 14:42:55 -0300 Subject: [PATCH 19/43] [SDKS-7557/7554] getTreatment/s byFlagset withConfig/s --- src/dtos/types.ts | 2 +- .../__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/types.ts | 114 +++++++++++++++++- 10 files changed, 421 insertions(+), 7 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 4fdf562c..03363928 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -164,7 +164,7 @@ export interface ISplit { configurations?: { [treatmentName: string]: string }, - sets: string[] + sets?: string[] } // Split definition used in offline mode diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index c400c983..648c09cb 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..700785fe 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} 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 From ea83dbfbe15bae267ebd4bf6aaed732a308b2246 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 22 Sep 2023 16:45:39 -0300 Subject: [PATCH 20/43] Fix response when flagset invalid --- src/evaluator/index.ts | 10 +++++----- src/sdkClient/client.ts | 4 +--- src/sdkClient/clientInputValidation.ts | 10 +++++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 700785fe..8c9c1810 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -95,20 +95,20 @@ export function evaluateFeaturesByFlagSets( attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, ): MaybeThenable> { - let storedSplitNames; + let storedFlagNames; // get ff by flagsets try { - storedSplitNames = storage.splits.getNamesByFlagSets(flagsets); + storedFlagNames = 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); + return thenable(storedFlagNames) ? + storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) : + evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage); } diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 7fed2261..c6afc365 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -98,9 +98,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl 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 + isStorageSync(settings) ? {} : Promise.resolve({}); // Promisify if async return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 87f5cada..0161a43d 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -44,7 +44,7 @@ export function clientInputValidationDecorator Date: Fri, 22 Sep 2023 16:54:31 -0300 Subject: [PATCH 21/43] make method abstract and fix typo --- src/storages/AbstractSplitsCacheAsync.ts | 2 +- src/storages/AbstractSplitsCacheSync.ts | 9 +++---- .../inLocalStorage/SplitsCacheInLocal.ts | 14 +++++------ src/storages/inMemory/SplitsCacheInMemory.ts | 24 +++++++++---------- src/storages/types.ts | 6 ++--- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index ea2c25fc..bbd33e78 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -18,7 +18,7 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { abstract getChangeNumber(): Promise abstract getAll(): Promise abstract getSplitNames(): Promise - abstract getNamesByFlagSets(flagsets: string[]): 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 26b5f473..92a29c18 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -1,7 +1,7 @@ import { ISplitsCacheSync } from './types'; import { ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; -import { ISet, _Set } from '../utils/lang/sets'; +import { ISet } from '../utils/lang/sets'; /** * This class provides a skeletal implementation of the ISplitsCacheSync interface @@ -78,11 +78,8 @@ 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([]); - } + + abstract getNamesByFlagSets(flagSets: string[]): ISet } diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index b959cd2b..8fff22fd 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -257,15 +257,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { // if the filter didn't change, nothing is done } - getNamesByFlagSets(flagsets: string[]): ISet{ + getNamesByFlagSets(flagSets: string[]): ISet{ let toReturn: ISet = new _Set([]); - flagsets.forEach(flagset => { - const flagsetKey = this.keys.buildFlagSetKey(flagset); - let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); + flagSets.forEach(flagSet => { + const flagSetKey = this.keys.buildFlagSetKey(flagSet); + let flagSetFromLocalStorage = localStorage.getItem(flagSetKey); - if (flagsetFromLocalStorage) { - const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage)); - toReturn = returnSetsUnion(toReturn, flagsetCache); + if (flagSetFromLocalStorage) { + const flagSetCache = new _Set(JSON.parse(flagSetFromLocalStorage)); + toReturn = returnSetsUnion(toReturn, flagSetCache); } }); return toReturn; diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 66a72dcc..223dae9f 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -9,16 +9,16 @@ import { ISet, _Set, returnSetsUnion } from '../../utils/lang/sets'; */ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { - private flagsetsFilter: string[]; + private flagSetsFilter: string[]; private splitsCache: Record = {}; private ttCache: Record = {}; private changeNumber: number = -1; private splitsWithSegmentsCount: number = 0; - private flagsetsCache: Record> = {}; + private flagSetsCache: Record> = {}; constructor(splitFiltersValidation: ISplitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }) { super(); - this.flagsetsFilter = splitFiltersValidation.groupedFilters.bySet; + this.flagSetsFilter = splitFiltersValidation.groupedFilters.bySet; } clear() { @@ -105,10 +105,10 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { return this.getChangeNumber() === -1 || this.splitsWithSegmentsCount > 0; } - getNamesByFlagSets(flagsets: string[]): ISet{ + getNamesByFlagSets(flagSets: string[]): ISet{ let toReturn: ISet = new _Set([]); - flagsets.forEach(flagset => { - const featureFlagNames = this.flagsetsCache[flagset]; + flagSets.forEach(flagSet => { + const featureFlagNames = this.flagSetsCache[flagSet]; if (featureFlagNames) { toReturn = returnSetsUnion(toReturn, featureFlagNames); } @@ -121,11 +121,11 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { if (!featureFlag.sets) return; featureFlag.sets.forEach(featureFlagSet => { - if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagSet => filterFlagSet === featureFlagSet)) return; + if (this.flagSetsFilter.length > 0 && !this.flagSetsFilter.some(filterFlagSet => filterFlagSet === featureFlagSet)) return; - if (!this.flagsetsCache[featureFlagSet]) this.flagsetsCache[featureFlagSet] = new _Set([]); + if (!this.flagSetsCache[featureFlagSet]) this.flagSetsCache[featureFlagSet] = new _Set([]); - this.flagsetsCache[featureFlagSet].add(featureFlag.name); + this.flagSetsCache[featureFlagSet].add(featureFlag.name); }); } @@ -137,9 +137,9 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { } private removeNames(flagsetName: string, featureFlagName: string) { - if (!this.flagsetsCache[flagsetName]) return; - this.flagsetsCache[flagsetName].delete(featureFlagName); - if (this.flagsetsCache[flagsetName].size === 0) delete this.flagsetsCache[flagsetName]; + if (!this.flagSetsCache[flagsetName]) return; + this.flagSetsCache[flagsetName].delete(featureFlagName); + if (this.flagSetsCache[flagsetName].size === 0) delete this.flagSetsCache[flagsetName]; } } diff --git a/src/storages/types.ts b/src/storages/types.ts index ab24a479..30832f5e 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -210,7 +210,7 @@ export interface ISplitsCacheBase { // 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, - getNamesByFlagSets(flagsets: string[]): MaybeThenable> + getNamesByFlagSets(flagSets: string[]): MaybeThenable> } export interface ISplitsCacheSync extends ISplitsCacheBase { @@ -227,7 +227,7 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { clear(): void, checkCache(): boolean, killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean, - getNamesByFlagSets(flagsets: string[]): ISet + getNamesByFlagSets(flagSets: string[]): ISet } export interface ISplitsCacheAsync extends ISplitsCacheBase { @@ -244,7 +244,7 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { clear(): Promise, checkCache(): Promise, killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise, - getNamesByFlagSets(flagsets: string[]): Promise> + getNamesByFlagSets(flagSets: string[]): Promise> } /** Segments cache */ From d1f701c53b61f277caf4172602db99deb5d7243a Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 22 Sep 2023 23:58:31 -0300 Subject: [PATCH 22/43] Return when calling --- .../__tests__/evaluate-features.spec.ts | 58 +++++++++++++++---- src/evaluator/index.ts | 34 +++++++++-- src/evaluator/types.ts | 5 ++ src/sdkClient/client.ts | 13 +++-- 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index 648c09cb..0d6116bc 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -39,6 +39,14 @@ const mockStorage = { }, getNamesByFlagSets(flagSets) { let toReturn = new _Set([]); + // Forced thenable delayed response for testing purposes + if (flagSets[0] === 'delay') { + return new Promise((resolve) => { + setTimeout(() => { + resolve(toReturn); + }, 86); + }); + } flagSets.forEach(flagset => { const featureFlagNames = flagSetsMock[flagset]; if (featureFlagNames) { @@ -124,6 +132,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre }); 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', @@ -134,29 +143,54 @@ test('EVALUATOR - Multiple evaluations at once by flagsets / should return right }, }; - const multipleEvaluationAtOnce = await evaluateFeaturesByFlagSets( - loggerMock, - 'fake-key', - ['reg_and_config', 'arch_and_killed'], - null, - mockStorage, - ); + const getResultsByFlagsets = (flagSets: string[]) => { + return evaluateFeaturesByFlagSets( + loggerMock, + 'fake-key', + flagSets, + null, + mockStorage, + ); + }; + + let multipleResultsAtOnceByFlagSets = await getResultsByFlagsets(['delay']); + expect(multipleResultsAtOnceByFlagSets.elapsedMilliseconds).toBeGreaterThanOrEqual(86); // defined 86 ms delay for testing purposes in mocked storage + + + multipleResultsAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config', 'arch_and_killed']); + let multipleEvaluationAtOnceByFlagSets = multipleResultsAtOnceByFlagSets.evaluations; // assert evaluationWithConfig - expect(multipleEvaluationAtOnce['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config. + expect(multipleEvaluationAtOnceByFlagSets['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. + 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(multipleEvaluationAtOnce['killed']).toEqual({ ...expectedOutput['config'], treatment: 'off', config: null, label: LabelsConstants.SPLIT_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(multipleEvaluationAtOnce['archived']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: LabelsConstants.SPLIT_ARCHIVED, config: null }); + 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 flagsets - expect(multipleEvaluationAtOnce['not_existent_split']).toEqual(undefined); + expect(multipleEvaluationAtOnceByFlagSets['not_existent_split']).toEqual(undefined); + + multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets([]); + expect(multipleEvaluationAtOnceByFlagSets.evaluations).toEqual({}); + + multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config']).evaluations; + expect(multipleEvaluationAtOnceByFlagSets['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(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(undefined); + // 'If the split is retrieved but is killed, we should get the right evaluation result, label and config. + + // assert archived + expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual(undefined); }); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 8c9c1810..31311d20 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -4,10 +4,11 @@ import * as LabelsConstants from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { ISplit, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; -import { IEvaluationResult } from './types'; +import { IByFlagSetsResult, IEvaluationResult } from './types'; import { SplitIO } from '../types'; import { ILogger } from '../logger/types'; -import { setToArray } from '../utils/lang/sets'; +import { ISet, setToArray } from '../utils/lang/sets'; +import { timer } from '../utils/timeTracker/timer'; const treatmentException = { treatment: CONTROL, @@ -94,8 +95,10 @@ export function evaluateFeaturesByFlagSets( flagsets: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, -): MaybeThenable> { - let storedFlagNames; +): MaybeThenable { + const stopTimer = timer(Date.now); + let elapsedMilliseconds: number; + let storedFlagNames: MaybeThenable>; // get ff by flagsets try { @@ -103,9 +106,30 @@ export function evaluateFeaturesByFlagSets( } catch (e) { // Exception on sync `getSplits` storage. Not possible ATM with InMemory and InLocal storages. // @todo - review exception - return treatmentsException(flagsets); + elapsedMilliseconds = stopTimer(); + return {evaluations:{}, elapsedMilliseconds}; + } + + const evaluatedFeatures = getByFlagSetsEvaluations(log, key, storedFlagNames, attributes, storage); + + if (thenable(evaluatedFeatures)) { + return evaluatedFeatures.then((evaluations) => { + elapsedMilliseconds = stopTimer(); + return {evaluations, elapsedMilliseconds}; + }); } + elapsedMilliseconds = stopTimer(); + return {evaluations: evaluatedFeatures, elapsedMilliseconds}; +} + +function getByFlagSetsEvaluations( + log: ILogger, + key: SplitIO.SplitKey, + storedFlagNames: MaybeThenable>, + attributes: SplitIO.Attributes | undefined, + storage: IStorageSync | IStorageAsync, +): MaybeThenable> { return thenable(storedFlagNames) ? storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) : evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage); diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 54078b5b..56d00290 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -27,6 +27,11 @@ export interface IEvaluation { export type IEvaluationResult = IEvaluation & { treatment: string } +export interface IByFlagSetsResult { + evaluations: Record, + elapsedMilliseconds: number +} + export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable export type IEvaluator = (key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index c6afc365..8e4148bb 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -5,7 +5,7 @@ import { validateSplitExistance } from '../utils/inputValidation/splitExistance' import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance'; import { SDK_NOT_READY } from '../utils/labels'; import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK } from '../utils/constants'; -import { IEvaluationResult } from '../evaluator/types'; +import { IByFlagSetsResult, IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; import { ISdkFactoryContext } from '../sdkFactory/types'; @@ -84,11 +84,12 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl 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 wrapUp = (evaluationResults: IByFlagSetsResult) => { const queue: ImpressionDTO[] = []; const treatments: Record = {}; - Object.keys(evaluationResults).forEach(featureFlagName => { - treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, attributes, withConfig, `getTreatmentsByFlagSets${withConfig ? 'WithConfig' : ''}`, queue); + const evaluations = evaluationResults.evaluations; + Object.keys(evaluations).forEach(featureFlagName => { + treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, `getTreatmentsByFlagSets${withConfig ? 'WithConfig' : ''}`, queue); }); impressionsTracker.track(queue, attributes); @@ -96,9 +97,11 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return treatments; }; + const emptyEvaluationByFlagSet = {evaluations: {}, elapsedMilliseconds: 0}; + const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage) : - isStorageSync(settings) ? {} : Promise.resolve({}); // Promisify if async + isStorageSync(settings) ? emptyEvaluationByFlagSet : Promise.resolve(emptyEvaluationByFlagSet); // Promisify if async return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations); } From d77011d05adc7616a09ef0f67b2b31766debe822 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Sat, 23 Sep 2023 00:05:20 -0300 Subject: [PATCH 23/43] remove comments --- src/evaluator/__tests__/evaluate-features.spec.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index 0d6116bc..c914599d 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -181,16 +181,9 @@ test('EVALUATOR - Multiple evaluations at once by flagsets / should return right expect(multipleEvaluationAtOnceByFlagSets.evaluations).toEqual({}); multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config']).evaluations; - expect(multipleEvaluationAtOnceByFlagSets['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(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['config']).toEqual(expectedOutput['config']); + expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], config: null }); expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual(undefined); - // 'If the split is retrieved but is killed, we should get the right evaluation result, label and config. - - // assert archived expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual(undefined); }); From 43035bc09f019cfecb2af954c63ca89cf5917b6e Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Sat, 23 Sep 2023 00:27:10 -0300 Subject: [PATCH 24/43] Refactor - remove unnecessary function --- src/evaluator/index.ts | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 31311d20..da4b22e2 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -97,21 +97,24 @@ export function evaluateFeaturesByFlagSets( storage: IStorageSync | IStorageAsync, ): MaybeThenable { const stopTimer = timer(Date.now); - let elapsedMilliseconds: number; + let elapsedMilliseconds: number = 0; let storedFlagNames: MaybeThenable>; - // get ff by flagsets + // get features by flagsets try { storedFlagNames = storage.splits.getNamesByFlagSets(flagsets); } catch (e) { - // Exception on sync `getSplits` storage. Not possible ATM with InMemory and InLocal storages. - // @todo - review exception + // return empty evaluations elapsedMilliseconds = stopTimer(); return {evaluations:{}, elapsedMilliseconds}; } - const evaluatedFeatures = getByFlagSetsEvaluations(log, key, storedFlagNames, attributes, storage); + // evaluate related features + const evaluatedFeatures = thenable(storedFlagNames) ? + storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) : + evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage); + // craft IByFlagSetsResult adding elapsedMilliseconds property if (thenable(evaluatedFeatures)) { return evaluatedFeatures.then((evaluations) => { elapsedMilliseconds = stopTimer(); @@ -122,20 +125,6 @@ export function evaluateFeaturesByFlagSets( return {evaluations: evaluatedFeatures, elapsedMilliseconds}; } - -function getByFlagSetsEvaluations( - log: ILogger, - key: SplitIO.SplitKey, - storedFlagNames: MaybeThenable>, - attributes: SplitIO.Attributes | undefined, - storage: IStorageSync | IStorageAsync, -): MaybeThenable> { - 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, From 3036d42aa43cff559a424b1740de393a68e56950 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 25 Sep 2023 11:47:38 -0300 Subject: [PATCH 25/43] refactor remaining flagset to flagSet ocurrences --- src/storages/KeyBuilder.ts | 4 +- src/storages/__tests__/KeyBuilder.spec.ts | 8 ++-- src/storages/__tests__/testUtils.ts | 2 +- .../inLocalStorage/SplitsCacheInLocal.ts | 44 +++++++++---------- .../__tests__/SplitsCacheInLocal.spec.ts | 4 +- src/storages/inMemory/SplitsCacheInMemory.ts | 16 +++---- .../inMemory/TelemetryCacheInMemory.ts | 2 +- .../__tests__/SplitsCacheInMemory.spec.ts | 4 +- src/storages/inRedis/RedisAdapter.ts | 2 +- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- .../pluggable/SplitsCachePluggable.ts | 2 +- src/sync/submitters/telemetrySubmitter.ts | 2 +- src/sync/submitters/types.ts | 4 +- src/utils/settingsValidation/splitFilters.ts | 6 +-- 14 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index a5f99f82..dc9581b5 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -20,8 +20,8 @@ export class KeyBuilder { return `${this.prefix}.trafficType.${trafficType}`; } - buildFlagSetKey(flagset: string) { - return `${this.prefix}.flagset.${flagset}`; + buildFlagSetKey(flagSet: string) { + return `${this.prefix}.flagset.${flagSet}`; } buildSplitKey(splitName: string) { diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index b91714fd..0965f3d1 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -67,14 +67,14 @@ test('KEYS / traffic type keys', () => { }); -test('KEYS / flagset keys', () => { +test('KEYS / flag set keys', () => { const prefix = 'unit_test.SPLITIO'; const builder = new KeyBuilder(prefix); - const flagsetName = 'flagset_x'; - const expectedKey = `${prefix}.flagset.${flagsetName}`; + const flagSetName = 'flagset_x'; + const expectedKey = `${prefix}.flagset.${flagSetName}`; - expect(builder.buildFlagSetKey(flagsetName)).toBe(expectedKey); + expect(builder.buildFlagSetKey(flagSetName)).toBe(expectedKey); }); diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index ff90bc3b..94e11c36 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -33,7 +33,7 @@ export const something: ISplit = { name: 'something' }; //@ts-ignore export const somethingElse: ISplit = { name: 'something else' }; -// - With flagsets +// - With flag sets //@ts-ignore export const featureFlagWithEmptyFS: ISplit = { name: 'ff_empty', sets: [] }; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 8fff22fd..9e02ee15 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -13,7 +13,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private readonly keys: KeyBuilderCS; private readonly splitFiltersValidation: ISplitFiltersValidation; - private readonly flagsetsFilter: string[]; + private readonly flagSetsFilter: string[]; private hasSync?: boolean; private updateNewFilter?: boolean; @@ -26,7 +26,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { super(); this.keys = keys; this.splitFiltersValidation = splitFiltersValidation; - this.flagsetsFilter = this.splitFiltersValidation.groupedFilters.bySet; + this.flagSetsFilter = this.splitFiltersValidation.groupedFilters.bySet; this._checkExpiration(expirationTimestamp); @@ -277,44 +277,44 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { featureFlag.sets.forEach(featureFlagSet => { - if (this.flagsetsFilter.length > 0 && !this.flagsetsFilter.some(filterFlagSet => filterFlagSet === featureFlagSet)) return; + if (this.flagSetsFilter.length > 0 && !this.flagSetsFilter.some(filterFlagSet => filterFlagSet === featureFlagSet)) return; - const flagsetKey = this.keys.buildFlagSetKey(featureFlagSet); + const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet); - let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); - if (!flagsetFromLocalStorage) flagsetFromLocalStorage = '[]'; + let flagSetFromLocalStorage = localStorage.getItem(flagSetKey); + if (!flagSetFromLocalStorage) flagSetFromLocalStorage = '[]'; - const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage)); - flagsetCache.add(featureFlag.name); + const flagSetCache = new _Set(JSON.parse(flagSetFromLocalStorage)); + flagSetCache.add(featureFlag.name); - localStorage.setItem(flagsetKey, JSON.stringify(setToArray(flagsetCache))); + localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); }); } - private removeFromFlagSets(featureFlagName: string, flagsets?: string[]) { - if (!flagsets) return; + private removeFromFlagSets(featureFlagName: string, flagSets?: string[]) { + if (!flagSets) return; - flagsets.forEach(flagset => { - this.removeNames(flagset, featureFlagName); + flagSets.forEach(flagSet => { + this.removeNames(flagSet, featureFlagName); }); } - private removeNames(flagsetName: string, featureFlagName: string) { - const flagsetKey = this.keys.buildFlagSetKey(flagsetName); + private removeNames(flagSetName: string, featureFlagName: string) { + const flagSetKey = this.keys.buildFlagSetKey(flagSetName); - let flagsetFromLocalStorage = localStorage.getItem(flagsetKey); + let flagSetFromLocalStorage = localStorage.getItem(flagSetKey); - if (!flagsetFromLocalStorage) return; + if (!flagSetFromLocalStorage) return; - const flagsetCache = new _Set(JSON.parse(flagsetFromLocalStorage)); - flagsetCache.delete(featureFlagName); + const flagSetCache = new _Set(JSON.parse(flagSetFromLocalStorage)); + flagSetCache.delete(featureFlagName); - if (flagsetCache.size === 0) { - localStorage.removeItem(flagsetKey); + if (flagSetCache.size === 0) { + localStorage.removeItem(flagSetKey); return; } - localStorage.setItem(flagsetKey, JSON.stringify(setToArray(flagsetCache))); + localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache))); } } diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index e7c0df11..b40f5f8a 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -162,7 +162,7 @@ test('SPLIT CACHE / LocalStorage / usesSegments', () => { expect(cache.usesSegments()).toBe(false); // 0 splits using segments }); -test('SPLIT CACHE / LocalStorage / flagset cache tests', () => { +test('SPLIT CACHE / LocalStorage / flag set cache tests', () => { // @ts-ignore const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user'), undefined, { groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new _Set([]); @@ -203,7 +203,7 @@ test('SPLIT CACHE / LocalStorage / flagset cache tests', () => { }); // if FlagSets are not defined, it should store all FlagSets in memory. -test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => { +test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { const cacheWithoutFilters = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); const emptySet = new _Set([]); diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 223dae9f..8cb45aef 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -129,17 +129,17 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { }); } - private removeFromFlagSets(featureFlagName :string, flagsets: string[] | undefined) { - if (!flagsets) return; - flagsets.forEach(flagset => { - this.removeNames(flagset, featureFlagName); + private removeFromFlagSets(featureFlagName :string, flagSets: string[] | undefined) { + if (!flagSets) return; + flagSets.forEach(flagSet => { + this.removeNames(flagSet, featureFlagName); }); } - private removeNames(flagsetName: string, featureFlagName: string) { - if (!this.flagSetsCache[flagsetName]) return; - this.flagSetsCache[flagsetName].delete(featureFlagName); - if (this.flagSetsCache[flagsetName].size === 0) delete this.flagSetsCache[flagsetName]; + private removeNames(flagSetName: string, featureFlagName: string) { + if (!this.flagSetsCache[flagSetName]) return; + this.flagSetsCache[flagSetName].delete(featureFlagName); + if (this.flagSetsCache[flagSetName].size === 0) delete this.flagSetsCache[flagSetName]; } } diff --git a/src/storages/inMemory/TelemetryCacheInMemory.ts b/src/storages/inMemory/TelemetryCacheInMemory.ts index c4ae5131..26fb2b17 100644 --- a/src/storages/inMemory/TelemetryCacheInMemory.ts +++ b/src/storages/inMemory/TelemetryCacheInMemory.ts @@ -181,7 +181,7 @@ export class TelemetryCacheInMemory implements ITelemetryCacheSync { this.e = false; } - private streamingEvents: StreamingEvent[] = [] + private streamingEvents: StreamingEvent[] = []; popStreamingEvents() { return this.streamingEvents.splice(0); diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 1752d000..865705cf 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -115,7 +115,7 @@ test('SPLITS CACHE / In Memory / killLocally', () => { }); -test('SPLITS CACHE / In Memory / flagset cache tests', () => { +test('SPLITS CACHE / In Memory / flag set cache tests', () => { // @ts-ignore const cache = new SplitsCacheInMemory({ groupedFilters: { bySet: ['o', 'n', 'e', 'x'] } }); const emptySet = new _Set([]); @@ -156,7 +156,7 @@ test('SPLITS CACHE / In Memory / flagset cache tests', () => { }); // if FlagSets are not defined, it should store all FlagSets in memory. -test('SPLIT CACHE / LocalStorage / flagset cache tests without filters', () => { +test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => { const cacheWithoutFilters = new SplitsCacheInMemory(); const emptySet = new _Set([]); diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 45ef7b15..7beb2988 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -33,7 +33,7 @@ interface IRedisCommand { * Redis adapter on top of the library of choice (written with ioredis) for some extra control. */ export class RedisAdapter extends ioredis { - private readonly log: ILogger + private readonly log: ILogger; private _options: object; private _notReadyCommandsQueue?: IRedisCommand[]; private _runningCommands: ISet>; diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index ac6004b0..310c014d 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -190,7 +190,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { } /** - * Get list of split names related to a given flagset names list. + * Get list of split names related to a given flag set 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 diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 4eba04e0..c6dce829 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -156,7 +156,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { } /** - * Get list of split names related to a given flagset names list. + * Get list of split names related to a given flag set 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 diff --git a/src/sync/submitters/telemetrySubmitter.ts b/src/sync/submitters/telemetrySubmitter.ts index 63996f95..a2289e08 100644 --- a/src/sync/submitters/telemetrySubmitter.ts +++ b/src/sync/submitters/telemetrySubmitter.ts @@ -40,7 +40,7 @@ function getRedundantActiveFactories() { } function getTelemetryFlagSetsStats(splitFiltersValidation: ISplitFiltersValidation) { - // Group every configured flagset in an unique array called originalSets + // Group every configured flag set in an unique array called originalSets let flagSetsTotal = 0; splitFiltersValidation.validFilters.forEach((filter: SplitIO.SplitFilter) => { if (filter.type === 'bySet') flagSetsTotal += filter.values.length; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 13b55391..5b7ff0c4 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -234,8 +234,8 @@ export type TelemetryConfigStatsPayload = TelemetryConfigStats & { nR: number, // SDKNotReadyUsage i?: Array, // integrations uC: number, // userConsent - fsT: number, // flagsetsTotal - fsI: number, // flagsetsInvalid + fsT: number, // flagSetsTotal + fsI: number, // flagSetsInvalid } export interface ISubmitterManager extends ISyncTask { diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index a4869ca4..264c9975 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -97,11 +97,11 @@ function queryStringBuilder(groupedFilters: Record { if (CAPITAL_LETTERS_REGEX.test(flagSet)){ log.warn(WARN_SPLITS_FILTER_LOWERCASE_SET,[flagSet]); From fb7e99fbbcc9edc4ade8ec1e6f04f73f0ca8fda1 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 26 Sep 2023 18:43:50 -0300 Subject: [PATCH 26/43] Fix telemetry --- .../__tests__/evaluate-features.spec.ts | 23 ++++--------- src/evaluator/index.ts | 28 ++++------------ src/evaluator/types.ts | 5 --- src/sdkClient/client.ts | 29 ++++++++--------- src/sdkClient/clientInputValidation.ts | 8 ++--- src/storages/KeyBuilderSS.ts | 4 +++ src/sync/submitters/types.ts | 6 +++- src/types.ts | 32 +++++++++---------- src/utils/constants/index.ts | 4 +++ 9 files changed, 60 insertions(+), 79 deletions(-) diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index c914599d..3b81a37b 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -39,14 +39,6 @@ const mockStorage = { }, getNamesByFlagSets(flagSets) { let toReturn = new _Set([]); - // Forced thenable delayed response for testing purposes - if (flagSets[0] === 'delay') { - return new Promise((resolve) => { - setTimeout(() => { - resolve(toReturn); - }, 86); - }); - } flagSets.forEach(flagset => { const featureFlagNames = flagSetsMock[flagset]; if (featureFlagNames) { @@ -131,7 +123,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre }); -test('EVALUATOR - Multiple evaluations at once by flagsets / should return right labels, treatments and configs if storage returns without errors.', async function () { +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: { @@ -153,16 +145,13 @@ test('EVALUATOR - Multiple evaluations at once by flagsets / should return right ); }; - let multipleResultsAtOnceByFlagSets = await getResultsByFlagsets(['delay']); - expect(multipleResultsAtOnceByFlagSets.elapsedMilliseconds).toBeGreaterThanOrEqual(86); // defined 86 ms delay for testing purposes in mocked storage - multipleResultsAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config', 'arch_and_killed']); - let multipleEvaluationAtOnceByFlagSets = multipleResultsAtOnceByFlagSets.evaluations; + 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 flagset not found - for input validations + // @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. @@ -174,13 +163,13 @@ test('EVALUATOR - Multiple evaluations at once by flagsets / should return right 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 flagsets + // 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.evaluations).toEqual({}); + expect(multipleEvaluationAtOnceByFlagSets).toEqual({}); - multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config']).evaluations; + 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); diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index da4b22e2..70dad68a 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -4,11 +4,10 @@ import * as LabelsConstants from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { ISplit, MaybeThenable } from '../dtos/types'; import { IStorageAsync, IStorageSync } from '../storages/types'; -import { IByFlagSetsResult, IEvaluationResult } from './types'; +import { IEvaluationResult } from './types'; import { SplitIO } from '../types'; import { ILogger } from '../logger/types'; import { ISet, setToArray } from '../utils/lang/sets'; -import { timer } from '../utils/timeTracker/timer'; const treatmentException = { treatment: CONTROL, @@ -92,37 +91,24 @@ export function evaluateFeatures( export function evaluateFeaturesByFlagSets( log: ILogger, key: SplitIO.SplitKey, - flagsets: string[], + flagSets: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, -): MaybeThenable { - const stopTimer = timer(Date.now); - let elapsedMilliseconds: number = 0; +): MaybeThenable> { let storedFlagNames: MaybeThenable>; - // get features by flagsets + // get features by flag sets try { - storedFlagNames = storage.splits.getNamesByFlagSets(flagsets); + storedFlagNames = storage.splits.getNamesByFlagSets(flagSets); } catch (e) { // return empty evaluations - elapsedMilliseconds = stopTimer(); - return {evaluations:{}, elapsedMilliseconds}; + return {}; } // evaluate related features - const evaluatedFeatures = thenable(storedFlagNames) ? + return thenable(storedFlagNames) ? storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) : evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage); - - // craft IByFlagSetsResult adding elapsedMilliseconds property - if (thenable(evaluatedFeatures)) { - return evaluatedFeatures.then((evaluations) => { - elapsedMilliseconds = stopTimer(); - return {evaluations, elapsedMilliseconds}; - }); - } - elapsedMilliseconds = stopTimer(); - return {evaluations: evaluatedFeatures, elapsedMilliseconds}; } function getEvaluation( diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 56d00290..54078b5b 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -27,11 +27,6 @@ export interface IEvaluation { export type IEvaluationResult = IEvaluation & { treatment: string } -export interface IByFlagSetsResult { - evaluations: Record, - elapsedMilliseconds: number -} - export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable export type IEvaluator = (key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index 8e4148bb..31aa5f74 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -4,12 +4,13 @@ import { getMatching, getBucketing } from '../utils/key'; import { validateSplitExistance } from '../utils/inputValidation/splitExistance'; import { validateTrafficTypeExistance } from '../utils/inputValidation/trafficTypeExistance'; import { SDK_NOT_READY } from '../utils/labels'; -import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK } from '../utils/constants'; -import { IByFlagSetsResult, IEvaluationResult } from '../evaluator/types'; +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 } from '../utils/constants'; +import { IEvaluationResult } from '../evaluator/types'; import { SplitIO, ImpressionDTO } from '../types'; import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants'; import { ISdkFactoryContext } from '../sdkFactory/types'; import { isStorageSync } from '../trackers/impressionObserver/utils'; +import { Method } from '../sync/submitters/types'; const treatmentNotReady = { treatment: CONTROL, label: SDK_NOT_READY }; @@ -81,13 +82,13 @@ 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); + function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS) { + const stopTelemetryTracker = telemetryTracker.trackEval(method); - const wrapUp = (evaluationResults: IByFlagSetsResult) => { + const wrapUp = (evaluationResults: Record) => { const queue: ImpressionDTO[] = []; const treatments: Record = {}; - const evaluations = evaluationResults.evaluations; + const evaluations = evaluationResults; Object.keys(evaluations).forEach(featureFlagName => { treatments[featureFlagName] = processEvaluation(evaluations[featureFlagName], featureFlagName, key, attributes, withConfig, `getTreatmentsByFlagSets${withConfig ? 'WithConfig' : ''}`, queue); }); @@ -97,25 +98,23 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl return treatments; }; - const emptyEvaluationByFlagSet = {evaluations: {}, elapsedMilliseconds: 0}; - const evaluations = readinessManager.isReady() || readinessManager.isReadyFromCache() ? evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage) : - isStorageSync(settings) ? emptyEvaluationByFlagSet : Promise.resolve(emptyEvaluationByFlagSet); // Promisify if async + isStorageSync(settings) ? {} : Promise.resolve({}); // 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 getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes: SplitIO.Attributes | undefined) { + return getTreatmentsByFlagSets(key, flagSetNames, attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS); } - function getTreatmentsByFlagSet(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, [featureFlagName], attributes); + function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) { + return getTreatmentsByFlagSets(key, [flagSetName], attributes, false, TREATMENTS_BY_FLAGSET); } - function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, featureFlagName: string, attributes: SplitIO.Attributes | undefined) { - return getTreatmentsByFlagSets(key, [featureFlagName], attributes, true); + function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes: SplitIO.Attributes | undefined) { + return getTreatmentsByFlagSets(key, [flagSetName], attributes, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET); } // Internal function diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 0161a43d..94edfafb 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -125,8 +125,8 @@ export function clientInputValidationDecorator = { ts: 'treatments', tc: 'treatmentWithConfig', tcs: 'treatmentsWithConfig', + tf: 'treatmentsByFlagSet', + tfs: 'treatmentsByFlagSets', + tcf: 'treatmentsWithConfigByFlagSet', + tcfs: 'treatmentsWithConfigByFlagSets', tr: 'track' }; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 5b7ff0c4..440f466a 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -124,7 +124,11 @@ export type TREATMENTS = 'ts'; export type TREATMENT_WITH_CONFIG = 'tc'; export type TREATMENTS_WITH_CONFIG = 'tcs'; export type TRACK = 'tr'; -export type Method = TREATMENT | TREATMENTS | TREATMENT_WITH_CONFIG | TREATMENTS_WITH_CONFIG | TRACK; +export type TREATMENTS_BY_FLAGSET = 'tf' +export type TREATMENTS_BY_FLAGSETS = 'tfs' +export type TREATMENTS_WITH_CONFIG_BY_FLAGSET = 'tcf' +export type TREATMENTS_WITH_CONFIG_BY_FLAGSETS = 'tcfs' +export type Method = TREATMENT | TREATMENTS | TREATMENT_WITH_CONFIG | TREATMENTS_WITH_CONFIG | TRACK | TREATMENTS_BY_FLAGSET | TREATMENTS_BY_FLAGSETS | TREATMENTS_WITH_CONFIG_BY_FLAGSET | TREATMENTS_WITH_CONFIG_BY_FLAGSETS; export type MethodLatencies = Partial>>; diff --git a/src/types.ts b/src/types.ts index 0bc0ea24..2fb862c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1161,41 +1161,41 @@ export namespace SplitIO { */ 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. + * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the features related to the given flag set. * 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 {string} flagSet - The flag set 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. + * Returns a TreatmentWithConfig value, which will be (or eventually be) an object with both treatment and config string for features related to the given flag set. * 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 {string} flagSet - The flag set 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. + * Returns a Treatments value, which will be (or eventually be) an object map with the treatments for the feature flags related to the given flag sets. * 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 {Array} flagSets - An array of the flag set 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. + * 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 flag sets. * 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 {Array} flagSets - An array of the flag set 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 */ @@ -1251,37 +1251,37 @@ export namespace SplitIO { */ 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. + * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * @function getTreatmentsByFlagSet * @param {string} key - The string key representing the consumer. - * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {string} flagSet - The flag set 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. + * 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 flag set. * @function getTreatmentsWithConfigByFlagSet * @param {string} key - The string key representing the consumer. - * @param {string} flagSet - The flagSet name we want to get the treatments. + * @param {string} flagSet - The flag set 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. + * 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 flag sets. * @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 {Array} flagSets - An array of the flag set 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. + * 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 flag sets. * @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 {Array} flagSets - An array of the flag set 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 */ diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index c55984a3..18dc1ccb 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -65,6 +65,10 @@ export const TREATMENT = 't'; export const TREATMENTS = 'ts'; export const TREATMENT_WITH_CONFIG = 'tc'; export const TREATMENTS_WITH_CONFIG = 'tcs'; +export const TREATMENTS_BY_FLAGSET = 'tf'; +export const TREATMENTS_BY_FLAGSETS = 'tfs'; +export const TREATMENTS_WITH_CONFIG_BY_FLAGSET = 'tcf'; +export const TREATMENTS_WITH_CONFIG_BY_FLAGSETS = 'tcfs'; export const TRACK = 'tr'; export const CONNECTION_ESTABLISHED = 0; From 2d9a3a91acfbab0b3f471bad3421113fec14385b Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 27 Sep 2023 11:10:27 -0300 Subject: [PATCH 27/43] [SDKS-7561] Input validation for evaluationcd --- src/logger/constants.ts | 1 + src/sdkClient/clientInputValidation.ts | 20 +++++---- .../__tests__/splitFilters.spec.ts | 44 ++++++++++++++++++- src/utils/settingsValidation/splitFilters.ts | 20 ++++++++- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 5e0e5591..4ac3d64a 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -99,6 +99,7 @@ export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223; export const STREAMING_PARSING_SPLIT_UPDATE = 224; export const WARN_SPLITS_FILTER_INVALID_SET = 225; export const WARN_SPLITS_FILTER_LOWERCASE_SET = 226; +export const WARN_FLAGSET_NOT_CONFIGURED = 227; export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; export const ERROR_LOGLEVEL_INVALID = 301; diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 94edfafb..28d4099c 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -17,6 +17,7 @@ import { IReadinessManager } from '../readiness/types'; import { MaybeThenable } from '../dtos/types'; import { ISettings, SplitIO } from '../types'; import { isStorageSync } from '../trackers/impressionObserver/utils'; +import { flagSetsAreValid } from '../utils/settingsValidation/splitFilters'; /** * Decorator that validates the input before actually executing the client methods. @@ -30,21 +31,22 @@ export function clientInputValidationDecorator 0) && attributes !== false; return { valid, @@ -126,20 +128,20 @@ export function clientInputValidationDecorator { @@ -128,4 +128,44 @@ describe('validateSplitFilters', () => { expect(loggerMock.warn.mock.calls.length).toEqual(12); expect(loggerMock.error.mock.calls.length).toEqual(3); }); + + test('flagSetsAreValid - Flag set validation for evaluations', () => { + + let flagSetsFilter = ['set_1', 'set_2']; + + // empty array + expect(flagSetsAreValid(loggerMock, 'test_method', [], flagSetsFilter)).toEqual([]); + + // must start with a letter or number + expect(flagSetsAreValid(loggerMock, 'test_method', ['_set_1'], flagSetsFilter)).toEqual([]); + expect(loggerMock.warn.mock.calls[0]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_1', regexp, '_set_1']]); + + // can contain _a-z0-9 + expect(flagSetsAreValid(loggerMock, 'test_method', ['set*1'], flagSetsFilter)).toEqual([]); + expect(loggerMock.warn.mock.calls[1]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*1', regexp, 'set*1']]); + + // have a max length of 50 characters + const longName = '1234567890_1234567890_1234567890_1234567890_1234567890'; + expect(flagSetsAreValid(loggerMock, 'test_method', [longName], flagSetsFilter)).toEqual([]); + expect(loggerMock.warn.mock.calls[2]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, [longName, regexp, longName]]); + + // both set names invalid -> empty list & warn + expect(flagSetsAreValid(loggerMock, 'test_method', ['set*1','set*3'], flagSetsFilter)).toEqual([]); + expect(loggerMock.warn.mock.calls[3]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*1', regexp, 'set*1']]); + expect(loggerMock.warn.mock.calls[4]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*3', regexp, 'set*3']]); + + // only set_1 is valid => [set_1] & warn + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_1','set*3'], flagSetsFilter)).toEqual(['set_1']); + expect(loggerMock.warn.mock.calls[5]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*3', regexp, 'set*3']]); + + // set_3 not included in configuration but set_1 included => [set_1] & warn + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_1','set_3'], flagSetsFilter)).toEqual(['set_1']); + expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['set_3']]); + + // set_3 not included in configuration => [] & warn + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_3'], flagSetsFilter)).toEqual([]); + expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['set_3']]); + + }); + }); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 264c9975..4810ccb0 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -3,7 +3,7 @@ import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, ERROR_EMPTY_ARRAY, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; import { objectAssign } from '../lang/objectAssign'; import { find, uniq } from '../lang'; @@ -187,3 +187,21 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: return res; } + +export function flagSetsAreValid(log: ILogger, method: string, flagSets: string[], flagSetsInConfig: string[]): string[] { + let toReturn: string[] = []; + if (flagSets.length === 0) { + log.error(ERROR_EMPTY_ARRAY, [method, 'flagSets']); + return toReturn; + } + const sets = validateSplits(log, flagSets, method, 'flag sets', 'flag set'); + toReturn = sets ? sanitizeFlagSets(log, sets) : []; + return toReturn.filter(flagSet => { + if (flagSetsInConfig.indexOf(flagSet) > -1) { + return true; + } + log.warn(WARN_FLAGSET_NOT_CONFIGURED, [flagSet]); + return false; + }); + +} From 925d55288bd6f9f8f7484f1056fdb3a5020c36a9 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 27 Sep 2023 11:47:44 -0300 Subject: [PATCH 28/43] Allow any set if flag sets in config is empty --- .../__tests__/splitFilters.spec.ts | 29 +++++++++++++++++++ src/utils/settingsValidation/splitFilters.ts | 17 ++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index bca919ed..8d6b80ca 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -166,6 +166,35 @@ describe('validateSplitFilters', () => { expect(flagSetsAreValid(loggerMock, 'test_method', ['set_3'], flagSetsFilter)).toEqual([]); expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['set_3']]); + // empty config + + + // must start with a letter or number + expect(flagSetsAreValid(loggerMock, 'test_method', ['_set_1'], [])).toEqual([]); + expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_1', regexp, '_set_1']]); + + // can contain _a-z0-9 + expect(flagSetsAreValid(loggerMock, 'test_method', ['set*1'], [])).toEqual([]); + expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*1', regexp, 'set*1']]); + + // have a max length of 50 characters + expect(flagSetsAreValid(loggerMock, 'test_method', [longName], [])).toEqual([]); + expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, [longName, regexp, longName]]); + + // both set names invalid -> empty list & warn + expect(flagSetsAreValid(loggerMock, 'test_method', ['set*1','set*3'], [])).toEqual([]); + expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*1', regexp, 'set*1']]); + expect(loggerMock.warn.mock.calls[12]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*3', regexp, 'set*3']]); + + // only set_1 is valid => [set_1] & warn + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_1','set*3'], [])).toEqual(['set_1']); + expect(loggerMock.warn.mock.calls[13]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set*3', regexp, 'set*3']]); + + // any set should be returned if there isn't flag sets in filter + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_1'], [])).toEqual(['set_1']); + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_1','set_2'], [])).toEqual(['set_1','set_2']); + expect(flagSetsAreValid(loggerMock, 'test_method', ['set_3'], [])).toEqual(['set_3']); + }); }); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 4810ccb0..1df96f1b 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -196,12 +196,15 @@ export function flagSetsAreValid(log: ILogger, method: string, flagSets: string[ } const sets = validateSplits(log, flagSets, method, 'flag sets', 'flag set'); toReturn = sets ? sanitizeFlagSets(log, sets) : []; - return toReturn.filter(flagSet => { - if (flagSetsInConfig.indexOf(flagSet) > -1) { - return true; - } - log.warn(WARN_FLAGSET_NOT_CONFIGURED, [flagSet]); - return false; - }); + if (flagSetsInConfig.length > 0) { + toReturn = toReturn.filter(flagSet => { + if (flagSetsInConfig.indexOf(flagSet) > -1) { + return true; + } + log.warn(WARN_FLAGSET_NOT_CONFIGURED, [flagSet]); + return false; + }); + } + return toReturn; } From d3a90a30d8aad05c144219a648dfb1be7f4530b1 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 28 Sep 2023 11:52:08 -0300 Subject: [PATCH 29/43] Update logic & error log when using sets with other filters --- package-lock.json | 4 ++-- package.json | 2 +- src/logger/constants.ts | 2 +- src/logger/messages/error.ts | 2 +- src/sync/polling/updaters/splitChangesUpdater.ts | 14 +++++++++++--- .../__tests__/splitFilters.spec.ts | 8 ++++---- src/utils/settingsValidation/splitFilters.ts | 4 ++-- 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index b345dd22..7b73fa71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.9.1-rc.0", + "version": "1.9.2-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.9.1-rc.0", + "version": "1.9.2-rc.0", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 543c6582..b3f4afa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.9.1-rc.0", + "version": "1.9.2-rc.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 4ac3d64a..961a1a98 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -129,7 +129,7 @@ export const ERROR_STORAGE_INVALID = 324; export const ERROR_NOT_BOOLEAN = 325; export const ERROR_MIN_CONFIG_PARAM = 326; export const ERROR_TOO_MANY_SETS = 327; -export const ERROR_SPLITS_FILTER_NAME_AND_SET = 328; +export const ERROR_SETS_FILTER_EXCLUSIVE = 328; // Log prefixes (a.k.a. tags or categories) export const LOG_PREFIX_SETTINGS = 'settings'; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 91f2481f..a75dbccb 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -35,5 +35,5 @@ export const codesError: [number, string][] = [ [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Falling back into default MEMORY storage'], [c.ERROR_MIN_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': the provided "%s" config param is lower than allowed. Setting to the minimum value %s seconds'], [c.ERROR_TOO_MANY_SETS, c.LOG_PREFIX_SETTINGS + ': the amount of flag sets provided are big causing uri length error.'], - [c.ERROR_SPLITS_FILTER_NAME_AND_SET, c.LOG_PREFIX_SETTINGS+': names and sets filter cannot be used at the same time. The sdk will proceed using sets filter.'], + [c.ERROR_SETS_FILTER_EXCLUSIVE, c.LOG_PREFIX_SETTINGS+': the Set filter is exclusive and cannot be used simultaneously with names or prefix filters. Ignoring names and prefixes.'], ]; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index ccbab176..3a7e616a 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -7,6 +7,7 @@ import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; +import { startsWith } from '../../../utils/lang'; type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise @@ -53,10 +54,17 @@ interface ISplitMutations { * @param filters splitFiltersValidation bySet | byName */ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { - const { bySet: setsFilter, byName: namesFilter } = filters.groupedFilters; + const { bySet: setsFilter, byName: namesFilter, byPrefix: prefixFilter} = filters.groupedFilters; if (setsFilter.length > 0) return featureFlag.sets && featureFlag.sets.some((featureFlagSet: string) => setsFilter.indexOf(featureFlagSet) > -1); - if (namesFilter.length > 0) return namesFilter.indexOf(featureFlag.name) > -1; - return true; + + const namesFilterConfigured = namesFilter.length > 0; + const prefixFilterConfigured = prefixFilter.length > 0; + + if (!namesFilterConfigured && !prefixFilterConfigured) return true; + + const matchNames = namesFilterConfigured && namesFilter.indexOf(featureFlag.name) > -1; + const matchPrefix = prefixFilterConfigured && prefixFilter.some(prefix => startsWith(featureFlag.name, prefix)); + return matchNames || matchPrefix; } /** diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index 8d6b80ca..0e042faa 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -7,7 +7,7 @@ import { splitFilters, queryStrings, groupedFilters } from '../../../__tests__/m // Test target import { flagSetsAreValid, validateSplitFilters } from '../splitFilters'; -import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../../logger/constants'; +import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, ERROR_SETS_FILTER_EXCLUSIVE, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../../logger/constants'; describe('validateSplitFilters', () => { @@ -108,7 +108,7 @@ describe('validateSplitFilters', () => { expect(loggerMock.warn.mock.calls[0]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_1']]); expect(loggerMock.warn.mock.calls[1]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', 'set_3 ']]); expect(loggerMock.warn.mock.calls[2]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_a ']]); - expect(loggerMock.error.mock.calls[0]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); + expect(loggerMock.error.mock.calls[0]).toEqual([ERROR_SETS_FILTER_EXCLUSIVE]); expect(validateSplitFilters(loggerMock, splitFilters[7], STANDALONE_MODE)).toEqual(getOutput(7)); // lowercase and regexp expect(loggerMock.warn.mock.calls[3]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['seT_c']]); // lowercase @@ -117,13 +117,13 @@ describe('validateSplitFilters', () => { expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set _3', regexp, 'set _3']]); // empty spaces expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['_set_2', regexp, '_set_2']]); // start with a letter expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters - expect(loggerMock.error.mock.calls[1]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); + expect(loggerMock.error.mock.calls[1]).toEqual([ERROR_SETS_FILTER_EXCLUSIVE]); expect(validateSplitFilters(loggerMock, splitFilters[8], STANDALONE_MODE)).toEqual(getOutput(8)); // lowercase and dedupe expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character - expect(loggerMock.error.mock.calls[2]).toEqual([ERROR_SPLITS_FILTER_NAME_AND_SET]); + expect(loggerMock.error.mock.calls[2]).toEqual([ERROR_SETS_FILTER_EXCLUSIVE]); expect(loggerMock.warn.mock.calls.length).toEqual(12); expect(loggerMock.error.mock.calls.length).toEqual(3); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 1df96f1b..06c9de8b 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -3,7 +3,7 @@ import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, ERROR_EMPTY_ARRAY, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SETS_FILTER_EXCLUSIVE, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, ERROR_EMPTY_ARRAY, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; import { objectAssign } from '../lang/objectAssign'; import { find, uniq } from '../lang'; @@ -177,7 +177,7 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: const setFilter = configuredFilter(res.validFilters, 'bySet'); // Clean all filters if set filter is present if (setFilter) { - if (configuredFilter(res.validFilters, 'byName')) log.error(ERROR_SPLITS_FILTER_NAME_AND_SET); + if (configuredFilter(res.validFilters, 'byName') || configuredFilter(res.validFilters, 'byPrefix')) log.error(ERROR_SETS_FILTER_EXCLUSIVE); objectAssign(res.groupedFilters, { byName: [], byPrefix: [] }); } From deaec3ff3b680440e1e16339a78bdee4a332537f Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 2 Oct 2023 11:02:04 -0300 Subject: [PATCH 30/43] Code optimization & warn declaration --- src/logger/messages/warn.ts | 1 + src/sdkClient/clientInputValidation.ts | 2 +- .../settingsValidation/__tests__/splitFilters.spec.ts | 4 ++-- src/utils/settingsValidation/splitFilters.ts | 9 ++------- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index b576095a..3699ac52 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -24,6 +24,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_NOT_EXISTENT_SPLIT, '%s: feature flag "%s" does not exist in this environment. Please double check what feature flags exist in the Split user interface.'], [c.WARN_LOWERCASE_TRAFFIC_TYPE, '%s: traffic_type_name should be all lowercase - converting string to lowercase.'], [c.WARN_NOT_EXISTENT_TT, '%s: traffic type "%s" does not have any corresponding feature flag in this environment, make sure you\'re tracking your events to a valid traffic type defined in the Split user interface.'], + [c.WARN_FLAGSET_NOT_CONFIGURED, '%s: : you passed %s wich is not part of the configured FlagSetsFilter, ignoring Flag Set.'], // initialization / settings validation [c.WARN_INTEGRATION_INVALID, c.LOG_PREFIX_SETTINGS+': %s integration item(s) at settings is invalid. %s'], [c.WARN_SPLITS_FILTER_IGNORED, c.LOG_PREFIX_SETTINGS+': feature flag filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], diff --git a/src/sdkClient/clientInputValidation.ts b/src/sdkClient/clientInputValidation.ts index 28d4099c..50860b9f 100644 --- a/src/sdkClient/clientInputValidation.ts +++ b/src/sdkClient/clientInputValidation.ts @@ -31,7 +31,7 @@ export function clientInputValidationDecorator { // set_3 not included in configuration but set_1 included => [set_1] & warn expect(flagSetsAreValid(loggerMock, 'test_method', ['set_1','set_3'], flagSetsFilter)).toEqual(['set_1']); - expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['set_3']]); + expect(loggerMock.warn.mock.calls[6]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['test_method', 'set_3']]); // set_3 not included in configuration => [] & warn expect(flagSetsAreValid(loggerMock, 'test_method', ['set_3'], flagSetsFilter)).toEqual([]); - expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['set_3']]); + expect(loggerMock.warn.mock.calls[7]).toEqual([WARN_FLAGSET_NOT_CONFIGURED, ['test_method','set_3']]); // empty config diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 1df96f1b..558a870c 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -189,19 +189,14 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: } export function flagSetsAreValid(log: ILogger, method: string, flagSets: string[], flagSetsInConfig: string[]): string[] { - let toReturn: string[] = []; - if (flagSets.length === 0) { - log.error(ERROR_EMPTY_ARRAY, [method, 'flagSets']); - return toReturn; - } const sets = validateSplits(log, flagSets, method, 'flag sets', 'flag set'); - toReturn = sets ? sanitizeFlagSets(log, sets) : []; + let toReturn = sets ? sanitizeFlagSets(log, sets) : []; if (flagSetsInConfig.length > 0) { toReturn = toReturn.filter(flagSet => { if (flagSetsInConfig.indexOf(flagSet) > -1) { return true; } - log.warn(WARN_FLAGSET_NOT_CONFIGURED, [flagSet]); + log.warn(WARN_FLAGSET_NOT_CONFIGURED, [method, flagSet]); return false; }); } From c9c89b81c52a77a71af75fa600a0b0364765077f Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 2 Oct 2023 11:03:21 -0300 Subject: [PATCH 31/43] Remove unused constant --- src/utils/settingsValidation/splitFilters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 558a870c..7b6a7411 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -3,7 +3,7 @@ import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, ERROR_EMPTY_ARRAY, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SPLITS_FILTER_NAME_AND_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; import { objectAssign } from '../lang/objectAssign'; import { find, uniq } from '../lang'; From 4520f667725b8bfa3ff65e4a02d1e87dff82b849 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Mon, 9 Oct 2023 19:01:19 -0300 Subject: [PATCH 32/43] [SDKS-7567] Type definitions --- package-lock.json | 4 ++-- package.json | 2 +- src/types.ts | 12 ++++-------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b92f0c5..89945d4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.9.2-rc.0", + "version": "1.9.2-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.9.2-rc.0", + "version": "1.9.2-rc.1", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 7d4d84f5..35c95e48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.9.2-rc.0", + "version": "1.9.2-rc.1", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/types.ts b/src/types.ts index 2fb862c8..6e37efa1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1253,39 +1253,35 @@ export namespace SplitIO { /** * Returns a Treatments value, which is an object map with the treatments for the feature flags related to the given flag set. * @function getTreatmentsByFlagSet - * @param {string} key - The string key representing the consumer. * @param {string} flagSet - The flag set 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, + getTreatmentsByFlagSet(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 flag set. * @function getTreatmentsWithConfigByFlagSet - * @param {string} key - The string key representing the consumer. * @param {string} flagSet - The flag set 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, + getTreatmentsWithConfigByFlagSet(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 flag sets. * @function getTreatmentsByFlagSets - * @param {string} key - The string key representing the consumer. * @param {Array} flagSets - An array of the flag set 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, + getTreatmentsByFlagSets(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 flag sets. * @function getTreatmentsWithConfigByFlagSets - * @param {string} key - The string key representing the consumer. * @param {Array} flagSets - An array of the flag set 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, + getTreatmentsWithConfigByFlagSets(flagSets: string[], attributes?: Attributes): TreatmentsWithConfig, /** * Tracks an event to be fed to the results product on Split user interface. * @function track From e44412f614f8300f167764e00f4751d11f1b7f89 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 20 Oct 2023 17:14:14 -0300 Subject: [PATCH 33/43] Remove condition to ignore splitFilters if not in standalone mode --- package-lock.json | 4 ++-- package.json | 2 +- src/utils/settingsValidation/__tests__/splitFilters.spec.ts | 3 +-- src/utils/settingsValidation/splitFilters.ts | 5 ----- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2dd4391..076a3177 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.0", + "version": "1.10.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.10.0", + "version": "1.10.1-rc.0", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 889dea78..347331d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.0", + "version": "1.10.1-rc.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index aacc428a..036f2f87 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -40,8 +40,7 @@ describe('validateSplitFilters', () => { expect(validateSplitFilters(loggerMock, 15, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array expect(validateSplitFilters(loggerMock, 'string', STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array expect(validateSplitFilters(loggerMock, [], STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(validateSplitFilters(loggerMock, [{ type: 'byName', values: ['split_1'] }], CONSUMER_MODE)).toEqual(defaultOutput); // splitFilters ignored if not in 'standalone' mode - expect(loggerMock.warn.mock.calls).toEqual([[WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_IGNORED, ['standalone']]]); + expect(loggerMock.warn.mock.calls).toEqual([[WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY]]); expect(loggerMock.debug).not.toBeCalled(); expect(loggerMock.error).not.toBeCalled(); diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index ca5693eb..971b4889 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -147,11 +147,6 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: // do nothing if `splitFilters` param is not a non-empty array or mode is not STANDALONE if (!maybeSplitFilters) return res; - // Warn depending on the mode - if (mode !== STANDALONE_MODE) { - log.warn(WARN_SPLITS_FILTER_IGNORED, [STANDALONE_MODE]); - return res; - } // Check collection type if (!Array.isArray(maybeSplitFilters) || maybeSplitFilters.length === 0) { log.warn(WARN_SPLITS_FILTER_EMPTY); From a0e1f6539e6cc8d745e0d783c9f4156625d9c6bc Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 20 Oct 2023 17:19:54 -0300 Subject: [PATCH 34/43] fix lint --- .../__tests__/splitFilters.spec.ts | 30 +++++++++---------- src/utils/settingsValidation/index.ts | 2 +- src/utils/settingsValidation/splitFilters.ts | 6 ++-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index 036f2f87..5fe5dfb2 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -1,13 +1,11 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { STANDALONE_MODE, CONSUMER_MODE } from '../../constants'; - // Split filter and QueryStrings examples import { splitFilters, queryStrings, groupedFilters } from '../../../__tests__/mocks/fetchSpecificSplits'; // Test target import { flagSetsAreValid, validateSplitFilters } from '../splitFilters'; -import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, ERROR_SETS_FILTER_EXCLUSIVE, WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../../logger/constants'; +import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, ERROR_SETS_FILTER_EXCLUSIVE, WARN_SPLITS_FILTER_INVALID, WARN_SPLITS_FILTER_EMPTY, WARN_TRIMMING, WARN_SPLITS_FILTER_INVALID_SET, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../../logger/constants'; describe('validateSplitFilters', () => { @@ -32,14 +30,14 @@ describe('validateSplitFilters', () => { test('Returns default output with empty values if `splitFilters` is an invalid object or `mode` is not \'standalone\'', () => { - expect(validateSplitFilters(loggerMock, undefined, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(validateSplitFilters(loggerMock, null, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, undefined)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, null)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array expect(loggerMock.warn).not.toBeCalled(); - expect(validateSplitFilters(loggerMock, true, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(validateSplitFilters(loggerMock, 15, STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(validateSplitFilters(loggerMock, 'string', STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array - expect(validateSplitFilters(loggerMock, [], STANDALONE_MODE)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, true)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, 15)).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, 'string')).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array + expect(validateSplitFilters(loggerMock, [])).toEqual(defaultOutput); // splitFilters ignored if not a non-empty array expect(loggerMock.warn.mock.calls).toEqual([[WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY], [WARN_SPLITS_FILTER_EMPTY]]); expect(loggerMock.debug).not.toBeCalled(); @@ -58,7 +56,7 @@ describe('validateSplitFilters', () => { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, }; - expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // filters without values + expect(validateSplitFilters(loggerMock, splitFilters)).toEqual(output); // filters without values expect(loggerMock.debug).toBeCalledWith(SETTINGS_SPLITS_FILTER, [null]); loggerMock.debug.mockClear(); @@ -68,7 +66,7 @@ describe('validateSplitFilters', () => { { type: null, values: [] }, { type: 'byName', values: [13] }); output.validFilters.push({ type: 'byName', values: [13] }); - expect(validateSplitFilters(loggerMock, splitFilters, STANDALONE_MODE)).toEqual(output); // some filters are invalid + expect(validateSplitFilters(loggerMock, splitFilters)).toEqual(output); // some filters are invalid expect(loggerMock.debug.mock.calls).toEqual([[SETTINGS_SPLITS_FILTER, [null]]]); expect(loggerMock.warn.mock.calls).toEqual([ [WARN_SPLITS_FILTER_INVALID, [4]], // invalid value of `type` property @@ -92,24 +90,24 @@ describe('validateSplitFilters', () => { queryString: queryStrings[i], groupedFilters: groupedFilters[i], }; - expect(validateSplitFilters(loggerMock, splitFilters[i], STANDALONE_MODE)).toEqual(output); // splitFilters #${i} + expect(validateSplitFilters(loggerMock, splitFilters[i])).toEqual(output); // splitFilters #${i} expect(loggerMock.debug).lastCalledWith(SETTINGS_SPLITS_FILTER, [queryStrings[i]]); } else { // tests where validateSplitFilters throws an exception - expect(() => validateSplitFilters(loggerMock, splitFilters[i], STANDALONE_MODE)).toThrow(queryStrings[i]); + expect(() => validateSplitFilters(loggerMock, splitFilters[i])).toThrow(queryStrings[i]); } } }); test('Validates flag set filters', () => { // extra spaces trimmed and sorted query output - expect(validateSplitFilters(loggerMock, splitFilters[6], STANDALONE_MODE)).toEqual(getOutput(6)); // trim & sort + expect(validateSplitFilters(loggerMock, splitFilters[6])).toEqual(getOutput(6)); // trim & sort expect(loggerMock.warn.mock.calls[0]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_1']]); expect(loggerMock.warn.mock.calls[1]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', 'set_3 ']]); expect(loggerMock.warn.mock.calls[2]).toEqual([WARN_TRIMMING, ['settings', 'bySet filter value', ' set_a ']]); expect(loggerMock.error.mock.calls[0]).toEqual([ERROR_SETS_FILTER_EXCLUSIVE]); - expect(validateSplitFilters(loggerMock, splitFilters[7], STANDALONE_MODE)).toEqual(getOutput(7)); // lowercase and regexp + expect(validateSplitFilters(loggerMock, splitFilters[7])).toEqual(getOutput(7)); // lowercase and regexp expect(loggerMock.warn.mock.calls[3]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['seT_c']]); // lowercase expect(loggerMock.warn.mock.calls[4]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase expect(loggerMock.warn.mock.calls[5]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_ 1', regexp, 'set_ 1']]); // empty spaces @@ -118,7 +116,7 @@ describe('validateSplitFilters', () => { expect(loggerMock.warn.mock.calls[8]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_1234567890_1234567890_234567890_1234567890_1234567890', regexp, 'set_1234567890_1234567890_234567890_1234567890_1234567890']]); // max of 50 characters expect(loggerMock.error.mock.calls[1]).toEqual([ERROR_SETS_FILTER_EXCLUSIVE]); - expect(validateSplitFilters(loggerMock, splitFilters[8], STANDALONE_MODE)).toEqual(getOutput(8)); // lowercase and dedupe + expect(validateSplitFilters(loggerMock, splitFilters[8])).toEqual(getOutput(8)); // lowercase and dedupe expect(loggerMock.warn.mock.calls[9]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['SET_2']]); // lowercase expect(loggerMock.warn.mock.calls[10]).toEqual([WARN_SPLITS_FILTER_LOWERCASE_SET, ['set_B']]); // lowercase expect(loggerMock.warn.mock.calls[11]).toEqual([WARN_SPLITS_FILTER_INVALID_SET, ['set_3!', regexp, 'set_3!']]); // special character diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 700ada43..8affa738 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -202,7 +202,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV } // validate the `splitFilters` settings and parse splits query - const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters, withDefaults.mode); + const splitFiltersValidation = validateSplitFilters(log, withDefaults.sync.splitFilters); withDefaults.sync.splitFilters = splitFiltersValidation.validFilters; withDefaults.sync.__splitFiltersValidation = splitFiltersValidation; diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 971b4889..023c3fc1 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -1,9 +1,8 @@ -import { STANDALONE_MODE } from '../constants'; import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SETS_FILTER_EXCLUSIVE, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS, ERROR_SETS_FILTER_EXCLUSIVE, WARN_SPLITS_FILTER_LOWERCASE_SET, WARN_SPLITS_FILTER_INVALID_SET, WARN_FLAGSET_NOT_CONFIGURED } from '../../logger/constants'; import { objectAssign } from '../lang/objectAssign'; import { find, uniq } from '../lang'; @@ -129,7 +128,6 @@ function configuredFilter(validFilters: SplitIO.SplitFilter[], filterType: Split * * @param {ILogger} log logger * @param {any} maybeSplitFilters split filters configuration param provided by the user - * @param {string} mode settings mode * @returns it returns an object with the following properties: * - `validFilters`: the validated `splitFilters` configuration object defined by the user. * - `queryString`: the parsed split filter query. it is null if all filters are invalid or all values in filters are invalid. @@ -137,7 +135,7 @@ function configuredFilter(validFilters: SplitIO.SplitFilter[], filterType: Split * * @throws Error if the some of the grouped list of values per filter exceeds the max allowed length */ -export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: string): ISplitFiltersValidation { +export function validateSplitFilters(log: ILogger, maybeSplitFilters: any): ISplitFiltersValidation { // Validation result schema const res = { validFilters: [], From 5e243f5b53be067cc96c696e19c517358ffe559e Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 24 Oct 2023 16:17:20 -0300 Subject: [PATCH 35/43] Remove WARN_SPLITS_FILTER_IGNORED --- src/logger/constants.ts | 1 - src/logger/messages/warn.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 961a1a98..18c86f9f 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -91,7 +91,6 @@ export const WARN_NOT_EXISTENT_SPLIT = 215; export const WARN_LOWERCASE_TRAFFIC_TYPE = 216; export const WARN_NOT_EXISTENT_TT = 217; export const WARN_INTEGRATION_INVALID = 218; -export const WARN_SPLITS_FILTER_IGNORED = 219; export const WARN_SPLITS_FILTER_INVALID = 220; export const WARN_SPLITS_FILTER_EMPTY = 221; export const WARN_SDK_KEY = 222; diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 7865d285..8c6f60ce 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -27,7 +27,6 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_FLAGSET_NOT_CONFIGURED, '%s: : you passed %s wich is not part of the configured FlagSetsFilter, ignoring Flag Set.'], // initialization / settings validation [c.WARN_INTEGRATION_INVALID, c.LOG_PREFIX_SETTINGS+': %s integration item(s) at settings is invalid. %s'], - [c.WARN_SPLITS_FILTER_IGNORED, c.LOG_PREFIX_SETTINGS+': feature flag filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], [c.WARN_SPLITS_FILTER_INVALID, c.LOG_PREFIX_SETTINGS+': feature flag filter at position %s is invalid. It must be an object with a valid filter type ("bySet", "byName" or "byPrefix") and a list of "values".'], [c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS+': feature flag filter configuration must be a non-empty array of filter objects.'], [c.WARN_SDK_KEY, c.LOG_PREFIX_SETTINGS+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'], From 4f59b03c1f73304d5008af0bff420072056746ef Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 1 Nov 2023 12:33:23 -0300 Subject: [PATCH 36/43] update sets property in SplitView type --- src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index df508cdf..b71f6e80 100644 --- a/src/types.ts +++ b/src/types.ts @@ -609,12 +609,12 @@ export namespace SplitIO { */ configs: { [treatmentName: string]: string - } + }, /** - * list of sets per feature flag + * List of sets of the feature flag. * @property {string[]} sets */ - sets?: string[], + sets: string[], /** * The default treatment of the feature flag. * @property {string} defaultTreatment From 246283846aeb6a0e9a9a458b2625887d4c5cca13 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 1 Nov 2023 17:11:10 -0300 Subject: [PATCH 37/43] [SDKS-7657] fix SDK_UPDATE issue --- .../__tests__/splitChangesUpdater.spec.ts | 40 ++++++++++++++++++- .../polling/updaters/splitChangesUpdater.ts | 15 ++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 3729eded..54dec32b 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -168,9 +168,9 @@ describe('splitChangesUpdater', () => { const readinessManager = readinessManagerFactory(EventEmitter); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); - const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; + let splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - const splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); + let splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, splitsCache, segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1); afterEach(() => { jest.clearAllMocks(); @@ -213,4 +213,40 @@ describe('splitChangesUpdater', () => { index++; } }); + + test('flag sets splits-arrived emition', async () => { + const payload = splitNotifications[3].decoded as Pick; + const setMocks = [ + { sets: [], shouldEmit: false }, /* should not emit if flag does not have any set */ + { sets: ['set_a'], shouldEmit: true }, /* should emit if flag is in configured sets */ + { sets: ['set_b'], shouldEmit: true }, /* should emit if flag was just removed from configured sets */ + { sets: ['set_b'], shouldEmit: false }, /* should NOT emit if flag is nor was just removed from configured sets */ + { sets: ['set_c'], shouldEmit: false }, /* should NOT emit if flag is nor was just removed from configured sets */ + { sets: ['set_a'], shouldEmit: true }, /* should emit if flag is back in configured sets */ + ]; + + splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, new SplitsCacheInMemory(), segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + + let index = 0; + let calls = 0; + // emit always if not configured sets + for (const setMock of setMocks) { + await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets: setMock.sets, status: 'ACTIVE'}, changeNumber: index })).resolves.toBe(true); + expect(splitsEmitSpy.mock.calls[index][0]).toBe('state::splits-arrived'); + index++; + } + + // @ts-ignore + splitFiltersValidation = { queryString: null, groupedFilters: { bySet: ['set_a'], byName: [], byPrefix: [] }, validFilters: [] }; + splitChangesUpdater = splitChangesUpdaterFactory(loggerMock, splitChangesFetcher, new SplitsCacheInMemory(), segmentsCache, splitFiltersValidation, readinessManager.splits, 1000, 1, true); + splitsEmitSpy.mockReset(); + index = 0; + for (const setMock of setMocks) { + await expect(splitChangesUpdater(undefined, undefined, { payload: {...payload, sets: setMock.sets, status: 'ACTIVE'}, changeNumber: index })).resolves.toBe(true); + if (setMock.shouldEmit) calls++; + expect(splitsEmitSpy.mock.calls.length).toBe(calls); + index++; + } + + }); }); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 3a7e616a..5f6575b3 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -127,6 +127,16 @@ export function splitChangesUpdaterFactory( return promise; } + /** Returns true if at least one split was updated */ + function update(flagsChange: [boolean | void, void | boolean[], void | boolean[], boolean | void] | [any, any, any]) { + const [, added, removed, ] = flagsChange; + // There is at least one added or modified feature flag + if (added && added.some((update: boolean) => update)) return true; + // There is at least one removed feature flag + if (removed && removed.some((update: boolean) => update)) return true; + return false; + } + /** * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. * Returned promise will not be rejected. @@ -163,8 +173,9 @@ export function splitChangesUpdaterFactory( splits.addSplits(mutation.added), splits.removeSplits(mutation.removed), segments.registerSegments(mutation.segments) - ]).then(() => { - + ]).then((flagsChange) => { + const triggerSdkUpdate = update(flagsChange); + if (!triggerSdkUpdate) return true; if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && (isClientSide || checkAllSegmentsExist(segments)))) From 2e7446bb3b58bb8c700f973ffd81838b13a78ee1 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 1 Nov 2023 17:42:20 -0300 Subject: [PATCH 38/43] update changes file --- CHANGES.txt | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0bb2cde5..92f937b5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,12 @@ +1.11.0 (November XX, 2023) + - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. + - getTreatmentsByFlagSet and getTreatmentsByFlagSets + - getTreatmentsWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets + - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. + - Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init. + - Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views. + 1.10.0 (October 20, 2023) - Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager (Related to issue https://github.com/splitio/javascript-commons/issues/225). - Updated log warning message to include the feature flag name when `getTreatment` method is called and the SDK client is not ready. diff --git a/package-lock.json b/package-lock.json index 076a3177..31eea521 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.0", + "version": "1.10.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.0", + "version": "1.10.1-rc.1", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 347331d3..5560e64e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.0", + "version": "1.10.1-rc.1", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 065c6e0f6c3ef9011c3605e17952ab2ced0c61ef Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Thu, 2 Nov 2023 14:57:04 -0300 Subject: [PATCH 39/43] approach improvement --- package-lock.json | 4 ++-- package.json | 2 +- src/sync/polling/updaters/splitChangesUpdater.ts | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31eea521..db3d8b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.1", + "version": "1.10.1-rc.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.1", + "version": "1.10.1-rc.3", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 5560e64e..2bd2eed9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.1", + "version": "1.10.1-rc.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 5f6575b3..583da077 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -128,7 +128,7 @@ export function splitChangesUpdaterFactory( } /** Returns true if at least one split was updated */ - function update(flagsChange: [boolean | void, void | boolean[], void | boolean[], boolean | void] | [any, any, any]) { + function isThereUpdate(flagsChange: [boolean | void, void | boolean[], void | boolean[], boolean | void] | [any, any, any]) { const [, added, removed, ] = flagsChange; // There is at least one added or modified feature flag if (added && added.some((update: boolean) => update)) return true; @@ -174,11 +174,9 @@ export function splitChangesUpdaterFactory( splits.removeSplits(mutation.removed), segments.registerSegments(mutation.segments) ]).then((flagsChange) => { - const triggerSdkUpdate = update(flagsChange); - if (!triggerSdkUpdate) return true; if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && (isClientSide || checkAllSegmentsExist(segments)))) + return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate(flagsChange) && (isClientSide || checkAllSegmentsExist(segments)))) .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events From 024760219a914cbb5dc60558228625180559e683 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 3 Nov 2023 12:57:21 -0300 Subject: [PATCH 40/43] fix SDK key validation in NodeJS --- CHANGES.txt | 1 + package-lock.json | 4 ++-- package.json | 2 +- src/readiness/readinessManager.ts | 5 +++++ src/readiness/types.ts | 1 + src/sync/polling/updaters/segmentChangesUpdater.ts | 2 +- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 92f937b5..4313d7bd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,7 @@ - Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload. - Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init. - Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views. + - Bugfixing - Fixed SDK key validation in NodeJS to ensure the SDK_READY_TIMED_OUT event is emitted when a client-side type SDK key is provided instead of a server-side one (Related to issue https://github.com/splitio/javascript-client/issues/768). 1.10.0 (October 20, 2023) - Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager (Related to issue https://github.com/splitio/javascript-commons/issues/225). diff --git a/package-lock.json b/package-lock.json index db3d8b54..bcc025b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.3", + "version": "1.10.1-rc.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.3", + "version": "1.10.1-rc.4", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 2bd2eed9..740f3717 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.3", + "version": "1.10.1-rc.4", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index b4557dc9..c5ac1c35 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -112,7 +112,12 @@ export function readinessManagerFactory( return readinessManagerFactory(EventEmitter, readyTimeout, splits); }, + // @TODO review/remove next methods when non-recoverable errors are reworked + // Called on consumer mode, when storage fails to connect timeout, + // Called on 403 error (client-side SDK key on server-side), to set the SDK as destroyed for + // tracking and evaluations, while keeping event listeners to emit SDK_READY_TIMED_OUT event + setDestroyed() { isDestroyed = true; }, destroy() { isDestroyed = true; diff --git a/src/readiness/types.ts b/src/readiness/types.ts index 71c8b215..d126b5ec 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -55,6 +55,7 @@ export interface IReadinessManager { isOperational(): boolean, timeout(): void, + setDestroyed(): void, destroy(): void, /** for client-side */ diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 32f3fe7f..39d147ff 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -96,7 +96,7 @@ export function segmentChangesUpdaterFactory( if (error && error.statusCode === 403) { // If the operation is forbidden, it may be due to permissions. Destroy the SDK instance. // @TODO although factory status is destroyed, synchronization is not stopped - if (readiness) readiness.destroy(); + if (readiness) readiness.setDestroyed(); log.error(`${LOG_PREFIX_INSTANTIATION}: you passed a client-side type authorizationKey, please grab an SDK Key from the Split user interface that is of type server-side.`); } else { log.warn(`${LOG_PREFIX_SYNC_SEGMENTS}Error while doing fetch of segments. ${error}`); From dec471bb4b870ed24d229fb2e67b1b03051a06a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 3 Nov 2023 17:12:46 -0300 Subject: [PATCH 41/43] fix issue with getNamesByFlagSets method in Redis and Pluggable split storage --- src/evaluator/index.ts | 7 +++++-- src/storages/inRedis/SplitsCacheInRedis.ts | 6 +++--- src/storages/pluggable/SplitsCachePluggable.ts | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 70dad68a..62f82e98 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -94,7 +94,7 @@ export function evaluateFeaturesByFlagSets( flagSets: string[], attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, -): MaybeThenable> { +): MaybeThenable> { let storedFlagNames: MaybeThenable>; // get features by flag sets @@ -107,7 +107,10 @@ export function evaluateFeaturesByFlagSets( // evaluate related features return thenable(storedFlagNames) ? - storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) : + storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage)) + .catch(() => { + return {}; + }) : evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage); } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 310c014d..754a370a 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -5,7 +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'; +import { ISet } from '../../utils/lang/sets'; /** * Discard errors for an answer of multiple operations. @@ -196,8 +196,8 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * @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([])); + this.log.warn(LOG_PREFIX + 'ByFlagSet/s evaluations are not supported in Redis storage yet.'); + return Promise.reject(); } /** diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index c6dce829..45ed0282 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -5,7 +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'; +import { ISet } from '../../utils/lang/sets'; /** * ISplitsCacheAsync implementation for pluggable storages. @@ -162,8 +162,8 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { * @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([])); + this.log.warn(LOG_PREFIX + 'ByFlagSet/s evaluations are not supported in pluggable storage yet.'); + return Promise.reject(); } /** From 827b91e1d3947e9aa1ffe361c6db1b47191a46d2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 3 Nov 2023 17:59:33 -0300 Subject: [PATCH 42/43] update log level and message --- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- src/storages/pluggable/SplitsCachePluggable.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 754a370a..f58f49ab 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -196,7 +196,7 @@ export class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * @todo this is a no-op method to be implemented */ getNamesByFlagSets(): Promise> { - this.log.warn(LOG_PREFIX + 'ByFlagSet/s evaluations are not supported in Redis storage yet.'); + this.log.error(LOG_PREFIX + 'ByFlagSet/s evaluations are not supported with Redis storage yet.'); return Promise.reject(); } diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 45ed0282..786fb8a5 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -162,7 +162,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { * @todo this is a no-op method to be implemented */ getNamesByFlagSets(): Promise> { - this.log.warn(LOG_PREFIX + 'ByFlagSet/s evaluations are not supported in pluggable storage yet.'); + this.log.error(LOG_PREFIX + 'ByFlagSet/s evaluations are not supported with pluggable storage yet.'); return Promise.reject(); } From ab7a76e4dcf34f3e1d42671027737fe7935aa426 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 3 Nov 2023 18:31:26 -0300 Subject: [PATCH 43/43] prepare release v1.11.0 --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/storages/KeyBuilder.ts | 2 +- src/storages/__tests__/KeyBuilder.spec.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4313d7bd..a893bd27 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -1.11.0 (November XX, 2023) +1.11.0 (November 3, 2023) - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. - getTreatmentsByFlagSet and getTreatmentsByFlagSets diff --git a/package-lock.json b/package-lock.json index bcc025b8..5c28d3a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.4", + "version": "1.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.4", + "version": "1.11.0", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.1" diff --git a/package.json b/package.json index 740f3717..2fda9f0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "1.10.1-rc.4", + "version": "1.11.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index dc9581b5..c3124ae9 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -21,7 +21,7 @@ export class KeyBuilder { } buildFlagSetKey(flagSet: string) { - return `${this.prefix}.flagset.${flagSet}`; + return `${this.prefix}.flagSet.${flagSet}`; } buildSplitKey(splitName: string) { diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index 0965f3d1..01a09efa 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -72,7 +72,7 @@ test('KEYS / flag set keys', () => { const builder = new KeyBuilder(prefix); const flagSetName = 'flagset_x'; - const expectedKey = `${prefix}.flagset.${flagSetName}`; + const expectedKey = `${prefix}.flagSet.${flagSetName}`; expect(builder.buildFlagSetKey(flagSetName)).toBe(expectedKey);