From ee47067629cb09ad740c3e974e1690b56b731fee Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 30 Apr 2025 20:55:58 -0300 Subject: [PATCH 1/4] RBS excluded segments --- src/dtos/types.ts | 7 ++++++- src/evaluator/matchers/__tests__/rbsegment.spec.ts | 4 +++- src/evaluator/matchers/rbsegment.ts | 4 ++-- src/sync/polling/updaters/splitChangesUpdater.ts | 5 ++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 66598a6e..ed70a493 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -199,6 +199,11 @@ export interface ISplitCondition { conditionType?: 'ROLLOUT' | 'WHITELIST' } +export interface IExcludedSegments { + type: string, + name: string, +} + export interface IRBSegment { name: string, changeNumber: number, @@ -206,7 +211,7 @@ export interface IRBSegment { conditions?: ISplitCondition[], excluded?: { keys?: string[], - segments?: string[] + segments?: IExcludedSegments[] } } diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts index c662776d..1cb17420 100644 --- a/src/evaluator/matchers/__tests__/rbsegment.spec.ts +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -24,7 +24,9 @@ const STORED_RBSEGMENTS: Record = { status: 'ACTIVE', excluded: { keys: ['mauro@split.io', 'gaston@split.io'], - segments: ['segment_test'] + segments: [ + { type: 'regular', name: 'segment_test' } + ] }, conditions: [ { diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 240eb07b..e0c624e5 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -35,8 +35,8 @@ 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(segment => { + return storage.segments.isInSegment(segment.name, matchingKey); }); return isInSegment.length && thenable(isInSegment[0]) ? diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 91b4070f..6a7ab30f 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -31,7 +31,10 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { */ export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set { const { conditions = [], excluded } = ruleEntity as IRBSegment; - const segments = new Set(excluded && excluded.segments); + + const segments = new Set( + excluded?.segments?.map(segment => segment.name) || [] + ); for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; From b68991aaf8e5d8775a2b76d978942f8c9993ea1c Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Fri, 2 May 2025 15:44:43 -0300 Subject: [PATCH 2/4] use ternary operator and truthiness casting --- src/sync/polling/updaters/splitChangesUpdater.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 6a7ab30f..20b48762 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -33,7 +33,7 @@ export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: type const { conditions = [], excluded } = ruleEntity as IRBSegment; const segments = new Set( - excluded?.segments?.map(segment => segment.name) || [] + excluded && excluded.segments ? excluded.segments.map(segment => segment.name) : [] ); for (let i = 0; i < conditions.length; i++) { From 688a8ffd53e5d267256dee839c20543508c4fc0f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 May 2025 17:27:09 -0300 Subject: [PATCH 3/4] Add support for large and rule-based segments in exclusion rules --- src/dtos/types.ts | 2 +- .../matchers/__tests__/rbsegment.spec.ts | 59 +++++++++++++++++-- src/evaluator/matchers/rbsegment.ts | 10 +++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index ed70a493..b0109f96 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -200,7 +200,7 @@ export interface ISplitCondition { } export interface IExcludedSegments { - type: string, + type: 'standard' | 'large' | 'rule-based', name: string, } diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts index 1cb17420..32b8f843 100644 --- a/src/evaluator/matchers/__tests__/rbsegment.spec.ts +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -13,10 +13,14 @@ const STORED_SPLITS: Record = { }; const STORED_SEGMENTS: Record> = { - '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> = { + 'excluded_large_segment': new Set(['emi-large@split.io']) +}; + const STORED_RBSEGMENTS: Record = { 'mauro_rule_based_segment': { changeNumber: 5, @@ -25,7 +29,9 @@ const STORED_RBSEGMENTS: Record = { excluded: { keys: ['mauro@split.io', 'gaston@split.io'], segments: [ - { type: 'regular', name: 'segment_test' } + { type: 'standard', name: 'excluded_standard_segment' }, + { type: 'large', name: 'excluded_large_segment' }, + { type: 'rule-based', name: 'excluded_rule_based_segment' } ] }, conditions: [ @@ -137,6 +143,31 @@ const STORED_RBSEGMENTS: Record = { } }] }, + '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 = { @@ -151,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]; @@ -170,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]); @@ -192,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); diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index e0c624e5..80893837 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -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(segment => { - return storage.segments.isInSegment(segment.name, 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]) ? From 5c9c743f144ea70a9f4b3f18d39d0ccdf0992b52 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 May 2025 17:46:45 -0300 Subject: [PATCH 4/4] Filter segments by type when parsing split and RBS definitions --- .../__tests__/splitChangesUpdater.spec.ts | 25 ++++++++++++++++--- .../polling/updaters/splitChangesUpdater.ts | 13 +++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 750f1c0d..b93a7176 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -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'; @@ -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', () => { diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 20b48762..fd4cedb9 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -26,15 +26,20 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { } /** - * 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 { const { conditions = [], excluded } = ruleEntity as IRBSegment; - const segments = new Set( - excluded && excluded.segments ? excluded.segments.map(segment => segment.name) : [] - ); + const segments = new Set(); + 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;