Skip to content

Commit a176436

Browse files
Merge branch 'rb_segments_baseline' into prerequisites_refactors
2 parents 280ed6e + 9c1de66 commit a176436

File tree

22 files changed

+243
-91
lines changed

22 files changed

+243
-91
lines changed

CHANGES.txt

Lines changed: 31 additions & 25 deletions
Large diffs are not rendered by default.

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": "2.2.1-rc.0",
3+
"version": "2.3.0",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/consent/__tests__/sdkUserConsent.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ test('createUserConsentAPI', () => {
77
const syncManager = { submitterManager: syncTaskFactory() };
88
const storage = {
99
events: { clear: jest.fn() },
10-
impressions: { clear: jest.fn() }
10+
impressions: { clear: jest.fn() },
11+
impressionCounts: { clear: jest.fn() },
12+
uniqueKeys: { clear: jest.fn() }
1113
};
1214

1315
// @ts-ignore

src/consent/sdkUserConsent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const ConsentStatus = {
1515
* The public user consent API exposed via SplitFactory, used to control if the SDK tracks and sends impressions and events or not.
1616
*/
1717
export function createUserConsentAPI(params: ISdkFactoryContext) {
18-
const { settings, settings: { log }, syncManager, storage: { events, impressions, impressionCounts } } = params;
18+
const { settings, settings: { log }, syncManager, storage: { events, impressions, impressionCounts, uniqueKeys } } = params;
1919

2020
if (!isConsentGranted(settings)) log.info(USER_CONSENT_INITIAL, [settings.userConsent]);
2121

@@ -41,7 +41,8 @@ export function createUserConsentAPI(params: ISdkFactoryContext) {
4141
// @ts-ignore, clear method is present in storage for standalone and partial consumer mode
4242
if (events.clear) events.clear(); // @ts-ignore
4343
if (impressions.clear) impressions.clear(); // @ts-ignore
44-
if (impressionCounts && impressionCounts.clear) impressionCounts.clear();
44+
if (impressionCounts.clear) impressionCounts.clear(); // @ts-ignore
45+
if (uniqueKeys.clear) uniqueKeys.clear();
4546
}
4647
} else {
4748
log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]);

src/dtos/types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,19 @@ export interface ISplitCondition {
199199
conditionType?: 'ROLLOUT' | 'WHITELIST'
200200
}
201201

202+
export interface IExcludedSegment {
203+
type: 'standard' | 'large' | 'rule-based',
204+
name: string,
205+
}
206+
202207
export interface IRBSegment {
203208
name: string,
204209
changeNumber: number,
205210
status: 'ACTIVE' | 'ARCHIVED',
206211
conditions?: ISplitCondition[],
207212
excluded?: {
208-
keys?: string[],
209-
segments?: string[]
213+
keys?: string[] | null,
214+
segments?: IExcludedSegment[] | null
210215
}
211216
}
212217

src/evaluator/matchers/__tests__/rbsegment.spec.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ const STORED_SPLITS: Record<string, ISplit> = {
1313
};
1414

1515
const STORED_SEGMENTS: Record<string, Set<string>> = {
16-
'segment_test': new Set(['[email protected]']),
16+
'excluded_standard_segment': new Set(['[email protected]']),
1717
'regular_segment': new Set(['[email protected]'])
1818
};
1919

20+
const STORED_LARGE_SEGMENTS: Record<string, Set<string>> = {
21+
'excluded_large_segment': new Set(['[email protected]'])
22+
};
23+
2024
const STORED_RBSEGMENTS: Record<string, IRBSegment> = {
2125
'mauro_rule_based_segment': {
2226
changeNumber: 5,
2327
name: 'mauro_rule_based_segment',
2428
status: 'ACTIVE',
2529
excluded: {
2630
27-
segments: ['segment_test']
31+
segments: [
32+
{ type: 'standard', name: 'excluded_standard_segment' },
33+
{ type: 'large', name: 'excluded_large_segment' },
34+
{ type: 'rule-based', name: 'excluded_rule_based_segment' }
35+
]
2836
},
2937
conditions: [
3038
{
@@ -87,8 +95,8 @@ const STORED_RBSEGMENTS: Record<string, IRBSegment> = {
8795
changeNumber: 123,
8896
status: 'ACTIVE',
8997
excluded: {
90-
keys: [],
91-
segments: []
98+
keys: null,
99+
segments: null,
92100
},
93101
conditions: [{
94102
matcherGroup: {
@@ -135,6 +143,37 @@ const STORED_RBSEGMENTS: Record<string, IRBSegment> = {
135143
}
136144
}]
137145
},
146+
'excluded_rule_based_segment': {
147+
name: 'excluded_rule_based_segment',
148+
changeNumber: 123,
149+
status: 'ACTIVE',
150+
conditions: [
151+
{
152+
matcherGroup: {
153+
combiner: 'AND',
154+
matchers: [
155+
{
156+
keySelector: null,
157+
matcherType: 'WHITELIST',
158+
negate: false,
159+
userDefinedSegmentMatcherData: null,
160+
whitelistMatcherData: {
161+
whitelist: ['[email protected]']
162+
},
163+
unaryNumericMatcherData: null,
164+
betweenMatcherData: null
165+
}
166+
]
167+
}
168+
}
169+
],
170+
},
171+
'rule_based_segment_without_conditions': {
172+
name: 'rule_based_segment_without_conditions',
173+
changeNumber: 123,
174+
status: 'ACTIVE',
175+
conditions: []
176+
}
138177
};
139178

140179
const mockStorageSync = {
@@ -149,6 +188,11 @@ const mockStorageSync = {
149188
return STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false;
150189
}
151190
},
191+
largeSegments: {
192+
isInSegment(segmentName: string, matchingKey: string) {
193+
return STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false;
194+
}
195+
},
152196
rbSegments: {
153197
get(rbsegmentName: string) {
154198
return STORED_RBSEGMENTS[rbsegmentName];
@@ -168,6 +212,11 @@ const mockStorageAsync = {
168212
return Promise.resolve(STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false);
169213
}
170214
},
215+
largeSegments: {
216+
isInSegment(segmentName: string, matchingKey: string) {
217+
return Promise.resolve(STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false);
218+
}
219+
},
171220
rbSegments: {
172221
get(rbsegmentName: string) {
173222
return Promise.resolve(STORED_RBSEGMENTS[rbsegmentName]);
@@ -190,18 +239,28 @@ describe.each([
190239
value: 'depend_on_mauro_rule_based_segment'
191240
} as IMatcherDto, mockStorage)!;
192241

193-
[matcher, dependentMatcher].forEach(async matcher => {
242+
[matcher, dependentMatcher].forEach(async (matcher) => {
194243

195244
// should return false if the provided key is excluded (even if some condition is met)
196245
let match = matcher({ key: '[email protected]', attributes: { location: 'mdp' } }, evaluateFeature);
197246
expect(thenable(match)).toBe(isAsync);
198247
expect(await match).toBe(false);
199248

200-
// should return false if the provided key is in some excluded segment (even if some condition is met)
249+
// should return false if the provided key is in some excluded standard segment (even if some condition is met)
201250
match = matcher({ key: '[email protected]', attributes: { location: 'tandil' } }, evaluateFeature);
202251
expect(thenable(match)).toBe(isAsync);
203252
expect(await match).toBe(false);
204253

254+
// should return false if the provided key is in some excluded large segment (even if some condition is met)
255+
match = matcher({ key: '[email protected]', attributes: { location: 'tandil' } }, evaluateFeature);
256+
expect(thenable(match)).toBe(isAsync);
257+
expect(await match).toBe(false);
258+
259+
// should return false if the provided key is in some excluded rule-based segment (even if some condition is met)
260+
match = matcher({ key: '[email protected]', attributes: { location: 'tandil' } }, evaluateFeature);
261+
expect(thenable(match)).toBe(isAsync);
262+
expect(await match).toBe(false);
263+
205264
// should return false if doesn't match any condition
206265
match = matcher({ key: '[email protected]' }, evaluateFeature);
207266
expect(thenable(match)).toBe(isAsync);
@@ -238,6 +297,14 @@ describe.each([
238297

239298
// should support feature flag dependency matcher
240299
expect(await matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Parent split returns one of the expected treatments, so the matcher returns true
300+
301+
const matcherTrueRuleBasedSegmentWithoutConditions = matcherFactory(loggerMock, {
302+
type: matcherTypes.IN_RULE_BASED_SEGMENT,
303+
value: 'rule_based_segment_without_conditions'
304+
} as IMatcherDto, mockStorageSync)!;
305+
306+
// should support rule-based segment without conditions
307+
expect(await matcherTrueRuleBasedSegmentWithoutConditions({ key: 'a-key' }, evaluateFeature)).toBe(false);
241308
});
242309

243310
});

src/evaluator/matchers/rbsegment.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
import { IRBSegment, MaybeThenable } from '../../dtos/types';
1+
import { IExcludedSegment, IRBSegment, MaybeThenable } from '../../dtos/types';
22
import { IStorageAsync, IStorageSync } from '../../storages/types';
33
import { ILogger } from '../../logger/types';
44
import { IDependencyMatcherValue, ISplitEvaluator } from '../types';
55
import { thenable } from '../../utils/promise/thenable';
66
import { getMatching, keyParser } from '../../utils/key';
77
import { parser } from '../parser';
8+
import { STANDARD_SEGMENT, RULE_BASED_SEGMENT, LARGE_SEGMENT } from '../../utils/constants';
89

910

1011
export function ruleBasedSegmentMatcherContext(segmentName: string, storage: IStorageSync | IStorageAsync, log: ILogger) {
1112

1213
return function ruleBasedSegmentMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable<boolean> {
14+
const matchingKey = getMatching(key);
1315

1416
function matchConditions(rbsegment: IRBSegment) {
1517
const conditions = rbsegment.conditions || [];
18+
19+
if (!conditions.length) return false;
20+
1621
const evaluator = parser(log, conditions, storage);
1722

1823
const evaluation = evaluator(
@@ -29,22 +34,29 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt
2934
evaluation ? true : false;
3035
}
3136

37+
function isInExcludedSegment({ type, name }: IExcludedSegment) {
38+
return type === STANDARD_SEGMENT ?
39+
storage.segments.isInSegment(name, matchingKey) :
40+
type === RULE_BASED_SEGMENT ?
41+
ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) :
42+
type === LARGE_SEGMENT && (storage as IStorageSync).largeSegments ?
43+
(storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) :
44+
false;
45+
}
46+
3247
function isExcluded(rbSegment: IRBSegment) {
33-
const matchingKey = getMatching(key);
3448
const excluded = rbSegment.excluded || {};
3549

3650
if (excluded.keys && excluded.keys.indexOf(matchingKey) !== -1) return true;
3751

38-
const isInSegment = (excluded.segments || []).map(segmentName => {
39-
return storage.segments.isInSegment(segmentName, matchingKey);
40-
});
41-
42-
return isInSegment.length && thenable(isInSegment[0]) ?
43-
Promise.all(isInSegment).then(results => results.some(result => result)) :
44-
isInSegment.some(result => result);
52+
return (excluded.segments || []).reduce<MaybeThenable<boolean>>((result, excludedSegment) => {
53+
return thenable(result) ?
54+
result.then(result => result || isInExcludedSegment(excludedSegment)) :
55+
result || isInExcludedSegment(excludedSegment);
56+
}, false);
4557
}
4658

47-
function isInSegment(rbSegment: IRBSegment | null) {
59+
function isInRBSegment(rbSegment: IRBSegment | null) {
4860
if (!rbSegment) return false;
4961
const excluded = isExcluded(rbSegment);
5062

@@ -56,7 +68,7 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt
5668
const rbSegment = storage.rbSegments.get(segmentName);
5769

5870
return thenable(rbSegment) ?
59-
rbSegment.then(isInSegment) :
60-
isInSegment(rbSegment);
71+
rbSegment.then(isInRBSegment) :
72+
isInRBSegment(rbSegment);
6173
};
6274
}

0 commit comments

Comments
 (0)