Skip to content

Commit e35cb7c

Browse files
Merge pull request #279 from splitio/development
Release v1.12.0
2 parents ba89b3b + 7fa8de5 commit e35cb7c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+824
-472
lines changed

CHANGES.txt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
1.12.0 (December 4, 2023)
2+
- Added support for Flag Sets in "consumer" and "partial consumer" modes for Pluggable and Redis storages.
3+
- Updated evaluation flow to log a warning when using flag sets that don't contain cached feature flags.
4+
- Updated Redis adapter to handle timeouts and queueing of some missing commands: 'hincrby', 'popNRaw', and 'pipeline.exec'.
5+
- Bugfixing - Fixed manager methods in consumer modes to return results in a promise when the SDK is not operational (not ready or destroyed).
6+
17
1.11.0 (November 3, 2023)
2-
- 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):
3-
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
4-
- getTreatmentsByFlagSet and getTreatmentsByFlagSets
5-
- getTreatmentsWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets
6-
- 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.
7-
- 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.
8-
- 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.
9-
- 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).
8+
- 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):
9+
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
10+
- getTreatmentsByFlagSet and getTreatmentsByFlagSets
11+
- getTreatmentsWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets
12+
- 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.
13+
- 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.
14+
- 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.
15+
- 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).
1016

1117
1.10.0 (October 20, 2023)
1218
- 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).
@@ -53,7 +59,7 @@
5359
- Added a new impressions mode for the SDK called NONE, to be used in factory when there is no desire to capture impressions on an SDK factory to feed Split's analytics engine. Running NONE mode, the SDK will only capture unique keys evaluated for a particular feature flag instead of full blown impressions.
5460
- Updated SDK telemetry to support pluggable storage, partial consumer mode, and synchronizer.
5561
- Updated storage implementations to improve the performance of feature flag evaluations (i.e., `getTreatment(s)` method calls) when using the default storage in memory.
56-
- Updated evaluation flow to avoid unnecessarily storage calls when the SDK is not ready.
62+
- Updated evaluation flow (i.e., `getTreatment(s)` method calls) to avoid calling the storage for cached feature flags when the SDK is not ready or ready from cache. It applies to all SDK modes.
5763

5864
1.6.1 (July 22, 2022)
5965
- Updated GoogleAnalyticsToSplit integration to validate `autoRequire` config parameter and avoid some wrong warning logs when mapping GA hit fields to Split event properties.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "1.11.0",
3+
"version": "1.12.0",
44
"description": "Split Javascript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/evaluator/__tests__/evaluate-features.spec.ts

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index';
33
import * as LabelsConstants from '../../utils/labels';
44
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
55
import { _Set } from '../../utils/lang/sets';
6-
import { returnSetsUnion } from '../../utils/lang/sets';
6+
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants';
77

88
const splitsMock = {
99
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' }] },
@@ -38,14 +38,7 @@ const mockStorage = {
3838
return splits;
3939
},
4040
getNamesByFlagSets(flagSets) {
41-
let toReturn = new _Set([]);
42-
flagSets.forEach(flagset => {
43-
const featureFlagNames = flagSetsMock[flagset];
44-
if (featureFlagNames) {
45-
toReturn = returnSetsUnion(toReturn, featureFlagNames);
46-
}
47-
});
48-
return toReturn;
41+
return flagSets.map(flagset => flagSetsMock[flagset] || new _Set());
4942
}
5043
}
5144
};
@@ -123,7 +116,7 @@ test('EVALUATOR - Multiple evaluations at once / should return right labels, tre
123116

124117
});
125118

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

