Skip to content

RBS excluded segments #406

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

Merged
merged 5 commits into from
May 8, 2025
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
7 changes: 6 additions & 1 deletion src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,19 @@ export interface ISplitCondition {
conditionType?: 'ROLLOUT' | 'WHITELIST'
}

export interface IExcludedSegments {
type: 'standard' | 'large' | 'rule-based',
name: string,
}

export interface IRBSegment {
name: string,
changeNumber: number,
status: 'ACTIVE' | 'ARCHIVED',
conditions?: ISplitCondition[],
excluded?: {
keys?: string[],
segments?: string[]
segments?: IExcludedSegments[]
}
}

Expand Down
61 changes: 57 additions & 4 deletions src/evaluator/matchers/__tests__/rbsegment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@ const STORED_SPLITS: Record<string, ISplit> = {
};

const STORED_SEGMENTS: Record<string, Set<string>> = {
'segment_test': new Set(['emi@split.io']),
'excluded_standard_segment': new Set(['emi@split.io']),
'regular_segment': new Set(['nadia@split.io'])
};

const STORED_LARGE_SEGMENTS: Record<string, Set<string>> = {
'excluded_large_segment': new Set(['emi-large@split.io'])
};

const STORED_RBSEGMENTS: Record<string, IRBSegment> = {
'mauro_rule_based_segment': {
changeNumber: 5,
name: 'mauro_rule_based_segment',
status: 'ACTIVE',
excluded: {
keys: ['mauro@split.io', 'gaston@split.io'],
segments: ['segment_test']
segments: [
{ type: 'standard', name: 'excluded_standard_segment' },
{ type: 'large', name: 'excluded_large_segment' },
{ type: 'rule-based', name: 'excluded_rule_based_segment' }
]
},
conditions: [
{
Expand Down Expand Up @@ -135,6 +143,31 @@ const STORED_RBSEGMENTS: Record<string, IRBSegment> = {
}
}]
},
'excluded_rule_based_segment': {
name: 'excluded_rule_based_segment',
changeNumber: 123,
status: 'ACTIVE',
conditions: [
{
matcherGroup: {
combiner: 'AND',
matchers: [
{
keySelector: null,
matcherType: 'WHITELIST',
negate: false,
userDefinedSegmentMatcherData: null,
whitelistMatcherData: {
whitelist: ['emi-rule-based@split.io']
},
unaryNumericMatcherData: null,
betweenMatcherData: null
}
]
}
}
],
}
};

const mockStorageSync = {
Expand All @@ -149,6 +182,11 @@ const mockStorageSync = {
return STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false;
}
},
largeSegments: {
isInSegment(segmentName: string, matchingKey: string) {
return STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false;
}
},
rbSegments: {
get(rbsegmentName: string) {
return STORED_RBSEGMENTS[rbsegmentName];
Expand All @@ -168,6 +206,11 @@ const mockStorageAsync = {
return Promise.resolve(STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false);
}
},
largeSegments: {
isInSegment(segmentName: string, matchingKey: string) {
return Promise.resolve(STORED_LARGE_SEGMENTS[segmentName] ? STORED_LARGE_SEGMENTS[segmentName].has(matchingKey) : false);
}
},
rbSegments: {
get(rbsegmentName: string) {
return Promise.resolve(STORED_RBSEGMENTS[rbsegmentName]);
Expand All @@ -190,18 +233,28 @@ describe.each([
value: 'depend_on_mauro_rule_based_segment'
} as IMatcherDto, mockStorage)!;

[matcher, dependentMatcher].forEach(async matcher => {
[matcher, dependentMatcher].forEach(async (matcher) => {

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

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

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

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

// should return false if doesn't match any condition
match = matcher({ key: 'zeta@split.io' }, evaluateFeature);
expect(thenable(match)).toBe(isAsync);
Expand Down
10 changes: 8 additions & 2 deletions src/evaluator/matchers/rbsegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt

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

const isInSegment = (excluded.segments || []).map(segmentName => {
return storage.segments.isInSegment(segmentName, matchingKey);
const isInSegment = (excluded.segments || []).map(({ type, name }) => {
return type === 'standard' ?
storage.segments.isInSegment(name, matchingKey) :
type === 'rule-based' ?
ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) :
type === 'large' && (storage as IStorageSync).largeSegments ?
(storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) :
false;
});

return isInSegment.length && thenable(isInSegment[0]) ?
Expand Down
25 changes: 22 additions & 3 deletions src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker';
import { splitNotifications } from '../../../streaming/__tests__/dataMocks';
import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory';
import { RB_SEGMENT_UPDATE, SPLIT_UPDATE } from '../../../streaming/constants';
import { IN_RULE_BASED_SEGMENT } from '../../../../utils/constants';

const ARCHIVED_FF = 'ARCHIVED';

Expand Down Expand Up @@ -84,13 +85,31 @@ const testFFEmptySet: ISplit =
conditions: [],
sets: []
};
// @ts-ignore
const rbsWithExcludedSegment: IRBSegment = {
name: 'rbs',
status: 'ACTIVE',
conditions: [],
excluded: {
segments: [{
type: 'standard',
name: 'C'
}, {
type: 'rule-based',
name: 'D'
}]
}
};

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

const segments = parseSegments(activeSplitWithSegments as ISplit);
segments = parseSegments(rbsWithExcludedSegment);
expect(segments).toEqual(new Set(['C']));

expect(segments.has('A')).toBe(true);
expect(segments.has('B')).toBe(true);
segments = parseSegments(rbsWithExcludedSegment, IN_RULE_BASED_SEGMENT);
expect(segments).toEqual(new Set(['D']));
});

test('splitChangesUpdater / compute splits mutation', () => {
Expand Down
12 changes: 10 additions & 2 deletions src/sync/polling/updaters/splitChangesUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise<boolean> {
}

/**
* Collect segments from a raw split definition.
* Collect segments from a raw FF or RBS definition.
* Exported for testing purposes.
*/
export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set<string> {
const { conditions = [], excluded } = ruleEntity as IRBSegment;
const segments = new Set<string>(excluded && excluded.segments);

const segments = new Set<string>();
if (excluded && excluded.segments) {
excluded.segments.forEach(({ type, name }) => {
if ((type === 'standard' && matcherType === IN_SEGMENT) || (type === 'rule-based' && matcherType === IN_RULE_BASED_SEGMENT)) {
segments.add(name);
}
});
}

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