Skip to content

Commit 79a46be

Browse files
Merge pull request #406 from splitio/rb_excluded_segments
RBS excluded segments
2 parents c17760b + 5c9c743 commit 79a46be

File tree

5 files changed

+103
-12
lines changed

5 files changed

+103
-12
lines changed

src/dtos/types.ts

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

202+
export interface IExcludedSegments {
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?: {
208213
keys?: string[],
209-
segments?: string[]
214+
segments?: IExcludedSegments[]
210215
}
211216
}
212217

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

Lines changed: 57 additions & 4 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(['emi@split.io']),
16+
'excluded_standard_segment': new Set(['emi@split.io']),
1717
'regular_segment': new Set(['nadia@split.io'])
1818
};
1919

20+
const STORED_LARGE_SEGMENTS: Record<string, Set<string>> = {
21+
'excluded_large_segment': new Set(['emi-large@split.io'])
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
keys: ['mauro@split.io', 'gaston@split.io'],
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
{
@@ -135,6 +143,31 @@ 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: ['emi-rule-based@split.io']
162+
},
163+
unaryNumericMatcherData: null,
164+
betweenMatcherData: null
165+
}
166+
]
167+
}
168+
}
169+
],
170+
}
138171
};
139172

140173
const mockStorageSync = {
@@ -149,6 +182,11 @@ const mockStorageSync = {
149182
return STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false;
150183
}
151184
},
185+
largeSegments: {
186+
isInSegment(segmentName: string, matchingKey: string) {
187+
return STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false;
188+
}
189+
},
152190
rbSegments: {
153191
get(rbsegmentName: string) {
154192
return STORED_RBSEGMENTS[rbsegmentName];
@@ -168,6 +206,11 @@ const mockStorageAsync = {
168206
return Promise.resolve(STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false);
169207
}
170208
},
209+
largeSegments: {
210+
isInSegment(segmentName: string, matchingKey: string) {
211+
return Promise.resolve(STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false);
212+
}
213+
},
171214
rbSegments: {
172215
get(rbsegmentName: string) {
173216
return Promise.resolve(STORED_RBSEGMENTS[rbsegmentName]);
@@ -190,18 +233,28 @@ describe.each([
190233
value: 'depend_on_mauro_rule_based_segment'
191234
} as IMatcherDto, mockStorage)!;
192235

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

195238
// should return false if the provided key is excluded (even if some condition is met)
196239
let match = matcher({ key: 'mauro@split.io', attributes: { location: 'mdp' } }, evaluateFeature);
197240
expect(thenable(match)).toBe(isAsync);
198241
expect(await match).toBe(false);
199242

200-
// should return false if the provided key is in some excluded segment (even if some condition is met)
243+
// should return false if the provided key is in some excluded standard segment (even if some condition is met)
201244
match = matcher({ key: 'emi@split.io', attributes: { location: 'tandil' } }, evaluateFeature);
202245
expect(thenable(match)).toBe(isAsync);
203246
expect(await match).toBe(false);
204247

248+
// should return false if the provided key is in some excluded large segment (even if some condition is met)
249+
match = matcher({ key: 'emi-large@split.io', attributes: { location: 'tandil' } }, evaluateFeature);
250+
expect(thenable(match)).toBe(isAsync);
251+
expect(await match).toBe(false);
252+
253+
// should return false if the provided key is in some excluded rule-based segment (even if some condition is met)
254+
match = matcher({ key: 'emi-rule-based@split.io', attributes: { location: 'tandil' } }, evaluateFeature);
255+
expect(thenable(match)).toBe(isAsync);
256+
expect(await match).toBe(false);
257+
205258
// should return false if doesn't match any condition
206259
match = matcher({ key: 'zeta@split.io' }, evaluateFeature);
207260
expect(thenable(match)).toBe(isAsync);

src/evaluator/matchers/rbsegment.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt
3535

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

38-
const isInSegment = (excluded.segments || []).map(segmentName => {
39-
return storage.segments.isInSegment(segmentName, matchingKey);
38+
const isInSegment = (excluded.segments || []).map(({ type, name }) => {
39+
return type === 'standard' ?
40+
storage.segments.isInSegment(name, matchingKey) :
41+
type === 'rule-based' ?
42+
ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) :
43+
type === 'large' && (storage as IStorageSync).largeSegments ?
44+
(storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) :
45+
false;
4046
});
4147

4248
return isInSegment.length && thenable(isInSegment[0]) ?

src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker';
1414
import { splitNotifications } from '../../../streaming/__tests__/dataMocks';
1515
import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory';
1616
import { RB_SEGMENT_UPDATE, SPLIT_UPDATE } from '../../../streaming/constants';
17+
import { IN_RULE_BASED_SEGMENT } from '../../../../utils/constants';
1718

1819
const ARCHIVED_FF = 'ARCHIVED';
1920

@@ -84,13 +85,31 @@ const testFFEmptySet: ISplit =
8485
conditions: [],
8586
sets: []
8687
};
88+
// @ts-ignore
89+
const rbsWithExcludedSegment: IRBSegment = {
90+
name: 'rbs',
91+
status: 'ACTIVE',
92+
conditions: [],
93+
excluded: {
94+
segments: [{
95+
type: 'standard',
96+
name: 'C'
97+
}, {
98+
type: 'rule-based',
99+
name: 'D'
100+
}]
101+
}
102+
};
87103

88104
test('splitChangesUpdater / segments parser', () => {
105+
let segments = parseSegments(activeSplitWithSegments as ISplit);
106+
expect(segments).toEqual(new Set(['A', 'B']));
89107

90-
const segments = parseSegments(activeSplitWithSegments as ISplit);
108+
segments = parseSegments(rbsWithExcludedSegment);
109+
expect(segments).toEqual(new Set(['C']));
91110

92-
expect(segments.has('A')).toBe(true);
93-
expect(segments.has('B')).toBe(true);
111+
segments = parseSegments(rbsWithExcludedSegment, IN_RULE_BASED_SEGMENT);
112+
expect(segments).toEqual(new Set(['D']));
94113
});
95114

96115
test('splitChangesUpdater / compute splits mutation', () => {

src/sync/polling/updaters/splitChangesUpdater.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,20 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise<boolean> {
2626
}
2727

2828
/**
29-
* Collect segments from a raw split definition.
29+
* Collect segments from a raw FF or RBS definition.
3030
* Exported for testing purposes.
3131
*/
3232
export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set<string> {
3333
const { conditions = [], excluded } = ruleEntity as IRBSegment;
34-
const segments = new Set<string>(excluded && excluded.segments);
34+
35+
const segments = new Set<string>();
36+
if (excluded && excluded.segments) {
37+
excluded.segments.forEach(({ type, name }) => {
38+
if ((type === 'standard' && matcherType === IN_SEGMENT) || (type === 'rule-based' && matcherType === IN_RULE_BASED_SEGMENT)) {
39+
segments.add(name);
40+
}
41+
});
42+
}
3543

3644
for (let i = 0; i < conditions.length; i++) {
3745
const matchers = conditions[i].matcherGroup.matchers;

0 commit comments

Comments
 (0)