128121
const expectedOutput = {
129122
config: {
@@ -135,44 +128,76 @@ test('EVALUATOR - Multiple evaluations at once by flag sets / should return righ
135128
},
136129
};
137130

138-
const getResultsByFlagsets = (flagSets: string[]) => {
131+
const getResultsByFlagsets = (flagSets: string[], storage = mockStorage) => {
139132
return evaluateFeaturesByFlagSets(
140133
loggerMock,
141134
'fake-key',
142135
flagSets,
143136
null,
144-
mockStorage,
137+
storage,
138+
'method-name'
145139
);
146140
};
147141

148-
149-
150-
let multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config', 'arch_and_killed']);
151-
152-
// assert evaluationWithConfig
153-
expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config.
154-
// @todo assert flag set not found - for input validations
155-
156-
// assert regular
157-
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.
158-
// assert killed
159-
expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual({ ...expectedOutput['config'], treatment: 'off', config: null, label: LabelsConstants.SPLIT_KILLED });
160-
// 'If the split is retrieved but is killed, we should get the right evaluation result, label and config.
161-
162-
// assert archived
163-
expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: LabelsConstants.SPLIT_ARCHIVED, config: null });
164-
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.
165-
166-
// assert not_existent_split not in evaluation if it is not related to defined flag sets
167-
expect(multipleEvaluationAtOnceByFlagSets['not_existent_split']).toEqual(undefined);
168-
169-
multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets([]);
170-
expect(multipleEvaluationAtOnceByFlagSets).toEqual({});
171-
172-
multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config']);
173-
expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']);
174-
expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], config: null });
175-
expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual(undefined);
176-
expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual(undefined);
177-
142+
test('should return right labels, treatments and configs if storage returns without errors', async () => {
143+
144+
let multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config', 'arch_and_killed']);
145+
146+
// assert evaluationWithConfig
147+
expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']); // If the split is retrieved successfully we should get the right evaluation result, label and config.
148+
// @todo assert flag set not found - for input validations
149+
150+
// assert regular
151+
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.
152+
// assert killed
153+
expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual({ ...expectedOutput['config'], treatment: 'off', config: null, label: LabelsConstants.SPLIT_KILLED });
154+
// 'If the split is retrieved but is killed, we should get the right evaluation result, label and config.
155+
156+
// assert archived
157+
expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual({ ...expectedOutput['config'], treatment: 'control', label: LabelsConstants.SPLIT_ARCHIVED, config: null });
158+
// If the split is retrieved but is archived, we should get the right evaluation result, label and config.
159+
160+
// assert not_existent_split not in evaluation if it is not related to defined flag sets
161+
expect(multipleEvaluationAtOnceByFlagSets['not_existent_split']).toEqual(undefined);
162+
163+
multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets([]);
164+
expect(multipleEvaluationAtOnceByFlagSets).toEqual({});
165+
166+
multipleEvaluationAtOnceByFlagSets = await getResultsByFlagsets(['reg_and_config']);
167+
expect(multipleEvaluationAtOnceByFlagSets['config']).toEqual(expectedOutput['config']);
168+
expect(multipleEvaluationAtOnceByFlagSets['regular']).toEqual({ ...expectedOutput['config'], config: null });
169+
expect(multipleEvaluationAtOnceByFlagSets['killed']).toEqual(undefined);
170+
expect(multipleEvaluationAtOnceByFlagSets['archived']).toEqual(undefined);
171+
});
172+
173+
test('should log a warning if evaluating with flag sets that doesn\'t contain cached feature flags', async () => {
174+
const getSplitsSpy = jest.spyOn(mockStorage.splits, 'getSplits');
175+
176+
// No flag set contains cached feature flags -> getSplits method is not called
177+
expect(getResultsByFlagsets(['inexistent_set1', 'inexistent_set2'])).toEqual({});
178+
expect(getSplitsSpy).not.toHaveBeenCalled();
179+
expect(loggerMock.warn.mock.calls).toEqual([
180+
[WARN_FLAGSET_WITHOUT_FLAGS, ['method-name', 'inexistent_set1']],
181+
[WARN_FLAGSET_WITHOUT_FLAGS, ['method-name', 'inexistent_set2']],
182+
]);
183+
184+
// One flag set contains cached feature flags -> getSplits method is called
185+
expect(getResultsByFlagsets(['inexistent_set3', 'reg_and_config'])).toEqual(getResultsByFlagsets(['reg_and_config']));
186+
expect(getSplitsSpy).toHaveBeenLastCalledWith(['regular', 'config']);
187+
expect(loggerMock.warn).toHaveBeenLastCalledWith(WARN_FLAGSET_WITHOUT_FLAGS, ['method-name', 'inexistent_set3']);
188+
189+
getSplitsSpy.mockRestore();
190+
loggerMock.warn.mockClear();
191+
192+
// Should support async storage too
193+
expect(await getResultsByFlagsets(['inexistent_set1', 'inexistent_set2'], {
194+
splits: {
195+
getNamesByFlagSets(flagSets) { return Promise.resolve(flagSets.map(flagset => flagSetsMock[flagset] || new _Set())); }
196+
}
197+
})).toEqual({});
198+
expect(loggerMock.warn.mock.calls).toEqual([
199+
[WARN_FLAGSET_WITHOUT_FLAGS, ['method-name', 'inexistent_set1']],
200+
[WARN_FLAGSET_WITHOUT_FLAGS, ['method-name', 'inexistent_set2']],
201+
]);
202+
});
178203
});

src/evaluator/index.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { IStorageAsync, IStorageSync } from '../storages/types';
77
import { IEvaluationResult } from './types';
88
import { SplitIO } from '../types';
99
import { ILogger } from '../logger/types';
10-
import { ISet, setToArray } from '../utils/lang/sets';
10+
import { ISet, setToArray, returnSetsUnion, _Set } from '../utils/lang/sets';
11+
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../logger/constants';
1112

