Skip to content

Commit 2228b93

Browse files
Merge pull request #402 from splitio/rb_segments_polishing
[Rule-based segments] Polishing
2 parents ea66074 + a934d95 commit 2228b93

File tree

9 files changed

+39
-25
lines changed

9 files changed

+39
-25
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2.3.0 (April XXX, 2025)
2+
- Added support for targeting rules based on rule-based segments.
3+
14
2.2.0 (March 28, 2025)
25
- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs.
36
- Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`:

src/dtos/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,10 @@ export interface IRBSegment {
203203
name: string,
204204
changeNumber: number,
205205
status: 'ACTIVE' | 'ARCHIVED',
206-
conditions: ISplitCondition[],
207-
excluded: {
208-
keys: string[],
209-
segments: string[]
206+
conditions?: ISplitCondition[],
207+
excluded?: {
208+
keys?: string[],
209+
segments?: string[]
210210
}
211211
}
212212

src/evaluator/matchers/rbsegment.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt
1212
return function ruleBasedSegmentMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable<boolean> {
1313

1414
function matchConditions(rbsegment: IRBSegment) {
15-
const conditions = rbsegment.conditions;
15+
const conditions = rbsegment.conditions || [];
1616
const evaluator = parser(log, conditions, storage);
1717

1818
const evaluation = evaluator(
@@ -31,10 +31,11 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt
3131

3232
function isExcluded(rbSegment: IRBSegment) {
3333
const matchingKey = getMatching(key);
34+
const excluded = rbSegment.excluded || {};
3435

35-
if (rbSegment.excluded.keys.indexOf(matchingKey) !== -1) return true;
36+
if (excluded.keys && excluded.keys.indexOf(matchingKey) !== -1) return true;
3637

37-
const isInSegment = rbSegment.excluded.segments.map(segmentName => {
38+
const isInSegment = (excluded.segments || []).map(segmentName => {
3839
return storage.segments.isInSegment(segmentName, matchingKey);
3940
});
4041

src/storages/KeyBuilderCS.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder {
1515
constructor(prefix: string, matchingKey: string) {
1616
super(prefix);
1717
this.matchingKey = matchingKey;
18-
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet|rbsegment)\\.`);
18+
this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet)\\.`);
1919
}
2020

2121
/**

src/storages/inLocalStorage/__tests__/validateCache.spec.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin
55
import { SplitsCacheInLocal } from '../SplitsCacheInLocal';
66
import { nearlyEqual } from '../../../__tests__/testUtils';
77
import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal';
8+
import { RBSegmentsCacheInLocal } from '../RBSegmentsCacheInLocal';
89

910
const FULL_SETTINGS_HASH = 'dc1f9817';
1011

@@ -14,9 +15,11 @@ describe('validateCache', () => {
1415
const segments = new MySegmentsCacheInLocal(fullSettings.log, keys);
1516
const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys);
1617
const splits = new SplitsCacheInLocal(fullSettings, keys);
18+
const rbSegments = new RBSegmentsCacheInLocal(fullSettings, keys);
1719

18-
jest.spyOn(splits, 'clear');
1920
jest.spyOn(splits, 'getChangeNumber');
21+
jest.spyOn(splits, 'clear');
22+
jest.spyOn(rbSegments, 'clear');
2023
jest.spyOn(segments, 'clear');
2124
jest.spyOn(largeSegments, 'clear');
2225

@@ -26,11 +29,12 @@ describe('validateCache', () => {
2629
});
2730

2831
test('if there is no cache, it should return false', () => {
29-
expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(false);
32+
expect(validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
3033

3134
expect(logSpy).not.toHaveBeenCalled();
3235

3336
expect(splits.clear).not.toHaveBeenCalled();
37+
expect(rbSegments.clear).not.toHaveBeenCalled();
3438
expect(segments.clear).not.toHaveBeenCalled();
3539
expect(largeSegments.clear).not.toHaveBeenCalled();
3640
expect(splits.getChangeNumber).toHaveBeenCalledTimes(1);
@@ -43,11 +47,12 @@ describe('validateCache', () => {
4347
localStorage.setItem(keys.buildSplitsTillKey(), '1');
4448
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
4549

46-
expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(true);
50+
expect(validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
4751

4852
expect(logSpy).not.toHaveBeenCalled();
4953

5054
expect(splits.clear).not.toHaveBeenCalled();
55+
expect(rbSegments.clear).not.toHaveBeenCalled();
5156
expect(segments.clear).not.toHaveBeenCalled();
5257
expect(largeSegments.clear).not.toHaveBeenCalled();
5358
expect(splits.getChangeNumber).toHaveBeenCalledTimes(1);
@@ -61,11 +66,12 @@ describe('validateCache', () => {
6166
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
6267
localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago
6368

64-
expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, segments, largeSegments)).toBe(false);
69+
expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
6570

6671
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache');
6772

6873
expect(splits.clear).toHaveBeenCalledTimes(1);
74+
expect(rbSegments.clear).toHaveBeenCalledTimes(1);
6975
expect(segments.clear).toHaveBeenCalledTimes(1);
7076
expect(largeSegments.clear).toHaveBeenCalledTimes(1);
7177

@@ -77,11 +83,12 @@ describe('validateCache', () => {
7783
localStorage.setItem(keys.buildSplitsTillKey(), '1');
7884
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
7985

80-
expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, segments, largeSegments)).toBe(false);
86+
expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
8187

8288
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache');
8389

8490
expect(splits.clear).toHaveBeenCalledTimes(1);
91+
expect(rbSegments.clear).toHaveBeenCalledTimes(1);
8592
expect(segments.clear).toHaveBeenCalledTimes(1);
8693
expect(largeSegments.clear).toHaveBeenCalledTimes(1);
8794

@@ -94,11 +101,12 @@ describe('validateCache', () => {
94101
localStorage.setItem(keys.buildSplitsTillKey(), '1');
95102
localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH);
96103

97-
expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false);
104+
expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
98105

99106
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
100107

101108
expect(splits.clear).toHaveBeenCalledTimes(1);
109+
expect(rbSegments.clear).toHaveBeenCalledTimes(1);
102110
expect(segments.clear).toHaveBeenCalledTimes(1);
103111
expect(largeSegments.clear).toHaveBeenCalledTimes(1);
104112

@@ -109,15 +117,16 @@ describe('validateCache', () => {
109117
// If cache is cleared, it should not clear again until a day has passed
110118
logSpy.mockClear();
111119
localStorage.setItem(keys.buildSplitsTillKey(), '1');
112-
expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(true);
120+
expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true);
113121
expect(logSpy).not.toHaveBeenCalled();
114122
expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed
115123

116124
// If a day has passed, it should clear again
117125
localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + '');
118-
expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false);
126+
expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false);
119127
expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
120128
expect(splits.clear).toHaveBeenCalledTimes(2);
129+
expect(rbSegments.clear).toHaveBeenCalledTimes(2);
121130
expect(segments.clear).toHaveBeenCalledTimes(2);
122131
expect(largeSegments.clear).toHaveBeenCalledTimes(2);
123132
expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true);

src/storages/inLocalStorage/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
5353
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
5454

5555
validateCache() {
56-
return validateCache(options, settings, keys, splits, segments, largeSegments);
56+
return validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments);
5757
},
5858

5959
destroy() { },

src/storages/inLocalStorage/validateCache.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang';
33
import { getStorageHash } from '../KeyBuilder';
44
import { LOG_PREFIX } from './constants';
55
import type { SplitsCacheInLocal } from './SplitsCacheInLocal';
6+
import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
67
import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
78
import { KeyBuilderCS } from '../KeyBuilderCS';
89
import SplitIO from '../../../types/splitio';
@@ -66,13 +67,14 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
6667
*
6768
* @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
6869
*/
69-
export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean {
70+
export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean {
7071

7172
const currentTimestamp = Date.now();
7273
const isThereCache = splits.getChangeNumber() > -1;
7374

7475
if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) {
7576
splits.clear();
77+
rbSegments.clear();
7678
segments.clear();
7779
largeSegments.clear();
7880

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,6 @@ test('splitChangesUpdater / compute splits mutation with filters', () => {
156156
});
157157

158158
describe('splitChangesUpdater', () => {
159-
160-
fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore
161-
const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory());
162-
const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges');
163-
const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges);
164-
165159
const splits = new SplitsCacheInMemory();
166160
const updateSplits = jest.spyOn(splits, 'update');
167161

@@ -173,6 +167,11 @@ describe('splitChangesUpdater', () => {
173167

174168
const storage = { splits, rbSegments, segments };
175169

170+
fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore
171+
const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory());
172+
const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges');
173+
const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges);
174+
176175
const readinessManager = readinessManagerFactory(EventEmitter, fullSettings);
177176
const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit');
178177

src/sync/polling/updaters/splitChangesUpdater.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise<boolean> {
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> {
33-
const { conditions, excluded } = ruleEntity as IRBSegment;
33+
const { conditions = [], excluded } = ruleEntity as IRBSegment;
3434
const segments = new Set<string>(excluded && excluded.segments);
3535

3636
for (let i = 0; i < conditions.length; i++) {

0 commit comments

Comments
 (0)