Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDKS-7463] Add new bySet filter #238

Merged
merged 2 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
82 changes: 68 additions & 14 deletions src/__tests__/mocks/fetchSpecificSplits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] = [
Expand Down Expand Up @@ -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] }],
];
3 changes: 3 additions & 0 deletions src/logger/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/logger/messages/warn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 %s should be all lowercase - converting string to lowercase.'],
]);
2 changes: 1 addition & 1 deletion src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/utils/settingsValidation/__tests__/settings.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const fullSettings: ISettings = {
__splitFiltersValidation: {
validFilters: [],
queryString: null,
groupedFilters: { byName: [], byPrefix: [] }
groupedFilters: { bySet: [], byName: [], byPrefix: [] }
},
enabled: true
},
Expand Down
54 changes: 46 additions & 8 deletions src/utils/settingsValidation/__tests__/splitFilters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@ 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: [] }
};

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\'', () => {
Expand All @@ -39,13 +50,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]);
Expand All @@ -60,9 +72,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([
Expand All @@ -73,7 +85,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 = {
Expand All @@ -90,4 +102,30 @@ describe('validateSplitFilters', () => {
}
});

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']]);
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);
});
});
Loading