1213
const treatmentException = {
1314
treatment: CONTROL,
@@ -94,8 +95,27 @@ export function evaluateFeaturesByFlagSets(
9495
flagSets: string[],
9596
attributes: SplitIO.Attributes | undefined,
9697
storage: IStorageSync | IStorageAsync,
98+
method: string,
9799
): MaybeThenable<Record<string, IEvaluationResult>> {
98-
let storedFlagNames: MaybeThenable<ISet<string>>;
100+
let storedFlagNames: MaybeThenable<ISet<string>[]>;
101+
102+
function evaluate(
103+
featureFlagsByFlagSets: ISet<string>[],
104+
) {
105+
let featureFlags = new _Set();
106+
for (let i = 0; i < flagSets.length; i++) {
107+
const featureFlagByFlagSet = featureFlagsByFlagSets[i];
108+
if (featureFlagByFlagSet.size) {
109+
featureFlags = returnSetsUnion(featureFlags, featureFlagByFlagSet);
110+
} else {
111+
log.warn(WARN_FLAGSET_WITHOUT_FLAGS, [method, flagSets[i]]);
112+
}
113+
}
114+
115+
return featureFlags.size ?
116+
evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage) :
117+
{};
118+
}
99119

100120
// get features by flag sets
101121
try {
@@ -107,11 +127,11 @@ export function evaluateFeaturesByFlagSets(
107127

108128
// evaluate related features
109129
return thenable(storedFlagNames) ?
110-
storedFlagNames.then((splitNames) => evaluateFeatures(log, key, setToArray(splitNames), attributes, storage))
130+
storedFlagNames.then((storedFlagNames) => evaluate(storedFlagNames))
111131
.catch(() => {
112132
return {};
113133
}) :
114-
evaluateFeatures(log, key, setToArray(storedFlagNames), attributes, storage);
134+
evaluate(storedFlagNames);
115135
}
116136

117137
function getEvaluation(

src/logger/constants.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@ export const WARN_NOT_EXISTENT_SPLIT = 215;
9191
export const WARN_LOWERCASE_TRAFFIC_TYPE = 216;
9292
export const WARN_NOT_EXISTENT_TT = 217;
9393
export const WARN_INTEGRATION_INVALID = 218;
94+
export const WARN_SPLITS_FILTER_IGNORED = 219;
9495
export const WARN_SPLITS_FILTER_INVALID = 220;
9596
export const WARN_SPLITS_FILTER_EMPTY = 221;
9697
export const WARN_SDK_KEY = 222;
9798
export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 223;
9899
export const STREAMING_PARSING_SPLIT_UPDATE = 224;
99-
export const WARN_SPLITS_FILTER_INVALID_SET = 225;
100-
export const WARN_SPLITS_FILTER_LOWERCASE_SET = 226;
100+
export const WARN_INVALID_FLAGSET = 225;
101+
export const WARN_LOWERCASE_FLAGSET = 226;
101102
export const WARN_FLAGSET_NOT_CONFIGURED = 227;
103+
export const WARN_FLAGSET_WITHOUT_FLAGS = 228;
102104

103105
export const ERROR_ENGINE_COMBINER_IFELSEIF = 300;
104106
export const ERROR_LOGLEVEL_INVALID = 301;

src/logger/messages/warn.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ export const codesWarn: [number, string][] = codesError.concat([
2424
[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.'],
2525
[c.WARN_LOWERCASE_TRAFFIC_TYPE, '%s: traffic_type_name should be all lowercase - converting string to lowercase.'],
2626
[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.'],
27-
[c.WARN_FLAGSET_NOT_CONFIGURED, '%s: : you passed %s wich is not part of the configured FlagSetsFilter, ignoring Flag Set.'],
27+
[c.WARN_FLAGSET_NOT_CONFIGURED, '%s: you passed %s which is not part of the configured FlagSetsFilter, ignoring Flag Set.'],
2828
// initialization / settings validation
29-
[c.WARN_INTEGRATION_INVALID, c.LOG_PREFIX_SETTINGS+': %s integration item(s) at settings is invalid. %s'],
30-
[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".'],
31-
[c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS+': feature flag filter configuration must be a non-empty array of filter objects.'],
32-
[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'],
29+
[c.WARN_INTEGRATION_INVALID, c.LOG_PREFIX_SETTINGS + ': %s integration item(s) at settings is invalid. %s'],
30+
[c.WARN_SPLITS_FILTER_IGNORED, c.LOG_PREFIX_SETTINGS + ': feature flag filters are not applicable for Consumer modes where the SDK does not keep rollout data in sync. Filters were discarded'],
31+
[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".'],
32+
[c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS + ': feature flag filter configuration must be a non-empty array of filter objects.'],
33+
[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'],
3334

3435
[c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'],
3536
[c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'],
36-
[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.'],
37-
[c.WARN_SPLITS_FILTER_LOWERCASE_SET, c.LOG_PREFIX_SETTINGS+': flag set %s should be all lowercase - converting string to lowercase.'],
37+
[c.WARN_INVALID_FLAGSET, '%s: 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.'],
38+
[c.WARN_LOWERCASE_FLAGSET, '%s: flag set %s should be all lowercase - converting string to lowercase.'],
39+
[c.WARN_FLAGSET_WITHOUT_FLAGS, '%s: you passed %s flag set that does not contain cached feature flag names. Please double check what flag sets are in use in the Split user interface.'],
3840
]);

0 commit comments

Comments
 (0)