From 54e9e8609136349bb98f9bb4a54ba8aadcae4c00 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 Feb 2025 14:44:36 -0300 Subject: [PATCH 01/62] Add RBSegmentsCache interface and implementations --- src/dtos/types.ts | 11 ++ src/storages/AbstractSplitsCacheSync.ts | 4 +- src/storages/KeyBuilder.ts | 12 ++ src/storages/KeyBuilderCS.ts | 6 +- src/storages/KeyBuilderSS.ts | 4 + .../inLocalStorage/RBSegmentsCacheInLocal.ts | 144 ++++++++++++++++++ src/storages/inLocalStorage/index.ts | 4 + src/storages/inMemory/InMemoryStorage.ts | 3 + src/storages/inMemory/InMemoryStorageCS.ts | 4 + .../inMemory/RBSegmentsCacheInMemory.ts | 68 +++++++++ .../inRedis/RBSegmentsCacheInRedis.ts | 79 ++++++++++ src/storages/inRedis/index.ts | 2 + .../pluggable/RBSegmentsCachePluggable.ts | 76 +++++++++ src/storages/pluggable/index.ts | 2 + src/storages/types.ts | 34 ++++- 15 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts create mode 100644 src/storages/inMemory/RBSegmentsCacheInMemory.ts create mode 100644 src/storages/inRedis/RBSegmentsCacheInRedis.ts create mode 100644 src/storages/pluggable/RBSegmentsCachePluggable.ts diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 7573183c..1bb49a21 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -194,6 +194,17 @@ export interface ISplitCondition { conditionType: 'ROLLOUT' | 'WHITELIST' } +export interface IRBSegment { + name: string, + changeNumber: number, + status: 'ACTIVE' | 'ARCHIVED', + excluded: { + keys: string[], + segments: string[] + }, + conditions: ISplitCondition[], +} + export interface ISplit { name: string, changeNumber: number, diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 512d990e..b03dbc7d 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -1,5 +1,5 @@ import { ISplitsCacheSync } from './types'; -import { ISplit } from '../dtos/types'; +import { IRBSegment, ISplit } from '../dtos/types'; import { objectAssign } from '../utils/lang/objectAssign'; import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants'; @@ -80,7 +80,7 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { * Given a parsed split, it returns a boolean flagging if its conditions use segments matchers (rules & whitelists). * This util is intended to simplify the implementation of `splitsCache::usesSegments` method */ -export function usesSegments(split: ISplit) { +export function usesSegments(split: ISplit | IRBSegment) { const conditions = split.conditions || []; for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 2f5dc800..dfd42f18 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -37,6 +37,18 @@ export class KeyBuilder { return `${this.prefix}.split.`; } + buildRBSegmentKey(splitName: string) { + return `${this.prefix}.rbsegment.${splitName}`; + } + + buildRBSegmentTillKey() { + return `${this.prefix}.rbsegments.till`; + } + + buildRBSegmentKeyPrefix() { + return `${this.prefix}.rbsegment.`; + } + buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index 23961f89..3ca446ba 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -15,7 +15,7 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { constructor(prefix: string, matchingKey: string) { super(prefix); this.matchingKey = matchingKey; - this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet)\\.`); + this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet|rbsegment)\\.`); } /** @@ -47,6 +47,10 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { return startsWith(key, `${this.prefix}.split.`); } + isRBSegmentKey(key: string) { + return startsWith(key, `${this.prefix}.rbsegment.`); + } + buildSplitsWithSegmentCountKey() { return `${this.prefix}.splits.usingSegments`; } diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 6232d88a..cf8d2156 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -53,6 +53,10 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.buildSplitKeyPrefix()}*`; } + searchPatternForRBSegmentKeys() { + return `${this.buildRBSegmentKeyPrefix()}*`; + } + /* Telemetry keys */ buildLatencyKey(method: Method, bucket: number) { diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts new file mode 100644 index 00000000..7c332543 --- /dev/null +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -0,0 +1,144 @@ +import { IRBSegment } from '../../dtos/types'; +import { ILogger } from '../../logger/types'; +import { ISettings } from '../../types'; +import { isFiniteNumber, isNaNNumber, toNumber } from '../../utils/lang'; +import { setToArray } from '../../utils/lang/sets'; +import { usesSegments } from '../AbstractSplitsCacheSync'; +import { KeyBuilderCS } from '../KeyBuilderCS'; +import { IRBSegmentsCacheSync } from '../types'; +import { LOG_PREFIX } from './constants'; + +export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { + + private readonly keys: KeyBuilderCS; + private readonly log: ILogger; + private hasSync?: boolean; + + constructor(settings: ISettings, keys: KeyBuilderCS) { + this.keys = keys; + this.log = settings.log; + } + + clear() { + this.hasSync = false; + // SplitsCacheInLocal.clear() does the rest of the job + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + this.setChangeNumber(changeNumber); + const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); + return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + } + + private setChangeNumber(changeNumber: number) { + try { + localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + ''); + localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); + this.hasSync = true; + } catch (e) { + this.log.error(LOG_PREFIX + e); + } + } + + private updateSegmentCount(diff: number){ + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + const count = toNumber(localStorage.getItem(segmentsCountKey)) - diff; + // @ts-expect-error + if (count > 0) localStorage.setItem(segmentsCountKey, count); + else localStorage.removeItem(segmentsCountKey); + } + + private add(rbSegment: IRBSegment): boolean { + try { + const name = rbSegment.name; + const rbSegmentKey = this.keys.buildRBSegmentKey(name); + const rbSegmentFromLocalStorage = localStorage.getItem(rbSegmentKey); + const previous = rbSegmentFromLocalStorage ? JSON.parse(rbSegmentFromLocalStorage) : null; + + localStorage.setItem(rbSegmentKey, JSON.stringify(rbSegment)); + + let usesSegmentsDiff = 0; + if (previous && usesSegments(previous)) usesSegmentsDiff--; + if (usesSegments(rbSegment)) usesSegmentsDiff++; + if (usesSegmentsDiff !== 0) this.updateSegmentCount(usesSegmentsDiff); + + return true; + } catch (e) { + this.log.error(LOG_PREFIX + e); + return false; + } + } + + private remove(name: string): boolean { + try { + const rbSegment = this.get(name); + if (!rbSegment) return false; + + localStorage.removeItem(this.keys.buildRBSegmentKey(name)); + + if (usesSegments(rbSegment)) this.updateSegmentCount(-1); + + return true; + + } catch (e) { + this.log.error(LOG_PREFIX + e); + return false; + } + } + + private getNames(): string[] { + const len = localStorage.length; + const accum = []; + + let cur = 0; + + while (cur < len) { + const key = localStorage.key(cur); + + if (key != null && this.keys.isRBSegmentKey(key)) accum.push(this.keys.extractKey(key)); + + cur++; + } + + return accum; + } + + get(name: string): IRBSegment | null { + const item = localStorage.getItem(this.keys.buildRBSegmentKey(name)); + return item && JSON.parse(item); + } + + contains(names: Set): boolean { + const namesArray = setToArray(names); + const namesInStorage = this.getNames(); + return namesArray.every(name => namesInStorage.indexOf(name) !== -1); + } + + getChangeNumber(): number { + const n = -1; + let value: string | number | null = localStorage.getItem(this.keys.buildRBSegmentTillKey()); + + if (value !== null) { + value = parseInt(value, 10); + + return isNaNNumber(value) ? n : value; + } + + return n; + } + + usesSegments(): boolean { + // If cache hasn't been synchronized, assume we need segments + if (!this.hasSync) return true; + + const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); + const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); + + if (isFiniteNumber(splitsWithSegmentsCount)) { + return splitsWithSegmentsCount > 0; + } else { + return true; + } + } + +} diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index c621141d..83136487 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -14,6 +14,7 @@ import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { getMatching } from '../../utils/key'; +import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal'; export interface InLocalStorageOptions { prefix?: string @@ -40,11 +41,13 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn const expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS; const splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp); + const rbSegments = new RBSegmentsCacheInLocal(settings, keys); const segments = new MySegmentsCacheInLocal(log, keys); const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)); return { splits, + rbSegments, segments, largeSegments, impressions: new ImpressionsCacheInMemory(impressionsQueueSize), @@ -60,6 +63,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyn return { splits: this.splits, + rbSegments: this.rbSegments, segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)), largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)), impressions: this.impressions, diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 7ec099d1..e89a875d 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -7,6 +7,7 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory'; import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory'; +import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory'; /** * InMemory storage factory for standalone server-side SplitFactory @@ -17,10 +18,12 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize, }, sync: { __splitFiltersValidation } } } = params; const splits = new SplitsCacheInMemory(__splitFiltersValidation); + const rbSegments = new RBSegmentsCacheInMemory(); const segments = new SegmentsCacheInMemory(); const storage = { splits, + rbSegments, segments, impressions: new ImpressionsCacheInMemory(impressionsQueueSize), impressionCounts: new ImpressionCountsCacheInMemory(), diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index bfaec159..5ae8351c 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -7,6 +7,7 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory'; import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants'; import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS'; +import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory'; /** * InMemory storage factory for standalone client-side SplitFactory @@ -17,11 +18,13 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize }, sync: { __splitFiltersValidation } } } = params; const splits = new SplitsCacheInMemory(__splitFiltersValidation); + const rbSegments = new RBSegmentsCacheInMemory(); const segments = new MySegmentsCacheInMemory(); const largeSegments = new MySegmentsCacheInMemory(); const storage = { splits, + rbSegments, segments, largeSegments, impressions: new ImpressionsCacheInMemory(impressionsQueueSize), @@ -36,6 +39,7 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag shared() { return { splits: this.splits, + rbSegments: this.rbSegments, segments: new MySegmentsCacheInMemory(), largeSegments: new MySegmentsCacheInMemory(), impressions: this.impressions, diff --git a/src/storages/inMemory/RBSegmentsCacheInMemory.ts b/src/storages/inMemory/RBSegmentsCacheInMemory.ts new file mode 100644 index 00000000..78debb86 --- /dev/null +++ b/src/storages/inMemory/RBSegmentsCacheInMemory.ts @@ -0,0 +1,68 @@ +import { IRBSegment } from '../../dtos/types'; +import { setToArray } from '../../utils/lang/sets'; +import { usesSegments } from '../AbstractSplitsCacheSync'; +import { IRBSegmentsCacheSync } from '../types'; + +export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync { + + private cache: Record = {}; + private changeNumber: number = -1; + private segmentsCount: number = 0; + + clear() { + this.cache = {}; + this.changeNumber = -1; + this.segmentsCount = 0; + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { + this.changeNumber = changeNumber; + const updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result); + return toRemove.map(toRemove => this.remove(toRemove.name)).some(result => result) || updated; + } + + private add(rbSegment: IRBSegment): boolean { + const name = rbSegment.name; + const previous = this.get(name); + if (previous && usesSegments(previous)) this.segmentsCount--; + + this.cache[name] = rbSegment; + if (usesSegments(rbSegment)) this.segmentsCount++; + + return true; + } + + private remove(name: string): boolean { + const rbSegment = this.get(name); + if (!rbSegment) return false; + + delete this.cache[name]; + + if (usesSegments(rbSegment)) this.segmentsCount--; + + return true; + } + + private getNames(): string[] { + return Object.keys(this.cache); + } + + get(name: string): IRBSegment | null { + return this.cache[name] || null; + } + + contains(names: Set): boolean { + const namesArray = setToArray(names); + const namesInStorage = this.getNames(); + return namesArray.every(name => namesInStorage.indexOf(name) !== -1); + } + + getChangeNumber(): number { + return this.changeNumber; + } + + usesSegments(): boolean { + return this.getChangeNumber() === -1 || this.segmentsCount > 0; + } + +} diff --git a/src/storages/inRedis/RBSegmentsCacheInRedis.ts b/src/storages/inRedis/RBSegmentsCacheInRedis.ts new file mode 100644 index 00000000..8e369d62 --- /dev/null +++ b/src/storages/inRedis/RBSegmentsCacheInRedis.ts @@ -0,0 +1,79 @@ +import { isNaNNumber } from '../../utils/lang'; +import { IRBSegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { IRBSegment } from '../../dtos/types'; +import { LOG_PREFIX } from './constants'; +import { setToArray } from '../../utils/lang/sets'; +import { RedisAdapter } from './RedisAdapter'; +import { KeyBuilderSS } from '../KeyBuilderSS'; + +export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilderSS; + private readonly redis: RedisAdapter; + + constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter) { + this.log = log; + this.keys = keys; + this.redis = redis; + } + + get(name: string): Promise { + return this.redis.get(this.keys.buildRBSegmentKey(name)) + .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment)); + } + + private getNames(): Promise { + return this.redis.keys(this.keys.searchPatternForRBSegmentKeys()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + contains(names: Set): Promise { + const namesArray = setToArray(names); + return this.getNames().then(namesInStorage => { + return namesArray.every(name => namesInStorage.includes(name)); + }); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(toAdd => { + const key = this.keys.buildRBSegmentKey(toAdd.name); + const stringifiedNewRBSegment = JSON.stringify(toAdd); + return this.redis.set(key, stringifiedNewRBSegment).then(() => true); + })), + Promise.all(toRemove.map(toRemove => { + const key = this.keys.buildRBSegmentKey(toRemove.name); + return this.redis.del(key).then(status => status === 1); + })) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + + setChangeNumber(changeNumber: number) { + return this.redis.set(this.keys.buildRBSegmentTillKey(), changeNumber + '').then( + status => status === 'OK' + ); + } + + getChangeNumber(): Promise { + return this.redis.get(this.keys.buildRBSegmentTillKey()).then((value: string | null) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO implement if required by DataLoader or producer mode + clear() { + return Promise.resolve(true); + } + +} diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index e548142d..2d66a1aa 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -11,6 +11,7 @@ import { TelemetryCacheInRedis } from './TelemetryCacheInRedis'; import { UniqueKeysCacheInRedis } from './UniqueKeysCacheInRedis'; import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis'; import { metadataBuilder } from '../utils'; +import { RBSegmentsCacheInRedis } from './RBSegmentsCacheInRedis'; export interface InRedisStorageOptions { prefix?: string @@ -50,6 +51,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsy return { splits: new SplitsCacheInRedis(log, keys, redisClient, settings.sync.__splitFiltersValidation), + rbSegments: new RBSegmentsCacheInRedis(log, keys, redisClient), segments: new SegmentsCacheInRedis(log, keys, redisClient), impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata), impressionCounts: impressionCountsCache, diff --git a/src/storages/pluggable/RBSegmentsCachePluggable.ts b/src/storages/pluggable/RBSegmentsCachePluggable.ts new file mode 100644 index 00000000..294e34d8 --- /dev/null +++ b/src/storages/pluggable/RBSegmentsCachePluggable.ts @@ -0,0 +1,76 @@ +import { isNaNNumber } from '../../utils/lang'; +import { KeyBuilder } from '../KeyBuilder'; +import { IPluggableStorageWrapper, IRBSegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { IRBSegment } from '../../dtos/types'; +import { LOG_PREFIX } from './constants'; +import { setToArray } from '../../utils/lang/sets'; + +export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilder; + private readonly wrapper: IPluggableStorageWrapper; + + constructor(log: ILogger, keys: KeyBuilder, wrapper: IPluggableStorageWrapper) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + } + + get(name: string): Promise { + return this.wrapper.get(this.keys.buildRBSegmentKey(name)) + .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment)); + } + + private getNames(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildRBSegmentKeyPrefix()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + contains(names: Set): Promise { + const namesArray = setToArray(names); + return this.getNames().then(namesInStorage => { + return namesArray.every(name => namesInStorage.includes(name)); + }); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(toAdd => { + const key = this.keys.buildRBSegmentKey(toAdd.name); + const stringifiedNewRBSegment = JSON.stringify(toAdd); + return this.wrapper.set(key, stringifiedNewRBSegment).then(() => true); + })), + Promise.all(toRemove.map(toRemove => { + const key = this.keys.buildRBSegmentKey(toRemove.name); + return this.wrapper.del(key); + })) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + + setChangeNumber(changeNumber: number) { + return this.wrapper.set(this.keys.buildRBSegmentTillKey(), changeNumber + ''); + } + + getChangeNumber(): Promise { + return this.wrapper.get(this.keys.buildRBSegmentTillKey()).then((value) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO implement if required by DataLoader or producer mode + clear() { + return Promise.resolve(true); + } + +} diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index ee8b1872..f29e4ec4 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -20,6 +20,7 @@ import { UniqueKeysCacheInMemory } from '../inMemory/UniqueKeysCacheInMemory'; import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS'; import { metadataBuilder } from '../utils'; import { LOG_PREFIX } from '../pluggable/constants'; +import { RBSegmentsCachePluggable } from './RBSegmentsCachePluggable'; const NO_VALID_WRAPPER = 'Expecting pluggable storage `wrapper` in options, but no valid wrapper instance was provided.'; const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.'; @@ -116,6 +117,7 @@ export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyn return { splits: new SplitsCachePluggable(log, keys, wrapper, settings.sync.__splitFiltersValidation), + rbSegments: new RBSegmentsCachePluggable(log, keys, wrapper), segments: new SegmentsCachePluggable(log, keys, wrapper), impressions: isPartialConsumer ? new ImpressionsCacheInMemory(impressionsQueueSize) : new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), impressionCounts: impressionCountsCache, diff --git a/src/storages/types.ts b/src/storages/types.ts index 71962254..bfc63f57 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,5 +1,5 @@ import SplitIO from '../../types/splitio'; -import { MaybeThenable, ISplit, IMySegmentsResponse } from '../dtos/types'; +import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse } from '../dtos/types'; import { MySegmentsData } from '../sync/polling/types'; import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types'; import { ISettings } from '../types'; @@ -225,6 +225,34 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { getNamesByFlagSets(flagSets: string[]): Promise[]> } +/** Rule-Based Segments cache */ + +export interface IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): MaybeThenable, + get(name: string): MaybeThenable, + getChangeNumber(): MaybeThenable, + clear(): MaybeThenable, + contains(names: Set): MaybeThenable, +} + +export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean, + get(name: string): IRBSegment | null, + getChangeNumber(): number, + clear(): void, + contains(names: Set): boolean, + // Used only for smart pausing in client-side standalone. Returns true if the storage contains a RBSegment using segments or large segments matchers + usesSegments(): boolean, +} + +export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise, + get(name: string): Promise, + getChangeNumber(): Promise, + clear(): Promise, + contains(names: Set): Promise, +} + /** Segments cache */ export interface ISegmentsCacheBase { @@ -423,6 +451,7 @@ export interface ITelemetryCacheAsync extends ITelemetryEvaluationProducerAsync, export interface IStorageBase< TSplitsCache extends ISplitsCacheBase = ISplitsCacheBase, + TRBSegmentsCache extends IRBSegmentsCacheBase = IRBSegmentsCacheBase, TSegmentsCache extends ISegmentsCacheBase = ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase = IImpressionsCacheBase, TImpressionsCountCache extends IImpressionCountsCacheBase = IImpressionCountsCacheBase, @@ -431,6 +460,7 @@ export interface IStorageBase< TUniqueKeysCache extends IUniqueKeysCacheBase = IUniqueKeysCacheBase > { splits: TSplitsCache, + rbSegments: TRBSegmentsCache, segments: TSegmentsCache, impressions: TImpressionsCache, impressionCounts: TImpressionsCountCache, @@ -443,6 +473,7 @@ export interface IStorageBase< export interface IStorageSync extends IStorageBase< ISplitsCacheSync, + IRBSegmentsCacheSync, ISegmentsCacheSync, IImpressionsCacheSync, IImpressionCountsCacheSync, @@ -456,6 +487,7 @@ export interface IStorageSync extends IStorageBase< export interface IStorageAsync extends IStorageBase< ISplitsCacheAsync, + IRBSegmentsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync | IImpressionsCacheSync, IImpressionCountsCacheBase, From 22cee2225a82b063545fe9d56cf1d0c19a9e50ef Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 Feb 2025 14:52:11 -0300 Subject: [PATCH 02/62] Add RBSegmentsCache interface and implement redis and pluggable caches --- src/dtos/types.ts | 11 +++ src/storages/KeyBuilder.ts | 12 +++ src/storages/KeyBuilderSS.ts | 4 + .../inRedis/RBSegmentsCacheInRedis.ts | 79 +++++++++++++++++++ .../pluggable/RBSegmentsCachePluggable.ts | 76 ++++++++++++++++++ src/storages/types.ts | 30 ++++++- 6 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/storages/inRedis/RBSegmentsCacheInRedis.ts create mode 100644 src/storages/pluggable/RBSegmentsCachePluggable.ts diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 7573183c..1bb49a21 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -194,6 +194,17 @@ export interface ISplitCondition { conditionType: 'ROLLOUT' | 'WHITELIST' } +export interface IRBSegment { + name: string, + changeNumber: number, + status: 'ACTIVE' | 'ARCHIVED', + excluded: { + keys: string[], + segments: string[] + }, + conditions: ISplitCondition[], +} + export interface ISplit { name: string, changeNumber: number, diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 2f5dc800..dfd42f18 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -37,6 +37,18 @@ export class KeyBuilder { return `${this.prefix}.split.`; } + buildRBSegmentKey(splitName: string) { + return `${this.prefix}.rbsegment.${splitName}`; + } + + buildRBSegmentTillKey() { + return `${this.prefix}.rbsegments.till`; + } + + buildRBSegmentKeyPrefix() { + return `${this.prefix}.rbsegment.`; + } + buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 6232d88a..cf8d2156 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -53,6 +53,10 @@ export class KeyBuilderSS extends KeyBuilder { return `${this.buildSplitKeyPrefix()}*`; } + searchPatternForRBSegmentKeys() { + return `${this.buildRBSegmentKeyPrefix()}*`; + } + /* Telemetry keys */ buildLatencyKey(method: Method, bucket: number) { diff --git a/src/storages/inRedis/RBSegmentsCacheInRedis.ts b/src/storages/inRedis/RBSegmentsCacheInRedis.ts new file mode 100644 index 00000000..8e369d62 --- /dev/null +++ b/src/storages/inRedis/RBSegmentsCacheInRedis.ts @@ -0,0 +1,79 @@ +import { isNaNNumber } from '../../utils/lang'; +import { IRBSegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { IRBSegment } from '../../dtos/types'; +import { LOG_PREFIX } from './constants'; +import { setToArray } from '../../utils/lang/sets'; +import { RedisAdapter } from './RedisAdapter'; +import { KeyBuilderSS } from '../KeyBuilderSS'; + +export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilderSS; + private readonly redis: RedisAdapter; + + constructor(log: ILogger, keys: KeyBuilderSS, redis: RedisAdapter) { + this.log = log; + this.keys = keys; + this.redis = redis; + } + + get(name: string): Promise { + return this.redis.get(this.keys.buildRBSegmentKey(name)) + .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment)); + } + + private getNames(): Promise { + return this.redis.keys(this.keys.searchPatternForRBSegmentKeys()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + contains(names: Set): Promise { + const namesArray = setToArray(names); + return this.getNames().then(namesInStorage => { + return namesArray.every(name => namesInStorage.includes(name)); + }); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(toAdd => { + const key = this.keys.buildRBSegmentKey(toAdd.name); + const stringifiedNewRBSegment = JSON.stringify(toAdd); + return this.redis.set(key, stringifiedNewRBSegment).then(() => true); + })), + Promise.all(toRemove.map(toRemove => { + const key = this.keys.buildRBSegmentKey(toRemove.name); + return this.redis.del(key).then(status => status === 1); + })) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + + setChangeNumber(changeNumber: number) { + return this.redis.set(this.keys.buildRBSegmentTillKey(), changeNumber + '').then( + status => status === 'OK' + ); + } + + getChangeNumber(): Promise { + return this.redis.get(this.keys.buildRBSegmentTillKey()).then((value: string | null) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO implement if required by DataLoader or producer mode + clear() { + return Promise.resolve(true); + } + +} diff --git a/src/storages/pluggable/RBSegmentsCachePluggable.ts b/src/storages/pluggable/RBSegmentsCachePluggable.ts new file mode 100644 index 00000000..294e34d8 --- /dev/null +++ b/src/storages/pluggable/RBSegmentsCachePluggable.ts @@ -0,0 +1,76 @@ +import { isNaNNumber } from '../../utils/lang'; +import { KeyBuilder } from '../KeyBuilder'; +import { IPluggableStorageWrapper, IRBSegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { IRBSegment } from '../../dtos/types'; +import { LOG_PREFIX } from './constants'; +import { setToArray } from '../../utils/lang/sets'; + +export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilder; + private readonly wrapper: IPluggableStorageWrapper; + + constructor(log: ILogger, keys: KeyBuilder, wrapper: IPluggableStorageWrapper) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + } + + get(name: string): Promise { + return this.wrapper.get(this.keys.buildRBSegmentKey(name)) + .then(maybeRBSegment => maybeRBSegment && JSON.parse(maybeRBSegment)); + } + + private getNames(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildRBSegmentKeyPrefix()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + contains(names: Set): Promise { + const namesArray = setToArray(names); + return this.getNames().then(namesInStorage => { + return namesArray.every(name => namesInStorage.includes(name)); + }); + } + + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise { + return Promise.all([ + this.setChangeNumber(changeNumber), + Promise.all(toAdd.map(toAdd => { + const key = this.keys.buildRBSegmentKey(toAdd.name); + const stringifiedNewRBSegment = JSON.stringify(toAdd); + return this.wrapper.set(key, stringifiedNewRBSegment).then(() => true); + })), + Promise.all(toRemove.map(toRemove => { + const key = this.keys.buildRBSegmentKey(toRemove.name); + return this.wrapper.del(key); + })) + ]).then(([, added, removed]) => { + return added.some(result => result) || removed.some(result => result); + }); + } + + setChangeNumber(changeNumber: number) { + return this.wrapper.set(this.keys.buildRBSegmentTillKey(), changeNumber + ''); + } + + getChangeNumber(): Promise { + return this.wrapper.get(this.keys.buildRBSegmentTillKey()).then((value) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO implement if required by DataLoader or producer mode + clear() { + return Promise.resolve(true); + } + +} diff --git a/src/storages/types.ts b/src/storages/types.ts index 71962254..9a1741c9 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,5 +1,5 @@ import SplitIO from '../../types/splitio'; -import { MaybeThenable, ISplit, IMySegmentsResponse } from '../dtos/types'; +import { MaybeThenable, ISplit, IRBSegment, IMySegmentsResponse } from '../dtos/types'; import { MySegmentsData } from '../sync/polling/types'; import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types'; import { ISettings } from '../types'; @@ -225,6 +225,34 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { getNamesByFlagSets(flagSets: string[]): Promise[]> } +/** Rule-Based Segments cache */ + +export interface IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): MaybeThenable, + get(name: string): MaybeThenable, + getChangeNumber(): MaybeThenable, + clear(): MaybeThenable, + contains(names: Set): MaybeThenable, +} + +export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean, + get(name: string): IRBSegment | null, + getChangeNumber(): number, + clear(): void, + contains(names: Set): boolean, + // Used only for smart pausing in client-side standalone. Returns true if the storage contains a RBSegment using segments or large segments matchers + usesSegments(): boolean, +} + +export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase { + update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): Promise, + get(name: string): Promise, + getChangeNumber(): Promise, + clear(): Promise, + contains(names: Set): Promise, +} + /** Segments cache */ export interface ISegmentsCacheBase { From a57b082123c9bd914b4b16e0a01629ef5e7f0926 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 Feb 2025 17:10:37 -0300 Subject: [PATCH 03/62] Unit tests --- src/storages/KeyBuilder.ts | 2 +- src/storages/__tests__/testUtils.ts | 8 ++- .../inLocalStorage/RBSegmentsCacheInLocal.ts | 10 +-- .../inLocalStorage/SplitsCacheInLocal.ts | 16 ++--- .../__tests__/RBSegmentsCacheSync.spec.ts | 64 +++++++++++++++++++ 5 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index dfd42f18..7e9b7e85 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -41,7 +41,7 @@ export class KeyBuilder { return `${this.prefix}.rbsegment.${splitName}`; } - buildRBSegmentTillKey() { + buildRBSegmentsTillKey() { return `${this.prefix}.rbsegments.till`; } diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index fa38944f..a4009b1c 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -1,4 +1,4 @@ -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; import { IStorageSync, IStorageAsync, IImpressionsCacheSync, IEventsCacheSync } from '../types'; // Assert that instances created by storage factories have the expected interface @@ -45,3 +45,9 @@ export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t','w','o'] }; export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t','h','r','e'] }; //@ts-ignore export const featureFlagWithoutFS: ISplit = { name: 'ff_four' }; + +// Rule-based segments +//@ts-ignore +export const rbSegment: IRBSegment = { name: 'rb_segment', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'EQUAL_TO', unaryNumericMatcherData: { value: 10 } }] } }] }; +//@ts-ignore +export const rbSegmentWithInSegmentMatcher: IRBSegment = { name: 'rb_segment_with_in_segment_matcher', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts index 7c332543..28c0d1ee 100644 --- a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -20,8 +20,9 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } clear() { + this.getNames().forEach(name => this.remove(name)); + localStorage.removeItem(this.keys.buildRBSegmentsTillKey()); this.hasSync = false; - // SplitsCacheInLocal.clear() does the rest of the job } update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { @@ -32,7 +33,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { private setChangeNumber(changeNumber: number) { try { - localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + ''); + localStorage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); this.hasSync = true; } catch (e) { @@ -42,7 +43,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { private updateSegmentCount(diff: number){ const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - const count = toNumber(localStorage.getItem(segmentsCountKey)) - diff; + const count = toNumber(localStorage.getItem(segmentsCountKey)) + diff; // @ts-expect-error if (count > 0) localStorage.setItem(segmentsCountKey, count); else localStorage.removeItem(segmentsCountKey); @@ -79,7 +80,6 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { if (usesSegments(rbSegment)) this.updateSegmentCount(-1); return true; - } catch (e) { this.log.error(LOG_PREFIX + e); return false; @@ -116,7 +116,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { getChangeNumber(): number { const n = -1; - let value: string | number | null = localStorage.getItem(this.keys.buildRBSegmentTillKey()); + let value: string | number | null = localStorage.getItem(this.keys.buildRBSegmentsTillKey()); if (value !== null) { value = parseInt(value, 10); diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 61988139..385125e3 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -57,16 +57,14 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { private _incrementCounts(split: ISplit) { try { - if (split) { - const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - // @ts-expect-error - localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1); + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + // @ts-expect-error + localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1); - if (usesSegments(split)) { - const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - // @ts-expect-error - localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1); - } + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + // @ts-expect-error + localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1); } } catch (e) { this.log.error(LOG_PREFIX + e); diff --git a/src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts new file mode 100644 index 00000000..16f4e6ae --- /dev/null +++ b/src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts @@ -0,0 +1,64 @@ +import { RBSegmentsCacheInMemory } from '../RBSegmentsCacheInMemory'; +import { RBSegmentsCacheInLocal } from '../../inLocalStorage/RBSegmentsCacheInLocal'; +import { KeyBuilderCS } from '../../KeyBuilderCS'; +import { rbSegment, rbSegmentWithInSegmentMatcher } from '../../__tests__/testUtils'; +import { IRBSegmentsCacheSync } from '../../types'; +import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; + +const cacheInMemory = new RBSegmentsCacheInMemory(); +const cacheInLocal = new RBSegmentsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); + +describe.each([cacheInMemory, cacheInLocal])('RB SEGMENTS CACHE', (cache: IRBSegmentsCacheSync) => { + + beforeEach(() => { + cache.clear(); + }); + + test('clear should reset the cache state', () => { + cache.update([rbSegment], [], 1); + expect(cache.getChangeNumber()).toBe(1); + expect(cache.get(rbSegment.name)).not.toBeNull(); + + cache.clear(); + expect(cache.getChangeNumber()).toBe(-1); + expect(cache.get(rbSegment.name)).toBeNull(); + }); + + test('update should add and remove segments correctly', () => { + // Add segments + const updated1 = cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + expect(updated1).toBe(true); + expect(cache.get(rbSegment.name)).toEqual(rbSegment); + expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(cache.getChangeNumber()).toBe(1); + + // Remove segments + const updated2 = cache.update([], [rbSegment], 2); + expect(updated2).toBe(true); + expect(cache.get(rbSegment.name)).toBeNull(); + expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(cache.getChangeNumber()).toBe(2); + }); + + test('contains should check for segment existence correctly', () => { + cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + + expect(cache.contains(new Set([rbSegment.name]))).toBe(true); + expect(cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); + expect(cache.contains(new Set(['nonexistent']))).toBe(false); + expect(cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); + }); + + test('usesSegments should track segments usage correctly', () => { + expect(cache.usesSegments()).toBe(true); // Initially true when changeNumber is -1 + + cache.update([rbSegment], [], 1); // rbSegment doesn't have IN_SEGMENT matcher + expect(cache.usesSegments()).toBe(false); + + cache.update([rbSegmentWithInSegmentMatcher], [], 2); // rbSegmentWithInSegmentMatcher has IN_SEGMENT matcher + expect(cache.usesSegments()).toBe(true); + + cache.clear(); + expect(cache.usesSegments()).toBe(true); // True after clear since changeNumber is -1 + }); +}); From 53b20735f08b7ec33871323857093a9327e6803d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 Feb 2025 17:16:20 -0300 Subject: [PATCH 04/62] Rename method --- src/storages/KeyBuilder.ts | 2 +- src/storages/__tests__/testUtils.ts | 8 +++++++- src/storages/inRedis/RBSegmentsCacheInRedis.ts | 4 ++-- src/storages/pluggable/RBSegmentsCachePluggable.ts | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index dfd42f18..7e9b7e85 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -41,7 +41,7 @@ export class KeyBuilder { return `${this.prefix}.rbsegment.${splitName}`; } - buildRBSegmentTillKey() { + buildRBSegmentsTillKey() { return `${this.prefix}.rbsegments.till`; } diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index fa38944f..a4009b1c 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -1,4 +1,4 @@ -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; import { IStorageSync, IStorageAsync, IImpressionsCacheSync, IEventsCacheSync } from '../types'; // Assert that instances created by storage factories have the expected interface @@ -45,3 +45,9 @@ export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t','w','o'] }; export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t','h','r','e'] }; //@ts-ignore export const featureFlagWithoutFS: ISplit = { name: 'ff_four' }; + +// Rule-based segments +//@ts-ignore +export const rbSegment: IRBSegment = { name: 'rb_segment', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'EQUAL_TO', unaryNumericMatcherData: { value: 10 } }] } }] }; +//@ts-ignore +export const rbSegmentWithInSegmentMatcher: IRBSegment = { name: 'rb_segment_with_in_segment_matcher', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; diff --git a/src/storages/inRedis/RBSegmentsCacheInRedis.ts b/src/storages/inRedis/RBSegmentsCacheInRedis.ts index 8e369d62..a9f047b7 100644 --- a/src/storages/inRedis/RBSegmentsCacheInRedis.ts +++ b/src/storages/inRedis/RBSegmentsCacheInRedis.ts @@ -55,13 +55,13 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { } setChangeNumber(changeNumber: number) { - return this.redis.set(this.keys.buildRBSegmentTillKey(), changeNumber + '').then( + return this.redis.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '').then( status => status === 'OK' ); } getChangeNumber(): Promise { - return this.redis.get(this.keys.buildRBSegmentTillKey()).then((value: string | null) => { + return this.redis.get(this.keys.buildRBSegmentsTillKey()).then((value: string | null) => { const i = parseInt(value as string, 10); return isNaNNumber(i) ? -1 : i; diff --git a/src/storages/pluggable/RBSegmentsCachePluggable.ts b/src/storages/pluggable/RBSegmentsCachePluggable.ts index 294e34d8..49b593f2 100644 --- a/src/storages/pluggable/RBSegmentsCachePluggable.ts +++ b/src/storages/pluggable/RBSegmentsCachePluggable.ts @@ -54,11 +54,11 @@ export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { } setChangeNumber(changeNumber: number) { - return this.wrapper.set(this.keys.buildRBSegmentTillKey(), changeNumber + ''); + return this.wrapper.set(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); } getChangeNumber(): Promise { - return this.wrapper.get(this.keys.buildRBSegmentTillKey()).then((value) => { + return this.wrapper.get(this.keys.buildRBSegmentsTillKey()).then((value) => { const i = parseInt(value as string, 10); return isNaNNumber(i) ? -1 : i; From 261b86e2bed2a0ccc79ac59f06172c127c5e14b4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Feb 2025 11:01:59 -0300 Subject: [PATCH 05/62] Unit tests --- .../__tests__/RBSegmentsCacheAsync.spec.ts | 59 +++++++++++++++++++ .../inRedis/RBSegmentsCacheInRedis.ts | 2 +- .../pluggable/RBSegmentsCachePluggable.ts | 2 +- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/storages/__tests__/RBSegmentsCacheAsync.spec.ts diff --git a/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts new file mode 100644 index 00000000..8c2d6678 --- /dev/null +++ b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts @@ -0,0 +1,59 @@ +import { RBSegmentsCacheInRedis } from '../inRedis/RBSegmentsCacheInRedis'; +import { RBSegmentsCachePluggable } from '../pluggable/RBSegmentsCachePluggable'; +import { KeyBuilderSS } from '../KeyBuilderSS'; +import { rbSegment, rbSegmentWithInSegmentMatcher } from '../__tests__/testUtils'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; +import { metadata } from './KeyBuilder.spec'; +import { RedisAdapter } from '../inRedis/RedisAdapter'; +import { wrapperMockFactory } from '../pluggable/__tests__/wrapper.mock'; + +const keys = new KeyBuilderSS('RBSEGMENT', metadata); + +const redisClient = new RedisAdapter(loggerMock); +const cacheInRedis = new RBSegmentsCacheInRedis(loggerMock, keys, redisClient); + +const storageWrapper = wrapperMockFactory(); +const cachePluggable = new RBSegmentsCachePluggable(loggerMock, keys, storageWrapper); + +describe.each([{ cache: cacheInRedis, wrapper: redisClient }, { cache: cachePluggable, wrapper: storageWrapper }])('Rule-based segments cache async (Redis & Pluggable)', ({ cache, wrapper }) => { + + afterAll(async () => { + await wrapper.del(keys.buildRBSegmentsTillKey()); + await wrapper.disconnect(); + }); + + test('update should add and remove segments correctly', async () => { + // Add segments + expect(await cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1)).toBe(true); + expect(await cache.get(rbSegment.name)).toEqual(rbSegment); + expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(await cache.getChangeNumber()).toBe(1); + + // Remove a segment + expect(await cache.update([], [rbSegment], 2)).toBe(true); + expect(await cache.get(rbSegment.name)).toBeNull(); + expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); + expect(await cache.getChangeNumber()).toBe(2); + + // Remove remaining segment + expect(await cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true); + expect(await cache.get(rbSegment.name)).toBeNull(); + expect(await cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull(); + expect(await cache.getChangeNumber()).toBe(3); + + // No changes + expect(await cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false); + expect(await cache.getChangeNumber()).toBe(4); + }); + + test('contains should check for segment existence correctly', async () => { + await cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + + expect(await cache.contains(new Set([rbSegment.name]))).toBe(true); + expect(await cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); + expect(await cache.contains(new Set(['nonexistent']))).toBe(false); + expect(await cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); + + await cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2); + }); +}); diff --git a/src/storages/inRedis/RBSegmentsCacheInRedis.ts b/src/storages/inRedis/RBSegmentsCacheInRedis.ts index a9f047b7..dc36f64c 100644 --- a/src/storages/inRedis/RBSegmentsCacheInRedis.ts +++ b/src/storages/inRedis/RBSegmentsCacheInRedis.ts @@ -73,7 +73,7 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync { // @TODO implement if required by DataLoader or producer mode clear() { - return Promise.resolve(true); + return Promise.resolve(); } } diff --git a/src/storages/pluggable/RBSegmentsCachePluggable.ts b/src/storages/pluggable/RBSegmentsCachePluggable.ts index 49b593f2..c1967f6d 100644 --- a/src/storages/pluggable/RBSegmentsCachePluggable.ts +++ b/src/storages/pluggable/RBSegmentsCachePluggable.ts @@ -70,7 +70,7 @@ export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync { // @TODO implement if required by DataLoader or producer mode clear() { - return Promise.resolve(true); + return Promise.resolve(); } } From e22b28601f1ae4ff46467978975cc1c56b02dc58 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Feb 2025 11:07:17 -0300 Subject: [PATCH 06/62] Update unit tests --- .../__tests__/RBSegmentsCacheSync.spec.ts | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) rename src/storages/{inMemory => }/__tests__/RBSegmentsCacheSync.spec.ts (62%) diff --git a/src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts similarity index 62% rename from src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts rename to src/storages/__tests__/RBSegmentsCacheSync.spec.ts index 16f4e6ae..1975b35e 100644 --- a/src/storages/inMemory/__tests__/RBSegmentsCacheSync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts @@ -1,14 +1,14 @@ -import { RBSegmentsCacheInMemory } from '../RBSegmentsCacheInMemory'; -import { RBSegmentsCacheInLocal } from '../../inLocalStorage/RBSegmentsCacheInLocal'; -import { KeyBuilderCS } from '../../KeyBuilderCS'; -import { rbSegment, rbSegmentWithInSegmentMatcher } from '../../__tests__/testUtils'; -import { IRBSegmentsCacheSync } from '../../types'; -import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { RBSegmentsCacheInMemory } from '../inMemory/RBSegmentsCacheInMemory'; +import { RBSegmentsCacheInLocal } from '../inLocalStorage/RBSegmentsCacheInLocal'; +import { KeyBuilderCS } from '../KeyBuilderCS'; +import { rbSegment, rbSegmentWithInSegmentMatcher } from '../__tests__/testUtils'; +import { IRBSegmentsCacheSync } from '../types'; +import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks'; const cacheInMemory = new RBSegmentsCacheInMemory(); const cacheInLocal = new RBSegmentsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user')); -describe.each([cacheInMemory, cacheInLocal])('RB SEGMENTS CACHE', (cache: IRBSegmentsCacheSync) => { +describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Memory & LocalStorage)', (cache: IRBSegmentsCacheSync) => { beforeEach(() => { cache.clear(); @@ -26,18 +26,26 @@ describe.each([cacheInMemory, cacheInLocal])('RB SEGMENTS CACHE', (cache: IRBSeg test('update should add and remove segments correctly', () => { // Add segments - const updated1 = cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); - expect(updated1).toBe(true); + expect(cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1)).toBe(true); expect(cache.get(rbSegment.name)).toEqual(rbSegment); expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); expect(cache.getChangeNumber()).toBe(1); - // Remove segments - const updated2 = cache.update([], [rbSegment], 2); - expect(updated2).toBe(true); + // Remove a segment + expect(cache.update([], [rbSegment], 2)).toBe(true); expect(cache.get(rbSegment.name)).toBeNull(); expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toEqual(rbSegmentWithInSegmentMatcher); expect(cache.getChangeNumber()).toBe(2); + + // Remove remaining segment + expect(cache.update([], [rbSegmentWithInSegmentMatcher], 3)).toBe(true); + expect(cache.get(rbSegment.name)).toBeNull(); + expect(cache.get(rbSegmentWithInSegmentMatcher.name)).toBeNull(); + expect(cache.getChangeNumber()).toBe(3); + + // No changes + expect(cache.update([], [rbSegmentWithInSegmentMatcher], 4)).toBe(false); + expect(cache.getChangeNumber()).toBe(4); }); test('contains should check for segment existence correctly', () => { @@ -47,6 +55,8 @@ describe.each([cacheInMemory, cacheInLocal])('RB SEGMENTS CACHE', (cache: IRBSeg expect(cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); expect(cache.contains(new Set(['nonexistent']))).toBe(false); expect(cache.contains(new Set([rbSegment.name, 'nonexistent']))).toBe(false); + + cache.update([], [rbSegment, rbSegmentWithInSegmentMatcher], 2); }); test('usesSegments should track segments usage correctly', () => { From 67a164b2df24b376b7444b7c44e085a56687b6d4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Feb 2025 12:53:41 -0300 Subject: [PATCH 07/62] Update splitChangesUpdater, splitChangesFetcher and related types --- src/dtos/types.ts | 12 +- src/logger/constants.ts | 1 + src/logger/messages/debug.ts | 5 +- src/services/types.ts | 2 +- .../polling/fetchers/splitChangesFetcher.ts | 3 +- src/sync/polling/fetchers/types.ts | 1 + .../__tests__/splitChangesUpdater.spec.ts | 30 +++-- .../polling/updaters/splitChangesUpdater.ts | 108 +++++++++++------- 8 files changed, 102 insertions(+), 60 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 1bb49a21..a2ffaad5 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -228,8 +228,16 @@ export type ISplitPartial = Pick Promise -export type IFetchSplitChanges = (since: number, noCache?: boolean, till?: number) => Promise +export type IFetchSplitChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean, till?: number) => Promise diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 9d1b27eb..d134601b 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -11,11 +11,12 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges since: number, noCache?: boolean, till?: number, + rbSince?: number, // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise ) { - let splitsPromise = fetchSplitChanges(since, noCache, till); + let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince); if (decorator) splitsPromise = decorator(splitsPromise); return splitsPromise.then(resp => resp.json()); diff --git a/src/sync/polling/fetchers/types.ts b/src/sync/polling/fetchers/types.ts index 72968a5f..8fe922ce 100644 --- a/src/sync/polling/fetchers/types.ts +++ b/src/sync/polling/fetchers/types.ts @@ -5,6 +5,7 @@ export type ISplitChangesFetcher = ( since: number, noCache?: boolean, till?: number, + rbSince?: number, decorator?: (promise: Promise) => Promise ) => Promise diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index d59c7013..a55ce8d9 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -4,7 +4,7 @@ import { splitApiFactory } from '../../../../services/splitApi'; import { SegmentsCacheInMemory } from '../../../../storages/inMemory/SegmentsCacheInMemory'; import { SplitsCacheInMemory } from '../../../../storages/inMemory/SplitsCacheInMemory'; import { splitChangesFetcherFactory } from '../../fetchers/splitChangesFetcher'; -import { splitChangesUpdaterFactory, parseSegments, computeSplitsMutation } from '../splitChangesUpdater'; +import { splitChangesUpdaterFactory, parseSegments, computeMutation } from '../splitChangesUpdater'; import splitChangesMock1 from '../../../../__tests__/mocks/splitchanges.since.-1.json'; import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { fullSettings, settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; @@ -12,6 +12,7 @@ import { EventEmitter } from '../../../../utils/MinEvents'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; import { telemetryTrackerFactory } from '../../../../trackers/telemetryTracker'; import { splitNotifications } from '../../../streaming/__tests__/dataMocks'; +import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory'; const ARCHIVED_FF = 'ARCHIVED'; @@ -94,19 +95,21 @@ test('splitChangesUpdater / segments parser', () => { test('splitChangesUpdater / compute splits mutation', () => { const splitFiltersValidation = { queryString: null, groupedFilters: { bySet: [], byName: [], byPrefix: [] }, validFilters: [] }; - let splitsMutation = computeSplitsMutation([activeSplitWithSegments, archivedSplit] as ISplit[], splitFiltersValidation); + let segments = new Set(); + let splitsMutation = computeMutation([activeSplitWithSegments, archivedSplit] as ISplit[], segments, splitFiltersValidation); expect(splitsMutation.added).toEqual([activeSplitWithSegments]); expect(splitsMutation.removed).toEqual([archivedSplit]); - expect(splitsMutation.segments).toEqual(['A', 'B']); + expect(Array.from(segments)).toEqual(['A', 'B']); // SDK initialization without sets // should process all the notifications - splitsMutation = computeSplitsMutation([testFFSetsAB, test2FFSetsX] as ISplit[], splitFiltersValidation); + segments = new Set(); + splitsMutation = computeMutation([testFFSetsAB, test2FFSetsX] as ISplit[], segments, splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFSetsAB, test2FFSetsX]); expect(splitsMutation.removed).toEqual([]); - expect(splitsMutation.segments).toEqual([]); + expect(Array.from(segments)).toEqual([]); }); test('splitChangesUpdater / compute splits mutation with filters', () => { @@ -114,38 +117,38 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { let splitFiltersValidation = { queryString: '&sets=set_a,set_b', groupedFilters: { bySet: ['set_a', 'set_b'], byName: ['name_1'], byPrefix: [] }, validFilters: [] }; // fetching new feature flag in sets A & B - let splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); + let splitsMutation = computeMutation([testFFSetsAB], new Set(), splitFiltersValidation); // should add it to mutations expect(splitsMutation.added).toEqual([testFFSetsAB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B - splitsMutation = computeSplitsMutation([testFFRemoveSetB], splitFiltersValidation); + splitsMutation = computeMutation([testFFRemoveSetB], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([testFFRemoveSetB]); expect(splitsMutation.removed).toEqual([]); // fetching existing test feature flag removed from set B - splitsMutation = computeSplitsMutation([testFFRemoveSetA], splitFiltersValidation); + splitsMutation = computeMutation([testFFRemoveSetA], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFRemoveSetA]); // fetching existing test feature flag removed from set B - splitsMutation = computeSplitsMutation([testFFEmptySet], splitFiltersValidation); + splitsMutation = computeMutation([testFFEmptySet], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFEmptySet]); // SDK initialization with names: ['test2'] splitFiltersValidation = { queryString: '&names=test2', groupedFilters: { bySet: [], byName: ['test2'], byPrefix: [] }, validFilters: [] }; - splitsMutation = computeSplitsMutation([testFFSetsAB], splitFiltersValidation); + splitsMutation = computeMutation([testFFSetsAB], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([]); expect(splitsMutation.removed).toEqual([testFFSetsAB]); - splitsMutation = computeSplitsMutation([test2FFSetsX, testFFEmptySet], splitFiltersValidation); + splitsMutation = computeMutation([test2FFSetsX, testFFEmptySet], new Set(), splitFiltersValidation); expect(splitsMutation.added).toEqual([test2FFSetsX]); expect(splitsMutation.removed).toEqual([testFFEmptySet]); @@ -161,10 +164,13 @@ describe('splitChangesUpdater', () => { const splits = new SplitsCacheInMemory(); const updateSplits = jest.spyOn(splits, 'update'); + const rbSegments = new RBSegmentsCacheInMemory(); + // @TODO spy on rbSegments + const segments = new SegmentsCacheInMemory(); const registerSegments = jest.spyOn(segments, 'registerSegments'); - const storage = { splits, segments }; + const storage = { splits, rbSegments, segments }; const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 065ecb89..bb61810d 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -1,13 +1,13 @@ import { ISegmentsCacheBase, IStorageBase } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; -import { ISplit, ISplitChangesResponse, ISplitFiltersValidation } from '../../../dtos/types'; +import { IRBSegment, ISplit, ISplitChangesResponse, ISplitFiltersValidation, MaybeThenable } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import { timeout } from '../../../utils/promise/timeout'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; -import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; +import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; -import { IN_SEGMENT } from '../../../utils/constants'; +import { IN_RULE_BASED_SEGMENT, IN_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise @@ -27,24 +27,23 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { * Collect segments from a raw split definition. * Exported for testing purposes. */ -export function parseSegments({ conditions }: ISplit): Set { +export function parseSegments({ conditions }: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set { let segments = new Set(); for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; matchers.forEach(matcher => { - if (matcher.matcherType === IN_SEGMENT) segments.add(matcher.userDefinedSegmentMatcherData.segmentName); + if (matcher.matcherType === matcherType) segments.add(matcher.userDefinedSegmentMatcherData.segmentName); }); } return segments; } -interface ISplitMutations { - added: ISplit[], - removed: ISplit[], - segments: string[] +interface ISplitMutations { + added: T[], + removed: T[] } /** @@ -68,30 +67,30 @@ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { return matchNames || matchPrefix; } +function isFF(ruleBasedEntity: IRBSegment | ISplit): ruleBasedEntity is ISplit { + return (ruleBasedEntity as ISplit).defaultTreatment !== undefined; +} + /** * Given the list of splits from /splitChanges endpoint, it returns the mutations, * i.e., an object with added splits, removed splits and used segments. * Exported for testing purposes. */ -export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersValidation): ISplitMutations { - const segments = new Set(); - const computed = entries.reduce((accum, split) => { - if (split.status === 'ACTIVE' && matchFilters(split, filters)) { - accum.added.push(split); +export function computeMutation(rules: Array, segments: Set, filters?: ISplitFiltersValidation): ISplitMutations { - parseSegments(split).forEach((segmentName: string) => { + return rules.reduce((accum, ruleBasedEntity) => { + if (ruleBasedEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleBasedEntity as ISplit, filters))) { + accum.added.push(ruleBasedEntity); + + parseSegments(ruleBasedEntity).forEach((segmentName: string) => { segments.add(segmentName); }); } else { - accum.removed.push(split); + accum.removed.push(ruleBasedEntity); } return accum; - }, { added: [], removed: [], segments: [] } as ISplitMutations); - - computed.segments = setToArray(segments); - - return computed; + }, { added: [], removed: [] } as ISplitMutations); } /** @@ -111,14 +110,14 @@ export function computeSplitsMutation(entries: ISplit[], filters: ISplitFiltersV export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - storage: Pick, + storage: Pick, splitFiltersValidation: ISplitFiltersValidation, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, isClientSide?: boolean ): ISplitChangesUpdater { - const { splits, segments } = storage; + const { splits, rbSegments, segments } = storage; let startingUp = true; @@ -135,35 +134,60 @@ export function splitChangesUpdaterFactory( * @param noCache - true to revalidate data to fetch * @param till - query param to bypass CDN requests */ - return function splitChangesUpdater(noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) { + return function splitChangesUpdater(noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }) { /** * @param since - current changeNumber at splitsCache * @param retry - current number of retry attempts */ - function _splitChangesUpdater(since: number, retry = 0): Promise { - log.debug(SYNC_SPLITS_FETCH, [since]); - const fetcherPromise = Promise.resolve(splitUpdateNotification ? - { splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } : - splitChangesFetcher(since, noCache, till, _promiseDecorator) + function _splitChangesUpdater(sinces: [number, number], retry = 0): Promise { + const [since, rbSince] = sinces; + log.debug(SYNC_SPLITS_FETCH, sinces); + const fetcherPromise = Promise.resolve( + splitUpdateNotification ? + isFF(splitUpdateNotification.payload) ? + // IFFU edge case: a change to a flag that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet + Promise.resolve(rbSegments.contains(parseSegments(splitUpdateNotification.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { + return contains ? + { ff: { d: [splitUpdateNotification.payload as ISplit], t: splitUpdateNotification.changeNumber } } : + splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator); + }) : + { rbs: { d: [splitUpdateNotification.payload as IRBSegment], t: splitUpdateNotification.changeNumber } } : + splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((splitChanges: ISplitChangesResponse) => { startingUp = false; - const mutation = computeSplitsMutation(splitChanges.splits, splitFiltersValidation); + const usedSegments = new Set(); + + let ffUpdate: MaybeThenable = false; + if (splitChanges.ff) { + const { added, removed } = computeMutation(splitChanges.ff.d, usedSegments, splitFiltersValidation); + log.debug(SYNC_SPLITS_UPDATE, [added.length, removed.length]); + ffUpdate = splits.update(added, removed, splitChanges.ff.t); + } - log.debug(SYNC_SPLITS_UPDATE, [mutation.added.length, mutation.removed.length, mutation.segments.length]); + let rbsUpdate: MaybeThenable = false; + if (splitChanges.rbs) { + const { added, removed } = computeMutation(splitChanges.rbs.d, usedSegments); + log.debug(SYNC_RBS_UPDATE, [added.length, removed.length]); + rbsUpdate = rbSegments.update(added, removed, splitChanges.rbs.t); + } - // Write into storage - // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency - return Promise.all([ - splits.update(mutation.added, mutation.removed, splitChanges.till), - segments.registerSegments(mutation.segments) - ]).then(([isThereUpdate]) => { + return Promise.all([ffUpdate, rbsUpdate, + // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted + segments.registerSegments(setToArray(usedSegments)) + ]).then(([ffChanged, rbsChanged]) => { if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && isThereUpdate && (isClientSide || checkAllSegmentsExist(segments)))) - .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) + return Promise.resolve(!splitsEventEmitter.splitsArrived || + ( + (!splitChanges.ff || since !== splitChanges.ff.t) && + (!splitChanges.rbs || rbSince !== splitChanges.rbs.t) && + (ffChanged || rbsChanged) && + (isClientSide || checkAllSegmentsExist(segments)) + ) + ) .then(emitSplitsArrivedEvent => { // emit SDK events if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); @@ -179,7 +203,7 @@ export function splitChangesUpdaterFactory( if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]); - return _splitChangesUpdater(since, retry); + return _splitChangesUpdater(sinces, retry); } else { startingUp = false; } @@ -196,7 +220,7 @@ export function splitChangesUpdaterFactory( return fetcherPromise; } - let sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error - return sincePromise.then(_splitChangesUpdater); + // `getChangeNumber` never rejects or throws error + return Promise.all([splits.getChangeNumber(), rbSegments.getChangeNumber()]).then(_splitChangesUpdater); }; } From 0e415d56dc47f74607d59b38296ee53d17073e60 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Feb 2025 12:57:48 -0300 Subject: [PATCH 08/62] Fix unit tests --- .../mocks/splitchanges.since.-1.json | 2655 +++++++++-------- .../__tests__/splitChangesUpdater.spec.ts | 3 +- src/utils/constants/index.ts | 1 + 3 files changed, 1332 insertions(+), 1327 deletions(-) diff --git a/src/__tests__/mocks/splitchanges.since.-1.json b/src/__tests__/mocks/splitchanges.since.-1.json index 8e24e581..18929ce7 100644 --- a/src/__tests__/mocks/splitchanges.since.-1.json +++ b/src/__tests__/mocks/splitchanges.since.-1.json @@ -1,1441 +1,1446 @@ { - "splits": [ - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "qc_team", - "seed": -1984784937, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "no", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "ff": { + "d": [ + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "qc_team", + "seed": -1984784937, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "no", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "tia@split.io", + "trevor@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": null, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "tia@split.io", - "trevor@split.io" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "yes", + "size": 100 } ] }, - "partitions": [ - { - "treatment": "yes", - "size": 100 - } - ] - }, - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "employees" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null, - "unaryStringMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "yes", - "size": 0 + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "unaryStringMatcherData": null + } + ] }, - { - "treatment": "no", - "size": 100 - } - ] - } - ], - "configurations": {} - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "whitelist", - "seed": 104328192, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "not_allowed", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": null, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "facundo@split.io" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null - } - ] - }, - "partitions": [ - { - "treatment": "allowed", - "size": 100 - } - ] - }, - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "treatment": "yes", + "size": 0 + }, { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "no", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "allowed", - "size": 0 + } + ], + "configurations": {} + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "whitelist", + "seed": 104328192, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "not_allowed", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "facundo@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "not_allowed", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "blacklist", - "seed": -1840071133, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "allowed", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "splitters" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "allowed", + "size": 100 } ] }, - "partitions": [ - { - "treatment": "allowed", - "size": 0 + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "not_allowed", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "splitters", - "seed": 1061596048, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "splitters" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "allowed", + "size": 0 + }, + { + "treatment": "not_allowed", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "blacklist", + "seed": -1840071133, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "allowed", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "splitters" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "developers", - "seed": 1461592538, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "developers" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "allowed", + "size": 0 + }, + { + "treatment": "not_allowed", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "splitters", + "seed": 1061596048, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "splitters" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "employees_between_21_and_50_and_chrome", - "seed": -1073105888, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "splitters" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 }, { - "keySelector": { - "trafficType": "user", - "attribute": "age" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": null, - "start": 21, - "end": 50 + "treatment": "off", + "size": 0 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "developers", + "seed": 1461592538, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "developers" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 }, { - "keySelector": { - "trafficType": "user", - "attribute": "agent" - }, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "chrome" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "off", + "size": 0 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_10_and_user_attr2_is_not_foo", - "seed": 481329258, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "attr" + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "employees_between_21_and_50_and_chrome", + "seed": -1073105888, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "splitters" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 + { + "keySelector": { + "trafficType": "user", + "attribute": "age" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": null, + "start": 21, + "end": 50 + } }, - "betweenMatcherData": null - }, + { + "keySelector": { + "trafficType": "user", + "attribute": "agent" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "chrome" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr2" - }, - "matcherType": "WHITELIST", - "negate": true, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "foo" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_whitelist", - "seed": -2122983143, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "account" - }, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "key_1@split.io", - "key_2@split.io", - "key_3@split.io", - "key_4@split.io", - "key_5@split.io" - ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_10_and_user_attr2_is_not_foo", + "seed": 481329258, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + { + "keySelector": { + "trafficType": "user", + "attribute": "attr2" + }, + "matcherType": "WHITELIST", + "negate": true, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "foo" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_employees", - "seed": 1107027749, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_whitelist", + "seed": -2122983143, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "account" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "key_1@split.io", + "key_2@split.io", + "key_3@split.io", + "key_4@split.io", + "key_5@split.io" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "account" - }, - "matcherType": "IN_SEGMENT", - "negate": false, - "userDefinedSegmentMatcherData": { - "segmentName": "employees" - }, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_all", - "seed": -790401804, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_employees", + "seed": 1107027749, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "account" + }, + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "account" - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_all_50_50", - "seed": 968686, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_all", + "seed": -790401804, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "account" + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "lower", - "size": 50 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_all_50_50", + "seed": 968686, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "higher", - "size": 50 - } - ] - } - ] - },{ - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_account_in_segment_all_50_50_2", - "seed": 96868, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "lower", + "size": 50 + }, + { + "treatment": "higher", + "size": 50 } ] - }, - "partitions": [ - { - "treatment": "lower", - "size": 50 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_account_in_segment_all_50_50_2", + "seed": 96868, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "higher", - "size": 50 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_btw_datetime_1458240947021_and_1458246884077", - "seed": 622265394, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": "DATETIME", - "start": 1458240947021, - "end": 1458246884077 - } + "treatment": "lower", + "size": 50 + }, + { + "treatment": "higher", + "size": 50 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_btw_number_10_and_20", - "seed": 1870594950, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": "NUMBER", - "start": 10, - "end": 20 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_btw_datetime_1458240947021_and_1458246884077", + "seed": 622265394, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": "DATETIME", + "start": 1458240947021, + "end": 1458246884077 + } } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_btw_10_and_20", - "seed": -976719381, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ - { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "BETWEEN", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": { - "dataType": null, - "start": 10, - "end": 20 + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_btw_number_10_and_20", + "seed": 1870594950, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": "NUMBER", + "start": 10, + "end": 20 + } } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_lte_datetime_1458240947021", - "seed": 455590578, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_btw_10_and_20", + "seed": -976719381, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "BETWEEN", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": { + "dataType": null, + "start": 10, + "end": 20 + } + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "DATETIME", - "value": 1458240947021 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_lte_number_10", - "seed": 1895728928, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_lte_datetime_1458240947021", + "seed": 455590578, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "DATETIME", + "value": 1458240947021 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "NUMBER", - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_lte_10", - "seed": 773481472, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_lte_number_10", + "seed": 1895728928, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "LESS_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_datetime_1458240947021", - "seed": 582849993, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_lte_10", + "seed": 773481472, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "DATETIME", - "value": 1458240947021 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_number_10", - "seed": -1710564342, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_datetime_1458240947021", + "seed": 582849993, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "DATETIME", + "value": 1458240947021 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "NUMBER", - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_gte_10", - "seed": 2016359772, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_number_10", + "seed": -1710564342, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "GREATER_THAN_OR_EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_eq_datetime_1458240947021", - "seed": -1927656676, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_gte_10", + "seed": 2016359772, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "DATETIME", - "value": 1458240947021 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_eq_number_ten", - "seed": 643770303, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_eq_datetime_1458240947021", + "seed": -1927656676, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "DATETIME", + "value": 1458240947021 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": "NUMBER", - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "user_attr_eq_ten", - "seed": 1276593955, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_eq_number_ten", + "seed": 643770303, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": "NUMBER", + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "attr" - }, - "matcherType": "EQUAL_TO", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": { - "dataType": null, - "value": 10 - }, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "hierarchical_dep_always_on", - "seed": -790396804, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "user_attr_eq_ten", + "seed": 1276593955, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "attr" + }, + "matcherType": "EQUAL_TO", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": { + "dataType": null, + "value": 10 + }, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "hierarchical dependency always on label" - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "hierarchical_dep_hierarchical", - "seed": 1276793945, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "hierarchical_dep_always_on", + "seed": -790396804, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SPLIT_TREATMENT", - "negate": false, - "dependencyMatcherData": { - "split": "hierarchical_dep_always_on", - "treatments": [ - "on", "partial" - ] - }, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "hierarchical dependency label" - } - ], - "configurations": {} - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "hierarchical_splits_test", - "seed": 1276793945, - "changeNumber": 2828282828, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + ], + "label": "hierarchical dependency always on label" + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "hierarchical_dep_hierarchical", + "seed": 1276793945, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "dependencyMatcherData": { + "split": "hierarchical_dep_always_on", + "treatments": [ + "on", + "partial" + ] + }, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "IN_SPLIT_TREATMENT", - "negate": false, - "dependencyMatcherData": { - "split": "hierarchical_dep_hierarchical", - "treatments": [ - "on", "partial" - ] - }, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ], - "label": "expected label" - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "always_on", - "seed": -790401604, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + ], + "label": "hierarchical dependency label" + } + ], + "configurations": {} + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "hierarchical_splits_test", + "seed": 1276793945, + "changeNumber": 2828282828, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "IN_SPLIT_TREATMENT", + "negate": false, + "dependencyMatcherData": { + "split": "hierarchical_dep_hierarchical", + "treatments": [ + "on", + "partial" + ] + }, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } - ] - }, - "partitions": [ - { - "treatment": "on", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "always_off", - "seed": -790401604, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "off", - "conditions": [ - { - "matcherGroup": { - "combiner": "AND", - "matchers": [ + ], + "label": "expected label" + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "always_on", + "seed": -790401604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "off", - "size": 100 - } - ] - } - ] - }, - { - "orgId": null, - "environment": null, - "trafficTypeId": null, - "trafficTypeName": null, - "name": "ta_bucket1_test", - "algo": 2, - "seed": -1222652054, - "trafficAllocation": 1, - "trafficAllocationSeed": -1667452163, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "default_treatment", - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "always_off", + "seed": -790401604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "off", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "rollout_treatment", - "size": 100 - } - ] - } - ] - }, - { - "trafficTypeName": null, - "name": "split_with_config", - "algo": 2, - "seed": -1222652064, - "trafficAllocation": 100, - "changeNumber": 828282828282, - "trafficAllocationSeed": -1667492163, - "status": "ACTIVE", - "killed": false, - "defaultTreatment": "on", - "conditions": [ - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "ta_bucket1_test", + "algo": 2, + "seed": -1222652054, + "trafficAllocation": 1, + "trafficAllocationSeed": -1667452163, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "default_treatment", + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": "group" - }, - "matcherType": "WHITELIST", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": { - "whitelist": [ - "value_without_config" - ] - }, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "rollout_treatment", + "size": 100 } ] - }, - "partitions": [ - { - "treatment": "on", - "size": 0 + } + ] + }, + { + "trafficTypeName": null, + "name": "split_with_config", + "algo": 2, + "seed": -1222652064, + "trafficAllocation": 100, + "changeNumber": 828282828282, + "trafficAllocationSeed": -1667492163, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "on", + "conditions": [ + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "group" + }, + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "value_without_config" + ] + }, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 100 - } - ] - }, - { - "conditionType": "ROLLOUT", - "matcherGroup": { - "combiner": "AND", - "matchers": [ + "partitions": [ { - "keySelector": { - "trafficType": "user", - "attribute": null - }, - "matcherType": "ALL_KEYS", - "negate": false, - "userDefinedSegmentMatcherData": null, - "whitelistMatcherData": null, - "unaryNumericMatcherData": null, - "betweenMatcherData": null + "treatment": "on", + "size": 0 + }, + { + "treatment": "off", + "size": 100 } ] }, - "partitions": [ - { - "treatment": "on", - "size": 100 + { + "conditionType": "ROLLOUT", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] }, - { - "treatment": "off", - "size": 0 - } - ], - "label": "another expected label" + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ], + "label": "another expected label" + } + ], + "configurations": { + "on": "{\"color\":\"brown\",\"dimensions\":{\"height\":12,\"width\":14},\"text\":{\"inner\":\"click me\"}}" } - ], - "configurations": { - "on": "{\"color\":\"brown\",\"dimensions\":{\"height\":12,\"width\":14},\"text\":{\"inner\":\"click me\"}}" } - } - ], - "since": -1, - "till": 1457552620999 -} + ], + "s": -1, + "t": 1457552620999 + } +} \ No newline at end of file diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index a55ce8d9..0a84ec8a 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -165,7 +165,6 @@ describe('splitChangesUpdater', () => { const updateSplits = jest.spyOn(splits, 'update'); const rbSegments = new RBSegmentsCacheInMemory(); - // @TODO spy on rbSegments const segments = new SegmentsCacheInMemory(); const registerSegments = jest.spyOn(segments, 'registerSegments'); @@ -186,7 +185,7 @@ describe('splitChangesUpdater', () => { test('test without payload', async () => { const result = await splitChangesUpdater(); expect(updateSplits).toBeCalledTimes(1); - expect(updateSplits).lastCalledWith(splitChangesMock1.splits, [], splitChangesMock1.till); + expect(updateSplits).lastCalledWith(splitChangesMock1.ff.d, [], splitChangesMock1.ff.t); expect(registerSegments).toBeCalledTimes(1); expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); expect(result).toBe(true); diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index cd11790f..24d3d31f 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -109,3 +109,4 @@ export const FLAG_SPEC_VERSION = '1.2'; // Matcher types export const IN_SEGMENT = 'IN_SEGMENT'; export const IN_LARGE_SEGMENT = 'IN_LARGE_SEGMENT'; +export const IN_RULE_BASED_SEGMENT = 'IN_RULE_BASED_SEGMENT'; From 9cec7a15f3ac61e78ede3ae7984edfbd3976bf75 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 27 Feb 2025 15:18:28 -0300 Subject: [PATCH 09/62] Add unit tests --- .../__tests__/splitChangesUpdater.spec.ts | 31 +++++++++++++++++-- .../polling/updaters/splitChangesUpdater.ts | 14 ++++----- src/sync/streaming/__tests__/dataMocks.ts | 8 ++--- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 0a84ec8a..da85fbdd 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -1,4 +1,4 @@ -import { ISplit } from '../../../../dtos/types'; +import { IRBSegment, ISplit } from '../../../../dtos/types'; import { readinessManagerFactory } from '../../../../readiness/readinessManager'; import { splitApiFactory } from '../../../../services/splitApi'; import { SegmentsCacheInMemory } from '../../../../storages/inMemory/SegmentsCacheInMemory'; @@ -165,6 +165,7 @@ describe('splitChangesUpdater', () => { const updateSplits = jest.spyOn(splits, 'update'); const rbSegments = new RBSegmentsCacheInMemory(); + const updateRbSegments = jest.spyOn(rbSegments, 'update'); const segments = new SegmentsCacheInMemory(); const registerSegments = jest.spyOn(segments, 'registerSegments'); @@ -184,22 +185,29 @@ describe('splitChangesUpdater', () => { test('test without payload', async () => { const result = await splitChangesUpdater(); + + expect(fetchSplitChanges).toBeCalledTimes(1); + expect(fetchSplitChanges).lastCalledWith(-1, undefined, undefined, -1); expect(updateSplits).toBeCalledTimes(1); expect(updateSplits).lastCalledWith(splitChangesMock1.ff.d, [], splitChangesMock1.ff.t); + expect(updateRbSegments).toBeCalledTimes(0); // no rbSegments to update expect(registerSegments).toBeCalledTimes(1); expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); expect(result).toBe(true); }); - test('test with payload', async () => { + test('test with ff payload', async () => { let index = 0; for (const notification of splitNotifications) { const payload = notification.decoded as Pick; const changeNumber = payload.changeNumber; await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); - // fetch not being called + + // fetch and RBSegments.update not being called expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateRbSegments).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(index + 1); // Change number being updated expect(updateSplits.mock.calls[index][2]).toEqual(changeNumber); @@ -214,6 +222,23 @@ describe('splitChangesUpdater', () => { } }); + test('test with rbsegment payload', async () => { + const payload = { name: 'rbsegment', status: 'ACTIVE', changeNumber: 1684329854385, conditions: [] } as unknown as IRBSegment; + const changeNumber = payload.changeNumber; + + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); + + // fetch and Splits.update not being called + expect(fetchSplitChanges).toBeCalledTimes(0); + expect(updateSplits).toBeCalledTimes(0); + + expect(updateRbSegments).toBeCalledTimes(1); + expect(updateRbSegments).toBeCalledWith([payload], [], changeNumber); + + expect(registerSegments).toBeCalledTimes(1); + expect(registerSegments).toBeCalledWith([]); + }); + test('flag sets splits-arrived emission', async () => { const payload = splitNotifications[3].decoded as Pick; const setMocks = [ diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index bb61810d..6f22dccc 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -10,7 +10,7 @@ import { startsWith } from '../../../utils/lang'; import { IN_RULE_BASED_SEGMENT, IN_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; -type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }) => Promise +type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }) => Promise // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment). // Returns a promise that could be rejected. @@ -134,7 +134,7 @@ export function splitChangesUpdaterFactory( * @param noCache - true to revalidate data to fetch * @param till - query param to bypass CDN requests */ - return function splitChangesUpdater(noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }) { + return function splitChangesUpdater(noCache?: boolean, till?: number, updateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }) { /** * @param since - current changeNumber at splitsCache @@ -144,15 +144,15 @@ export function splitChangesUpdaterFactory( const [since, rbSince] = sinces; log.debug(SYNC_SPLITS_FETCH, sinces); const fetcherPromise = Promise.resolve( - splitUpdateNotification ? - isFF(splitUpdateNotification.payload) ? + updateNotification ? + isFF(updateNotification.payload) ? // IFFU edge case: a change to a flag that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet - Promise.resolve(rbSegments.contains(parseSegments(splitUpdateNotification.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { + Promise.resolve(rbSegments.contains(parseSegments(updateNotification.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { return contains ? - { ff: { d: [splitUpdateNotification.payload as ISplit], t: splitUpdateNotification.changeNumber } } : + { ff: { d: [updateNotification.payload as ISplit], t: updateNotification.changeNumber } } : splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator); }) : - { rbs: { d: [splitUpdateNotification.payload as IRBSegment], t: splitUpdateNotification.changeNumber } } : + { rbs: { d: [updateNotification.payload as IRBSegment], t: updateNotification.changeNumber } } : splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((splitChanges: ISplitChangesResponse) => { diff --git a/src/sync/streaming/__tests__/dataMocks.ts b/src/sync/streaming/__tests__/dataMocks.ts index 289843b5..cb7007d8 100644 --- a/src/sync/streaming/__tests__/dataMocks.ts +++ b/src/sync/streaming/__tests__/dataMocks.ts @@ -255,21 +255,21 @@ export const splitNotifications = [ { compression: 0, data: 'eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684329854385,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'off',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]} + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684329854385, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'off', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] } }, { compression: 1, // GZIP data: 'H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684333081259,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'v5',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]} + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684333081259, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'v5', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] } }, { compression: 2, // ZLIB data: 'eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684265694505,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'v5',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]}, + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684265694505, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'v5', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] }, }, { compression: 2, // ZLIB data: 'eJxsUdFu4jAQ/JVqnx3JDjTh/JZCrj2JBh0EqtOBIuNswKqTIMeuxKH8+ykhiKrqiyXvzM7O7lzAGlEUSqbnEyaiRODgGjRAQOXAIQ/puPB96tHHIPQYQ/QmFNErxEgG44DKnI2AQHXtTOI0my6WcXZAmxoUtsTKvil7nNZVoQ5RYdFERh7VBwK5TY60rqWwqq6AM0q/qa8Qc+As/EHZ5HHMCDR9wQ/9kIajcEygscK6BjhEy+nLr008AwLvSuuOVgjdIIEcC+H03RZw2Hg/n88JEJBHUR0wceUeDXAWTAIWPAYsZEFAQOhDDdwnIPslnOk9NcAvNwEOly3IWtdmC3wLe+1wCy0Q2Hh/zNvTV9xg3sFtr5irQe3v5f7twgAOy8V8vlinQKAUVh7RPJvanbrBsi73qurMQpTM7oSrzjueV6hR2tp05E8J39MV1hq1d7YrWWxsZ2cQGYjzeLXK0pcoyRbLLP69juZZuuiyxoPo2oa7ukqYc+JKNEq+XgVmwopucC6sGMSS9etTvAQCH0I7BO7Ttt21BE7C2E8XsN+l06h/CJy25CveH/eGM0rbHQEt9qiHnR62jtKR7N/8wafQ7tr/AQAA//8S4fPB', - decoded: {trafficTypeName:'user',id:'d704f220-0567-11ee-80ee-fa3c6460cd13',name:'NET_CORE_getTreatmentWithConfigAfterArchive',trafficAllocation:100,trafficAllocationSeed:179018541,seed:272707374,status:'ARCHIVED',killed:false,defaultTreatment:'V-FGyN',changeNumber:1686165617166,algo:2,configurations:{'V-FGyN':'{"color":"blue"}','V-YrWB':'{"color":"red"}'},conditions:[{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user',attribute:'test'},matcherType:'LESS_THAN_OR_EQUAL_TO',negate:false,unaryNumericMatcherData:{dataType:'NUMBER',value:20}}]},partitions:[{treatment:'V-FGyN',size:0},{treatment:'V-YrWB',size:100}],label:'test \u003c\u003d 20'}]} + decoded: { trafficTypeName: 'user', id: 'd704f220-0567-11ee-80ee-fa3c6460cd13', name: 'NET_CORE_getTreatmentWithConfigAfterArchive', trafficAllocation: 100, trafficAllocationSeed: 179018541, seed: 272707374, status: 'ARCHIVED', killed: false, defaultTreatment: 'V-FGyN', changeNumber: 1686165617166, algo: 2, configurations: { 'V-FGyN': '{"color":"blue"}', 'V-YrWB': '{"color":"red"}' }, conditions: [{ conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user', attribute: 'test' }, matcherType: 'LESS_THAN_OR_EQUAL_TO', negate: false, unaryNumericMatcherData: { dataType: 'NUMBER', value: 20 } }] }, partitions: [{ treatment: 'V-FGyN', size: 0 }, { treatment: 'V-YrWB', size: 100 }], label: 'test \u003c\u003d 20' }] } } ]; From a242745bccb45904e12007c095f2caf5584baaff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 28 Feb 2025 14:57:15 -0300 Subject: [PATCH 10/62] Types polishing --- src/evaluator/combiners/ifelseif.ts | 2 +- src/evaluator/condition/engineUtils.ts | 2 +- src/evaluator/condition/index.ts | 4 ++-- src/evaluator/index.ts | 14 +++++++------- src/evaluator/types.ts | 4 ++-- src/evaluator/value/sanitize.ts | 2 +- .../__tests__/RBSegmentsCacheAsync.spec.ts | 1 + src/storages/__tests__/RBSegmentsCacheSync.spec.ts | 1 + src/sync/streaming/__tests__/dataMocks.ts | 8 ++++---- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/evaluator/combiners/ifelseif.ts b/src/evaluator/combiners/ifelseif.ts index 68fe5725..b2bfdcd0 100644 --- a/src/evaluator/combiners/ifelseif.ts +++ b/src/evaluator/combiners/ifelseif.ts @@ -35,7 +35,7 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): return undefined; } - function ifElseIfCombiner(key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + function ifElseIfCombiner(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // In Async environments we are going to have async predicates. There is none way to know // before hand so we need to evaluate all the predicates, verify for thenables, and finally, // define how to return the treatment (wrap result into a Promise or not). diff --git a/src/evaluator/condition/engineUtils.ts b/src/evaluator/condition/engineUtils.ts index bacd3b10..398ea6cc 100644 --- a/src/evaluator/condition/engineUtils.ts +++ b/src/evaluator/condition/engineUtils.ts @@ -5,7 +5,7 @@ import { bucket } from '../../utils/murmur3/murmur3'; /** * Get the treatment name given a key, a seed, and the percentage of each treatment. */ -export function getTreatment(log: ILogger, key: string, seed: number, treatments: { getTreatmentFor: (x: number) => string }) { +export function getTreatment(log: ILogger, key: string, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }) { const _bucket = bucket(key, seed); const treatment = treatments.getTreatmentFor(_bucket); diff --git a/src/evaluator/condition/index.ts b/src/evaluator/condition/index.ts index 7ffaef79..4fd6d372 100644 --- a/src/evaluator/condition/index.ts +++ b/src/evaluator/condition/index.ts @@ -7,7 +7,7 @@ import SplitIO from '../../../types/splitio'; import { ILogger } from '../../logger/types'; // Build Evaluation object if and only if matchingResult is true -function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { +function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { if (matchingResult) { const treatment = getTreatment(log, bucketingKey as string, seed, treatments); @@ -24,7 +24,7 @@ function match(log: ILogger, matchingResult: boolean, bucketingKey: string | und // Condition factory export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator { - return function conditionEvaluator(key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + return function conditionEvaluator(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions. if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation as number, (key as SplitIO.SplitKeyObject).bucketingKey as string, trafficAllocationSeed as number)) { diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 2d06ad10..7268962a 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -43,8 +43,8 @@ export function evaluateFeature( if (thenable(parsedSplit)) { return parsedSplit.then((split) => getEvaluation( log, - split, key, + split, attributes, storage, )).catch( @@ -56,8 +56,8 @@ export function evaluateFeature( return getEvaluation( log, - parsedSplit, key, + parsedSplit, attributes, storage, ); @@ -80,13 +80,13 @@ export function evaluateFeatures( } return thenable(parsedSplits) ? - parsedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) + parsedSplits.then(splits => getEvaluations(log, key, splitNames, splits, attributes, storage)) .catch(() => { // Exception on async `getSplits` storage. For example, when the storage is redis or // pluggable and there is a connection issue and we can't retrieve the split to be evaluated return treatmentsException(splitNames); }) : - getEvaluations(log, splitNames, parsedSplits, key, attributes, storage); + getEvaluations(log, key, splitNames, parsedSplits, attributes, storage); } export function evaluateFeaturesByFlagSets( @@ -136,8 +136,8 @@ export function evaluateFeaturesByFlagSets( function getEvaluation( log: ILogger, - splitJSON: ISplit | null, key: SplitIO.SplitKey, + splitJSON: ISplit | null, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, ): MaybeThenable { @@ -172,9 +172,9 @@ function getEvaluation( function getEvaluations( log: ILogger, + key: SplitIO.SplitKey, splitNames: string[], splits: Record, - key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync, ): MaybeThenable> { @@ -183,8 +183,8 @@ function getEvaluations( splitNames.forEach(splitName => { const evaluation = getEvaluation( log, - splits[splitName], key, + splits[splitName], attributes, storage ); diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 79bcda18..7d94b31b 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -4,7 +4,7 @@ import SplitIO from '../../types/splitio'; import { ILogger } from '../logger/types'; export interface IDependencyMatcherValue { - key: SplitIO.SplitKey, + key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes } @@ -29,6 +29,6 @@ export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDi export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable -export type IEvaluator = (key: SplitIO.SplitKey, seed: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable +export type IEvaluator = (key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable export type IMatcher = (...args: any) => MaybeThenable diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 9fbf74f7..b2af6f21 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -41,7 +41,7 @@ function sanitizeBoolean(val: any): boolean | undefined { return undefined; } -function dependencyProcessor(sanitizedValue: string, attributes?: SplitIO.Attributes): IDependencyMatcherValue { +function dependencyProcessor(sanitizedValue: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes): IDependencyMatcherValue { return { key: sanitizedValue, attributes diff --git a/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts index 8c2d6678..2e222f32 100644 --- a/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheAsync.spec.ts @@ -49,6 +49,7 @@ describe.each([{ cache: cacheInRedis, wrapper: redisClient }, { cache: cachePlug test('contains should check for segment existence correctly', async () => { await cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + expect(await cache.contains(new Set())).toBe(true); expect(await cache.contains(new Set([rbSegment.name]))).toBe(true); expect(await cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); expect(await cache.contains(new Set(['nonexistent']))).toBe(false); diff --git a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts index 1975b35e..ad47af3c 100644 --- a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts @@ -51,6 +51,7 @@ describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Me test('contains should check for segment existence correctly', () => { cache.update([rbSegment, rbSegmentWithInSegmentMatcher], [], 1); + expect(cache.contains(new Set())).toBe(true); expect(cache.contains(new Set([rbSegment.name]))).toBe(true); expect(cache.contains(new Set([rbSegment.name, rbSegmentWithInSegmentMatcher.name]))).toBe(true); expect(cache.contains(new Set(['nonexistent']))).toBe(false); diff --git a/src/sync/streaming/__tests__/dataMocks.ts b/src/sync/streaming/__tests__/dataMocks.ts index 289843b5..cb7007d8 100644 --- a/src/sync/streaming/__tests__/dataMocks.ts +++ b/src/sync/streaming/__tests__/dataMocks.ts @@ -255,21 +255,21 @@ export const splitNotifications = [ { compression: 0, data: 'eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684329854385,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'off',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]} + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684329854385, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'off', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] } }, { compression: 1, // GZIP data: 'H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684333081259,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'v5',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]} + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684333081259, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'v5', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] } }, { compression: 2, // ZLIB data: 'eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=', - decoded: {trafficTypeName:'user',id:'d431cdd0-b0be-11ea-8a80-1660ada9ce39',name:'mauro_java',trafficAllocation:100,trafficAllocationSeed:-92391491,seed:-1769377604,status:'ACTIVE',killed:false,defaultTreatment:'off',changeNumber:1684265694505,algo:2,configurations:{},conditions:[{conditionType:'WHITELIST',matcherGroup:{combiner:'AND',matchers:[{matcherType:'WHITELIST',negate:false,whitelistMatcherData:{whitelist:['admin','mauro','nico']}}]},partitions:[{treatment:'v5',size:100}],label:'whitelisted'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'IN_SEGMENT',negate:false,userDefinedSegmentMatcherData:{segmentName:'maur-2'}}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'in segment maur-2'},{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user'},matcherType:'ALL_KEYS',negate:false}]},partitions:[{treatment:'on',size:0},{treatment:'off',size:100},{treatment:'V4',size:0},{treatment:'v5',size:0}],label:'default rule'}]}, + decoded: { trafficTypeName: 'user', id: 'd431cdd0-b0be-11ea-8a80-1660ada9ce39', name: 'mauro_java', trafficAllocation: 100, trafficAllocationSeed: -92391491, seed: -1769377604, status: 'ACTIVE', killed: false, defaultTreatment: 'off', changeNumber: 1684265694505, algo: 2, configurations: {}, conditions: [{ conditionType: 'WHITELIST', matcherGroup: { combiner: 'AND', matchers: [{ matcherType: 'WHITELIST', negate: false, whitelistMatcherData: { whitelist: ['admin', 'mauro', 'nico'] } }] }, partitions: [{ treatment: 'v5', size: 100 }], label: 'whitelisted' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'IN_SEGMENT', negate: false, userDefinedSegmentMatcherData: { segmentName: 'maur-2' } }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'in segment maur-2' }, { conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user' }, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ treatment: 'on', size: 0 }, { treatment: 'off', size: 100 }, { treatment: 'V4', size: 0 }, { treatment: 'v5', size: 0 }], label: 'default rule' }] }, }, { compression: 2, // ZLIB data: 'eJxsUdFu4jAQ/JVqnx3JDjTh/JZCrj2JBh0EqtOBIuNswKqTIMeuxKH8+ykhiKrqiyXvzM7O7lzAGlEUSqbnEyaiRODgGjRAQOXAIQ/puPB96tHHIPQYQ/QmFNErxEgG44DKnI2AQHXtTOI0my6WcXZAmxoUtsTKvil7nNZVoQ5RYdFERh7VBwK5TY60rqWwqq6AM0q/qa8Qc+As/EHZ5HHMCDR9wQ/9kIajcEygscK6BjhEy+nLr008AwLvSuuOVgjdIIEcC+H03RZw2Hg/n88JEJBHUR0wceUeDXAWTAIWPAYsZEFAQOhDDdwnIPslnOk9NcAvNwEOly3IWtdmC3wLe+1wCy0Q2Hh/zNvTV9xg3sFtr5irQe3v5f7twgAOy8V8vlinQKAUVh7RPJvanbrBsi73qurMQpTM7oSrzjueV6hR2tp05E8J39MV1hq1d7YrWWxsZ2cQGYjzeLXK0pcoyRbLLP69juZZuuiyxoPo2oa7ukqYc+JKNEq+XgVmwopucC6sGMSS9etTvAQCH0I7BO7Ttt21BE7C2E8XsN+l06h/CJy25CveH/eGM0rbHQEt9qiHnR62jtKR7N/8wafQ7tr/AQAA//8S4fPB', - decoded: {trafficTypeName:'user',id:'d704f220-0567-11ee-80ee-fa3c6460cd13',name:'NET_CORE_getTreatmentWithConfigAfterArchive',trafficAllocation:100,trafficAllocationSeed:179018541,seed:272707374,status:'ARCHIVED',killed:false,defaultTreatment:'V-FGyN',changeNumber:1686165617166,algo:2,configurations:{'V-FGyN':'{"color":"blue"}','V-YrWB':'{"color":"red"}'},conditions:[{conditionType:'ROLLOUT',matcherGroup:{combiner:'AND',matchers:[{keySelector:{trafficType:'user',attribute:'test'},matcherType:'LESS_THAN_OR_EQUAL_TO',negate:false,unaryNumericMatcherData:{dataType:'NUMBER',value:20}}]},partitions:[{treatment:'V-FGyN',size:0},{treatment:'V-YrWB',size:100}],label:'test \u003c\u003d 20'}]} + decoded: { trafficTypeName: 'user', id: 'd704f220-0567-11ee-80ee-fa3c6460cd13', name: 'NET_CORE_getTreatmentWithConfigAfterArchive', trafficAllocation: 100, trafficAllocationSeed: 179018541, seed: 272707374, status: 'ARCHIVED', killed: false, defaultTreatment: 'V-FGyN', changeNumber: 1686165617166, algo: 2, configurations: { 'V-FGyN': '{"color":"blue"}', 'V-YrWB': '{"color":"red"}' }, conditions: [{ conditionType: 'ROLLOUT', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: { trafficType: 'user', attribute: 'test' }, matcherType: 'LESS_THAN_OR_EQUAL_TO', negate: false, unaryNumericMatcherData: { dataType: 'NUMBER', value: 20 } }] }, partitions: [{ treatment: 'V-FGyN', size: 0 }, { treatment: 'V-YrWB', size: 100 }], label: 'test \u003c\u003d 20' }] } } ]; From bef92522ad028b0209cfa722506470893e1a1b2a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Mar 2025 16:26:10 -0300 Subject: [PATCH 11/62] Types polishing --- src/evaluator/types.ts | 2 +- src/evaluator/value/sanitize.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index 7d94b31b..db0d4e28 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -4,7 +4,7 @@ import SplitIO from '../../types/splitio'; import { ILogger } from '../logger/types'; export interface IDependencyMatcherValue { - key: SplitIO.SplitKeyObject, + key: SplitIO.SplitKey, attributes?: SplitIO.Attributes } diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index b2af6f21..630a4b38 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -41,7 +41,7 @@ function sanitizeBoolean(val: any): boolean | undefined { return undefined; } -function dependencyProcessor(sanitizedValue: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes): IDependencyMatcherValue { +function dependencyProcessor(sanitizedValue: SplitIO.SplitKey, attributes?: SplitIO.Attributes): IDependencyMatcherValue { return { key: sanitizedValue, attributes From 680b82dcc2b5d73f2c99bd9ebec8d6d53852cc31 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Mar 2025 17:29:29 -0300 Subject: [PATCH 12/62] Update fetchSplitChanges method with rbSince param --- src/services/__tests__/splitApi.spec.ts | 8 ++++---- src/services/splitApi.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index d935c6de..196266a3 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -40,10 +40,10 @@ describe('splitApi', () => { assertHeaders(settings, headers); expect(url).toBe('sdk/segmentChanges/segmentName?since=-1&till=90'); - splitApi.fetchSplitChanges(-1, false, 100); + splitApi.fetchSplitChanges(-1, false, 100, -1); [url, { headers }] = fetchMock.mock.calls[3]; assertHeaders(settings, headers); - expect(url).toBe(expecteFlagsUrl(-1, 100, settings.validateFilters || false, settings)); + expect(url).toBe(expectedFlagsUrl(-1, 100, settings.validateFilters || false, settings, -1)); splitApi.postEventsBulk('fake-body'); assertHeaders(settings, fetchMock.mock.calls[4][1].headers); @@ -66,9 +66,9 @@ describe('splitApi', () => { fetchMock.mockClear(); - function expecteFlagsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings) { + function expectedFlagsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings, rbSince?: number) { const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; - return `sdk/splitChanges?s=1.1&since=${since}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`; + return `sdk/splitChanges?s=1.1&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`; } }); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 0b86b58d..b7163b93 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -53,8 +53,8 @@ export function splitApiFactory( return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN)); }, - fetchSplitChanges(since: number, noCache?: boolean, till?: number) { - const url = `${urls.sdk}/splitChanges?s=${flagSpecVersion}&since=${since}${filterQueryString || ''}${till ? '&till=' + till : ''}`; + fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) { + const url = `${urls.sdk}/splitChanges?s=${flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) .catch((err) => { if (err.statusCode === 414) settings.log.error(ERROR_TOO_MANY_SETS); From 1cecdf8907c77af57a9b55e4c777ae9b0dbed23e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Mar 2025 17:34:50 -0300 Subject: [PATCH 13/62] Handle RBSEGMENT_UPDATE notification --- src/logger/messages/warn.ts | 2 +- src/sync/polling/types.ts | 4 +- .../polling/updaters/segmentChangesUpdater.ts | 2 +- src/sync/streaming/SSEHandler/index.ts | 3 +- src/sync/streaming/SSEHandler/types.ts | 4 +- .../UpdateWorkers/SplitsUpdateWorker.ts | 159 ++++++++++-------- .../__tests__/SplitsUpdateWorker.spec.ts | 8 +- src/sync/streaming/constants.ts | 1 + src/sync/streaming/parseUtils.ts | 4 +- src/sync/streaming/pushManager.ts | 22 +-- src/sync/streaming/types.ts | 5 +- 11 files changed, 116 insertions(+), 98 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 52487f95..2b0b85ca 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -33,7 +33,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_SDK_KEY, c.LOG_PREFIX_SETTINGS + ': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'], [c.STREAMING_PARSING_MEMBERSHIPS_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching Memberships due to an error processing %s notification: %s'], - [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing SPLIT_UPDATE notification: %s'], + [c.STREAMING_PARSING_SPLIT_UPDATE, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching SplitChanges due to an error processing %s notification: %s'], [c.WARN_INVALID_FLAGSET, '%s: you passed %s, flag set must adhere to the regular expressions %s. This means a flag set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. %s was discarded.'], [c.WARN_LOWERCASE_FLAGSET, '%s: flag set %s should be all lowercase - converting string to lowercase.'], [c.WARN_FLAGSET_WITHOUT_FLAGS, '%s: you passed %s flag set that does not contain cached feature flag names. Please double check what flag sets are in use in the Split user interface.'], diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index c542fec9..4ff29c83 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -1,10 +1,10 @@ -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; import { IReadinessManager } from '../../readiness/types'; import { IStorageSync } from '../../storages/types'; import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../streaming/types'; import { ITask, ISyncTask } from '../types'; -export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit, changeNumber: number }], boolean> { } +export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }], boolean> { } export interface ISegmentsSyncTask extends ISyncTask<[fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number], boolean> { } diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index c1009077..679c7f6e 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -51,7 +51,7 @@ export function segmentChangesUpdaterFactory( * Returned promise will not be rejected. * * @param fetchOnlyNew - if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1. - * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. + * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE or RBSEGMENT_UPDATE notifications. * @param segmentName - segment name to fetch. By passing `undefined` it fetches the list of segments registered at the storage * @param noCache - true to revalidate data to fetch on a SEGMENT_UPDATE notifications. * @param till - till target for the provided segmentName, for CDN bypass. diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index fbbe329c..6a8d99fc 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,6 +1,6 @@ import { errorParser, messageParser } from './NotificationParser'; import { notificationKeeperFactory } from './NotificationKeeper'; -import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE } from '../constants'; +import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RBSEGMENT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; @@ -84,6 +84,7 @@ export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, case MEMBERSHIPS_MS_UPDATE: case MEMBERSHIPS_LS_UPDATE: case SPLIT_KILL: + case RBSEGMENT_UPDATE: pushEmitter.emit(parsedData.type, parsedData); break; diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index 192583c3..48e9f2c6 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -1,5 +1,5 @@ import { ControlType } from '../constants'; -import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../types'; +import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RBSEGMENT_UPDATE } from '../types'; export enum Compression { None = 0, @@ -42,7 +42,7 @@ export interface ISegmentUpdateData { } export interface ISplitUpdateData { - type: SPLIT_UPDATE, + type: SPLIT_UPDATE | RBSEGMENT_UPDATE, changeNumber: number, pcn?: number, d?: string, diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index 580fe9cb..8e49474a 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -1,4 +1,5 @@ -import { ISplit } from '../../../dtos/types'; +import { IRBSegment, ISplit } from '../../../dtos/types'; +import { STREAMING_PARSING_SPLIT_UPDATE } from '../../../logger/constants'; import { ILogger } from '../../../logger/types'; import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ISplitsEventEmitter } from '../../../readiness/types'; @@ -7,6 +8,8 @@ import { ITelemetryTracker } from '../../../trackers/types'; import { Backoff } from '../../../utils/Backoff'; import { SPLITS } from '../../../utils/constants'; import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types'; +import { RBSEGMENT_UPDATE } from '../constants'; +import { parseFFUpdatePayload } from '../parseUtils'; import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types'; import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT, FETCH_BACKOFF_MAX_RETRIES } from './constants'; import { IUpdateWorker } from './types'; @@ -14,87 +17,111 @@ import { IUpdateWorker } from './types'; /** * SplitsUpdateWorker factory */ -export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData, payload?: ISplit]> & { killSplit(event: ISplitKillData): void } { +export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killSplit(event: ISplitKillData): void } { - let maxChangeNumber = 0; - let handleNewEvent = false; - let isHandlingEvent: boolean; - let cdnBypass: boolean; - let payload: ISplit | undefined; - const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT); + function SplitsUpdateWorker() { + let maxChangeNumber = 0; + let handleNewEvent = false; + let isHandlingEvent: boolean; + let cdnBypass: boolean; + let payload: ISplit | IRBSegment | undefined; + const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT); - function __handleSplitUpdateCall() { - isHandlingEvent = true; - if (maxChangeNumber > splitsCache.getChangeNumber()) { - handleNewEvent = false; - const splitUpdateNotification = payload ? { payload, changeNumber: maxChangeNumber } : undefined; - // fetch splits revalidating data if cached - splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, splitUpdateNotification).then(() => { - if (!isHandlingEvent) return; // halt if `stop` has been called - if (handleNewEvent) { - __handleSplitUpdateCall(); - } else { - if (splitUpdateNotification) telemetryTracker.trackUpdatesFromSSE(SPLITS); - // fetch new registered segments for server-side API. Not retrying on error - if (segmentsSyncTask) segmentsSyncTask.execute(true); + function __handleSplitUpdateCall() { + isHandlingEvent = true; + if (maxChangeNumber > splitsCache.getChangeNumber()) { + handleNewEvent = false; + const splitUpdateNotification = payload ? { payload, changeNumber: maxChangeNumber } : undefined; + // fetch splits revalidating data if cached + splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, splitUpdateNotification).then(() => { + if (!isHandlingEvent) return; // halt if `stop` has been called + if (handleNewEvent) { + __handleSplitUpdateCall(); + } else { + if (splitUpdateNotification) telemetryTracker.trackUpdatesFromSSE(SPLITS); + // fetch new registered segments for server-side API. Not retrying on error + if (segmentsSyncTask) segmentsSyncTask.execute(true); - const attempts = backoff.attempts + 1; + const attempts = backoff.attempts + 1; - if (maxChangeNumber <= splitsCache.getChangeNumber()) { - log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); - isHandlingEvent = false; - return; - } + if (maxChangeNumber <= splitsCache.getChangeNumber()) { + log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); + isHandlingEvent = false; + return; + } - if (attempts < FETCH_BACKOFF_MAX_RETRIES) { - backoff.scheduleCall(); - return; - } + if (attempts < FETCH_BACKOFF_MAX_RETRIES) { + backoff.scheduleCall(); + return; + } - if (cdnBypass) { - log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`); - isHandlingEvent = false; - } else { - backoff.reset(); - cdnBypass = true; - __handleSplitUpdateCall(); + if (cdnBypass) { + log.debug(`No changes fetched after ${attempts} attempts with CDN bypassed.`); + isHandlingEvent = false; + } else { + backoff.reset(); + cdnBypass = true; + __handleSplitUpdateCall(); + } } - } - }); - } else { - isHandlingEvent = false; + }); + } else { + isHandlingEvent = false; + } } - } - /** - * Invoked by NotificationProcessor on SPLIT_UPDATE event - * - * @param changeNumber - change number of the SPLIT_UPDATE notification - */ - function put({ changeNumber, pcn }: ISplitUpdateData, _payload?: ISplit) { - const currentChangeNumber = splitsCache.getChangeNumber(); + return { + /** + * Invoked by NotificationProcessor on SPLIT_UPDATE or RBSEGMENT_UPDATE event + * + * @param changeNumber - change number of the notification + */ + put({ changeNumber, pcn }: ISplitUpdateData, _payload?: ISplit | IRBSegment) { + const currentChangeNumber = splitsCache.getChangeNumber(); - if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return; + if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return; - maxChangeNumber = changeNumber; - handleNewEvent = true; - cdnBypass = false; - payload = undefined; + maxChangeNumber = changeNumber; + handleNewEvent = true; + cdnBypass = false; + payload = undefined; - if (_payload && currentChangeNumber === pcn) { - payload = _payload; - } + if (_payload && currentChangeNumber === pcn) { + payload = _payload; + } - if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall(); - backoff.reset(); + if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall(); + backoff.reset(); + }, + stop() { + isHandlingEvent = false; + backoff.reset(); + } + }; } + const ff = SplitsUpdateWorker(); + const rbs = SplitsUpdateWorker(); + return { - put, + put(parsedData) { + if (parsedData.d && parsedData.c !== undefined) { + try { + const payload = parseFFUpdatePayload(parsedData.c, parsedData.d); + if (payload) { + (parsedData.type === RBSEGMENT_UPDATE ? rbs : ff).put(parsedData, payload); + return; + } + } catch (e) { + log.warn(STREAMING_PARSING_SPLIT_UPDATE, [parsedData.type, e]); + } + } + (parsedData.type === RBSEGMENT_UPDATE ? rbs : ff).put(parsedData); + }, /** * Invoked by NotificationProcessor on SPLIT_KILL event * - * @param changeNumber - change number of the SPLIT_UPDATE notification + * @param changeNumber - change number of the notification * @param splitName - name of split to kill * @param defaultTreatment - default treatment value */ @@ -104,12 +131,12 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true); } // queues the SplitChanges fetch (only if changeNumber is newer) - put({ changeNumber } as ISplitUpdateData); + ff.put({ changeNumber } as ISplitUpdateData); }, stop() { - isHandlingEvent = false; - backoff.reset(); + ff.stop(); + rbs.stop(); } }; } diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index 4de69ca0..735c7ee0 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -219,7 +219,7 @@ describe('SplitsUpdateWorker', () => { const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); const payload = notification.decoded; const changeNumber = payload.changeNumber; - splitUpdateWorker.put({ changeNumber, pcn }, payload); // queued + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); // queued expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, { changeNumber, payload }]); }); @@ -237,7 +237,7 @@ describe('SplitsUpdateWorker', () => { let splitsSyncTask = splitsSyncTaskMock(cache); let splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); - splitUpdateWorker.put({ changeNumber, pcn }, notification.decoded); + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); splitsSyncTask.execute.mockClear(); @@ -250,7 +250,7 @@ describe('SplitsUpdateWorker', () => { splitsSyncTask = splitsSyncTaskMock(cache); splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); - splitUpdateWorker.put({ changeNumber, pcn }, notification.decoded); + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); splitsSyncTask.execute.mockClear(); @@ -263,7 +263,7 @@ describe('SplitsUpdateWorker', () => { splitsSyncTask = splitsSyncTaskMock(cache); splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); - splitUpdateWorker.put({ changeNumber, pcn }, notification.decoded); + splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, { payload: notification.decoded, changeNumber }]); diff --git a/src/sync/streaming/constants.ts b/src/sync/streaming/constants.ts index ed958ee7..1aaa10e4 100644 --- a/src/sync/streaming/constants.ts +++ b/src/sync/streaming/constants.ts @@ -30,6 +30,7 @@ export const MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export const SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export const SPLIT_KILL = 'SPLIT_KILL'; export const SPLIT_UPDATE = 'SPLIT_UPDATE'; +export const RBSEGMENT_UPDATE = 'RBSEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export const CONTROL = 'CONTROL'; diff --git a/src/sync/streaming/parseUtils.ts b/src/sync/streaming/parseUtils.ts index 97fde935..a34f2dc9 100644 --- a/src/sync/streaming/parseUtils.ts +++ b/src/sync/streaming/parseUtils.ts @@ -2,7 +2,7 @@ import { algorithms } from '../../utils/decompress'; import { decodeFromBase64 } from '../../utils/base64'; import { hash } from '../../utils/murmur3/murmur3'; import { Compression, IMembershipMSUpdateData, KeyList } from './SSEHandler/types'; -import { ISplit } from '../../dtos/types'; +import { IRBSegment, ISplit } from '../../dtos/types'; const GZIP = 1; const ZLIB = 2; @@ -82,7 +82,7 @@ export function isInBitmap(bitmap: Uint8Array, hash64hex: string) { /** * Parse feature flags notifications for instant feature flag updates */ -export function parseFFUpdatePayload(compression: Compression, data: string): ISplit | undefined { +export function parseFFUpdatePayload(compression: Compression, data: string): ISplit | IRBSegment | undefined { return compression > 0 ? parseKeyList(data, compression, false) : JSON.parse(decodeFromBase64(data)); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index c87f4945..8dade79f 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -11,10 +11,10 @@ import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; import { getMatching } from '../../utils/key'; -import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; -import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE, STREAMING_PARSING_SPLIT_UPDATE } from '../../logger/constants'; +import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RBSEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; +import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE } from '../../logger/constants'; import { IMembershipMSUpdateData, IMembershipLSUpdateData, KeyList, UpdateStrategy } from './SSEHandler/types'; -import { getDelay, isInBitmap, parseBitmap, parseFFUpdatePayload, parseKeyList } from './parseUtils'; +import { getDelay, isInBitmap, parseBitmap, parseKeyList } from './parseUtils'; import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; import { IAuthTokenPushEnabled } from './AuthClient/types'; import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants'; @@ -219,20 +219,8 @@ export function pushManagerFactory( /** Functions related to synchronization (Queues and Workers in the spec) */ pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); - pushEmitter.on(SPLIT_UPDATE, (parsedData) => { - if (parsedData.d && parsedData.c !== undefined) { - try { - const payload = parseFFUpdatePayload(parsedData.c, parsedData.d); - if (payload) { - splitsUpdateWorker.put(parsedData, payload); - return; - } - } catch (e) { - log.warn(STREAMING_PARSING_SPLIT_UPDATE, [e]); - } - } - splitsUpdateWorker.put(parsedData); - }); + pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); + pushEmitter.on(RBSEGMENT_UPDATE, splitsUpdateWorker.put); function handleMySegmentsUpdate(parsedData: IMembershipMSUpdateData | IMembershipLSUpdateData) { switch (parsedData.u) { diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index ec80781e..7ad46349 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -16,18 +16,19 @@ export type MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export type SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export type SPLIT_KILL = 'SPLIT_KILL'; export type SPLIT_UPDATE = 'SPLIT_UPDATE'; +export type RBSEGMENT_UPDATE = 'RBSEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export type CONTROL = 'CONTROL'; export type OCCUPANCY = 'OCCUPANCY'; -export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | ControlType.STREAMING_RESET +export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | RBSEGMENT_UPDATE | ControlType.STREAMING_RESET type IParsedData = T extends MEMBERSHIPS_MS_UPDATE ? IMembershipMSUpdateData : T extends MEMBERSHIPS_LS_UPDATE ? IMembershipLSUpdateData : T extends SEGMENT_UPDATE ? ISegmentUpdateData : - T extends SPLIT_UPDATE ? ISplitUpdateData : + T extends SPLIT_UPDATE | RBSEGMENT_UPDATE ? ISplitUpdateData : T extends SPLIT_KILL ? ISplitKillData : INotificationData; /** From ebc547aabbd3a40ff41bf481a0d199d3bd7e7bec Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Mar 2025 17:41:22 -0300 Subject: [PATCH 14/62] Unit test --- .../mocks/message.RBSEGMENT_UPDATE.1457552620999.json | 4 ++++ src/sync/streaming/SSEHandler/__tests__/index.spec.ts | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json diff --git a/src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json b/src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json new file mode 100644 index 00000000..a313d48f --- /dev/null +++ b/src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"id\":\"mc4i3NENoA:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:MTM2ODE2NDMxNA==\",\"timestamp\":1457552621899,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw==_splits\",\"data\":\"{\\\"type\\\":\\\"RBSEGMENT_UPDATE\\\",\\\"changeNumber\\\":1457552620999}\"}" +} \ No newline at end of file diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index e85b22d8..5d54074b 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -1,10 +1,11 @@ // @ts-nocheck import { SSEHandlerFactory } from '..'; -import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, ControlType } from '../../constants'; +import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RBSEGMENT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, ControlType } from '../../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; // update messages import splitUpdateMessage from '../../../../__tests__/mocks/message.SPLIT_UPDATE.1457552620999.json'; +import rbsegmentUpdateMessage from '../../../../__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json'; import splitKillMessage from '../../../../__tests__/mocks/message.SPLIT_KILL.1457552650000.json'; import segmentUpdateMessage from '../../../../__tests__/mocks/message.SEGMENT_UPDATE.1457552640000.json'; @@ -144,6 +145,10 @@ test('`handlerMessage` for update notifications (NotificationProcessor) and stre sseHandler.handleMessage(splitUpdateMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, ...expectedParams); // must emit SPLIT_UPDATE with the message change number + expectedParams = [{ type: 'RBSEGMENT_UPDATE', changeNumber: 1457552620999 }]; + sseHandler.handleMessage(rbsegmentUpdateMessage); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(RBSEGMENT_UPDATE, ...expectedParams); // must emit RBSEGMENT_UPDATE with the message change number + expectedParams = [{ type: 'SPLIT_KILL', changeNumber: 1457552650000, splitName: 'whitelist', defaultTreatment: 'not_allowed' }]; sseHandler.handleMessage(splitKillMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_KILL, ...expectedParams); // must emit SPLIT_KILL with the message change number, split name and default treatment From 598c0d400bef6b4332fe84eb2ff684944e65f0a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Mar 2025 17:59:20 -0300 Subject: [PATCH 15/62] Update flag spec version --- src/utils/constants/index.ts | 2 +- src/utils/settingsValidation/__tests__/settings.mocks.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 24d3d31f..eab99bbb 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -104,7 +104,7 @@ export const DISABLED = 0; export const ENABLED = 1; export const PAUSED = 2; -export const FLAG_SPEC_VERSION = '1.2'; +export const FLAG_SPEC_VERSION = '1.3'; // Matcher types export const IN_SEGMENT = 'IN_SEGMENT'; diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index a2a3fb14..bc55891f 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -67,7 +67,7 @@ export const fullSettings: ISettings = { groupedFilters: { bySet: [], byName: [], byPrefix: [] }, }, enabled: true, - flagSpecVersion: '1.2' + flagSpecVersion: '1.3' }, version: 'jest', runtime: { From b6f5514b7db9a34005eb57c2704c4679101a116c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 Mar 2025 17:06:40 -0300 Subject: [PATCH 16/62] Rename notification type --- ....json => message.RB_SEGMENT_UPDATE.1457552620999.json} | 2 +- src/sync/polling/updaters/segmentChangesUpdater.ts | 2 +- src/sync/streaming/SSEHandler/__tests__/index.spec.ts | 8 ++++---- src/sync/streaming/SSEHandler/index.ts | 4 ++-- src/sync/streaming/SSEHandler/types.ts | 4 ++-- src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts | 8 ++++---- src/sync/streaming/constants.ts | 2 +- src/sync/streaming/pushManager.ts | 4 ++-- src/sync/streaming/types.ts | 6 +++--- 9 files changed, 20 insertions(+), 20 deletions(-) rename src/__tests__/mocks/{message.RBSEGMENT_UPDATE.1457552620999.json => message.RB_SEGMENT_UPDATE.1457552620999.json} (70%) diff --git a/src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json b/src/__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json similarity index 70% rename from src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json rename to src/__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json index a313d48f..bd994511 100644 --- a/src/__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json +++ b/src/__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json @@ -1,4 +1,4 @@ { "type": "message", - "data": "{\"id\":\"mc4i3NENoA:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:MTM2ODE2NDMxNA==\",\"timestamp\":1457552621899,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw==_splits\",\"data\":\"{\\\"type\\\":\\\"RBSEGMENT_UPDATE\\\",\\\"changeNumber\\\":1457552620999}\"}" + "data": "{\"id\":\"mc4i3NENoA:0:0\",\"clientId\":\"NDEzMTY5Mzg0MA==:MTM2ODE2NDMxNA==\",\"timestamp\":1457552621899,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw==_splits\",\"data\":\"{\\\"type\\\":\\\"RB_SEGMENT_UPDATE\\\",\\\"changeNumber\\\":1457552620999}\"}" } \ No newline at end of file diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 679c7f6e..ab951b24 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -51,7 +51,7 @@ export function segmentChangesUpdaterFactory( * Returned promise will not be rejected. * * @param fetchOnlyNew - if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1. - * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE or RBSEGMENT_UPDATE notifications. + * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE or RB_SEGMENT_UPDATE notifications. * @param segmentName - segment name to fetch. By passing `undefined` it fetches the list of segments registered at the storage * @param noCache - true to revalidate data to fetch on a SEGMENT_UPDATE notifications. * @param till - till target for the provided segmentName, for CDN bypass. diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index 5d54074b..90bdc8cd 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -1,11 +1,11 @@ // @ts-nocheck import { SSEHandlerFactory } from '..'; -import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RBSEGMENT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, ControlType } from '../../constants'; +import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RB_SEGMENT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, ControlType } from '../../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; // update messages import splitUpdateMessage from '../../../../__tests__/mocks/message.SPLIT_UPDATE.1457552620999.json'; -import rbsegmentUpdateMessage from '../../../../__tests__/mocks/message.RBSEGMENT_UPDATE.1457552620999.json'; +import rbsegmentUpdateMessage from '../../../../__tests__/mocks/message.RB_SEGMENT_UPDATE.1457552620999.json'; import splitKillMessage from '../../../../__tests__/mocks/message.SPLIT_KILL.1457552650000.json'; import segmentUpdateMessage from '../../../../__tests__/mocks/message.SEGMENT_UPDATE.1457552640000.json'; @@ -145,9 +145,9 @@ test('`handlerMessage` for update notifications (NotificationProcessor) and stre sseHandler.handleMessage(splitUpdateMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, ...expectedParams); // must emit SPLIT_UPDATE with the message change number - expectedParams = [{ type: 'RBSEGMENT_UPDATE', changeNumber: 1457552620999 }]; + expectedParams = [{ type: 'RB_SEGMENT_UPDATE', changeNumber: 1457552620999 }]; sseHandler.handleMessage(rbsegmentUpdateMessage); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(RBSEGMENT_UPDATE, ...expectedParams); // must emit RBSEGMENT_UPDATE with the message change number + expect(pushEmitter.emit).toHaveBeenLastCalledWith(RB_SEGMENT_UPDATE, ...expectedParams); // must emit RB_SEGMENT_UPDATE with the message change number expectedParams = [{ type: 'SPLIT_KILL', changeNumber: 1457552650000, splitName: 'whitelist', defaultTreatment: 'not_allowed' }]; sseHandler.handleMessage(splitKillMessage); diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index 6a8d99fc..f7a39c8b 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,6 +1,6 @@ import { errorParser, messageParser } from './NotificationParser'; import { notificationKeeperFactory } from './NotificationKeeper'; -import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RBSEGMENT_UPDATE } from '../constants'; +import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; @@ -84,7 +84,7 @@ export function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventEmitter, case MEMBERSHIPS_MS_UPDATE: case MEMBERSHIPS_LS_UPDATE: case SPLIT_KILL: - case RBSEGMENT_UPDATE: + case RB_SEGMENT_UPDATE: pushEmitter.emit(parsedData.type, parsedData); break; diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index 48e9f2c6..a39b8000 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -1,5 +1,5 @@ import { ControlType } from '../constants'; -import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RBSEGMENT_UPDATE } from '../types'; +import { SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE, RB_SEGMENT_UPDATE } from '../types'; export enum Compression { None = 0, @@ -42,7 +42,7 @@ export interface ISegmentUpdateData { } export interface ISplitUpdateData { - type: SPLIT_UPDATE | RBSEGMENT_UPDATE, + type: SPLIT_UPDATE | RB_SEGMENT_UPDATE, changeNumber: number, pcn?: number, d?: string, diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index 8e49474a..8deeee4f 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -8,7 +8,7 @@ import { ITelemetryTracker } from '../../../trackers/types'; import { Backoff } from '../../../utils/Backoff'; import { SPLITS } from '../../../utils/constants'; import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types'; -import { RBSEGMENT_UPDATE } from '../constants'; +import { RB_SEGMENT_UPDATE } from '../constants'; import { parseFFUpdatePayload } from '../parseUtils'; import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types'; import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT, FETCH_BACKOFF_MAX_RETRIES } from './constants'; @@ -72,7 +72,7 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, return { /** - * Invoked by NotificationProcessor on SPLIT_UPDATE or RBSEGMENT_UPDATE event + * Invoked by NotificationProcessor on SPLIT_UPDATE or RB_SEGMENT_UPDATE event * * @param changeNumber - change number of the notification */ @@ -109,14 +109,14 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, try { const payload = parseFFUpdatePayload(parsedData.c, parsedData.d); if (payload) { - (parsedData.type === RBSEGMENT_UPDATE ? rbs : ff).put(parsedData, payload); + (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData, payload); return; } } catch (e) { log.warn(STREAMING_PARSING_SPLIT_UPDATE, [parsedData.type, e]); } } - (parsedData.type === RBSEGMENT_UPDATE ? rbs : ff).put(parsedData); + (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData); }, /** * Invoked by NotificationProcessor on SPLIT_KILL event diff --git a/src/sync/streaming/constants.ts b/src/sync/streaming/constants.ts index 1aaa10e4..dd230a61 100644 --- a/src/sync/streaming/constants.ts +++ b/src/sync/streaming/constants.ts @@ -30,7 +30,7 @@ export const MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export const SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export const SPLIT_KILL = 'SPLIT_KILL'; export const SPLIT_UPDATE = 'SPLIT_UPDATE'; -export const RBSEGMENT_UPDATE = 'RBSEGMENT_UPDATE'; +export const RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export const CONTROL = 'CONTROL'; diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 8dade79f..902d6522 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -11,7 +11,7 @@ import { authenticateFactory, hashUserKey } from './AuthClient'; import { forOwn } from '../../utils/lang'; import { SSEClient } from './SSEClient'; import { getMatching } from '../../utils/key'; -import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RBSEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; +import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RB_SEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE } from '../../logger/constants'; import { IMembershipMSUpdateData, IMembershipLSUpdateData, KeyList, UpdateStrategy } from './SSEHandler/types'; import { getDelay, isInBitmap, parseBitmap, parseKeyList } from './parseUtils'; @@ -220,7 +220,7 @@ export function pushManagerFactory( pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); - pushEmitter.on(RBSEGMENT_UPDATE, splitsUpdateWorker.put); + pushEmitter.on(RB_SEGMENT_UPDATE, splitsUpdateWorker.put); function handleMySegmentsUpdate(parsedData: IMembershipMSUpdateData | IMembershipLSUpdateData) { switch (parsedData.u) { diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index 7ad46349..fcf5048e 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -16,19 +16,19 @@ export type MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE'; export type SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export type SPLIT_KILL = 'SPLIT_KILL'; export type SPLIT_UPDATE = 'SPLIT_UPDATE'; -export type RBSEGMENT_UPDATE = 'RBSEGMENT_UPDATE'; +export type RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'; // Control-type push notifications, handled by NotificationKeeper export type CONTROL = 'CONTROL'; export type OCCUPANCY = 'OCCUPANCY'; -export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | RBSEGMENT_UPDATE | ControlType.STREAMING_RESET +export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MEMBERSHIPS_MS_UPDATE | MEMBERSHIPS_LS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | RB_SEGMENT_UPDATE | ControlType.STREAMING_RESET type IParsedData = T extends MEMBERSHIPS_MS_UPDATE ? IMembershipMSUpdateData : T extends MEMBERSHIPS_LS_UPDATE ? IMembershipLSUpdateData : T extends SEGMENT_UPDATE ? ISegmentUpdateData : - T extends SPLIT_UPDATE | RBSEGMENT_UPDATE ? ISplitUpdateData : + T extends SPLIT_UPDATE | RB_SEGMENT_UPDATE ? ISplitUpdateData : T extends SPLIT_KILL ? ISplitKillData : INotificationData; /** From 781d8ce0c7d3c57a168e3f6a398833340c00b42e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 Mar 2025 22:04:21 -0300 Subject: [PATCH 17/62] Refactor usesSegments method and SplitsUpdateWorker --- README.md | 1 + src/storages/__tests__/KeyBuilder.spec.ts | 12 +++++----- .../__tests__/RBSegmentsCacheSync.spec.ts | 4 ++-- .../inLocalStorage/RBSegmentsCacheInLocal.ts | 6 ----- .../inMemory/RBSegmentsCacheInMemory.ts | 2 +- .../UpdateWorkers/SplitsUpdateWorker.ts | 18 +++++++------- .../__tests__/SplitsUpdateWorker.spec.ts | 24 +++++++++---------- src/sync/streaming/pushManager.ts | 2 +- 8 files changed, 32 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 133917cb..85f791cd 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Split has built and maintains SDKs for: * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) * Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) +* Elixir thin-client [Github](https://github.com/splitio/elixir-thin-client) [Docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) * Flutter [Github](https://github.com/splitio/flutter-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index 45af194c..6c3fe5ff 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -106,16 +106,16 @@ test('KEYS / latency and exception keys (telemetry)', () => { test('getStorageHash', () => { expect(getStorageHash({ core: { authorizationKey: '' }, - sync: { __splitFiltersValidation: { queryString: '&names=p1__split,p2__split' }, flagSpecVersion: '1.2' } - } as ISettings)).toBe('7ccd6b31'); + sync: { __splitFiltersValidation: { queryString: '&names=p1__split,p2__split' }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('2ce5cc38'); expect(getStorageHash({ core: { authorizationKey: '' }, - sync: { __splitFiltersValidation: { queryString: '&names=p2__split,p3__split' }, flagSpecVersion: '1.2' } - } as ISettings)).toBe('2a25d0e1'); + sync: { __splitFiltersValidation: { queryString: '&names=p2__split,p3__split' }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('e65079c6'); expect(getStorageHash({ core: { authorizationKey: '' }, - sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.2' } - } as ISettings)).toBe('db8943b4'); + sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('193e6f3f'); }); diff --git a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts index ad47af3c..03579351 100644 --- a/src/storages/__tests__/RBSegmentsCacheSync.spec.ts +++ b/src/storages/__tests__/RBSegmentsCacheSync.spec.ts @@ -61,7 +61,7 @@ describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Me }); test('usesSegments should track segments usage correctly', () => { - expect(cache.usesSegments()).toBe(true); // Initially true when changeNumber is -1 + expect(cache.usesSegments()).toBe(false); // No rbSegments, so false cache.update([rbSegment], [], 1); // rbSegment doesn't have IN_SEGMENT matcher expect(cache.usesSegments()).toBe(false); @@ -70,6 +70,6 @@ describe.each([cacheInMemory, cacheInLocal])('Rule-based segments cache sync (Me expect(cache.usesSegments()).toBe(true); cache.clear(); - expect(cache.usesSegments()).toBe(true); // True after clear since changeNumber is -1 + expect(cache.usesSegments()).toBe(false); // False after clear since there are no rbSegments }); }); diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts index 28c0d1ee..85f73a56 100644 --- a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -12,7 +12,6 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { private readonly keys: KeyBuilderCS; private readonly log: ILogger; - private hasSync?: boolean; constructor(settings: ISettings, keys: KeyBuilderCS) { this.keys = keys; @@ -22,7 +21,6 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { clear() { this.getNames().forEach(name => this.remove(name)); localStorage.removeItem(this.keys.buildRBSegmentsTillKey()); - this.hasSync = false; } update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean { @@ -35,7 +33,6 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { try { localStorage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + ''); localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + ''); - this.hasSync = true; } catch (e) { this.log.error(LOG_PREFIX + e); } @@ -128,9 +125,6 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } usesSegments(): boolean { - // If cache hasn't been synchronized, assume we need segments - if (!this.hasSync) return true; - const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); diff --git a/src/storages/inMemory/RBSegmentsCacheInMemory.ts b/src/storages/inMemory/RBSegmentsCacheInMemory.ts index 78debb86..568b0deb 100644 --- a/src/storages/inMemory/RBSegmentsCacheInMemory.ts +++ b/src/storages/inMemory/RBSegmentsCacheInMemory.ts @@ -62,7 +62,7 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync { } usesSegments(): boolean { - return this.getChangeNumber() === -1 || this.segmentsCount > 0; + return this.segmentsCount > 0; } } diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index 8deeee4f..b151477c 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -3,7 +3,7 @@ import { STREAMING_PARSING_SPLIT_UPDATE } from '../../../logger/constants'; import { ILogger } from '../../../logger/types'; import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ISplitsEventEmitter } from '../../../readiness/types'; -import { ISplitsCacheSync } from '../../../storages/types'; +import { IRBSegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; import { ITelemetryTracker } from '../../../trackers/types'; import { Backoff } from '../../../utils/Backoff'; import { SPLITS } from '../../../utils/constants'; @@ -17,9 +17,9 @@ import { IUpdateWorker } from './types'; /** * SplitsUpdateWorker factory */ -export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killSplit(event: ISplitKillData): void } { +export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killSplit(event: ISplitKillData): void } { - function SplitsUpdateWorker() { + function SplitsUpdateWorker(cache: ISplitsCacheSync | IRBSegmentsCacheSync) { let maxChangeNumber = 0; let handleNewEvent = false; let isHandlingEvent: boolean; @@ -29,7 +29,7 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, function __handleSplitUpdateCall() { isHandlingEvent = true; - if (maxChangeNumber > splitsCache.getChangeNumber()) { + if (maxChangeNumber > cache.getChangeNumber()) { handleNewEvent = false; const splitUpdateNotification = payload ? { payload, changeNumber: maxChangeNumber } : undefined; // fetch splits revalidating data if cached @@ -44,7 +44,7 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, const attempts = backoff.attempts + 1; - if (maxChangeNumber <= splitsCache.getChangeNumber()) { + if (maxChangeNumber <= cache.getChangeNumber()) { log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); isHandlingEvent = false; return; @@ -77,7 +77,7 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, * @param changeNumber - change number of the notification */ put({ changeNumber, pcn }: ISplitUpdateData, _payload?: ISplit | IRBSegment) { - const currentChangeNumber = splitsCache.getChangeNumber(); + const currentChangeNumber = cache.getChangeNumber(); if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return; @@ -100,8 +100,8 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, }; } - const ff = SplitsUpdateWorker(); - const rbs = SplitsUpdateWorker(); + const ff = SplitsUpdateWorker(storage.splits); + const rbs = SplitsUpdateWorker(storage.rbSegments); return { put(parsedData) { @@ -126,7 +126,7 @@ export function SplitsUpdateWorker(log: ILogger, splitsCache: ISplitsCacheSync, * @param defaultTreatment - default treatment value */ killSplit({ changeNumber, splitName, defaultTreatment }: ISplitKillData) { - if (splitsCache.killLocally(splitName, defaultTreatment, changeNumber)) { + if (storage.splits.killLocally(splitName, defaultTreatment, changeNumber)) { // trigger an SDK_UPDATE if Split was killed locally splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true); } diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index 735c7ee0..bf3c294d 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -65,7 +65,7 @@ describe('SplitsUpdateWorker', () => { const splitsSyncTask = splitsSyncTaskMock(cache); Backoff.__TEST__BASE_MILLIS = 1; // retry immediately - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); // assert calling `splitsSyncTask.execute` if `isExecuting` is false expect(splitsSyncTask.isExecuting()).toBe(false); @@ -104,7 +104,7 @@ describe('SplitsUpdateWorker', () => { Backoff.__TEST__BASE_MILLIS = 50; const cache = new SplitsCacheInMemory(); const splitsSyncTask = splitsSyncTaskMock(cache, [90, 90, 90]); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); // while fetch fails, should retry with backoff splitUpdateWorker.put({ changeNumber: 100 }); @@ -123,7 +123,7 @@ describe('SplitsUpdateWorker', () => { Backoff.__TEST__MAX_MILLIS = 60; // 60 millis instead of 1 min const cache = new SplitsCacheInMemory(); const splitsSyncTask = splitsSyncTaskMock(cache, [...Array(FETCH_BACKOFF_MAX_RETRIES).fill(90), 90, 100]); // 12 executions. Last one is valid - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); // queued @@ -148,7 +148,7 @@ describe('SplitsUpdateWorker', () => { Backoff.__TEST__MAX_MILLIS = 60; // 60 millis instead of 1 min const cache = new SplitsCacheInMemory(); const splitsSyncTask = splitsSyncTaskMock(cache, Array(FETCH_BACKOFF_MAX_RETRIES * 2).fill(90)); // 20 executions. No one is valid - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); // queued @@ -169,11 +169,11 @@ describe('SplitsUpdateWorker', () => { test('killSplit', async () => { // setup const cache = new SplitsCacheInMemory(); - cache.addSplit({ name: 'something'}); - cache.addSplit({ name: 'something else'}); + cache.addSplit({ name: 'something' }); + cache.addSplit({ name: 'something else' }); const splitsSyncTask = splitsSyncTaskMock(cache); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); // assert killing split locally, emitting SDK_SPLITS_ARRIVED event, and synchronizing splits if changeNumber is new splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'something', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous @@ -200,7 +200,7 @@ describe('SplitsUpdateWorker', () => { const cache = new SplitsCacheInMemory(); const splitsSyncTask = splitsSyncTaskMock(cache, [95]); Backoff.__TEST__BASE_MILLIS = 1; - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); @@ -216,7 +216,7 @@ describe('SplitsUpdateWorker', () => { splitNotifications.forEach(notification => { const pcn = cache.getChangeNumber(); const splitsSyncTask = splitsSyncTaskMock(cache); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); const payload = notification.decoded; const changeNumber = payload.changeNumber; splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); // queued @@ -236,7 +236,7 @@ describe('SplitsUpdateWorker', () => { const notification = splitNotifications[0]; let splitsSyncTask = splitsSyncTaskMock(cache); - let splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + let splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); @@ -249,7 +249,7 @@ describe('SplitsUpdateWorker', () => { cache.setChangeNumber(ccn); splitsSyncTask = splitsSyncTaskMock(cache); - splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); @@ -262,7 +262,7 @@ describe('SplitsUpdateWorker', () => { cache.setChangeNumber(ccn); splitsSyncTask = splitsSyncTaskMock(cache); - splitUpdateWorker = SplitsUpdateWorker(loggerMock, cache, splitsSyncTask, telemetryTracker); + splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, { payload: notification.decoded, changeNumber }]); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 902d6522..9122c176 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -56,7 +56,7 @@ export function pushManagerFactory( // MySegmentsUpdateWorker (client-side) are initiated in `add` method const segmentsUpdateWorker = userKey ? undefined : SegmentsUpdateWorker(log, pollingManager.segmentsSyncTask as ISegmentsSyncTask, storage.segments); // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments - const splitsUpdateWorker = SplitsUpdateWorker(log, storage.splits, pollingManager.splitsSyncTask, readiness.splits, telemetryTracker, userKey ? undefined : pollingManager.segmentsSyncTask as ISegmentsSyncTask); + const splitsUpdateWorker = SplitsUpdateWorker(log, storage, pollingManager.splitsSyncTask, readiness.splits, telemetryTracker, userKey ? undefined : pollingManager.segmentsSyncTask as ISegmentsSyncTask); // [Only for client-side] map of hashes to user keys, to dispatch membership update events to the corresponding MySegmentsUpdateWorker const userKeyHashes: Record = {}; From 9f507fe75392cddb891c11ac0812cb663dd1c1f7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Mar 2025 15:23:22 -0300 Subject: [PATCH 18/62] Update types and support evaluation for rule-based segments, i.e., without partitions --- src/dtos/types.ts | 21 +++++++++------- src/evaluator/Engine.ts | 2 +- src/evaluator/combiners/and.ts | 9 +++---- src/evaluator/combiners/ifelseif.ts | 16 ++++++------- src/evaluator/condition/index.ts | 24 +++++++++---------- .../matchers/__tests__/between.spec.ts | 4 ++-- .../__tests__/segment/client_side.spec.ts | 8 +++---- src/evaluator/matchers/index.ts | 2 +- src/evaluator/parser/index.ts | 6 ++--- src/evaluator/types.ts | 4 ++-- src/evaluator/value/index.ts | 4 ++-- src/evaluator/value/sanitize.ts | 6 ++--- src/sdkManager/index.ts | 2 +- src/utils/lang/index.ts | 2 +- 14 files changed, 57 insertions(+), 53 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index a2ffaad5..423de3a8 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -66,6 +66,11 @@ interface IInSegmentMatcher extends ISplitMatcherBase { userDefinedSegmentMatcherData: IInSegmentMatcherData } +interface IInRBSegmentMatcher extends ISplitMatcherBase { + matcherType: 'IN_RULE_BASED_SEGMENT', + userDefinedSegmentMatcherData: IInSegmentMatcherData +} + interface IInLargeSegmentMatcher extends ISplitMatcherBase { matcherType: 'IN_LARGE_SEGMENT', userDefinedLargeSegmentMatcherData: IInLargeSegmentMatcherData @@ -176,7 +181,7 @@ export type ISplitMatcher = IAllKeysMatcher | IInSegmentMatcher | IWhitelistMatc ILessThanOrEqualToMatcher | IBetweenMatcher | IEqualToSetMatcher | IContainsAnyOfSetMatcher | IContainsAllOfSetMatcher | IPartOfSetMatcher | IStartsWithMatcher | IEndsWithMatcher | IContainsStringMatcher | IInSplitTreatmentMatcher | IEqualToBooleanMatcher | IMatchesStringMatcher | IEqualToSemverMatcher | IGreaterThanOrEqualToSemverMatcher | ILessThanOrEqualToSemverMatcher | IBetweenSemverMatcher | IInListSemverMatcher | - IInLargeSegmentMatcher + IInLargeSegmentMatcher | IInRBSegmentMatcher /** Split object */ export interface ISplitPartition { @@ -189,30 +194,30 @@ export interface ISplitCondition { combiner: 'AND', matchers: ISplitMatcher[] } - partitions: ISplitPartition[] - label: string - conditionType: 'ROLLOUT' | 'WHITELIST' + partitions?: ISplitPartition[] + label?: string + conditionType?: 'ROLLOUT' | 'WHITELIST' } export interface IRBSegment { name: string, changeNumber: number, status: 'ACTIVE' | 'ARCHIVED', + conditions: ISplitCondition[], excluded: { keys: string[], segments: string[] - }, - conditions: ISplitCondition[], + } } export interface ISplit { name: string, changeNumber: number, + status: 'ACTIVE' | 'ARCHIVED', + conditions: ISplitCondition[], killed: boolean, defaultTreatment: string, trafficTypeName: string, - conditions: ISplitCondition[], - status: 'ACTIVE' | 'ARCHIVED', seed: number, trafficAllocation?: number, trafficAllocationSeed?: number diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 36f52cb4..233ecc2a 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -73,7 +73,7 @@ export class Engine { trafficAllocationSeed, attributes, splitEvaluator - ); + ) as MaybeThenable; // Evaluation could be async, so we should handle that case checking for a // thenable object diff --git a/src/evaluator/combiners/and.ts b/src/evaluator/combiners/and.ts index b229a22b..fd239753 100644 --- a/src/evaluator/combiners/and.ts +++ b/src/evaluator/combiners/and.ts @@ -2,10 +2,11 @@ import { findIndex } from '../../utils/lang'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { MaybeThenable } from '../../dtos/types'; -import { IMatcher } from '../types'; +import { ISplitEvaluator } from '../types'; import { ENGINE_COMBINER_AND } from '../../logger/constants'; +import SplitIO from '../../../types/splitio'; -export function andCombinerContext(log: ILogger, matchers: IMatcher[]) { +export function andCombinerContext(log: ILogger, matchers: Array<(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable>) { function andResults(results: boolean[]): boolean { // Array.prototype.every is supported by target environments @@ -15,8 +16,8 @@ export function andCombinerContext(log: ILogger, matchers: IMatcher[]) { return hasMatchedAll; } - return function andCombiner(...params: any): MaybeThenable { - const matcherResults = matchers.map(matcher => matcher(...params)); + return function andCombiner(key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator): MaybeThenable { + const matcherResults = matchers.map(matcher => matcher(key, attributes, splitEvaluator)); // If any matching result is a thenable we should use Promise.all if (findIndex(matcherResults, thenable) !== -1) { diff --git a/src/evaluator/combiners/ifelseif.ts b/src/evaluator/combiners/ifelseif.ts index b2bfdcd0..aaba4b27 100644 --- a/src/evaluator/combiners/ifelseif.ts +++ b/src/evaluator/combiners/ifelseif.ts @@ -1,4 +1,4 @@ -import { findIndex } from '../../utils/lang'; +import { findIndex, isBoolean } from '../../utils/lang'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; import { UNSUPPORTED_MATCHER_TYPE } from '../../utils/labels'; @@ -18,14 +18,12 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): }; } - function computeTreatment(predicateResults: Array) { - const len = predicateResults.length; - - for (let i = 0; i < len; i++) { + function computeEvaluation(predicateResults: Array): IEvaluation | boolean | undefined { + for (let i = 0, len = predicateResults.length; i < len; i++) { const evaluation = predicateResults[i]; if (evaluation !== undefined) { - log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); + if (!isBoolean(evaluation)) log.debug(ENGINE_COMBINER_IFELSEIF, [evaluation.treatment]); return evaluation; } @@ -35,7 +33,7 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): return undefined; } - function ifElseIfCombiner(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + function ifElseIfCombiner(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // In Async environments we are going to have async predicates. There is none way to know // before hand so we need to evaluate all the predicates, verify for thenables, and finally, // define how to return the treatment (wrap result into a Promise or not). @@ -43,10 +41,10 @@ export function ifElseIfCombinerContext(log: ILogger, predicates: IEvaluator[]): // if we find a thenable if (findIndex(predicateResults, thenable) !== -1) { - return Promise.all(predicateResults).then(results => computeTreatment(results)); + return Promise.all(predicateResults).then(results => computeEvaluation(results)); } - return computeTreatment(predicateResults as IEvaluation[]); + return computeEvaluation(predicateResults as IEvaluation[]); } // if there is none predicates, then there was an error in parsing phase diff --git a/src/evaluator/condition/index.ts b/src/evaluator/condition/index.ts index 4fd6d372..5facaa5c 100644 --- a/src/evaluator/condition/index.ts +++ b/src/evaluator/condition/index.ts @@ -7,14 +7,14 @@ import SplitIO from '../../../types/splitio'; import { ILogger } from '../../logger/types'; // Build Evaluation object if and only if matchingResult is true -function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed: number | undefined, treatments: { getTreatmentFor: (x: number) => string }, label: string): IEvaluation | undefined { +function match(log: ILogger, matchingResult: boolean, bucketingKey: string | undefined, seed?: number, treatments?: { getTreatmentFor: (x: number) => string }, label?: string): IEvaluation | boolean | undefined { if (matchingResult) { - const treatment = getTreatment(log, bucketingKey as string, seed, treatments); - - return { - treatment, - label - }; + return treatments ? // Feature flag + { + treatment: getTreatment(log, bucketingKey as string, seed, treatments), + label: label! + } : // Rule-based segment + true; } // else we should notify the engine to continue evaluating @@ -22,12 +22,12 @@ function match(log: ILogger, matchingResult: boolean, bucketingKey: string | und } // Condition factory -export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) => MaybeThenable, treatments: { getTreatmentFor: (x: number) => string }, label: string, conditionType: 'ROLLOUT' | 'WHITELIST'): IEvaluator { +export function conditionContext(log: ILogger, matcherEvaluator: (key: SplitIO.SplitKeyObject, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable, treatments?: { getTreatmentFor: (x: number) => string }, label?: string, conditionType?: 'ROLLOUT' | 'WHITELIST'): IEvaluator { - return function conditionEvaluator(key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { + return function conditionEvaluator(key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) { // Whitelisting has more priority than traffic allocation, so we don't apply this filtering to those conditions. - if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation as number, (key as SplitIO.SplitKeyObject).bucketingKey as string, trafficAllocationSeed as number)) { + if (conditionType === 'ROLLOUT' && !shouldApplyRollout(trafficAllocation!, key.bucketingKey, trafficAllocationSeed!)) { return { treatment: undefined, // treatment value is assigned later label: NOT_IN_SPLIT @@ -41,10 +41,10 @@ export function conditionContext(log: ILogger, matcherEvaluator: (...args: any) const matches = matcherEvaluator(key, attributes, splitEvaluator); if (thenable(matches)) { - return matches.then(result => match(log, result, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label)); + return matches.then(result => match(log, result, key.bucketingKey, seed, treatments, label)); } - return match(log, matches, (key as SplitIO.SplitKeyObject).bucketingKey, seed, treatments, label); + return match(log, matches, key.bucketingKey, seed, treatments, label); }; } diff --git a/src/evaluator/matchers/__tests__/between.spec.ts b/src/evaluator/matchers/__tests__/between.spec.ts index 5b76186b..eaf78106 100644 --- a/src/evaluator/matchers/__tests__/between.spec.ts +++ b/src/evaluator/matchers/__tests__/between.spec.ts @@ -19,6 +19,6 @@ test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and expect(matcher(15)).toBe(true); // 15 is between 10 and 20 expect(matcher(20)).toBe(true); // 20 is between 10 and 20 expect(matcher(21)).toBe(false); // 21 is not between 10 and 20 - expect(matcher(undefined)).toBe(false); // undefined is not between 10 and 20 - expect(matcher(null)).toBe(false); // null is not between 10 and 20 + expect(matcher(undefined as any)).toBe(false); // undefined is not between 10 and 20 + expect(matcher(null as any)).toBe(false); // null is not between 10 and 20 }); diff --git a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts index 5e192829..c4e3470e 100644 --- a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts @@ -29,8 +29,8 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i } } as IStorageSync) as IMatcher; - expect(await matcherTrue()).toBe(true); // segment found in mySegments list - expect(await matcherFalse()).toBe(false); // segment not found in mySegments list + expect(await matcherTrue('key')).toBe(true); // segment found in mySegments list + expect(await matcherFalse('key')).toBe(false); // segment not found in mySegments list }); test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { @@ -54,6 +54,6 @@ test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is def largeSegments: undefined } as IStorageSync) as IMatcher; - expect(await matcherTrue()).toBe(true); // large segment found in mySegments list - expect(await matcherFalse()).toBe(false); // large segment storage is not defined + expect(await matcherTrue('key')).toBe(true); // large segment found in mySegments list + expect(await matcherFalse('key')).toBe(false); // large segment storage is not defined }); diff --git a/src/evaluator/matchers/index.ts b/src/evaluator/matchers/index.ts index d50c38dd..3fd840f4 100644 --- a/src/evaluator/matchers/index.ts +++ b/src/evaluator/matchers/index.ts @@ -64,5 +64,5 @@ export function matcherFactory(log: ILogger, matcherDto: IMatcherDto, storage?: let matcherFn; // @ts-ignore if (matchers[type]) matcherFn = matchers[type](value, storage, log); // There is no index-out-of-bound exception in JavaScript - return matcherFn; + return matcherFn as IMatcher; } diff --git a/src/evaluator/parser/index.ts b/src/evaluator/parser/index.ts index a398aa0b..d12edf1a 100644 --- a/src/evaluator/parser/index.ts +++ b/src/evaluator/parser/index.ts @@ -37,7 +37,7 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt } // Evaluator function. - return (key: string, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator) => { + return (key: SplitIO.SplitKey, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => { const value = sanitizeValue(log, key, matcherDto, attributes); let result: MaybeThenable = false; @@ -71,12 +71,12 @@ export function parser(log: ILogger, conditions: ISplitCondition[], storage: ISt predicates.push(conditionContext( log, andCombinerContext(log, expressions), - Treatments.parse(partitions), + partitions && Treatments.parse(partitions), label, conditionType )); } - // Instanciate evaluator given the set of conditions using if else if logic + // Instantiate evaluator given the set of conditions using if else if logic return ifElseIfCombinerContext(log, predicates); } diff --git a/src/evaluator/types.ts b/src/evaluator/types.ts index db0d4e28..92806ddf 100644 --- a/src/evaluator/types.ts +++ b/src/evaluator/types.ts @@ -29,6 +29,6 @@ export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDi export type ISplitEvaluator = (log: ILogger, key: SplitIO.SplitKey, splitName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable -export type IEvaluator = (key: SplitIO.SplitKey, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable +export type IEvaluator = (key: SplitIO.SplitKeyObject, seed?: number, trafficAllocation?: number, trafficAllocationSeed?: number, attributes?: SplitIO.Attributes, splitEvaluator?: ISplitEvaluator) => MaybeThenable -export type IMatcher = (...args: any) => MaybeThenable +export type IMatcher = (value: string | number | boolean | string[] | IDependencyMatcherValue, splitEvaluator?: ISplitEvaluator) => MaybeThenable diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index 95b4000c..06184aa5 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -4,7 +4,7 @@ import { ILogger } from '../../logger/types'; import { sanitize } from './sanitize'; import { ENGINE_VALUE, ENGINE_VALUE_NO_ATTRIBUTES, ENGINE_VALUE_INVALID } from '../../logger/constants'; -function parseValue(log: ILogger, key: string, attributeName: string | null, attributes?: SplitIO.Attributes) { +function parseValue(log: ILogger, key: SplitIO.SplitKey, attributeName: string | null, attributes?: SplitIO.Attributes) { let value = undefined; if (attributeName) { if (attributes) { @@ -23,7 +23,7 @@ function parseValue(log: ILogger, key: string, attributeName: string | null, att /** * Defines value to be matched (key / attribute). */ -export function sanitizeValue(log: ILogger, key: string, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { +export function sanitizeValue(log: ILogger, key: SplitIO.SplitKey, matcherDto: IMatcherDto, attributes?: SplitIO.Attributes) { const attributeName = matcherDto.attribute; const valueToMatch = parseValue(log, key, attributeName, attributes); const sanitizedValue = sanitize(log, matcherDto.type, valueToMatch, matcherDto.dataType, attributes); diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 630a4b38..5d0d6e28 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -69,9 +69,9 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { /** * Sanitize matcher value */ -export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | undefined, dataType: string, attributes?: SplitIO.Attributes) { +export function sanitize(log: ILogger, matcherTypeID: number, value: string | number | boolean | Array | SplitIO.SplitKey | undefined, dataType: string, attributes?: SplitIO.Attributes) { const processor = getProcessingFunction(matcherTypeID, dataType); - let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; + let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; switch (dataType) { case matcherDataTypes.NUMBER: @@ -88,7 +88,7 @@ export function sanitize(log: ILogger, matcherTypeID: number, value: string | nu sanitizedValue = sanitizeBoolean(value); break; case matcherDataTypes.NOT_SPECIFIED: - sanitizedValue = value; + sanitizedValue = value as any; break; default: sanitizedValue = undefined; diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 82423016..831771c9 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -17,7 +17,7 @@ function collectTreatments(splitObject: ISplit) { // Localstorage mode could fall into a no rollout conditions state. Take the first condition in that case. if (!allTreatmentsCondition) allTreatmentsCondition = conditions[0]; // Then extract the treatments from the partitions - return allTreatmentsCondition ? allTreatmentsCondition.partitions.map(v => v.treatment) : []; + return allTreatmentsCondition ? allTreatmentsCondition.partitions!.map(v => v.treatment) : []; } function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 11b6afd0..4735e608 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -111,7 +111,7 @@ export function groupBy>(source: T[], prop: string /** * Checks if a given value is a boolean. */ -export function isBoolean(val: any): boolean { +export function isBoolean(val: any): val is boolean { return val === true || val === false; } From 51ca496ed5843c9e74d927eb96b69fded9f31a1f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 Mar 2025 16:22:53 -0300 Subject: [PATCH 19/62] Add rule-based segment matcher --- .../matchers/__tests__/rbsegment.spec.ts | 244 ++++++++++++++++++ src/evaluator/matchers/index.ts | 2 + src/evaluator/matchers/matcherTypes.ts | 1 + src/evaluator/matchers/rbsegment.ts | 61 +++++ src/evaluator/matchersTransform/index.ts | 3 + src/evaluator/value/sanitize.ts | 1 + src/sync/polling/pollingManagerCS.ts | 14 +- .../polling/updaters/mySegmentsUpdater.ts | 4 +- src/sync/syncManagerOnline.ts | 4 +- 9 files changed, 323 insertions(+), 11 deletions(-) create mode 100644 src/evaluator/matchers/__tests__/rbsegment.spec.ts create mode 100644 src/evaluator/matchers/rbsegment.ts diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts new file mode 100644 index 00000000..c2ee536b --- /dev/null +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -0,0 +1,244 @@ +import { matcherTypes } from '../matcherTypes'; +import { matcherFactory } from '..'; +import { evaluateFeature } from '../../index'; +import { IMatcherDto } from '../../types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { IRBSegment, ISplit } from '../../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../../storages/types'; +import { thenable } from '../../../utils/promise/thenable'; + +const ALWAYS_ON_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets': [] } as ISplit; + +const STORED_SPLITS: Record = { + 'always-on': ALWAYS_ON_SPLIT +}; + +const STORED_SEGMENTS: Record> = { + 'segment_test': new Set(['emi@split.io']), + 'regular_segment': new Set(['nadia@split.io']) +}; + +const STORED_RBSEGMENTS: Record = { + 'mauro_rule_based_segment': { + changeNumber: 5, + name: 'mauro_rule_based_segment', + status: 'ACTIVE', + excluded: { + keys: ['mauro@split.io', 'gaston@split.io'], + segments: ['segment_test'] + }, + conditions: [ + { + matcherGroup: { + combiner: 'AND', + matchers: [ + { + keySelector: { + trafficType: 'user', + attribute: 'location', + }, + matcherType: 'WHITELIST', + negate: false, + whitelistMatcherData: { + whitelist: [ + 'mdp', + 'tandil', + 'bsas' + ] + } + }, + { + keySelector: { + trafficType: 'user', + attribute: null + }, + matcherType: 'ENDS_WITH', + negate: false, + whitelistMatcherData: { + whitelist: [ + '@split.io' + ] + } + } + ] + } + }, + { + matcherGroup: { + combiner: 'AND', + matchers: [ + { + keySelector: { + trafficType: 'user', + attribute: null + }, + matcherType: 'IN_SEGMENT', + negate: false, + userDefinedSegmentMatcherData: { + segmentName: 'regular_segment' + } + } + ] + } + } + ] + }, + 'depend_on_always_on': { + name: 'depend_on_always_on', + changeNumber: 123, + status: 'ACTIVE', + excluded: { + keys: [], + segments: [] + }, + conditions: [{ + matcherGroup: { + combiner: 'AND', + matchers: [{ + matcherType: 'IN_SPLIT_TREATMENT', + keySelector: { + trafficType: 'user', + attribute: null + }, + negate: false, + dependencyMatcherData: { + split: 'always-on', + treatments: [ + 'on', + ] + } + }] + } + }] + }, + 'depend_on_mauro_rule_based_segment': { + name: 'depend_on_mauro_rule_based_segment', + changeNumber: 123, + status: 'ACTIVE', + excluded: { + keys: [], + segments: [] + }, + conditions: [{ + matcherGroup: { + combiner: 'AND', + matchers: [{ + matcherType: 'IN_RULE_BASED_SEGMENT', + keySelector: { + trafficType: 'user', + attribute: null + }, + negate: false, + userDefinedSegmentMatcherData: { + segmentName: 'mauro_rule_based_segment' + } + }] + } + }] + }, +}; + +const mockStorageSync = { + isSync: true, + splits: { + getSplit(name: string) { + return STORED_SPLITS[name]; + } + }, + segments: { + isInSegment(segmentName: string, matchingKey: string) { + return STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false; + } + }, + rbSegments: { + get(rbsegmentName: string) { + return STORED_RBSEGMENTS[rbsegmentName]; + } + } +} as unknown as IStorageSync; + +const mockStorageAsync = { + isSync: false, + splits: { + getSplit(name: string) { + return Promise.resolve(STORED_SPLITS[name]); + } + }, + segments: { + isInSegment(segmentName: string, matchingKey: string) { + return Promise.resolve(STORED_SEGMENTS[segmentName] ? STORED_SEGMENTS[segmentName].has(matchingKey) : false); + } + }, + rbSegments: { + get(rbsegmentName: string) { + return Promise.resolve(STORED_RBSEGMENTS[rbsegmentName]); + } + } +} as unknown as IStorageAsync; + +describe.each([ + { mockStorage: mockStorageSync, isAsync: false }, + { mockStorage: mockStorageAsync, isAsync: true } +])('MATCHER IN_RULE_BASED_SEGMENT', ({ mockStorage, isAsync }) => { + test('should support excluded keys, excluded segments, and multiple conditions', async () => { + const matcher = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'mauro_rule_based_segment' + } as IMatcherDto, mockStorage)!; + + const dependentMatcher = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'depend_on_mauro_rule_based_segment' + } as IMatcherDto, mockStorage)!; + + [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) + match = matcher({ key: 'emi@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); + expect(await match).toBe(false); + match = matcher({ key: { matchingKey: 'zeta@split.io', bucketingKey: '123' }, attributes: { location: 'italy' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(false); + + // should return true if match the first condition: location attribute in whitelist and key ends with '@split.io' + match = matcher({ key: 'emma@split.io', attributes: { location: 'tandil' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(true); + + // should return true if match the second condition: key in regular_segment + match = matcher({ key: { matchingKey: 'nadia@split.io', bucketingKey: '123' }, attributes: { location: 'mdp' } }, evaluateFeature); + expect(thenable(match)).toBe(isAsync); + expect(await match).toBe(true); + }); + }); + + test('edge cases', async () => { + const matcherNotExist = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'non_existent_segment' + } as IMatcherDto, mockStorageSync)!; + + // should return false if the provided segment does not exist + expect(await matcherNotExist({ key: 'a-key' }, evaluateFeature)).toBe(false); + + const matcherTrueAlwaysOn = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'depend_on_always_on' + } as IMatcherDto, mockStorageSync)!; + + // should support feature flag dependency matcher + expect(await matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Parent split returns one of the expected treatments, so the matcher returns true + }); + +}); diff --git a/src/evaluator/matchers/index.ts b/src/evaluator/matchers/index.ts index 3fd840f4..f54cc313 100644 --- a/src/evaluator/matchers/index.ts +++ b/src/evaluator/matchers/index.ts @@ -24,6 +24,7 @@ import { inListSemverMatcherContext } from './semver_inlist'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { IMatcher, IMatcherDto } from '../types'; import { ILogger } from '../../logger/types'; +import { ruleBasedSegmentMatcherContext } from './rbsegment'; const matchers = [ undefined, // UNDEFINED: 0 @@ -50,6 +51,7 @@ const matchers = [ betweenSemverMatcherContext, // BETWEEN_SEMVER: 21 inListSemverMatcherContext, // IN_LIST_SEMVER: 22 largeSegmentMatcherContext, // IN_LARGE_SEGMENT: 23 + ruleBasedSegmentMatcherContext // IN_RULE_BASED_SEGMENT: 24 ]; /** diff --git a/src/evaluator/matchers/matcherTypes.ts b/src/evaluator/matchers/matcherTypes.ts index f09d50bf..0c5faf4b 100644 --- a/src/evaluator/matchers/matcherTypes.ts +++ b/src/evaluator/matchers/matcherTypes.ts @@ -23,6 +23,7 @@ export const matcherTypes: Record = { BETWEEN_SEMVER: 21, IN_LIST_SEMVER: 22, IN_LARGE_SEGMENT: 23, + IN_RULE_BASED_SEGMENT: 24, }; export const matcherDataTypes = { diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts new file mode 100644 index 00000000..68318320 --- /dev/null +++ b/src/evaluator/matchers/rbsegment.ts @@ -0,0 +1,61 @@ +import { IRBSegment, MaybeThenable } from '../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../storages/types'; +import { ILogger } from '../../logger/types'; +import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; +import { thenable } from '../../utils/promise/thenable'; +import { getMatching, keyParser } from '../../utils/key'; +import { parser } from '../parser'; + + +export function ruleBasedSegmentMatcherContext(segmentName: string, storage: IStorageSync | IStorageAsync, log: ILogger) { + + return function ruleBasedSegmentMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { + + function matchConditions(rbsegment: IRBSegment) { + const conditions = rbsegment.conditions; + const evaluator = parser(log, conditions, storage); + + const evaluation = evaluator( + keyParser(key), + undefined, + undefined, + undefined, + attributes, + splitEvaluator + ); + + return thenable(evaluation) ? + evaluation.then(evaluation => evaluation ? true : false) : + evaluation ? true : false; + } + + function isExcluded(rbSegment: IRBSegment) { + const matchingKey = getMatching(key); + + if (rbSegment.excluded.keys.indexOf(matchingKey) !== -1) return true; + + const isInSegment = rbSegment.excluded.segments.map(segmentName => { + return storage.segments.isInSegment(segmentName, matchingKey); + }); + + return isInSegment.length && thenable(isInSegment[0]) ? + Promise.all(isInSegment).then(results => results.some(result => result)) : + isInSegment.some(result => result); + } + + function isInSegment(rbSegment: IRBSegment | null) { + if (!rbSegment) return false; + const excluded = isExcluded(rbSegment); + + return thenable(excluded) ? + excluded.then(excluded => excluded ? false : matchConditions(rbSegment)) : + excluded ? false : matchConditions(rbSegment); + } + + const rbSegment = storage.rbSegments.get(segmentName); + + return thenable(rbSegment) ? + rbSegment.then(isInSegment) : + isInSegment(rbSegment); + }; +} diff --git a/src/evaluator/matchersTransform/index.ts b/src/evaluator/matchersTransform/index.ts index a5be15e3..6219c4dc 100644 --- a/src/evaluator/matchersTransform/index.ts +++ b/src/evaluator/matchersTransform/index.ts @@ -95,6 +95,9 @@ export function matchersTransform(matchers: ISplitMatcher[]): IMatcherDto[] { type === matcherTypes.LESS_THAN_OR_EQUAL_TO_SEMVER ) { value = stringMatcherData; + } else if (type === matcherTypes.IN_RULE_BASED_SEGMENT) { + value = segmentTransform(userDefinedSegmentMatcherData as IInSegmentMatcherData); + dataType = matcherDataTypes.NOT_SPECIFIED; } return { diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 5d0d6e28..de92efc7 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -60,6 +60,7 @@ function getProcessingFunction(matcherTypeID: number, dataType: string) { case matcherTypes.BETWEEN: return dataType === 'DATETIME' ? zeroSinceSS : undefined; case matcherTypes.IN_SPLIT_TREATMENT: + case matcherTypes.IN_RULE_BASED_SEGMENT: return dependencyProcessor; default: return undefined; diff --git a/src/sync/polling/pollingManagerCS.ts b/src/sync/polling/pollingManagerCS.ts index 4ce0882a..6a5ba679 100644 --- a/src/sync/polling/pollingManagerCS.ts +++ b/src/sync/polling/pollingManagerCS.ts @@ -43,10 +43,10 @@ export function pollingManagerCSFactory( // smart pausing readiness.splits.on(SDK_SPLITS_ARRIVED, () => { if (!splitsSyncTask.isRunning()) return; // noop if not doing polling - const splitsHaveSegments = storage.splits.usesSegments(); - if (splitsHaveSegments !== mySegmentsSyncTask.isRunning()) { - log.info(POLLING_SMART_PAUSING, [splitsHaveSegments ? 'ON' : 'OFF']); - if (splitsHaveSegments) { + const usingSegments = storage.splits.usesSegments() || storage.rbSegments.usesSegments(); + if (usingSegments !== mySegmentsSyncTask.isRunning()) { + log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']); + if (usingSegments) { startMySegmentsSyncTasks(); } else { stopMySegmentsSyncTasks(); @@ -59,9 +59,9 @@ export function pollingManagerCSFactory( // smart ready function smartReady() { - if (!readiness.isReady() && !storage.splits.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + if (!readiness.isReady() && !storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); } - if (!storage.splits.usesSegments()) setTimeout(smartReady, 0); + if (!storage.splits.usesSegments() && !storage.rbSegments.usesSegments()) setTimeout(smartReady, 0); else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady); mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask; @@ -77,7 +77,7 @@ export function pollingManagerCSFactory( log.info(POLLING_START); splitsSyncTask.start(); - if (storage.splits.usesSegments()) startMySegmentsSyncTasks(); + if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) startMySegmentsSyncTasks(); }, // Stop periodic fetching (polling) diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index 32d9f78e..501e3b7a 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -27,7 +27,7 @@ export function mySegmentsUpdaterFactory( matchingKey: string ): IMySegmentsUpdater { - const { splits, segments, largeSegments } = storage; + const { splits, rbSegments, segments, largeSegments } = storage; let readyOnAlreadyExistentState = true; let startingUp = true; @@ -51,7 +51,7 @@ export function mySegmentsUpdaterFactory( } // Notify update if required - if (splits.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { + if ((splits.usesSegments() || rbSegments.usesSegments()) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { readyOnAlreadyExistentState = false; segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); } diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 5410c17f..109b71f0 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -149,14 +149,14 @@ export function syncManagerOnlineFactory( if (pushManager) { if (pollingManager.isRunning()) { // if doing polling, we must start the periodic fetch of data - if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); + if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); } else { // if not polling, we must execute the sync task for the initial fetch // of segments since `syncAll` was already executed when starting the main client mySegmentsSyncTask.execute(); } } else { - if (storage.splits.usesSegments()) mySegmentsSyncTask.start(); + if (storage.splits.usesSegments() || storage.rbSegments.usesSegments()) mySegmentsSyncTask.start(); } } else { if (!readinessManager.isReady()) mySegmentsSyncTask.execute(); From 6606b667cd5ab288a246015b7e02c1c87ae2701a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 10:11:39 -0300 Subject: [PATCH 20/62] Refactor Engine class into engineParser function for improved clarity and code reduction --- src/evaluator/Engine.ts | 93 ++++++++++------------------------------- src/evaluator/index.ts | 12 +++--- 2 files changed, 27 insertions(+), 78 deletions(-) diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 233ecc2a..a1465a51 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -1,13 +1,13 @@ -import { get } from '../utils/lang'; +import { get, isString } from '../utils/lang'; import { parser } from './parser'; import { keyParser } from '../utils/key'; import { thenable } from '../utils/promise/thenable'; -import { EXCEPTION, NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED } from '../utils/labels'; +import { NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED } from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { ISplit, MaybeThenable } from '../dtos/types'; import SplitIO from '../../types/splitio'; import { IStorageAsync, IStorageSync } from '../storages/types'; -import { IEvaluation, IEvaluationResult, IEvaluator, ISplitEvaluator } from './types'; +import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { @@ -17,84 +17,35 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str }; } -export class Engine { +export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) { + const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions } = split; - constructor(private baseInfo: ISplit, private evaluator: IEvaluator) { + const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; - // in case we don't have a default treatment in the instantiation, use 'control' - if (typeof this.baseInfo.defaultTreatment !== 'string') { - this.baseInfo.defaultTreatment = CONTROL; - } - } - - static parse(log: ILogger, splitFlatStructure: ISplit, storage: IStorageSync | IStorageAsync) { - const conditions = splitFlatStructure.conditions; - const evaluator = parser(log, conditions, storage); + const evaluator = parser(log, conditions, storage); - return new Engine(splitFlatStructure, evaluator); - } + return { - getKey() { - return this.baseInfo.name; - } + getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { - getTreatment(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { - const { - killed, - seed, - defaultTreatment, - trafficAllocation, - trafficAllocationSeed - } = this.baseInfo; - let parsedKey; - let treatment; - let label; + const parsedKey = keyParser(key); - try { - parsedKey = keyParser(key); - } catch (err) { - return { + if (status === 'ARCHIVED') return { treatment: CONTROL, - label: EXCEPTION + label: SPLIT_ARCHIVED }; - } - - if (this.isGarbage()) { - treatment = CONTROL; - label = SPLIT_ARCHIVED; - } else if (killed) { - treatment = defaultTreatment; - label = SPLIT_KILLED; - } else { - const evaluation = this.evaluator( - parsedKey, - seed, - trafficAllocation, - trafficAllocationSeed, - attributes, - splitEvaluator - ) as MaybeThenable; - // Evaluation could be async, so we should handle that case checking for a - // thenable object - if (thenable(evaluation)) { - return evaluation.then(result => evaluationResult(result, defaultTreatment)); - } else { - return evaluationResult(evaluation, defaultTreatment); - } - } + if (killed) return { + treatment: defaultTreatment, + label: SPLIT_KILLED + }; - return { - treatment, - label - }; - } + const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; - isGarbage() { - return this.baseInfo.status === 'ARCHIVED'; - } + return thenable(evaluation) ? + evaluation.then(result => evaluationResult(result, defaultTreatment)) : + evaluationResult(evaluation, defaultTreatment); + } + }; - getChangeNumber() { - return this.baseInfo.changeNumber; - } } diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 7268962a..e465b9bf 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -1,4 +1,4 @@ -import { Engine } from './Engine'; +import { engineParser } from './Engine'; import { thenable } from '../utils/promise/thenable'; import { EXCEPTION, SPLIT_NOT_FOUND } from '../utils/labels'; import { CONTROL } from '../utils/constants'; @@ -99,9 +99,7 @@ export function evaluateFeaturesByFlagSets( ): MaybeThenable> { let storedFlagNames: MaybeThenable[]>; - function evaluate( - featureFlagsByFlagSets: Set[], - ) { + function evaluate(featureFlagsByFlagSets: Set[]) { let featureFlags = new Set(); for (let i = 0; i < flagSets.length; i++) { const featureFlagByFlagSet = featureFlagsByFlagSets[i]; @@ -148,20 +146,20 @@ function getEvaluation( }; if (splitJSON) { - const split = Engine.parse(log, splitJSON, storage); + const split = engineParser(log, splitJSON, storage); evaluation = split.getTreatment(key, attributes, evaluateFeature); // If the storage is async and the evaluated flag uses segments or dependencies, evaluation is thenable if (thenable(evaluation)) { return evaluation.then(result => { - result.changeNumber = split.getChangeNumber(); + result.changeNumber = splitJSON.changeNumber; result.config = splitJSON.configurations && splitJSON.configurations[result.treatment] || null; result.impressionsDisabled = splitJSON.impressionsDisabled; return result; }); } else { - evaluation.changeNumber = split.getChangeNumber(); // Always sync and optional + evaluation.changeNumber = splitJSON.changeNumber; evaluation.config = splitJSON.configurations && splitJSON.configurations[evaluation.treatment] || null; evaluation.impressionsDisabled = splitJSON.impressionsDisabled; } From fc5ecd5bc3768e4b0dcf2a335026ea027aabc36c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:10:57 -0300 Subject: [PATCH 21/62] Add prerequisites support to ISplit and enhance engineParser for prerequisite evaluation --- src/dtos/types.ts | 4 ++++ src/evaluator/Engine.ts | 30 +++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 423de3a8..95dd0e6c 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -215,6 +215,10 @@ export interface ISplit { changeNumber: number, status: 'ACTIVE' | 'ARCHIVED', conditions: ISplitCondition[], + prerequisites?: { + n: string, + ts: string[] + }[] killed: boolean, defaultTreatment: string, trafficTypeName: string, diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index a1465a51..68b8d3e0 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -18,7 +18,7 @@ function evaluationResult(result: IEvaluation | undefined, defaultTreatment: str } export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync | IStorageAsync) { - const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions } = split; + const { killed, seed, trafficAllocation, trafficAllocationSeed, status, conditions, prerequisites } = split; const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; @@ -30,6 +30,19 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const parsedKey = keyParser(key); + function evaluate(matchPrerequisites: boolean) { + if (!matchPrerequisites) return { + treatment: defaultTreatment, + label: NO_CONDITION_MATCH + }; + + const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; + + return thenable(evaluation) ? + evaluation.then(result => evaluationResult(result, defaultTreatment)) : + evaluationResult(evaluation, defaultTreatment); + } + if (status === 'ARCHIVED') return { treatment: CONTROL, label: SPLIT_ARCHIVED @@ -40,11 +53,18 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync label: SPLIT_KILLED }; - const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; + const matchPrerequisites = prerequisites && prerequisites.length ? + prerequisites.map(prerequisite => { + const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); + return thenable(evaluation) ? + evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) : + prerequisite.ts.indexOf(evaluation.treatment!) === -1; + }) : + true; - return thenable(evaluation) ? - evaluation.then(result => evaluationResult(result, defaultTreatment)) : - evaluationResult(evaluation, defaultTreatment); + return thenable(matchPrerequisites) ? + matchPrerequisites.then(evaluate) : + evaluate(matchPrerequisites as boolean); } }; From d67582dc3b97b65115105fcadc0b0ff5849576eb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:15:36 -0300 Subject: [PATCH 22/62] Add debug logging for prerequisite evaluation --- src/evaluator/Engine.ts | 23 +++++++++++++++-------- src/logger/constants.ts | 1 + src/logger/messages/debug.ts | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 68b8d3e0..1238497e 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -9,6 +9,7 @@ import SplitIO from '../../types/splitio'; import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; +import { ENGINE_DEFAULT } from '../logger/constants'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { return { @@ -31,10 +32,13 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const parsedKey = keyParser(key); function evaluate(matchPrerequisites: boolean) { - if (!matchPrerequisites) return { - treatment: defaultTreatment, - label: NO_CONDITION_MATCH - }; + if (!matchPrerequisites) { + log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); + return { + treatment: defaultTreatment, + label: NO_CONDITION_MATCH + }; + } const evaluation = evaluator(parsedKey, seed, trafficAllocation, trafficAllocationSeed, attributes, splitEvaluator) as MaybeThenable; @@ -48,10 +52,13 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync label: SPLIT_ARCHIVED }; - if (killed) return { - treatment: defaultTreatment, - label: SPLIT_KILLED - }; + if (killed) { + log.debug(ENGINE_DEFAULT, ['Flag is killed']); + return { + treatment: defaultTreatment, + label: SPLIT_KILLED + }; + } const matchPrerequisites = prerequisites && prerequisites.length ? prerequisites.map(prerequisite => { diff --git a/src/logger/constants.ts b/src/logger/constants.ts index a2e53e15..729da6e1 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -28,6 +28,7 @@ export const SYNC_TASK_EXECUTE = 37; export const SYNC_TASK_STOP = 38; export const SETTINGS_SPLITS_FILTER = 39; export const ENGINE_MATCHER_RESULT = 40; +export const ENGINE_DEFAULT = 41; export const CLIENT_READY_FROM_CACHE = 100; export const CLIENT_READY = 101; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index d9bce2ab..8f8028e3 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -12,6 +12,7 @@ export const codesDebug: [number, string][] = codesInfo.concat([ [c.ENGINE_VALUE, c.LOG_PREFIX_ENGINE_VALUE + 'Extracted attribute `%s`. %s will be used for matching.'], [c.ENGINE_SANITIZE, c.LOG_PREFIX_ENGINE + ':sanitize: Attempted to sanitize %s which should be of type %s. Sanitized and processed value => %s'], [c.ENGINE_MATCHER_RESULT, c.LOG_PREFIX_ENGINE_MATCHER + '[%s] Result: %s. Rule value: %s. Evaluation value: %s'], + [c.ENGINE_DEFAULT, c.LOG_PREFIX_ENGINE + 'Evaluates to default treatment. %s'], // SDK [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'], [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'], From d93091d4f08d16cd66435e2222590135e357cf53 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:48:41 -0300 Subject: [PATCH 23/62] New label --- src/evaluator/Engine.ts | 4 ++-- src/utils/labels/index.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 1238497e..7073f3a7 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -2,7 +2,7 @@ import { get, isString } from '../utils/lang'; import { parser } from './parser'; import { keyParser } from '../utils/key'; import { thenable } from '../utils/promise/thenable'; -import { NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED } from '../utils/labels'; +import { NO_CONDITION_MATCH, SPLIT_ARCHIVED, SPLIT_KILLED, PREREQUISITES_NOT_MET } from '../utils/labels'; import { CONTROL } from '../utils/constants'; import { ISplit, MaybeThenable } from '../dtos/types'; import SplitIO from '../../types/splitio'; @@ -36,7 +36,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); return { treatment: defaultTreatment, - label: NO_CONDITION_MATCH + label: PREREQUISITES_NOT_MET }; } diff --git a/src/utils/labels/index.ts b/src/utils/labels/index.ts index b5765299..957100d7 100644 --- a/src/utils/labels/index.ts +++ b/src/utils/labels/index.ts @@ -6,3 +6,4 @@ export const EXCEPTION = 'exception'; export const SPLIT_ARCHIVED = 'archived'; export const NOT_IN_SPLIT = 'not in split'; export const UNSUPPORTED_MATCHER_TYPE = 'targeting rule type unsupported by sdk'; +export const PREREQUISITES_NOT_MET = 'prerequisites not met'; From 77d44b6f36608fa296f3eabedb94b21707c3124e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 11:52:21 -0300 Subject: [PATCH 24/62] Add prerequisites field to SplitView --- src/sdkManager/__tests__/mocks/input.json | 14 ++++++++++++-- src/sdkManager/__tests__/mocks/output.json | 8 ++++++-- src/sdkManager/index.ts | 3 ++- types/splitio.d.ts | 6 +++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/sdkManager/__tests__/mocks/input.json b/src/sdkManager/__tests__/mocks/input.json index 7aea3413..2e951c2c 100644 --- a/src/sdkManager/__tests__/mocks/input.json +++ b/src/sdkManager/__tests__/mocks/input.json @@ -41,5 +41,15 @@ "configurations": { "on": "\"color\": \"green\"" }, - "sets": ["set_a"] -} + "sets": [ + "set_a" + ], + "prerequisites": [ + { + "n": "some_flag", + "ts": [ + "on" + ] + } + ] +} \ No newline at end of file diff --git a/src/sdkManager/__tests__/mocks/output.json b/src/sdkManager/__tests__/mocks/output.json index 52a0b636..7a4f72ef 100644 --- a/src/sdkManager/__tests__/mocks/output.json +++ b/src/sdkManager/__tests__/mocks/output.json @@ -9,5 +9,9 @@ }, "sets": ["set_a"], "defaultTreatment": "off", - "impressionsDisabled": false -} + "impressionsDisabled": false, + "prerequisites": [{ + "flagName": "some_flag", + "treatments": ["on"] + }] +} \ No newline at end of file diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index 831771c9..d241b82e 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -32,7 +32,8 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null { configs: splitObject.configurations || {}, sets: splitObject.sets || [], defaultTreatment: splitObject.defaultTreatment, - impressionsDisabled: splitObject.impressionsDisabled === true + impressionsDisabled: splitObject.impressionsDisabled === true, + prerequisites: (splitObject.prerequisites || []).map(p => ({ flagName: p.n, treatments: p.ts })), }; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 0cab4b66..4d83238d 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -842,7 +842,7 @@ declare namespace SplitIO { /** * The list of treatments available for the feature flag. */ - treatments: Array; + treatments: string[]; /** * Current change number of the feature flag. */ @@ -866,6 +866,10 @@ declare namespace SplitIO { * Whether the feature flag has impressions tracking disabled or not. */ impressionsDisabled: boolean; + /** + * Prerequisites for the feature flag. + */ + prerequisites: Array<{ flagName: string, treatments: string[] }>; }; /** * A promise that resolves to a feature flag view or null if the feature flag is not found. From fe893b041f37c9057ae1ad4ab5533d02845adc6e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Mar 2025 13:43:26 -0300 Subject: [PATCH 25/62] Implement prerequisites matcher and refactor engineParser for prerequisite evaluation --- src/evaluator/Engine.ts | 21 ++++++++------------- src/evaluator/matchers/prerequisites.ts | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 src/evaluator/matchers/prerequisites.ts diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 7073f3a7..9a6df6f1 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -10,6 +10,7 @@ import { IStorageAsync, IStorageSync } from '../storages/types'; import { IEvaluation, IEvaluationResult, ISplitEvaluator } from './types'; import { ILogger } from '../logger/types'; import { ENGINE_DEFAULT } from '../logger/constants'; +import { prerequisitesMatcherContext } from './matchers/prerequisites'; function evaluationResult(result: IEvaluation | undefined, defaultTreatment: string): IEvaluationResult { return { @@ -24,6 +25,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const defaultTreatment = isString(split.defaultTreatment) ? split.defaultTreatment : CONTROL; const evaluator = parser(log, conditions, storage); + const prerequisiteMatcher = prerequisitesMatcherContext(prerequisites, storage, log); return { @@ -31,8 +33,8 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync const parsedKey = keyParser(key); - function evaluate(matchPrerequisites: boolean) { - if (!matchPrerequisites) { + function evaluate(prerequisitesMet: boolean) { + if (!prerequisitesMet) { log.debug(ENGINE_DEFAULT, ['Prerequisite not met']); return { treatment: defaultTreatment, @@ -60,18 +62,11 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync }; } - const matchPrerequisites = prerequisites && prerequisites.length ? - prerequisites.map(prerequisite => { - const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); - return thenable(evaluation) ? - evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) : - prerequisite.ts.indexOf(evaluation.treatment!) === -1; - }) : - true; + const prerequisitesMet = prerequisiteMatcher(key, attributes, splitEvaluator); - return thenable(matchPrerequisites) ? - matchPrerequisites.then(evaluate) : - evaluate(matchPrerequisites as boolean); + return thenable(prerequisitesMet) ? + prerequisitesMet.then(evaluate) : + evaluate(prerequisitesMet); } }; diff --git a/src/evaluator/matchers/prerequisites.ts b/src/evaluator/matchers/prerequisites.ts new file mode 100644 index 00000000..a9b3b031 --- /dev/null +++ b/src/evaluator/matchers/prerequisites.ts @@ -0,0 +1,25 @@ +import { ISplit, MaybeThenable } from '../../dtos/types'; +import { IStorageAsync, IStorageSync } from '../../storages/types'; +import { ILogger } from '../../logger/types'; +import { thenable } from '../../utils/promise/thenable'; +import { ISplitEvaluator } from '../types'; +import SplitIO from '../../../types/splitio'; + +export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger) { + + return function prerequisitesMatcher(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { + + function evaluatePrerequisite(prerequisite: { n: string; ts: string[] }): MaybeThenable { + const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); + return thenable(evaluation) + ? evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) + : prerequisite.ts.indexOf(evaluation.treatment!) === -1; + } + + return prerequisites.reduce>((prerequisitesMet, prerequisite) => { + return thenable(prerequisitesMet) ? + prerequisitesMet.then(prerequisitesMet => !prerequisitesMet ? false : evaluatePrerequisite(prerequisite)) : + !prerequisitesMet ? false : evaluatePrerequisite(prerequisite); + }, true); + }; +} From aee263127fe6fc24612aae5d78b059f499fdf3a9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 Mar 2025 15:14:50 -0300 Subject: [PATCH 26/62] Reuse FF mocks --- .../matchers/__tests__/dependency.spec.ts | 4 +--- .../matchers/__tests__/rbsegment.spec.ts | 3 +-- src/storages/__tests__/testUtils.ts | 22 +++++++++---------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/evaluator/matchers/__tests__/dependency.spec.ts b/src/evaluator/matchers/__tests__/dependency.spec.ts index 74d833d6..e9e9babc 100644 --- a/src/evaluator/matchers/__tests__/dependency.spec.ts +++ b/src/evaluator/matchers/__tests__/dependency.spec.ts @@ -5,9 +5,7 @@ import { IMatcher, IMatcherDto } from '../../types'; import { IStorageSync } from '../../../storages/types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { ISplit } from '../../../dtos/types'; - -const ALWAYS_ON_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets':[] } as ISplit; -const ALWAYS_OFF_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-off', 'trafficAllocation': 100, 'trafficAllocationSeed': -331690370, 'seed': 403891040, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'on', 'changeNumber': 1494365020316, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 0 }, { 'treatment': 'off', 'size': 100 }], 'label': 'in segment all' }], 'sets':[] } as ISplit; +import { ALWAYS_ON_SPLIT, ALWAYS_OFF_SPLIT } from '../../../storages/__tests__/testUtils'; const STORED_SPLITS: Record = { 'always-on': ALWAYS_ON_SPLIT, diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts index c2ee536b..c662776d 100644 --- a/src/evaluator/matchers/__tests__/rbsegment.spec.ts +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -6,8 +6,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { IRBSegment, ISplit } from '../../../dtos/types'; import { IStorageAsync, IStorageSync } from '../../../storages/types'; import { thenable } from '../../../utils/promise/thenable'; - -const ALWAYS_ON_SPLIT = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, 'algo': 2, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets': [] } as ISplit; +import { ALWAYS_ON_SPLIT } from '../../../storages/__tests__/testUtils'; const STORED_SPLITS: Record = { 'always-on': ALWAYS_ON_SPLIT diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index a4009b1c..b2ae79dc 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -22,15 +22,13 @@ export function assertSyncRecorderCacheInterface(cache: IEventsCacheSync | IImpr // Split mocks -//@ts-ignore -export const splitWithUserTT: ISplit = { name: 'user_ff', trafficTypeName: 'user_tt', conditions: [] }; -//@ts-ignore -export const splitWithAccountTT: ISplit = { name: 'account_ff', trafficTypeName: 'account_tt', conditions: [] }; -//@ts-ignore -export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'account_tt', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; -//@ts-ignore -export const something: ISplit = { name: 'something' }; -//@ts-ignore + +export const ALWAYS_ON_SPLIT: ISplit = { 'trafficTypeName': 'user', 'name': 'always-on', 'trafficAllocation': 100, 'trafficAllocationSeed': 1012950810, 'seed': -725161385, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'off', 'changeNumber': 1494364996459, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 100 }, { 'treatment': 'off', 'size': 0 }], 'label': 'in segment all' }], 'sets': [] }; +export const ALWAYS_OFF_SPLIT: ISplit = { 'trafficTypeName': 'user', 'name': 'always-off', 'trafficAllocation': 100, 'trafficAllocationSeed': -331690370, 'seed': 403891040, 'status': 'ACTIVE', 'killed': false, 'defaultTreatment': 'on', 'changeNumber': 1494365020316, 'conditions': [{ 'conditionType': 'ROLLOUT', 'matcherGroup': { 'combiner': 'AND', 'matchers': [{ 'keySelector': { 'trafficType': 'user', 'attribute': null }, 'matcherType': 'ALL_KEYS', 'negate': false, 'userDefinedSegmentMatcherData': null, 'whitelistMatcherData': null, 'unaryNumericMatcherData': null, 'betweenMatcherData': null }] }, 'partitions': [{ 'treatment': 'on', 'size': 0 }, { 'treatment': 'off', 'size': 100 }], 'label': 'in segment all' }], 'sets': [] }; //@ts-ignore +export const splitWithUserTT: ISplit = { name: 'user_ff', trafficTypeName: 'user_tt', conditions: [] }; //@ts-ignore +export const splitWithAccountTT: ISplit = { name: 'account_ff', trafficTypeName: 'account_tt', conditions: [] }; //@ts-ignore +export const splitWithAccountTTAndUsesSegments: ISplit = { trafficTypeName: 'account_tt', conditions: [{ matcherGroup: { matchers: [{ matcherType: 'IN_SEGMENT', userDefinedSegmentMatcherData: { segmentName: 'employees' } }] } }] }; //@ts-ignore +export const something: ISplit = { name: 'something' }; //@ts-ignore export const somethingElse: ISplit = { name: 'something else' }; // - With flag sets @@ -38,11 +36,11 @@ export const somethingElse: ISplit = { name: 'something else' }; //@ts-ignore export const featureFlagWithEmptyFS: ISplit = { name: 'ff_empty', sets: [] }; //@ts-ignore -export const featureFlagOne: ISplit = { name: 'ff_one', sets: ['o','n','e'] }; +export const featureFlagOne: ISplit = { name: 'ff_one', sets: ['o', 'n', 'e'] }; //@ts-ignore -export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t','w','o'] }; +export const featureFlagTwo: ISplit = { name: 'ff_two', sets: ['t', 'w', 'o'] }; //@ts-ignore -export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t','h','r','e'] }; +export const featureFlagThree: ISplit = { name: 'ff_three', sets: ['t', 'h', 'r', 'e'] }; //@ts-ignore export const featureFlagWithoutFS: ISplit = { name: 'ff_four' }; From fada384a72e2968b26064bea05eff1315b294f0b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 16:27:28 -0300 Subject: [PATCH 27/62] Polishing --- src/storages/AbstractSplitsCacheSync.ts | 7 +++++-- src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts | 10 ++++------ src/storages/inLocalStorage/SplitsCacheInLocal.ts | 8 +++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index b03dbc7d..97f1e383 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -80,8 +80,8 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync { * Given a parsed split, it returns a boolean flagging if its conditions use segments matchers (rules & whitelists). * This util is intended to simplify the implementation of `splitsCache::usesSegments` method */ -export function usesSegments(split: ISplit | IRBSegment) { - const conditions = split.conditions || []; +export function usesSegments(ruleEntity: ISplit | IRBSegment) { + const conditions = ruleEntity.conditions || []; for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; @@ -91,5 +91,8 @@ export function usesSegments(split: ISplit | IRBSegment) { } } + const excluded = (ruleEntity as IRBSegment).excluded; + if (excluded && excluded.segments && excluded.segments.length > 0) return true; + return false; } diff --git a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts index 85f73a56..37f6ad8e 100644 --- a/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts @@ -38,7 +38,7 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { } } - private updateSegmentCount(diff: number){ + private updateSegmentCount(diff: number) { const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); const count = toNumber(localStorage.getItem(segmentsCountKey)) + diff; // @ts-expect-error @@ -128,11 +128,9 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync { const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); - if (isFiniteNumber(splitsWithSegmentsCount)) { - return splitsWithSegmentsCount > 0; - } else { - return true; - } + return isFiniteNumber(splitsWithSegmentsCount) ? + splitsWithSegmentsCount > 0 : + true; } } diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 385125e3..f506b46d 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -206,11 +206,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync { const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey()); const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount); - if (isFiniteNumber(splitsWithSegmentsCount)) { - return splitsWithSegmentsCount > 0; - } else { - return true; - } + return isFiniteNumber(splitsWithSegmentsCount) ? + splitsWithSegmentsCount > 0 : + true; } /** From a5e0d3b728080843e2097191dbebd2eccc2f9b14 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 16:49:11 -0300 Subject: [PATCH 28/62] Polishing --- .../__tests__/splitChangesUpdater.spec.ts | 9 ++-- .../polling/updaters/splitChangesUpdater.ts | 47 ++++++++----------- .../UpdateWorkers/SplitsUpdateWorker.ts | 17 +++---- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index da85fbdd..a77e9516 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -13,6 +13,7 @@ import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; 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'; const ARCHIVED_FF = 'ARCHIVED'; @@ -202,7 +203,7 @@ describe('splitChangesUpdater', () => { const payload = notification.decoded as Pick; const changeNumber = payload.changeNumber; - await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: SPLIT_UPDATE })).resolves.toBe(true); // fetch and RBSegments.update not being called expect(fetchSplitChanges).toBeCalledTimes(0); @@ -226,7 +227,7 @@ describe('splitChangesUpdater', () => { const payload = { name: 'rbsegment', status: 'ACTIVE', changeNumber: 1684329854385, conditions: [] } as unknown as IRBSegment; const changeNumber = payload.changeNumber; - await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload, changeNumber: changeNumber, type: RB_SEGMENT_UPDATE })).resolves.toBe(true); // fetch and Splits.update not being called expect(fetchSplitChanges).toBeCalledTimes(0); @@ -256,7 +257,7 @@ describe('splitChangesUpdater', () => { let calls = 0; // emit always if not configured sets for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index, type: SPLIT_UPDATE })).resolves.toBe(true); expect(splitsEmitSpy.mock.calls[index][0]).toBe('state::splits-arrived'); index++; } @@ -268,7 +269,7 @@ describe('splitChangesUpdater', () => { splitsEmitSpy.mockReset(); index = 0; for (const setMock of setMocks) { - await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index })).resolves.toBe(true); + await expect(splitChangesUpdater(undefined, undefined, { payload: { ...payload, sets: setMock.sets, status: 'ACTIVE' }, changeNumber: index, type: SPLIT_UPDATE })).resolves.toBe(true); if (setMock.shouldEmit) calls++; expect(splitsEmitSpy.mock.calls.length).toBe(calls); index++; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 6f22dccc..eccb7d09 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -9,8 +9,10 @@ import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FET import { startsWith } from '../../../utils/lang'; import { IN_RULE_BASED_SEGMENT, IN_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; +import { SPLIT_UPDATE } from '../../streaming/constants'; -type ISplitChangesUpdater = (noCache?: boolean, till?: number, splitUpdateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }) => Promise +export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string }; +type SplitChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment). // Returns a promise that could be rejected. @@ -27,8 +29,9 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { * Collect segments from a raw split definition. * Exported for testing purposes. */ -export function parseSegments({ conditions }: ISplit | IRBSegment, matcherType: typeof IN_SEGMENT | typeof IN_RULE_BASED_SEGMENT = IN_SEGMENT): Set { - let segments = new Set(); +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); for (let i = 0; i < conditions.length; i++) { const matchers = conditions[i].matcherGroup.matchers; @@ -67,10 +70,6 @@ function matchFilters(featureFlag: ISplit, filters: ISplitFiltersValidation) { return matchNames || matchPrefix; } -function isFF(ruleBasedEntity: IRBSegment | ISplit): ruleBasedEntity is ISplit { - return (ruleBasedEntity as ISplit).defaultTreatment !== undefined; -} - /** * Given the list of splits from /splitChanges endpoint, it returns the mutations, * i.e., an object with added splits, removed splits and used segments. @@ -78,15 +77,15 @@ function isFF(ruleBasedEntity: IRBSegment | ISplit): ruleBasedEntity is ISplit { */ export function computeMutation(rules: Array, segments: Set, filters?: ISplitFiltersValidation): ISplitMutations { - return rules.reduce((accum, ruleBasedEntity) => { - if (ruleBasedEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleBasedEntity as ISplit, filters))) { - accum.added.push(ruleBasedEntity); + return rules.reduce((accum, ruleEntity) => { + if (ruleEntity.status === 'ACTIVE' && (!filters || matchFilters(ruleEntity as ISplit, filters))) { + accum.added.push(ruleEntity); - parseSegments(ruleBasedEntity).forEach((segmentName: string) => { + parseSegments(ruleEntity).forEach((segmentName: string) => { segments.add(segmentName); }); } else { - accum.removed.push(ruleBasedEntity); + accum.removed.push(ruleEntity); } return accum; @@ -116,7 +115,7 @@ export function splitChangesUpdaterFactory( requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, isClientSide?: boolean -): ISplitChangesUpdater { +): SplitChangesUpdater { const { splits, rbSegments, segments } = storage; let startingUp = true; @@ -134,7 +133,7 @@ export function splitChangesUpdaterFactory( * @param noCache - true to revalidate data to fetch * @param till - query param to bypass CDN requests */ - return function splitChangesUpdater(noCache?: boolean, till?: number, updateNotification?: { payload: ISplit | IRBSegment, changeNumber: number }) { + return function splitChangesUpdater(noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) { /** * @param since - current changeNumber at splitsCache @@ -144,15 +143,15 @@ export function splitChangesUpdaterFactory( const [since, rbSince] = sinces; log.debug(SYNC_SPLITS_FETCH, sinces); const fetcherPromise = Promise.resolve( - updateNotification ? - isFF(updateNotification.payload) ? + instantUpdate ? + instantUpdate.type === SPLIT_UPDATE ? // IFFU edge case: a change to a flag that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet - Promise.resolve(rbSegments.contains(parseSegments(updateNotification.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { + Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => { return contains ? - { ff: { d: [updateNotification.payload as ISplit], t: updateNotification.changeNumber } } : + { ff: { d: [instantUpdate.payload as ISplit], t: instantUpdate.changeNumber } } : splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator); }) : - { rbs: { d: [updateNotification.payload as IRBSegment], t: updateNotification.changeNumber } } : + { rbs: { d: [instantUpdate.payload as IRBSegment], t: instantUpdate.changeNumber } } : splitChangesFetcher(since, noCache, till, rbSince, _promiseDecorator) ) .then((splitChanges: ISplitChangesResponse) => { @@ -180,14 +179,8 @@ export function splitChangesUpdaterFactory( ]).then(([ffChanged, rbsChanged]) => { if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - return Promise.resolve(!splitsEventEmitter.splitsArrived || - ( - (!splitChanges.ff || since !== splitChanges.ff.t) && - (!splitChanges.rbs || rbSince !== splitChanges.rbs.t) && - (ffChanged || rbsChanged) && - (isClientSide || checkAllSegmentsExist(segments)) - ) - ) + return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments)))) + .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) .then(emitSplitsArrivedEvent => { // emit SDK events if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index b151477c..ac421269 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -8,6 +8,7 @@ import { ITelemetryTracker } from '../../../trackers/types'; import { Backoff } from '../../../utils/Backoff'; import { SPLITS } from '../../../utils/constants'; import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types'; +import { InstantUpdate } from '../../polling/updaters/splitChangesUpdater'; import { RB_SEGMENT_UPDATE } from '../constants'; import { parseFFUpdatePayload } from '../parseUtils'; import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types'; @@ -24,26 +25,26 @@ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSy let handleNewEvent = false; let isHandlingEvent: boolean; let cdnBypass: boolean; - let payload: ISplit | IRBSegment | undefined; + let instantUpdate: InstantUpdate | undefined; const backoff = new Backoff(__handleSplitUpdateCall, FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT); function __handleSplitUpdateCall() { isHandlingEvent = true; if (maxChangeNumber > cache.getChangeNumber()) { handleNewEvent = false; - const splitUpdateNotification = payload ? { payload, changeNumber: maxChangeNumber } : undefined; // fetch splits revalidating data if cached - splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, splitUpdateNotification).then(() => { + splitsSyncTask.execute(true, cdnBypass ? maxChangeNumber : undefined, instantUpdate).then(() => { if (!isHandlingEvent) return; // halt if `stop` has been called if (handleNewEvent) { __handleSplitUpdateCall(); } else { - if (splitUpdateNotification) telemetryTracker.trackUpdatesFromSSE(SPLITS); + if (instantUpdate) telemetryTracker.trackUpdatesFromSSE(SPLITS); // fetch new registered segments for server-side API. Not retrying on error if (segmentsSyncTask) segmentsSyncTask.execute(true); const attempts = backoff.attempts + 1; + // @TODO and with RBS and FF if (maxChangeNumber <= cache.getChangeNumber()) { log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); isHandlingEvent = false; @@ -76,7 +77,7 @@ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSy * * @param changeNumber - change number of the notification */ - put({ changeNumber, pcn }: ISplitUpdateData, _payload?: ISplit | IRBSegment) { + put({ changeNumber, pcn, type }: ISplitUpdateData, payload?: ISplit | IRBSegment) { const currentChangeNumber = cache.getChangeNumber(); if (changeNumber <= currentChangeNumber || changeNumber <= maxChangeNumber) return; @@ -84,10 +85,10 @@ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSy maxChangeNumber = changeNumber; handleNewEvent = true; cdnBypass = false; - payload = undefined; + instantUpdate = undefined; - if (_payload && currentChangeNumber === pcn) { - payload = _payload; + if (payload && currentChangeNumber === pcn) { + instantUpdate = { payload, changeNumber, type }; } if (backoff.timeoutID || !isHandlingEvent) __handleSplitUpdateCall(); From f2b18dfb4c1d8b5eb8b247115be86fbad6875715 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 19:01:47 -0300 Subject: [PATCH 29/62] Refactor SplitsUpdateWorker to force sync based on splits and rbSegments change number --- .../UpdateWorkers/SplitsUpdateWorker.ts | 15 ++-- .../__tests__/SplitsUpdateWorker.spec.ts | 74 +++++++++---------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index ac421269..6be94e0c 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -20,8 +20,11 @@ import { IUpdateWorker } from './types'; */ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSyncTask: ISplitsSyncTask, splitsEventEmitter: ISplitsEventEmitter, telemetryTracker: ITelemetryTracker, segmentsSyncTask?: ISegmentsSyncTask): IUpdateWorker<[updateData: ISplitUpdateData]> & { killSplit(event: ISplitKillData): void } { + const ff = SplitsUpdateWorker(storage.splits); + const rbs = SplitsUpdateWorker(storage.rbSegments); + function SplitsUpdateWorker(cache: ISplitsCacheSync | IRBSegmentsCacheSync) { - let maxChangeNumber = 0; + let maxChangeNumber = -1; let handleNewEvent = false; let isHandlingEvent: boolean; let cdnBypass: boolean; @@ -44,8 +47,7 @@ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSy const attempts = backoff.attempts + 1; - // @TODO and with RBS and FF - if (maxChangeNumber <= cache.getChangeNumber()) { + if (ff.isSync() && rbs.isSync()) { log.debug(`Refresh completed${cdnBypass ? ' bypassing the CDN' : ''} in ${attempts} attempts.`); isHandlingEvent = false; return; @@ -97,19 +99,20 @@ export function SplitsUpdateWorker(log: ILogger, storage: IStorageSync, splitsSy stop() { isHandlingEvent = false; backoff.reset(); + }, + isSync() { + return maxChangeNumber <= cache.getChangeNumber(); } }; } - const ff = SplitsUpdateWorker(storage.splits); - const rbs = SplitsUpdateWorker(storage.rbSegments); - return { put(parsedData) { if (parsedData.d && parsedData.c !== undefined) { try { const payload = parseFFUpdatePayload(parsedData.c, parsedData.d); if (payload) { + console.log('payload ', JSON.stringify(payload)); (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData, payload); return; } diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index bf3c294d..2bcffbd4 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { SDK_SPLITS_ARRIVED } from '../../../../readiness/constants'; import { SplitsCacheInMemory } from '../../../../storages/inMemory/SplitsCacheInMemory'; +import { RBSegmentsCacheInMemory } from '../../../../storages/inMemory/RBSegmentsCacheInMemory'; import { SplitsUpdateWorker } from '../SplitsUpdateWorker'; import { FETCH_BACKOFF_MAX_RETRIES } from '../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; @@ -53,19 +54,26 @@ const telemetryTracker = telemetryTrackerFactory(); // no-op telemetry tracker describe('SplitsUpdateWorker', () => { + const storage = { + splits: new SplitsCacheInMemory(), + rbSegments: new RBSegmentsCacheInMemory() + }; + afterEach(() => { // restore Backoff.__TEST__BASE_MILLIS = undefined; Backoff.__TEST__MAX_MILLIS = undefined; + + storage.splits.clear(); + storage.rbSegments.clear(); }); test('put', async () => { // setup - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache); + const splitsSyncTask = splitsSyncTaskMock(storage.splits); Backoff.__TEST__BASE_MILLIS = 1; // retry immediately - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); // assert calling `splitsSyncTask.execute` if `isExecuting` is false expect(splitsSyncTask.isExecuting()).toBe(false); @@ -102,9 +110,8 @@ describe('SplitsUpdateWorker', () => { test('put, backoff', async () => { // setup Backoff.__TEST__BASE_MILLIS = 50; - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, [90, 90, 90]); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, [90, 90, 90]); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); // while fetch fails, should retry with backoff splitUpdateWorker.put({ changeNumber: 100 }); @@ -121,9 +128,8 @@ describe('SplitsUpdateWorker', () => { // setup Backoff.__TEST__BASE_MILLIS = 10; // 10 millis instead of 10 sec Backoff.__TEST__MAX_MILLIS = 60; // 60 millis instead of 1 min - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, [...Array(FETCH_BACKOFF_MAX_RETRIES).fill(90), 90, 100]); // 12 executions. Last one is valid - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, [...Array(FETCH_BACKOFF_MAX_RETRIES).fill(90), 90, 100]); // 12 executions. Last one is valid + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); // queued @@ -146,9 +152,8 @@ describe('SplitsUpdateWorker', () => { // setup Backoff.__TEST__BASE_MILLIS = 10; // 10 millis instead of 10 sec Backoff.__TEST__MAX_MILLIS = 60; // 60 millis instead of 1 min - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, Array(FETCH_BACKOFF_MAX_RETRIES * 2).fill(90)); // 20 executions. No one is valid - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, Array(FETCH_BACKOFF_MAX_RETRIES * 2).fill(90)); // 20 executions. No one is valid + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); // queued @@ -168,18 +173,17 @@ describe('SplitsUpdateWorker', () => { test('killSplit', async () => { // setup - const cache = new SplitsCacheInMemory(); - cache.addSplit({ name: 'something' }); - cache.addSplit({ name: 'something else' }); + storage.splits.addSplit({ name: 'something' }); + storage.splits.addSplit({ name: 'something else' }); - const splitsSyncTask = splitsSyncTaskMock(cache); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); + const splitsSyncTask = splitsSyncTaskMock(storage.splits); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, splitsEventEmitterMock, telemetryTracker); // assert killing split locally, emitting SDK_SPLITS_ARRIVED event, and synchronizing splits if changeNumber is new splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'something', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous expect(splitsSyncTask.execute).toBeCalledTimes(1); // synchronizes splits if `isExecuting` is false expect(splitsEventEmitterMock.emit.mock.calls).toEqual([[SDK_SPLITS_ARRIVED, true]]); // emits `SDK_SPLITS_ARRIVED` with `isSplitKill` flag in true, if split kill resolves with update - assertKilledSplit(cache, 100, 'something', 'off'); + assertKilledSplit(storage.splits, 100, 'something', 'off'); // assert not killing split locally, not emitting SDK_SPLITS_ARRIVED event, and not synchronizes splits, if changeNumber is old splitsSyncTask.__resolveSplitsUpdaterCall(100); @@ -192,15 +196,14 @@ describe('SplitsUpdateWorker', () => { expect(splitsSyncTask.execute).toBeCalledTimes(0); // doesn't synchronize splits if killLocally resolved without update expect(splitsEventEmitterMock.emit).toBeCalledTimes(0); // doesn't emit `SDK_SPLITS_ARRIVED` if killLocally resolved without update - assertKilledSplit(cache, 100, 'something', 'off'); // calling `killLocally` with an old changeNumber made no effect + assertKilledSplit(storage.splits, 100, 'something', 'off'); // calling `killLocally` with an old changeNumber made no effect }); test('stop', async () => { // setup - const cache = new SplitsCacheInMemory(); - const splitsSyncTask = splitsSyncTaskMock(cache, [95]); + const splitsSyncTask = splitsSyncTaskMock(storage.splits, [95]); Backoff.__TEST__BASE_MILLIS = 1; - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber: 100 }); @@ -212,11 +215,10 @@ describe('SplitsUpdateWorker', () => { test('put, avoid fetching if payload sent', async () => { - const cache = new SplitsCacheInMemory(); splitNotifications.forEach(notification => { - const pcn = cache.getChangeNumber(); - const splitsSyncTask = splitsSyncTaskMock(cache); - const splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + const pcn = storage.splits.getChangeNumber(); + const splitsSyncTask = splitsSyncTaskMock(storage.splits); + const splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); const payload = notification.decoded; const changeNumber = payload.changeNumber; splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); // queued @@ -226,17 +228,15 @@ describe('SplitsUpdateWorker', () => { }); test('put, ccn and pcn validation for IFF', () => { - const cache = new SplitsCacheInMemory(); - // ccn = 103 & pcn = 104: Something was missed -> fetch split changes let ccn = 103; let pcn = 104; let changeNumber = 105; - cache.setChangeNumber(ccn); + storage.splits.setChangeNumber(ccn); const notification = splitNotifications[0]; - let splitsSyncTask = splitsSyncTaskMock(cache); - let splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + let splitsSyncTask = splitsSyncTaskMock(storage.splits); + let splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); @@ -246,10 +246,10 @@ describe('SplitsUpdateWorker', () => { ccn = 110; pcn = 0; changeNumber = 111; - cache.setChangeNumber(ccn); + storage.splits.setChangeNumber(ccn); - splitsSyncTask = splitsSyncTaskMock(cache); - splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + splitsSyncTask = splitsSyncTaskMock(storage.splits); + splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, undefined]); @@ -259,10 +259,10 @@ describe('SplitsUpdateWorker', () => { ccn = 120; pcn = 120; changeNumber = 121; - cache.setChangeNumber(ccn); + storage.splits.setChangeNumber(ccn); - splitsSyncTask = splitsSyncTaskMock(cache); - splitUpdateWorker = SplitsUpdateWorker(loggerMock, { splits: cache }, splitsSyncTask, telemetryTracker); + splitsSyncTask = splitsSyncTaskMock(storage.splits); + splitUpdateWorker = SplitsUpdateWorker(loggerMock, storage, splitsSyncTask, telemetryTracker); splitUpdateWorker.put({ changeNumber, pcn, d: notification.data, c: notification.compression }); expect(splitsSyncTask.execute).toBeCalledTimes(1); expect(splitsSyncTask.execute.mock.calls[0]).toEqual([true, undefined, { payload: notification.decoded, changeNumber }]); From 58747c5c46ee73ab2ad443c82063e1f755cbeb80 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 19:12:12 -0300 Subject: [PATCH 30/62] Rename parameter in buildRBSegmentKey for clarity --- src/storages/KeyBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 7e9b7e85..4167d860 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -37,8 +37,8 @@ export class KeyBuilder { return `${this.prefix}.split.`; } - buildRBSegmentKey(splitName: string) { - return `${this.prefix}.rbsegment.${splitName}`; + buildRBSegmentKey(rbsegmentName: string) { + return `${this.prefix}.rbsegment.${rbsegmentName}`; } buildRBSegmentsTillKey() { From cf8c517bd1a86a098cf628d54a90c8cf622010eb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 19:20:11 -0300 Subject: [PATCH 31/62] Refactor test function syntax to use arrow functions for consistency --- .../__tests__/evaluate-feature.spec.ts | 4 +- .../__tests__/evaluate-features.spec.ts | 4 +- src/evaluator/combiners/__tests__/and.spec.ts | 4 +- .../combiners/__tests__/ifelseif.spec.ts | 7 +- src/evaluator/matchers/__tests__/all.spec.ts | 2 +- .../matchers/__tests__/between.spec.ts | 2 +- .../matchers/__tests__/boolean.spec.ts | 2 +- .../matchers/__tests__/cont_all.spec.ts | 2 +- .../matchers/__tests__/cont_any.spec.ts | 2 +- .../matchers/__tests__/cont_str.spec.ts | 2 +- .../matchers/__tests__/dependency.spec.ts | 4 +- src/evaluator/matchers/__tests__/eq.spec.ts | 2 +- .../matchers/__tests__/eq_set.spec.ts | 2 +- src/evaluator/matchers/__tests__/ew.spec.ts | 4 +- src/evaluator/matchers/__tests__/gte.spec.ts | 2 +- src/evaluator/matchers/__tests__/lte.spec.ts | 2 +- .../matchers/__tests__/part_of.spec.ts | 2 +- .../__tests__/segment/client_side.spec.ts | 4 +- .../__tests__/segment/server_side.spec.ts | 2 +- src/evaluator/matchers/__tests__/sw.spec.ts | 2 +- .../matchers/__tests__/whitelist.spec.ts | 2 +- .../__tests__/segment.spec.ts | 4 +- .../__tests__/whitelist.spec.ts | 2 +- .../parser/__tests__/boolean.spec.ts | 2 +- src/evaluator/parser/__tests__/index.spec.ts | 36 ++++---- .../parser/__tests__/invalidMatcher.spec.ts | 6 +- src/evaluator/parser/__tests__/regex.spec.ts | 2 +- src/evaluator/parser/__tests__/set.spec.ts | 86 +++++++++---------- src/evaluator/parser/__tests__/string.spec.ts | 74 ++++++++-------- .../__tests__/trafficAllocation.spec.ts | 8 +- src/utils/promise/__tests__/wrapper.spec.ts | 2 +- 31 files changed, 139 insertions(+), 142 deletions(-) diff --git a/src/evaluator/__tests__/evaluate-feature.spec.ts b/src/evaluator/__tests__/evaluate-feature.spec.ts index 711c701f..ffda4687 100644 --- a/src/evaluator/__tests__/evaluate-feature.spec.ts +++ b/src/evaluator/__tests__/evaluate-feature.spec.ts @@ -25,7 +25,7 @@ const mockStorage = { } }; -test('EVALUATOR / should return label exception, treatment control and config null on error', async function () { +test('EVALUATOR / should return label exception, treatment control and config null on error', async () => { const expectedOutput = { treatment: 'control', label: EXCEPTION, @@ -46,7 +46,7 @@ test('EVALUATOR / should return label exception, treatment control and config nu }); -test('EVALUATOR / should return right label, treatment and config if storage returns without errors.', async function () { +test('EVALUATOR / should return right label, treatment and config if storage returns without errors.', async () => { const expectedOutput = { treatment: 'on', label: 'in segment all', config: '{color:\'black\'}', changeNumber: 1487277320548 diff --git a/src/evaluator/__tests__/evaluate-features.spec.ts b/src/evaluator/__tests__/evaluate-features.spec.ts index 761f2804..e42fc6d3 100644 --- a/src/evaluator/__tests__/evaluate-features.spec.ts +++ b/src/evaluator/__tests__/evaluate-features.spec.ts @@ -42,7 +42,7 @@ const mockStorage = { } }; -test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async function () { +test('EVALUATOR - Multiple evaluations at once / should return label exception, treatment control and config null on error', async () => { const expectedOutput = { throw_exception: { treatment: 'control', @@ -65,7 +65,7 @@ test('EVALUATOR - Multiple evaluations at once / should return label exception, }); -test('EVALUATOR - Multiple evaluations at once / should return right labels, treatments and configs if storage returns without errors.', async function () { +test('EVALUATOR - Multiple evaluations at once / should return right labels, treatments and configs if storage returns without errors.', async () => { const expectedOutput = { config: { treatment: 'on', label: 'in segment all', diff --git a/src/evaluator/combiners/__tests__/and.spec.ts b/src/evaluator/combiners/__tests__/and.spec.ts index 8d31a9d4..58e732d0 100644 --- a/src/evaluator/combiners/__tests__/and.spec.ts +++ b/src/evaluator/combiners/__tests__/and.spec.ts @@ -1,14 +1,14 @@ import { andCombinerContext } from '../and'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('COMBINER AND / should always return true', async function () { +test('COMBINER AND / should always return true', async () => { let AND = andCombinerContext(loggerMock, [() => true, () => true, () => true]); expect(await AND('always true')).toBe(true); // should always return true }); -test('COMBINER AND / should always return false', async function () { +test('COMBINER AND / should always return false', async () => { let AND = andCombinerContext(loggerMock, [() => true, () => true, () => false]); diff --git a/src/evaluator/combiners/__tests__/ifelseif.spec.ts b/src/evaluator/combiners/__tests__/ifelseif.spec.ts index 983b21a1..b890e410 100644 --- a/src/evaluator/combiners/__tests__/ifelseif.spec.ts +++ b/src/evaluator/combiners/__tests__/ifelseif.spec.ts @@ -2,7 +2,7 @@ import { ifElseIfCombinerContext } from '../ifelseif'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('IF ELSE IF COMBINER / should correctly propagate context parameters and predicates returns value', async function () { +test('IF ELSE IF COMBINER / should correctly propagate context parameters and predicates returns value', async () => { let inputKey = 'sample'; let inputSeed = 1234; let inputAttributes = {}; @@ -20,10 +20,9 @@ test('IF ELSE IF COMBINER / should correctly propagate context parameters and pr let ifElseIfEvaluator = ifElseIfCombinerContext(loggerMock, predicates); expect(await ifElseIfEvaluator(inputKey, inputSeed, inputAttributes) === evaluationResult).toBe(true); - console.log(`evaluator should return ${evaluationResult}`); }); -test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a treatment', async function () { +test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a treatment', async () => { let predicates = [ function undef() { return undefined; @@ -41,7 +40,7 @@ test('IF ELSE IF COMBINER / should stop evaluating when one matcher return a tre expect(await ifElseIfEvaluator()).toBe('exclude'); // exclude treatment found }); -test('IF ELSE IF COMBINER / should return undefined if there is none matching rule', async function () { +test('IF ELSE IF COMBINER / should return undefined if there is none matching rule', async () => { const predicates = [ function undef() { return undefined; diff --git a/src/evaluator/matchers/__tests__/all.spec.ts b/src/evaluator/matchers/__tests__/all.spec.ts index 106dea8b..2c6a5f72 100644 --- a/src/evaluator/matchers/__tests__/all.spec.ts +++ b/src/evaluator/matchers/__tests__/all.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER ALL_KEYS / should always return true', function () { +test('MATCHER ALL_KEYS / should always return true', () => { const matcher = matcherFactory(loggerMock, { type: matcherTypes.ALL_KEYS, value: undefined diff --git a/src/evaluator/matchers/__tests__/between.spec.ts b/src/evaluator/matchers/__tests__/between.spec.ts index eaf78106..34d44eeb 100644 --- a/src/evaluator/matchers/__tests__/between.spec.ts +++ b/src/evaluator/matchers/__tests__/between.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and 20', function () { +test('MATCHER BETWEEN / should return true ONLY when the value is between 10 and 20', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.BETWEEN, diff --git a/src/evaluator/matchers/__tests__/boolean.spec.ts b/src/evaluator/matchers/__tests__/boolean.spec.ts index 8a166e9c..a5f9d5bc 100644 --- a/src/evaluator/matchers/__tests__/boolean.spec.ts +++ b/src/evaluator/matchers/__tests__/boolean.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER BOOLEAN / should return true ONLY when the value is true', function () { +test('MATCHER BOOLEAN / should return true ONLY when the value is true', () => { const matcher = matcherFactory(loggerMock, { type: matcherTypes.EQUAL_TO_BOOLEAN, value: true diff --git a/src/evaluator/matchers/__tests__/cont_all.spec.ts b/src/evaluator/matchers/__tests__/cont_all.spec.ts index 353877db..3b99cff1 100644 --- a/src/evaluator/matchers/__tests__/cont_all.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_all.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER CONTAINS_ALL_OF_SET / should return true ONLY when value contains all of set ["update", "add"]', function () { +test('MATCHER CONTAINS_ALL_OF_SET / should return true ONLY when value contains all of set ["update", "add"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_ALL_OF_SET, diff --git a/src/evaluator/matchers/__tests__/cont_any.spec.ts b/src/evaluator/matchers/__tests__/cont_any.spec.ts index 478d6e1c..f8ff75fa 100644 --- a/src/evaluator/matchers/__tests__/cont_any.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_any.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER CONTAINS_ANY_OF_SET / should return true ONLY when value contains any of set ["update", "add"]', function () { +test('MATCHER CONTAINS_ANY_OF_SET / should return true ONLY when value contains any of set ["update", "add"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_ANY_OF_SET, diff --git a/src/evaluator/matchers/__tests__/cont_str.spec.ts b/src/evaluator/matchers/__tests__/cont_str.spec.ts index be7c2870..a24fe2bc 100644 --- a/src/evaluator/matchers/__tests__/cont_str.spec.ts +++ b/src/evaluator/matchers/__tests__/cont_str.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER CONTAINS_STRING / should return true ONLY when the value is contained in ["roni", "bad", "ar"]', function () { +test('MATCHER CONTAINS_STRING / should return true ONLY when the value is contained in ["roni", "bad", "ar"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.CONTAINS_STRING, diff --git a/src/evaluator/matchers/__tests__/dependency.spec.ts b/src/evaluator/matchers/__tests__/dependency.spec.ts index e9e9babc..7cb184d6 100644 --- a/src/evaluator/matchers/__tests__/dependency.spec.ts +++ b/src/evaluator/matchers/__tests__/dependency.spec.ts @@ -18,7 +18,7 @@ const mockStorage = { } }; -test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split returns one of the expected treatments', function () { +test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split returns one of the expected treatments', () => { const matcherTrueAlwaysOn = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { @@ -57,7 +57,7 @@ test('MATCHER IN_SPLIT_TREATMENT / should return true ONLY when parent split ret expect(matcherFalseAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(false); // Parent split returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false }); -test('MATCHER IN_SPLIT_TREATMENT / Edge cases', function () { +test('MATCHER IN_SPLIT_TREATMENT / Edge cases', () => { const matcherParentNotExist = matcherFactory(loggerMock, { type: matcherTypes.IN_SPLIT_TREATMENT, value: { diff --git a/src/evaluator/matchers/__tests__/eq.spec.ts b/src/evaluator/matchers/__tests__/eq.spec.ts index 6527b874..b9921ff7 100644 --- a/src/evaluator/matchers/__tests__/eq.spec.ts +++ b/src/evaluator/matchers/__tests__/eq.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER EQUAL / should return true ONLY when the value is equal to 10', function () { +test('MATCHER EQUAL / should return true ONLY when the value is equal to 10', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.EQUAL_TO, diff --git a/src/evaluator/matchers/__tests__/eq_set.spec.ts b/src/evaluator/matchers/__tests__/eq_set.spec.ts index dc60bdc3..ff575675 100644 --- a/src/evaluator/matchers/__tests__/eq_set.spec.ts +++ b/src/evaluator/matchers/__tests__/eq_set.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER EQUAL_TO_SET / should return true ONLY when value is equal to set ["update", "add"]', function () { +test('MATCHER EQUAL_TO_SET / should return true ONLY when value is equal to set ["update", "add"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.EQUAL_TO_SET, diff --git a/src/evaluator/matchers/__tests__/ew.spec.ts b/src/evaluator/matchers/__tests__/ew.spec.ts index beefef29..bc13e7ed 100644 --- a/src/evaluator/matchers/__tests__/ew.spec.ts +++ b/src/evaluator/matchers/__tests__/ew.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", "b", "c"]', function () { +test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", "b", "c"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.ENDS_WITH, @@ -17,7 +17,7 @@ test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["a", expect(matcher('manager')).toBe(false); // manager doesn't end with ["a", "b", "c"] }); -test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["demo.test.org"]', function () { +test('MATCHER ENDS_WITH / should return true ONLY when the value ends with ["demo.test.org"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.ENDS_WITH, diff --git a/src/evaluator/matchers/__tests__/gte.spec.ts b/src/evaluator/matchers/__tests__/gte.spec.ts index f38bd62f..9ecba7b0 100644 --- a/src/evaluator/matchers/__tests__/gte.spec.ts +++ b/src/evaluator/matchers/__tests__/gte.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER GREATER THAN OR EQUAL / should return true ONLY when the value is greater than or equal to 10', function () { +test('MATCHER GREATER THAN OR EQUAL / should return true ONLY when the value is greater than or equal to 10', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.GREATER_THAN_OR_EQUAL_TO, diff --git a/src/evaluator/matchers/__tests__/lte.spec.ts b/src/evaluator/matchers/__tests__/lte.spec.ts index b6aef174..84b190fe 100644 --- a/src/evaluator/matchers/__tests__/lte.spec.ts +++ b/src/evaluator/matchers/__tests__/lte.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER LESS THAN OR EQUAL / should return true ONLY when the value is less than or equal to 10', function () { +test('MATCHER LESS THAN OR EQUAL / should return true ONLY when the value is less than or equal to 10', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.LESS_THAN_OR_EQUAL_TO, diff --git a/src/evaluator/matchers/__tests__/part_of.spec.ts b/src/evaluator/matchers/__tests__/part_of.spec.ts index 3b740f82..89c249e3 100644 --- a/src/evaluator/matchers/__tests__/part_of.spec.ts +++ b/src/evaluator/matchers/__tests__/part_of.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER PART_OF_SET / should return true ONLY when value is part of of set ["update", "add", "delete"]', function () { +test('MATCHER PART_OF_SET / should return true ONLY when value is part of of set ["update", "add", "delete"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.PART_OF_SET, diff --git a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts index c4e3470e..7cb25079 100644 --- a/src/evaluator/matchers/__tests__/segment/client_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/client_side.spec.ts @@ -4,7 +4,7 @@ import { IMatcher, IMatcherDto } from '../../../types'; import { IStorageSync } from '../../../../storages/types'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { +test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async () => { const segment = 'employees'; const matcherTrue = matcherFactory(loggerMock, { @@ -33,7 +33,7 @@ test('MATCHER IN_SEGMENT / should return true ONLY when the segment is defined i expect(await matcherFalse('key')).toBe(false); // segment not found in mySegments list }); -test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async function () { +test('MATCHER IN_LARGE_SEGMENT / should return true ONLY when the segment is defined inside the segment storage', async () => { const segment = 'employees'; const matcherTrue = matcherFactory(loggerMock, { diff --git a/src/evaluator/matchers/__tests__/segment/server_side.spec.ts b/src/evaluator/matchers/__tests__/segment/server_side.spec.ts index 906aec62..d0819c11 100644 --- a/src/evaluator/matchers/__tests__/segment/server_side.spec.ts +++ b/src/evaluator/matchers/__tests__/segment/server_side.spec.ts @@ -4,7 +4,7 @@ import { IMatcher, IMatcherDto } from '../../../types'; import { IStorageSync } from '../../../../storages/types'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER IN_SEGMENT / should return true ONLY when the key is defined inside the segment', async function () { +test('MATCHER IN_SEGMENT / should return true ONLY when the key is defined inside the segment', async () => { const segment = 'employees'; const matcher = matcherFactory(loggerMock, { diff --git a/src/evaluator/matchers/__tests__/sw.spec.ts b/src/evaluator/matchers/__tests__/sw.spec.ts index ef8c4f80..72e22258 100644 --- a/src/evaluator/matchers/__tests__/sw.spec.ts +++ b/src/evaluator/matchers/__tests__/sw.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER STARTS_WITH / should return true ONLY when the value starts with ["a", "b", "c"]', function () { +test('MATCHER STARTS_WITH / should return true ONLY when the value starts with ["a", "b", "c"]', () => { const matcher = matcherFactory(loggerMock, { negate: false, type: matcherTypes.STARTS_WITH, diff --git a/src/evaluator/matchers/__tests__/whitelist.spec.ts b/src/evaluator/matchers/__tests__/whitelist.spec.ts index 5ed12e77..1553c3bf 100644 --- a/src/evaluator/matchers/__tests__/whitelist.spec.ts +++ b/src/evaluator/matchers/__tests__/whitelist.spec.ts @@ -3,7 +3,7 @@ import { matcherFactory } from '..'; import { IMatcher, IMatcherDto } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('MATCHER WHITELIST / should return true ONLY when the value is in the list', function () { +test('MATCHER WHITELIST / should return true ONLY when the value is in the list', () => { const matcher = matcherFactory(loggerMock, { type: matcherTypes.WHITELIST, value: ['key'] diff --git a/src/evaluator/matchersTransform/__tests__/segment.spec.ts b/src/evaluator/matchersTransform/__tests__/segment.spec.ts index 1d25811e..cb9dd8fb 100644 --- a/src/evaluator/matchersTransform/__tests__/segment.spec.ts +++ b/src/evaluator/matchersTransform/__tests__/segment.spec.ts @@ -1,6 +1,6 @@ import { segmentTransform } from '../segment'; -test('TRANSFORMS / a segment object should be flatten to a string', function () { +test('TRANSFORMS / a segment object should be flatten to a string', () => { const segmentName = 'employees'; const sample = { segmentName @@ -11,7 +11,7 @@ test('TRANSFORMS / a segment object should be flatten to a string', function () expect(segmentName).toBe(plainSegmentName); // extracted segmentName matches }); -test('TRANSFORMS / if there is none segmentName entry, returns undefined', function () { +test('TRANSFORMS / if there is none segmentName entry, returns undefined', () => { const sample = undefined; const undefinedSegmentName = segmentTransform(sample); diff --git a/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts b/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts index 07483817..e4ed770a 100644 --- a/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts +++ b/src/evaluator/matchersTransform/__tests__/whitelist.spec.ts @@ -1,6 +1,6 @@ import { whitelistTransform } from '../whitelist'; -test('TRANSFORMS / the whitelist array should be extracted', function () { +test('TRANSFORMS / the whitelist array should be extracted', () => { let sample = { whitelist: [ 'u1', diff --git a/src/evaluator/parser/__tests__/boolean.spec.ts b/src/evaluator/parser/__tests__/boolean.spec.ts index 7d304ec9..255a5cf6 100644 --- a/src/evaluator/parser/__tests__/boolean.spec.ts +++ b/src/evaluator/parser/__tests__/boolean.spec.ts @@ -4,7 +4,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user.boolean is true then split 100%:on', async function () { +test('PARSER / if user.boolean is true then split 100%:on', async () => { // @ts-ignore const evaluator = parser(loggerMock, [{ diff --git a/src/evaluator/parser/__tests__/index.spec.ts b/src/evaluator/parser/__tests__/index.spec.ts index 41176fbd..d2ef5496 100644 --- a/src/evaluator/parser/__tests__/index.spec.ts +++ b/src/evaluator/parser/__tests__/index.spec.ts @@ -5,7 +5,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { bucket } from '../../../utils/murmur3/murmur3'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user is in segment all 100%:on', async function () { +test('PARSER / if user is in segment all 100%:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -31,7 +31,7 @@ test('PARSER / if user is in segment all 100%:on', async function () { expect(evaluation.label).toBe('in segment all'); // in segment all }); -test('PARSER / if user is in segment all 100%:off', async function () { +test('PARSER / if user is in segment all 100%:off', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -59,7 +59,7 @@ test('PARSER / if user is in segment all 100%:off', async function () { expect(evaluation.label === 'in segment all').toBe(true); // in segment all }); -test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async function () { +test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -84,7 +84,7 @@ test('PARSER / NEGATED if user is in segment all 100%:on, then no match', async expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on', async function () { +test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -123,7 +123,7 @@ test('PARSER / if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on expect(evaluation.label === 'whitelisted').toBe(true); // whitelisted }); -test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on, negated results', async function () { +test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split 100%:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -162,7 +162,7 @@ test('PARSER / NEGATED if user is in segment ["u1", "u2", "u3", "u4"] then split expect(evaluation).toBe(undefined); // evaluation should throw undefined }); -test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', async function () { +test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -208,7 +208,7 @@ test('PARSER / if user.account is in list ["v1", "v2", "v3"] then split 100:on', expect(evaluation === undefined).toBe(true); // v4 is not defined inside the whitelist }); -test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -255,7 +255,7 @@ test('PARSER / NEGATED if user.account is in list ["v1", "v2", "v3"] then split expect(evaluation.label === 'whitelisted').toBe(true); // label should be "whitelisted" }); -test('PARSER / if user.account is in segment all then split 100:on', async function () { +test('PARSER / if user.account is in segment all then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', @@ -280,7 +280,7 @@ test('PARSER / if user.account is in segment all then split 100:on', async funct expect(evaluation.treatment === 'on').toBe(true); // ALL_KEYS always matches }); -test('PARSER / if user.attr is between 10 and 20 then split 100:on', async function () { +test('PARSER / if user.attr is between 10 and 20 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -321,7 +321,7 @@ test('PARSER / if user.attr is between 10 and 20 then split 100:on', async funct expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // undefined is not between 10 and 20 }); -test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -363,7 +363,7 @@ test('PARSER / NEGATED if user.attr is between 10 and 20 then split 100:on, nega expect(evaluation.treatment === 'on').toBe(true); // undefined is not between 10 and 20 }); -test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async function () { +test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -408,7 +408,7 @@ test('PARSER / if user.attr <= datetime 1458240947021 then split 100:on', async expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // missing attributes in the parameters list }); -test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -454,7 +454,7 @@ test('PARSER / NEGATED if user.attr <= datetime 1458240947021 then split 100:on, expect(evaluation.treatment === 'on').toBe(true); // missing attributes in the parameters list }); -test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async function () { +test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -499,7 +499,7 @@ test('PARSER / if user.attr >= datetime 1458240947021 then split 100:on', async expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // missing attributes in the parameters list }); -test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -545,7 +545,7 @@ test('PARSER / NEGATED if user.attr >= datetime 1458240947021 then split 100:on, expect(evaluation.treatment === 'on').toBe(true); // missing attributes in the parameters list }); -test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async function () { +test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -590,7 +590,7 @@ test('PARSER / if user.attr = datetime 1458240947021 then split 100:on', async f expect(await evaluator(keyParser('test@split.io'), 31, 100, 31)).toBe(undefined); // missing attributes should be evaluated to false }); -test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, negated results', async function () { +test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, negated results', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -636,7 +636,7 @@ test('PARSER / NEGATED if user.attr = datetime 1458240947021 then split 100:on, expect(evaluation.treatment).toBe('on'); // missing attributes should be evaluated to false }); -test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async function () { +test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', @@ -662,8 +662,6 @@ test('PARSER / if user is in segment all then split 20%:A,20%:B,60%:A', async fu let evaluation = await evaluator(keyParser('aa'), 31, 100, 31); expect(evaluation.treatment).toBe('A'); // 20%:A // bucket 6 with murmur3 - console.log(bucket('b297', 31)); - evaluation = await evaluator(keyParser('b297'), 31, 100, 31); expect(evaluation.treatment).toBe('B'); // 20%:B // bucket 34 with murmur3 diff --git a/src/evaluator/parser/__tests__/invalidMatcher.spec.ts b/src/evaluator/parser/__tests__/invalidMatcher.spec.ts index c69c8ded..87cfc422 100644 --- a/src/evaluator/parser/__tests__/invalidMatcher.spec.ts +++ b/src/evaluator/parser/__tests__/invalidMatcher.spec.ts @@ -3,7 +3,7 @@ import { parser } from '..'; import { ISplitCondition } from '../../../dtos/types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / handle invalid matcher as control', async function () { +test('PARSER / handle invalid matcher as control', async () => { const evaluator = parser(loggerMock, [{ matcherGroup: { combiner: 'AND', @@ -32,7 +32,7 @@ test('PARSER / handle invalid matcher as control', async function () { expect(evaluation.label).toBe('targeting rule type unsupported by sdk'); // track invalid as targeting rule type unsupported by sdk }); -test('PARSER / handle invalid matcher as control (complex example)', async function () { +test('PARSER / handle invalid matcher as control (complex example)', async () => { const evaluator = parser(loggerMock, [ { 'conditionType': 'WHITELIST', @@ -132,7 +132,7 @@ test('PARSER / handle invalid matcher as control (complex example)', async funct } }); -test('PARSER / handle invalid matcher as control (complex example mixing invalid and valid matchers)', async function () { +test('PARSER / handle invalid matcher as control (complex example mixing invalid and valid matchers)', async () => { const evaluator = parser(loggerMock, [ { 'conditionType': 'WHITELIST', diff --git a/src/evaluator/parser/__tests__/regex.spec.ts b/src/evaluator/parser/__tests__/regex.spec.ts index 176c961a..736e93d3 100644 --- a/src/evaluator/parser/__tests__/regex.spec.ts +++ b/src/evaluator/parser/__tests__/regex.spec.ts @@ -4,7 +4,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user.string is true then split 100%:on', async function () { +test('PARSER / if user.string is true then split 100%:on', async () => { // @ts-ignore const evaluator = parser(loggerMock, [{ matcherGroup: { diff --git a/src/evaluator/parser/__tests__/set.spec.ts b/src/evaluator/parser/__tests__/set.spec.ts index 5d46abb8..6a6d8c35 100644 --- a/src/evaluator/parser/__tests__/set.spec.ts +++ b/src/evaluator/parser/__tests__/set.spec.ts @@ -7,7 +7,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; // // EQUAL_TO_SET // -test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on', async function () { +test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -42,7 +42,7 @@ test('PARSER / if user.permissions ["read", "write"] equal to set ["read", "writ expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "write"] then split 100:on', async function () { +test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "write"] then split 100:on', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -77,7 +77,7 @@ test('PARSER / if user.permissions ["write", "read"] equal to set ["read", "writ expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 100:on', async function () { +test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 100:on', async () => { const label = 'permissions = ["1", "2"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -112,7 +112,7 @@ test('PARSER / if user.permissions ["1", 2] equal to set ["1", "2"] then split 1 expect(evaluation.label).toBe(label); // label should be correct }); -test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["read", "write"] then not match', async function () { +test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["read", "write"] then not match', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -146,7 +146,7 @@ test('PARSER / if user.permissions ["read", "write", "delete"] equal to set ["re expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then not match', async function () { +test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then not match', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -180,7 +180,7 @@ test('PARSER / if user.permissions ["read"] equal to set ["read", "write"] then expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "write"] then not match', async function () { +test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "write"] then not match', async () => { const label = 'permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -214,7 +214,7 @@ test('PARSER / if user.permissions ["read", "delete"] equal to set ["read", "wri expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argentina"] then split 100:on', async function () { +test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argentina"] then split 100:on', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -248,7 +248,7 @@ test('PARSER / if user.countries ["argentina", "usa"] equal to set ["usa","argen expect(evaluation.label).toBe(label); // label should match }); -test('PARSER / if attribute is not an array we should not match equal to set', async function () { +test('PARSER / if attribute is not an array we should not match equal to set', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -283,7 +283,7 @@ test('PARSER / if attribute is not an array we should not match equal to set', a expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match equal to set', async function () { +test('PARSER / if attribute is an EMPTY array we should not match equal to set', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -316,7 +316,7 @@ test('PARSER / if attribute is an EMPTY array we should not match equal to set', expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read", "write"] then split 100:on should not match', async () => { const label = 'not permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -349,7 +349,7 @@ test('PARSER / NEGATED if user.permissions ["read", "write"] equal to set ["read expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write"] false, then match', async function () { +test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write"] false, then match', async () => { const label = 'not permissions = ["read", "write"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -383,7 +383,7 @@ test('PARSER / NEGATED if user.permissions ["read"] equal to set ["read", "write expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match equal to set, so match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match equal to set, so match', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -420,7 +420,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match equal to expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal to set, so match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal to set, so match', async () => { const label = 'countries = ["usa","argentina"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -457,7 +457,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match equal // // CONTAINS_ALL_OF_SET // -test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on', async function () { +test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -492,7 +492,7 @@ test('PARSER / if user.permissions ["read", "edit", "delete"] contains all of se expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of set ["read", "edit"] then split 100:on', async function () { +test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of set ["read", "edit"] then split 100:on', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -527,7 +527,7 @@ test('PARSER / if user.permissions ["edit", "read", "delete"] contains all of se expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1", "edit"] then split 100:on', async function () { +test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1", "edit"] then split 100:on', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -562,7 +562,7 @@ test('PARSER / if user.permissions [1, "edit", "delete"] contains all of set ["1 expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] then not match', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -596,7 +596,7 @@ test('PARSER / if user.permissions ["read"] contains all of set ["read", "edit"] expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of set ["read", "edit"] then not match', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -630,7 +630,7 @@ test('PARSER / if user.permissions ["read", "delete", "manage"] contains all of expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is not an array we should not match contains all', async function () { +test('PARSER / if attribute is not an array we should not match contains all', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -665,7 +665,7 @@ test('PARSER / if attribute is not an array we should not match contains all', a expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match contains all', async function () { +test('PARSER / if attribute is an EMPTY array we should not match contains all', async () => { const label = 'permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -698,7 +698,7 @@ test('PARSER / if attribute is an EMPTY array we should not match contains all', expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains all of set ["read", "edit"] then split 100:on should not match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -731,7 +731,7 @@ test('PARSER / NEGATED if user.permissions ["read", "edit", "delete"] contains a expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", "edit"] false, so match', async function () { +test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", "edit"] false, so match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -765,7 +765,7 @@ test('PARSER / NEGATED if user.permissions ["read"] contains all of set ["read", expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match contains all, so match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match contains all, so match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -802,7 +802,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match contains expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains all, so match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains all, so match', async () => { const label = 'not permissions contains ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -839,7 +839,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match contai // // PART_OF_SET // -test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -874,7 +874,7 @@ test('PARSER / if user.permissions ["read", "edit"] is part of set ["read", "edi expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -909,7 +909,7 @@ test('PARSER / if user.permissions ["edit", "read"] is part of set ["read", "edi expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["1", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -944,7 +944,7 @@ test('PARSER / if user.permissions [1, "edit"] is part of set ["1", "edit", "del expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "edit"] then not match', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -978,7 +978,7 @@ test('PARSER / if user.permissions ["admin", "magic"] is part of set ["read", "e expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if attribute is not an array we should not match part of', async function () { +test('PARSER / if attribute is not an array we should not match part of', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1013,7 +1013,7 @@ test('PARSER / if attribute is not an array we should not match part of', async expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match part of', async function () { +test('PARSER / if attribute is an EMPTY array we should not match part of', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1046,7 +1046,7 @@ test('PARSER / if attribute is an EMPTY array we should not match part of', asyn expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["read", "edit", "delete"] then split 100:on should not match', async () => { const label = 'not permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1079,7 +1079,7 @@ test('PARSER / NEGATED if user.permissions ["read", "edit"] is part of set ["rea expect(evaluation).toBe(undefined); // evaluation should return treatment undefined }); -test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["read", "edit"] false, then match', async function () { +test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["read", "edit"] false, then match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1113,7 +1113,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "magic"] is part of set ["r expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match part of, so match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match part of, so match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1150,7 +1150,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match part of, expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match part of, so match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match part of, so match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1187,7 +1187,7 @@ test('PARSER / NEGATED if attribute is an EMPTY array we should not match part o // // CONTAINS_ANY_OF_SET // -test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1222,7 +1222,7 @@ test('PARSER / if user.permissions ["admin", "edit"] contains any of set ["read" expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1", "delete"] then split 100:on', async function () { +test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1", "delete"] then split 100:on', async () => { const label = 'permissions part of ["read", "1", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1257,7 +1257,7 @@ test('PARSER / if user.permissions ["admin", 1] contains any of set ["read", "1" expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] then not match', async function () { +test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] then not match', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1291,7 +1291,7 @@ test('PARSER / if user.permissions ["admin", "magic"] contains any of set ["read expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if attribute is not an array we should not match contains any', async function () { +test('PARSER / if attribute is not an array we should not match contains any', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1326,7 +1326,7 @@ test('PARSER / if attribute is not an array we should not match contains any', a expect(evaluation).toBe(undefined); // evaluator should not match }); -test('PARSER / if attribute is an EMPTY array we should not match contains any', async function () { +test('PARSER / if attribute is an EMPTY array we should not match contains any', async () => { const label = 'permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1359,7 +1359,7 @@ test('PARSER / if attribute is an EMPTY array we should not match contains any', expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on should not match', async function () { +test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set ["read", "edit", "delete"] then split 100:on should not match', async () => { const label = 'not permissions part of ["read", "edit", "delete"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1392,7 +1392,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "edit"] contains any of set expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] false, then should match', async function () { +test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of set ["read", "edit"] false, then should match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1426,7 +1426,7 @@ test('PARSER / NEGATED if user.permissions ["admin", "magic"] contains any of se expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is not an array we should not match contains any, then should match', async function () { +test('PARSER / NEGATED if attribute is not an array we should not match contains any, then should match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1458,7 +1458,7 @@ test('PARSER / NEGATED if attribute is not an array we should not match contains expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains any, then should match', async function () { +test('PARSER / NEGATED if attribute is an EMPTY array we should not match contains any, then should match', async () => { const label = 'not permissions part of ["read", "edit"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { diff --git a/src/evaluator/parser/__tests__/string.spec.ts b/src/evaluator/parser/__tests__/string.spec.ts index dc10f3c6..81fe9b9d 100644 --- a/src/evaluator/parser/__tests__/string.spec.ts +++ b/src/evaluator/parser/__tests__/string.spec.ts @@ -7,7 +7,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; // // STARTS WITH // -test('PARSER / if user.email starts with ["nico"] then split 100:on', async function () { +test('PARSER / if user.email starts with ["nico"] then split 100:on', async () => { const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -41,7 +41,7 @@ test('PARSER / if user.email starts with ["nico"] then split 100:on', async func expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should match', async function () { +test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should match', async () => { const label = 'email starts with ["1"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -75,7 +75,7 @@ test('PARSER / if user.email = 123, starts with ["1"] then split 100:on should m expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async function () { +test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async () => { const label = 'email starts with ["nico", "marcio", "facu"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -109,7 +109,7 @@ test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 1 expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async function () { +test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 100:on', async () => { const label = 'email starts with ["nico", "marcio", "facu"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -143,7 +143,7 @@ test('PARSER / if user.email starts with ["nico", "marcio", "facu"] then split 1 expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email does not start with ["nico"] then not match', async function () { +test('PARSER / if user.email does not start with ["nico"] then not match', async () => { // const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -175,7 +175,7 @@ test('PARSER / if user.email does not start with ["nico"] then not match', async expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not match', async function () { +test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not match', async () => { // const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -205,7 +205,7 @@ test('PARSER / if user.email is an EMPTY string, start with ["nico"] should not expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is not a string, start with ["nico"] should not match', async function () { +test('PARSER / if user.email is not a string, start with ["nico"] should not match', async () => { // const label = 'email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -238,7 +238,7 @@ test('PARSER / if user.email is not a string, start with ["nico"] should not mat expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so not match', async function () { +test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so not match', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -271,7 +271,7 @@ test('PARSER / NEGATED if user.email starts with ["nico"] then split 100:on, so expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email does not start with ["nico"] should not match, then match', async function () { +test('PARSER / NEGATED if user.email does not start with ["nico"] should not match, then match', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -305,7 +305,7 @@ test('PARSER / NEGATED if user.email does not start with ["nico"] should not mat expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] should not match, so negation should', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -337,7 +337,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, start with ["nico"] sho expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should not match, so negation should', async () => { const label = 'not email starts with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -376,7 +376,7 @@ test('PARSER / NEGATED if user.email is not a string, start with ["nico"] should // // ENDS WITH // -test('PARSER / if user.email ends with ["split.io"] then split 100:on', async function () { +test('PARSER / if user.email ends with ["split.io"] then split 100:on', async () => { const label = 'email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -410,7 +410,7 @@ test('PARSER / if user.email ends with ["split.io"] then split 100:on', async fu expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should match', async function () { +test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should match', async () => { const label = 'email starts with ["3"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -443,7 +443,7 @@ test('PARSER / if user.email = 123, ends with ["3"] then split 100:on should mat expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async function () { +test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async () => { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -477,7 +477,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async function () { +test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] then split 100:on', async () => { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -511,7 +511,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] but attribute is "" then split 100:on', async function () { +test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] but attribute is "" then split 100:on', async () => { const label = 'email ends with ["gmail.com", "split.io", "hotmail.com"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -544,7 +544,7 @@ test('PARSER / if user.email ends with ["gmail.com", "split.io", "hotmail.com"] expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email does not end with ["split.io"] then not match', async function () { +test('PARSER / if user.email does not end with ["split.io"] then not match', async () => { const label = 'email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -577,7 +577,7 @@ test('PARSER / if user.email does not end with ["split.io"] then not match', asy expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not match', async function () { +test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not match', async () => { // const label = 'email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -607,7 +607,7 @@ test('PARSER / if user.email is an EMPTY string, end with ["nico"] should not ma expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is not a string, end with ["nico"] should not match', async function () { +test('PARSER / if user.email is not a string, end with ["nico"] should not match', async () => { // const label = 'email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -642,7 +642,7 @@ test('PARSER / if user.email is not a string, end with ["nico"] should not match expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, so not match', async function () { +test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, so not match', async () => { const label = 'not email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -674,7 +674,7 @@ test('PARSER / NEGATED if user.email ends with ["split.io"] then split 100:on, s expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email does not end with ["split.io"] then no match, so match', async function () { +test('PARSER / NEGATED if user.email does not end with ["split.io"] then no match, so match', async () => { const label = 'not email ends with ["split.io"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -707,7 +707,7 @@ test('PARSER / NEGATED if user.email does not end with ["split.io"] then no matc expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] should not match, so negation should', async () => { const label = 'not email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -739,7 +739,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, end with ["nico"] shoul expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should not match, so negation should', async () => { const label = 'not email ends with ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -778,7 +778,7 @@ test('PARSER / NEGATED if user.email is not a string, end with ["nico"] should n // // CONTAINS STRING // -test('PARSER / if user.email contains ["@split"] then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -812,7 +812,7 @@ test('PARSER / if user.email contains ["@split"] then split 100:on', async funct expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email = 123, contains ["2"] then split 100:on should match', async function () { +test('PARSER / if user.email = 123, contains ["2"] then split 100:on should match', async () => { const label = 'email contains ["2"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -846,7 +846,7 @@ test('PARSER / if user.email = 123, contains ["2"] then split 100:on should matc expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -880,7 +880,7 @@ test('PARSER / if user.email contains ["@split"] (beginning) then split 100:on', expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -914,7 +914,7 @@ test('PARSER / if user.email contains ["@split"] (end) then split 100:on', async expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split"] (whole string matches) then split 100:on', async function () { +test('PARSER / if user.email contains ["@split"] (whole string matches) then split 100:on', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -948,7 +948,7 @@ test('PARSER / if user.email contains ["@split"] (whole string matches) then spl expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async function () { +test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async () => { const label = 'email contains ["@split", "@gmail", "@hotmail"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -982,7 +982,7 @@ test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then spli expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async function () { +test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then split 100:on', async () => { const label = 'email contains ["@split", "@gmail", "@hotmail"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1016,7 +1016,7 @@ test('PARSER / if user.email contains ["@split", "@gmail", "@hotmail"] then spli expect(evaluation.label).toBe(label); // }); -test('PARSER / if user.email does not contain ["@split"] then not match', async function () { +test('PARSER / if user.email does not contain ["@split"] then not match', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1049,7 +1049,7 @@ test('PARSER / if user.email does not contain ["@split"] then not match', async expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not match', async function () { +test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not match', async () => { // const label = 'email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1079,7 +1079,7 @@ test('PARSER / if user.email is an EMPTY string, contains ["nico"] should not ma expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / if user.email is not a string, contains ["nico"] should not match', async function () { +test('PARSER / if user.email is not a string, contains ["nico"] should not match', async () => { // const label = 'email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1114,7 +1114,7 @@ test('PARSER / if user.email is not a string, contains ["nico"] should not match expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then no match', async function () { +test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then no match', async () => { const label = 'not email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1147,7 +1147,7 @@ test('PARSER / NEGATED if user.email contains ["@split"] then split 100:on, then expect(evaluation).toBe(undefined); // evaluator should return undefined }); -test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, so match', async function () { +test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, so match', async () => { const label = 'email contains ["@split"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1180,7 +1180,7 @@ test('PARSER / NEGATED if user.email does not contain ["@split"] then not match, expect(evaluation.label).toBe(label); // }); -test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] should not match, so negation should', async () => { const label = 'not email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { @@ -1212,7 +1212,7 @@ test('PARSER / NEGATED if user.email is an EMPTY string, contains ["nico"] shoul expect(evaluation.label).toBe(label); // evaluator should return correct label }); -test('PARSER / NEGATED if user.email is not a string, contains ["nico"] should not match, so negation should', async function () { +test('PARSER / NEGATED if user.email is not a string, contains ["nico"] should not match, so negation should', async () => { const label = 'not email contains ["nico"]'; const evaluator = parser(loggerMock, [{ matcherGroup: { diff --git a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts index d9af5ca9..a71d6dee 100644 --- a/src/evaluator/parser/__tests__/trafficAllocation.spec.ts +++ b/src/evaluator/parser/__tests__/trafficAllocation.spec.ts @@ -5,7 +5,7 @@ import { ISplitCondition } from '../../../dtos/types'; import { IEvaluation } from '../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', async function () { +test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', @@ -32,7 +32,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 0%', a expect(evaluation.label).toBe('not in split'); // label should be fixed string }); -test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% with bucket below 99', async function () { +test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% with bucket below 99', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', @@ -59,7 +59,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% wi expect(evaluation.label).toBe('in segment all'); // in segment all }); -test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% and bucket returns 100', async function () { +test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% and bucket returns 100', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'ROLLOUT', @@ -86,7 +86,7 @@ test('PARSER / if user is in segment all 100%:on but trafficAllocation is 99% an expect(evaluation.label).toBe('not in split'); // label should be fixed string }); -test('PARSER / if user is whitelisted and in segment all 100%:off with trafficAllocation as 0%', async function () { +test('PARSER / if user is whitelisted and in segment all 100%:off with trafficAllocation as 0%', async () => { const evaluator = parser(loggerMock, [{ conditionType: 'WHITELIST', diff --git a/src/utils/promise/__tests__/wrapper.spec.ts b/src/utils/promise/__tests__/wrapper.spec.ts index ca0d418a..ab44f9d2 100644 --- a/src/utils/promise/__tests__/wrapper.spec.ts +++ b/src/utils/promise/__tests__/wrapper.spec.ts @@ -120,7 +120,7 @@ test('Promise utils / promise wrapper', function (done) { }); -test('Promise utils / promise wrapper: async/await', async function () { +test('Promise utils / promise wrapper: async/await', async () => { expect.assertions(8); // number of passHandler, passHandlerWithThrow and passHandlerFinally From a40ab685ebcf1b73810897ef514d925e3561a4dc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 Mar 2025 23:35:10 -0300 Subject: [PATCH 32/62] Remove unused import --- src/evaluator/parser/__tests__/index.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/evaluator/parser/__tests__/index.spec.ts b/src/evaluator/parser/__tests__/index.spec.ts index d2ef5496..c3829a3d 100644 --- a/src/evaluator/parser/__tests__/index.spec.ts +++ b/src/evaluator/parser/__tests__/index.spec.ts @@ -2,7 +2,6 @@ import { parser } from '..'; import { keyParser } from '../../../utils/key'; import { ISplitCondition } from '../../../dtos/types'; -import { bucket } from '../../../utils/murmur3/murmur3'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; test('PARSER / if user is in segment all 100%:on', async () => { From 454f6c4be103e8ae19b1d48974d8f2e7bd2bb93d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 Mar 2025 13:25:48 -0300 Subject: [PATCH 33/62] rc --- package-lock.json | 311 +++++++++++----------------------------------- package.json | 2 +- 2 files changed, 75 insertions(+), 238 deletions(-) diff --git a/package-lock.json b/package-lock.json index f15fce99..2b5cab24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.1.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.1.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -67,13 +67,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -266,18 +267,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -293,38 +294,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "node_modules/@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.10" }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -495,9 +484,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -507,14 +496,14 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -542,14 +531,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1979,18 +1967,6 @@ "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2379,20 +2355,6 @@ } ] }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -2459,21 +2421,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2812,15 +2759,6 @@ "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -3978,15 +3916,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -7465,18 +7394,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -7577,15 +7494,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8164,13 +8072,14 @@ } }, "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { @@ -8319,15 +8228,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true }, "@babel/helper-validator-option": { @@ -8337,33 +8246,24 @@ "dev": true }, "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" } }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "@babel/parser": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/types": "^7.26.10" } }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -8482,23 +8382,23 @@ } }, "@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "requires": { "regenerator-runtime": "^0.14.0" } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" } }, "@babel/traverse": { @@ -8520,14 +8420,13 @@ } }, "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" } }, "@bcoe/v8-coverage": { @@ -9608,15 +9507,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -9898,17 +9788,6 @@ "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -9956,21 +9835,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -10228,12 +10092,6 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -11089,12 +10947,6 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -13670,15 +13522,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "supports-hyperlinks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", @@ -13757,12 +13600,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index d51bc14f..ae29cd2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.1.0", + "version": "2.1.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From ea660749d0ead20d157c8bbfc4556ad0dac621b8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 1 Apr 2025 18:10:14 -0300 Subject: [PATCH 34/62] Fix tests --- .github/workflows/ci.yml | 2 +- src/storages/__tests__/KeyBuilder.spec.ts | 17 +++++++++++------ .../__tests__/validateCache.spec.ts | 6 +++--- .../__tests__/settings.mocks.ts | 3 ++- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 181fa4af..2b4cd7bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Set up nodejs - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 'lts/*' cache: 'npm' diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index 6c3fe5ff..bd21fa66 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -105,17 +105,22 @@ test('KEYS / latency and exception keys (telemetry)', () => { test('getStorageHash', () => { expect(getStorageHash({ - core: { authorizationKey: '' }, + core: { authorizationKey: 'sdk-key' }, sync: { __splitFiltersValidation: { queryString: '&names=p1__split,p2__split' }, flagSpecVersion: '1.3' } - } as ISettings)).toBe('2ce5cc38'); + } as ISettings)).toBe('d700da23'); expect(getStorageHash({ - core: { authorizationKey: '' }, + core: { authorizationKey: 'sdk-key' }, sync: { __splitFiltersValidation: { queryString: '&names=p2__split,p3__split' }, flagSpecVersion: '1.3' } - } as ISettings)).toBe('e65079c6'); + } as ISettings)).toBe('8c8a8789'); expect(getStorageHash({ - core: { authorizationKey: '' }, + core: { authorizationKey: 'aaaabbbbcccc1234' }, sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.3' } - } as ISettings)).toBe('193e6f3f'); + } as ISettings)).toBe('dc1f9817'); + + expect(getStorageHash({ + core: { authorizationKey: 'another-sdk-key' }, + sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.3' } + } as ISettings)).toBe('45c6ba5d'); }); diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index 27050a56..b9386de7 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -6,7 +6,7 @@ import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; import { nearlyEqual } from '../../../__tests__/testUtils'; import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; -const FULL_SETTINGS_HASH = '404832b3'; +const FULL_SETTINGS_HASH = 'dc1f9817'; describe('validateCache', () => { const keys = new KeyBuilderCS('SPLITIO', 'user'); @@ -77,7 +77,7 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another' } }, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); @@ -85,7 +85,7 @@ describe('validateCache', () => { expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); - expect(localStorage.getItem(keys.buildHashKey())).toBe('aa4877c2'); + expect(localStorage.getItem(keys.buildHashKey())).toBe('45c6ba5d'); expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); }); diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index bc55891f..f850f0bf 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -1,6 +1,7 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; import { ISettings } from '../../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { FLAG_SPEC_VERSION } from '../../constants'; export const settingsWithKey = { core: { @@ -67,7 +68,7 @@ export const fullSettings: ISettings = { groupedFilters: { bySet: [], byName: [], byPrefix: [] }, }, enabled: true, - flagSpecVersion: '1.3' + flagSpecVersion: FLAG_SPEC_VERSION }, version: 'jest', runtime: { From cec23c8d128e29f7eb538101396f81950d5c50db Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Apr 2025 11:48:02 -0300 Subject: [PATCH 35/62] make IRBSegment conditions and excluded properties optional --- src/dtos/types.ts | 8 ++++---- src/evaluator/matchers/rbsegment.ts | 7 ++++--- src/sync/polling/updaters/splitChangesUpdater.ts | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 423de3a8..66598a6e 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -203,10 +203,10 @@ export interface IRBSegment { name: string, changeNumber: number, status: 'ACTIVE' | 'ARCHIVED', - conditions: ISplitCondition[], - excluded: { - keys: string[], - segments: string[] + conditions?: ISplitCondition[], + excluded?: { + keys?: string[], + segments?: string[] } } diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 68318320..240eb07b 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -12,7 +12,7 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt return function ruleBasedSegmentMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { function matchConditions(rbsegment: IRBSegment) { - const conditions = rbsegment.conditions; + const conditions = rbsegment.conditions || []; const evaluator = parser(log, conditions, storage); const evaluation = evaluator( @@ -31,10 +31,11 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt function isExcluded(rbSegment: IRBSegment) { const matchingKey = getMatching(key); + const excluded = rbSegment.excluded || {}; - if (rbSegment.excluded.keys.indexOf(matchingKey) !== -1) return true; + if (excluded.keys && excluded.keys.indexOf(matchingKey) !== -1) return true; - const isInSegment = rbSegment.excluded.segments.map(segmentName => { + const isInSegment = (excluded.segments || []).map(segmentName => { return storage.segments.isInSegment(segmentName, matchingKey); }); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 54a31b0b..91b4070f 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -30,7 +30,7 @@ function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { * 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 { conditions = [], excluded } = ruleEntity as IRBSegment; const segments = new Set(excluded && excluded.segments); for (let i = 0; i < conditions.length; i++) { From fc2e7b4e7118f4e5593d31bdfd09279b1c1e9b8a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Apr 2025 11:51:41 -0300 Subject: [PATCH 36/62] Add changelog entry --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 3100c540..c2534182 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.3.0 (April XXX, 2025) + - Added support for targeting rules based on rule-based segments. + 2.2.0 (March 28, 2025) - 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. - Added two new configuration options for the SDK storage in browsers when using storage type `LOCALSTORAGE`: From 5b056eb4dea45964105f08cc1a0c07fece202b6d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Apr 2025 16:31:30 -0300 Subject: [PATCH 37/62] Cache expiration for rbSegments --- src/storages/KeyBuilderCS.ts | 2 +- .../__tests__/validateCache.spec.ts | 25 +++++++++++++------ src/storages/inLocalStorage/index.ts | 2 +- src/storages/inLocalStorage/validateCache.ts | 4 ++- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index 107ccabb..deae16af 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -15,7 +15,7 @@ export class KeyBuilderCS extends KeyBuilder implements MySegmentsKeyBuilder { constructor(prefix: string, matchingKey: string) { super(prefix); this.matchingKey = matchingKey; - this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet|rbsegment)\\.`); + this.regexSplitsCacheKey = new RegExp(`^${prefix}\\.(splits?|trafficType|flagSet)\\.`); } /** diff --git a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts index b9386de7..b87fa67b 100644 --- a/src/storages/inLocalStorage/__tests__/validateCache.spec.ts +++ b/src/storages/inLocalStorage/__tests__/validateCache.spec.ts @@ -5,6 +5,7 @@ import { fullSettings } from '../../../utils/settingsValidation/__tests__/settin import { SplitsCacheInLocal } from '../SplitsCacheInLocal'; import { nearlyEqual } from '../../../__tests__/testUtils'; import { MySegmentsCacheInLocal } from '../MySegmentsCacheInLocal'; +import { RBSegmentsCacheInLocal } from '../RBSegmentsCacheInLocal'; const FULL_SETTINGS_HASH = 'dc1f9817'; @@ -14,9 +15,11 @@ describe('validateCache', () => { const segments = new MySegmentsCacheInLocal(fullSettings.log, keys); const largeSegments = new MySegmentsCacheInLocal(fullSettings.log, keys); const splits = new SplitsCacheInLocal(fullSettings, keys); + const rbSegments = new RBSegmentsCacheInLocal(fullSettings, keys); - jest.spyOn(splits, 'clear'); jest.spyOn(splits, 'getChangeNumber'); + jest.spyOn(splits, 'clear'); + jest.spyOn(rbSegments, 'clear'); jest.spyOn(segments, 'clear'); jest.spyOn(largeSegments, 'clear'); @@ -26,11 +29,12 @@ describe('validateCache', () => { }); test('if there is no cache, it should return false', () => { - expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).not.toHaveBeenCalled(); expect(splits.clear).not.toHaveBeenCalled(); + expect(rbSegments.clear).not.toHaveBeenCalled(); expect(segments.clear).not.toHaveBeenCalled(); expect(largeSegments.clear).not.toHaveBeenCalled(); expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); @@ -43,11 +47,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({}, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(validateCache({}, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); expect(logSpy).not.toHaveBeenCalled(); expect(splits.clear).not.toHaveBeenCalled(); + expect(rbSegments.clear).not.toHaveBeenCalled(); expect(segments.clear).not.toHaveBeenCalled(); expect(largeSegments.clear).not.toHaveBeenCalled(); expect(splits.getChangeNumber).toHaveBeenCalledTimes(1); @@ -61,11 +66,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); localStorage.setItem(keys.buildLastUpdatedKey(), Date.now() - 1000 * 60 * 60 * 24 * 2 + ''); // 2 days ago - expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({ expirationDays: 1 }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: Cache expired more than 1 days ago. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); + expect(rbSegments.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); @@ -77,11 +83,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({}, { ...fullSettings, core: { ...fullSettings.core, authorizationKey: 'another-sdk-key' } }, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: SDK key, flags filter criteria, or flags spec version has changed. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); + expect(rbSegments.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); @@ -94,11 +101,12 @@ describe('validateCache', () => { localStorage.setItem(keys.buildSplitsTillKey(), '1'); localStorage.setItem(keys.buildHashKey(), FULL_SETTINGS_HASH); - expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(1); + expect(rbSegments.clear).toHaveBeenCalledTimes(1); expect(segments.clear).toHaveBeenCalledTimes(1); expect(largeSegments.clear).toHaveBeenCalledTimes(1); @@ -109,15 +117,16 @@ describe('validateCache', () => { // If cache is cleared, it should not clear again until a day has passed logSpy.mockClear(); localStorage.setItem(keys.buildSplitsTillKey(), '1'); - expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(true); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(true); expect(logSpy).not.toHaveBeenCalled(); expect(localStorage.getItem(keys.buildLastClear())).toBe(lastClear); // Last clear should not have changed // If a day has passed, it should clear again localStorage.setItem(keys.buildLastClear(), (Date.now() - 1000 * 60 * 60 * 24 - 1) + ''); - expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, segments, largeSegments)).toBe(false); + expect(validateCache({ clearOnInit: true }, fullSettings, keys, splits, rbSegments, segments, largeSegments)).toBe(false); expect(logSpy).toHaveBeenCalledWith('storage:localstorage: clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'); expect(splits.clear).toHaveBeenCalledTimes(2); + expect(rbSegments.clear).toHaveBeenCalledTimes(2); expect(segments.clear).toHaveBeenCalledTimes(2); expect(largeSegments.clear).toHaveBeenCalledTimes(2); expect(nearlyEqual(parseInt(localStorage.getItem(keys.buildLastClear()) as string), Date.now())).toBe(true); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 670b5e63..8924b84d 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -53,7 +53,7 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt uniqueKeys: new UniqueKeysCacheInMemoryCS(), validateCache() { - return validateCache(options, settings, keys, splits, segments, largeSegments); + return validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments); }, destroy() { }, diff --git a/src/storages/inLocalStorage/validateCache.ts b/src/storages/inLocalStorage/validateCache.ts index c9bd78d2..93d3144c 100644 --- a/src/storages/inLocalStorage/validateCache.ts +++ b/src/storages/inLocalStorage/validateCache.ts @@ -3,6 +3,7 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import { getStorageHash } from '../KeyBuilder'; import { LOG_PREFIX } from './constants'; import type { SplitsCacheInLocal } from './SplitsCacheInLocal'; +import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal'; import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal'; import { KeyBuilderCS } from '../KeyBuilderCS'; import SplitIO from '../../../types/splitio'; @@ -66,13 +67,14 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS * * @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache) */ -export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { +export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean { const currentTimestamp = Date.now(); const isThereCache = splits.getChangeNumber() > -1; if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) { splits.clear(); + rbSegments.clear(); segments.clear(); largeSegments.clear(); From 65ae7b1ba25e791453bd23c59c644bc2ab4a3484 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 3 Apr 2025 17:39:39 -0300 Subject: [PATCH 38/62] Unit tests --- src/evaluator/Engine.ts | 2 +- .../matchers/__tests__/prerequisites.spec.ts | 106 ++++++++++++++++++ src/evaluator/matchers/prerequisites.ts | 15 ++- 3 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 src/evaluator/matchers/__tests__/prerequisites.spec.ts diff --git a/src/evaluator/Engine.ts b/src/evaluator/Engine.ts index 9a6df6f1..4228316f 100644 --- a/src/evaluator/Engine.ts +++ b/src/evaluator/Engine.ts @@ -62,7 +62,7 @@ export function engineParser(log: ILogger, split: ISplit, storage: IStorageSync }; } - const prerequisitesMet = prerequisiteMatcher(key, attributes, splitEvaluator); + const prerequisitesMet = prerequisiteMatcher({ key, attributes }, splitEvaluator); return thenable(prerequisitesMet) ? prerequisitesMet.then(evaluate) : diff --git a/src/evaluator/matchers/__tests__/prerequisites.spec.ts b/src/evaluator/matchers/__tests__/prerequisites.spec.ts new file mode 100644 index 00000000..82059fc9 --- /dev/null +++ b/src/evaluator/matchers/__tests__/prerequisites.spec.ts @@ -0,0 +1,106 @@ +import { evaluateFeature } from '../../index'; +import { IStorageSync } from '../../../storages/types'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { ISplit } from '../../../dtos/types'; +import { ALWAYS_ON_SPLIT, ALWAYS_OFF_SPLIT } from '../../../storages/__tests__/testUtils'; +import { prerequisitesMatcherContext } from '../prerequisites'; + +const STORED_SPLITS: Record = { + 'always-on': ALWAYS_ON_SPLIT, + 'always-off': ALWAYS_OFF_SPLIT +}; + +const mockStorage = { + splits: { + getSplit: (name: string) => STORED_SPLITS[name] + } +} as IStorageSync; + +test('MATCHER PREREQUISITES / should return true when all prerequisites are met', () => { + // A single prerequisite + const matcherTrueAlwaysOn = prerequisitesMatcherContext([{ + n: 'always-on', + ts: ['not-existing', 'on', 'other'] // We should match from a list of treatments + }], mockStorage, loggerMock); + expect(matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true + + const matcherFalseAlwaysOn = prerequisitesMatcherContext([{ + n: 'always-on', + ts: ['off', 'v1'] + }], mockStorage, loggerMock); + expect(matcherFalseAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false + + const matcherTrueAlwaysOff = prerequisitesMatcherContext([{ + n: 'always-off', + ts: ['not-existing', 'off'] + }], mockStorage, loggerMock); + expect(matcherTrueAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(true); // Feature flag returns one of the expected treatments, so the matcher returns true + + const matcherFalseAlwaysOff = prerequisitesMatcherContext([{ + n: 'always-off', + ts: ['v1', 'on'] + }], mockStorage, loggerMock); + expect(matcherFalseAlwaysOff({ key: 'a-key' }, evaluateFeature)).toBe(false); // Feature flag returns treatment "on", but we are expecting ["off", "v1"], so the matcher returns false + + // Multiple prerequisites + const matcherTrueMultiplePrerequisites = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: ['on'] + }, + { + n: 'always-off', + ts: ['off'] + } + ], mockStorage, loggerMock); + expect(matcherTrueMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); // All prerequisites are met, so the matcher returns true + + const matcherFalseMultiplePrerequisites = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: ['on'] + }, + { + n: 'always-off', + ts: ['on'] + } + ], mockStorage, loggerMock); + expect(matcherFalseMultiplePrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(false); // One of the prerequisites is not met, so the matcher returns false +}); + +test('MATCHER PREREQUISITES / Edge cases', () => { + // No prerequisites + const matcherTrueNoPrerequisites = prerequisitesMatcherContext(undefined, mockStorage, loggerMock); + expect(matcherTrueNoPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); + + const matcherTrueEmptyPrerequisites = prerequisitesMatcherContext([], mockStorage, loggerMock); + expect(matcherTrueEmptyPrerequisites({ key: 'a-key' }, evaluateFeature)).toBe(true); + + // Non existent feature flag + const matcherParentNotExist = prerequisitesMatcherContext([{ + n: 'not-existent-feature-flag', + ts: ['on', 'off'] + }], mockStorage, loggerMock); + expect(matcherParentNotExist({ key: 'a-key' }, evaluateFeature)).toBe(false); // If the feature flag does not exist, matcher should return false + + // Empty treatments list + const matcherNoTreatmentsExpected = prerequisitesMatcherContext([ + { + n: 'always-on', + ts: [] + }], mockStorage, loggerMock); + expect(matcherNoTreatmentsExpected({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list is empty, matcher should return false (no treatment will match) + + const matcherExpectedTreatmentWrongTypeMatching = prerequisitesMatcherContext([{ + n: 'always-on', // @ts-ignore + ts: [null, [1, 2], 3, {}, true, 'on'] + + }], mockStorage, loggerMock); + expect(matcherExpectedTreatmentWrongTypeMatching({ key: 'a-key' }, evaluateFeature)).toBe(true); // If treatments expectation list has elements of the wrong type, those elements are overlooked. + + const matcherExpectedTreatmentWrongTypeNotMatching = prerequisitesMatcherContext([{ + n: 'always-off', // @ts-ignore + ts: [null, [1, 2], 3, {}, true, 'on'] + }], mockStorage, loggerMock); + expect(matcherExpectedTreatmentWrongTypeNotMatching({ key: 'a-key' }, evaluateFeature)).toBe(false); // If treatments expectation list has elements of the wrong type, those elements are overlooked. +}); diff --git a/src/evaluator/matchers/prerequisites.ts b/src/evaluator/matchers/prerequisites.ts index a9b3b031..9bee45b3 100644 --- a/src/evaluator/matchers/prerequisites.ts +++ b/src/evaluator/matchers/prerequisites.ts @@ -2,24 +2,23 @@ import { ISplit, MaybeThenable } from '../../dtos/types'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { thenable } from '../../utils/promise/thenable'; -import { ISplitEvaluator } from '../types'; -import SplitIO from '../../../types/splitio'; +import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; export function prerequisitesMatcherContext(prerequisites: ISplit['prerequisites'] = [], storage: IStorageSync | IStorageAsync, log: ILogger) { - return function prerequisitesMatcher(key: SplitIO.SplitKey, attributes: SplitIO.Attributes | undefined, splitEvaluator: ISplitEvaluator): MaybeThenable { + return function prerequisitesMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { function evaluatePrerequisite(prerequisite: { n: string; ts: string[] }): MaybeThenable { const evaluation = splitEvaluator(log, key, prerequisite.n, attributes, storage); - return thenable(evaluation) - ? evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) === -1) - : prerequisite.ts.indexOf(evaluation.treatment!) === -1; + return thenable(evaluation) ? + evaluation.then(evaluation => prerequisite.ts.indexOf(evaluation.treatment!) !== -1) : + prerequisite.ts.indexOf(evaluation.treatment!) !== -1; } return prerequisites.reduce>((prerequisitesMet, prerequisite) => { return thenable(prerequisitesMet) ? - prerequisitesMet.then(prerequisitesMet => !prerequisitesMet ? false : evaluatePrerequisite(prerequisite)) : - !prerequisitesMet ? false : evaluatePrerequisite(prerequisite); + prerequisitesMet.then(prerequisitesMet => prerequisitesMet ? evaluatePrerequisite(prerequisite) : false) : + prerequisitesMet ? evaluatePrerequisite(prerequisite) : false; }, true); }; } From d0a0986ad4076689142c3651fa77e63d78ed06ef Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 4 Apr 2025 16:04:43 -0300 Subject: [PATCH 39/62] Update changelog entry --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index c2534182..2fa36d3b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 2.3.0 (April XXX, 2025) - Added support for targeting rules based on rule-based segments. + - Added support for feature flag prerequisites. 2.2.0 (March 28, 2025) - 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. From 6bb988ccec2b8290522d029b74355b01085e60b4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 14 Apr 2025 16:10:11 -0300 Subject: [PATCH 40/62] Update splitChangesFetcher to handle proxy error with spec v1.3 --- src/services/splitApi.ts | 5 +- .../polling/fetchers/splitChangesFetcher.ts | 59 +++++++++++++++++-- src/sync/polling/syncTasks/splitsSyncTask.ts | 2 +- .../__tests__/splitChangesUpdater.spec.ts | 11 ++-- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index b7163b93..6860b022 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -29,7 +29,6 @@ export function splitApiFactory( const urls = settings.urls; const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; const SplitSDKImpressionsMode = settings.sync.impressionsMode; - const flagSpecVersion = settings.sync.flagSpecVersion; const splitHttpClient = splitHttpClientFactory(settings, platform); return { @@ -45,7 +44,7 @@ export function splitApiFactory( }, fetchAuth(userMatchingKeys?: string[]) { - let url = `${urls.auth}/v2/auth?s=${flagSpecVersion}`; + let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`; if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&'); if (queryParams) url += '&' + queryParams; @@ -54,7 +53,7 @@ export function splitApiFactory( }, fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) { - const url = `${urls.sdk}/splitChanges?s=${flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; + const url = `${urls.sdk}/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`; return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS)) .catch((err) => { if (err.statusCode === 414) settings.log.error(ERROR_TOO_MANY_SETS); diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index d134601b..be9ad3a4 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,11 +1,26 @@ +import { ISettings } from '../../../../types/splitio'; +import { ISplitChangesResponse } from '../../../dtos/types'; import { IFetchSplitChanges, IResponse } from '../../../services/types'; +import { IStorageBase } from '../../../storages/types'; +import { FLAG_SPEC_VERSION } from '../../../utils/constants'; +import { base } from '../../../utils/settingsValidation'; import { ISplitChangesFetcher } from './types'; +const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side +const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side + +function sdkEndpointOverriden(settings: ISettings) { + return settings.urls.sdk !== base.urls.sdk; +} + /** * Factory of SplitChanges fetcher. * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors. */ -export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges): ISplitChangesFetcher { +export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { + + const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; + let _lastProxyCheckTimestamp: number | undefined; return function splitChangesFetcher( since: number, @@ -14,12 +29,48 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges rbSince?: number, // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker decorator?: (promise: Promise) => Promise - ) { + ): Promise { + + if (_lastProxyCheckTimestamp && (Date.now() - _lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { + settings.sync.flagSpecVersion = FLAG_SPEC_VERSION; + } + + let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince) + // Handle proxy errors with spec 1.3 + .catch((err) => { + if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { + _lastProxyCheckTimestamp = Date.now(); + settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec + return fetchSplitChanges(since, noCache, till); // retry request without rbSince + } + throw err; + }); - let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince); if (decorator) splitsPromise = decorator(splitsPromise); - return splitsPromise.then(resp => resp.json()); + return splitsPromise + .then(resp => resp.json()) + .then(data => { + // Using flag spec version 1.2 + if (data.splits) { + return { + ff: { + d: data.splits, + s: data.since, + t: data.till + } + }; + } + + // Proxy recovery + if (_lastProxyCheckTimestamp) { + _lastProxyCheckTimestamp = undefined; + return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) + .then(() => splitChangesFetcher(-1, undefined, undefined, -1)); + } + + return data; + }); }; } diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index d6fed5a2..d385bf77 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -21,7 +21,7 @@ export function splitsSyncTaskFactory( settings.log, splitChangesUpdaterFactory( settings.log, - splitChangesFetcherFactory(fetchSplitChanges), + splitChangesFetcherFactory(fetchSplitChanges, settings, storage), storage, settings.sync.__splitFiltersValidation, readiness.splits, diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index a77e9516..750f1c0d 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -156,12 +156,6 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { }); describe('splitChangesUpdater', () => { - - fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore - const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); - const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); - const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); - const splits = new SplitsCacheInMemory(); const updateSplits = jest.spyOn(splits, 'update'); @@ -173,6 +167,11 @@ describe('splitChangesUpdater', () => { const storage = { splits, rbSegments, segments }; + fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore + const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); + const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); + const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges, fullSettings, storage); + const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); From a934d955b1caa962ad1ce535ae2e0bac748006d7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 14 Apr 2025 16:12:08 -0300 Subject: [PATCH 41/62] Test update --- .../updaters/__tests__/splitChangesUpdater.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index a77e9516..b1bc79d8 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -156,12 +156,6 @@ test('splitChangesUpdater / compute splits mutation with filters', () => { }); describe('splitChangesUpdater', () => { - - fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore - const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); - const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); - const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); - const splits = new SplitsCacheInMemory(); const updateSplits = jest.spyOn(splits, 'update'); @@ -173,6 +167,11 @@ describe('splitChangesUpdater', () => { const storage = { splits, rbSegments, segments }; + fetchMock.once('*', { status: 200, body: splitChangesMock1 }); // @ts-ignore + const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => fetchMock }, telemetryTrackerFactory()); + const fetchSplitChanges = jest.spyOn(splitApi, 'fetchSplitChanges'); + const splitChangesFetcher = splitChangesFetcherFactory(splitApi.fetchSplitChanges); + const readinessManager = readinessManagerFactory(EventEmitter, fullSettings); const splitsEmitSpy = jest.spyOn(readinessManager.splits, 'emit'); From 8429d400ec14109cba96334364c8bf784003b76e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 12:56:04 -0300 Subject: [PATCH 42/62] Implementation fixes --- .../polling/fetchers/splitChangesFetcher.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index be9ad3a4..928ab6e4 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -17,10 +17,11 @@ function sdkEndpointOverriden(settings: ISettings) { * Factory of SplitChanges fetcher. * SplitChanges fetcher is a wrapper around `splitChanges` API service that parses the response and handle errors. */ +// @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; - let _lastProxyCheckTimestamp: number | undefined; + let lastProxyCheckTimestamp: number | undefined; return function splitChangesFetcher( since: number, @@ -31,15 +32,16 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges decorator?: (promise: Promise) => Promise ): Promise { - if (_lastProxyCheckTimestamp && (Date.now() - _lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { + // Recheck proxy + if (lastProxyCheckTimestamp && (Date.now() - lastProxyCheckTimestamp) > PROXY_CHECK_INTERVAL_MILLIS) { settings.sync.flagSpecVersion = FLAG_SPEC_VERSION; } - let splitsPromise = fetchSplitChanges(since, noCache, till, rbSince) - // Handle proxy errors with spec 1.3 + let splitsPromise = fetchSplitChanges(since, noCache, till, settings.sync.flagSpecVersion === FLAG_SPEC_VERSION ? rbSince : undefined) + // Handle proxy error with spec 1.3 .catch((err) => { if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { - _lastProxyCheckTimestamp = Date.now(); + lastProxyCheckTimestamp = Date.now(); settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec return fetchSplitChanges(since, noCache, till); // retry request without rbSince } @@ -63,10 +65,10 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges } // Proxy recovery - if (_lastProxyCheckTimestamp) { - _lastProxyCheckTimestamp = undefined; + if (lastProxyCheckTimestamp) { + lastProxyCheckTimestamp = undefined; return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) - .then(() => splitChangesFetcher(-1, undefined, undefined, -1)); + .then(() => splitChangesFetcher(storage.splits.getChangeNumber() as number, undefined, undefined, storage.rbSegments.getChangeNumber() as number)); } return data; From 1118e4949f6bedd0cb7ab4e97aad075e2b78d871 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 13:04:39 -0300 Subject: [PATCH 43/62] Add logs --- src/sync/polling/fetchers/splitChangesFetcher.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 928ab6e4..58f87e9a 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,10 +1,11 @@ -import { ISettings } from '../../../../types/splitio'; +import { ISettings } from '../../../types'; import { ISplitChangesResponse } from '../../../dtos/types'; import { IFetchSplitChanges, IResponse } from '../../../services/types'; import { IStorageBase } from '../../../storages/types'; import { FLAG_SPEC_VERSION } from '../../../utils/constants'; import { base } from '../../../utils/settingsValidation'; import { ISplitChangesFetcher } from './types'; +import { LOG_PREFIX_SYNC_SPLITS } from '../../../logger/constants'; const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side @@ -20,6 +21,7 @@ function sdkEndpointOverriden(settings: ISettings) { // @TODO breaking: drop support for Split Proxy below v5.10.0 and simplify the implementation export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges, settings: ISettings, storage: Pick): ISplitChangesFetcher { + const log = settings.log; const PROXY_CHECK_INTERVAL_MILLIS = settings.core.key !== undefined ? PROXY_CHECK_INTERVAL_MILLIS_CS : PROXY_CHECK_INTERVAL_MILLIS_SS; let lastProxyCheckTimestamp: number | undefined; @@ -41,6 +43,7 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges // Handle proxy error with spec 1.3 .catch((err) => { if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { + log.error(LOG_PREFIX_SYNC_SPLITS + 'Proxy error detected. If you are using Split Proxy, please upgrade to latest version'); lastProxyCheckTimestamp = Date.now(); settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec return fetchSplitChanges(since, noCache, till); // retry request without rbSince @@ -66,6 +69,7 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges // Proxy recovery if (lastProxyCheckTimestamp) { + log.info(LOG_PREFIX_SYNC_SPLITS + 'Proxy error recovered'); lastProxyCheckTimestamp = undefined; return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) .then(() => splitChangesFetcher(storage.splits.getChangeNumber() as number, undefined, undefined, storage.rbSegments.getChangeNumber() as number)); From 280ed6e61ddfb214a52e8cd6c92ad005045188d9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 13:59:29 -0300 Subject: [PATCH 44/62] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a5d556b..51246fd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index e7912d5c..9807d793 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 2083e0017002b7a432bce11ddb4db4e293c9b669 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 15 Apr 2025 17:35:49 -0300 Subject: [PATCH 45/62] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a5d556b..544d7feb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.1", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index e7912d5c..ac8421de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.0", + "version": "2.2.1-rc.1", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From ee47067629cb09ad740c3e974e1690b56b731fee Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 30 Apr 2025 20:55:58 -0300 Subject: [PATCH 46/62] 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 47/62] 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 8847e72cd4fce9de681b262cb596e81b37f04f3d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 7 May 2025 15:29:16 -0300 Subject: [PATCH 48/62] Fix SplitsCacheInMemory::clear method, to clear flag sets cache --- src/storages/inMemory/SplitsCacheInMemory.ts | 1 + src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index a8be688a..461d15e6 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -24,6 +24,7 @@ export class SplitsCacheInMemory extends AbstractSplitsCacheSync { this.ttCache = {}; this.changeNumber = -1; this.segmentsCount = 0; + this.flagSetsCache = {}; } addSplit(split: ISplit): boolean { diff --git a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts index 2f907eca..56ca1300 100644 --- a/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SplitsCacheInMemory.spec.ts @@ -161,6 +161,9 @@ test('SPLITS CACHE / In Memory / flag set cache tests', () => { cache.addSplit(featureFlagWithoutFS); expect(cache.getNamesByFlagSets([])).toEqual([]); + + cache.clear(); + expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([emptySet, emptySet, emptySet]); }); // if FlagSets are not defined, it should store all FlagSets in memory. From e352043c9447313187666df0009be4670ab19e07 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 May 2025 09:50:32 -0300 Subject: [PATCH 49/62] Proxy recovery: clear after fetch --- src/sync/polling/fetchers/splitChangesFetcher.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 58f87e9a..c0fb9816 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -71,8 +71,11 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges if (lastProxyCheckTimestamp) { log.info(LOG_PREFIX_SYNC_SPLITS + 'Proxy error recovered'); lastProxyCheckTimestamp = undefined; - return Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) - .then(() => splitChangesFetcher(storage.splits.getChangeNumber() as number, undefined, undefined, storage.rbSegments.getChangeNumber() as number)); + return splitChangesFetcher(-1, undefined, undefined, -1) + .then((splitChangesResponse: ISplitChangesResponse) => + Promise.all([storage.splits.clear(), storage.rbSegments.clear()]) + .then(() => splitChangesResponse) + ); } return data; From 688a8ffd53e5d267256dee839c20543508c4fc0f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 May 2025 17:27:09 -0300 Subject: [PATCH 50/62] 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 51/62] 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; From 3046752f362419ab6de0e0d2d7ce520e29bc98ca Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 10:03:13 -0300 Subject: [PATCH 52/62] refactor: use segment type constants instead of string literals --- src/evaluator/matchers/rbsegment.ts | 7 ++++--- src/sync/polling/updaters/splitChangesUpdater.ts | 4 ++-- src/utils/constants/index.ts | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 80893837..22036f6f 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -5,6 +5,7 @@ import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; import { thenable } from '../../utils/promise/thenable'; import { getMatching, keyParser } from '../../utils/key'; import { parser } from '../parser'; +import { STANDARD_SEGMENT, RULE_BASED_SEGMENT, LARGE_SEGMENT } from '../../utils/constants'; export function ruleBasedSegmentMatcherContext(segmentName: string, storage: IStorageSync | IStorageAsync, log: ILogger) { @@ -36,11 +37,11 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt if (excluded.keys && excluded.keys.indexOf(matchingKey) !== -1) return true; const isInSegment = (excluded.segments || []).map(({ type, name }) => { - return type === 'standard' ? + return type === STANDARD_SEGMENT ? storage.segments.isInSegment(name, matchingKey) : - type === 'rule-based' ? + type === RULE_BASED_SEGMENT ? ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) : - type === 'large' && (storage as IStorageSync).largeSegments ? + type === LARGE_SEGMENT && (storage as IStorageSync).largeSegments ? (storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) : false; }); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index fd4cedb9..ea5e5e44 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -7,7 +7,7 @@ import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_UPDATE, SYNC_RBS_UPDATE, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; import { startsWith } from '../../../utils/lang'; -import { IN_RULE_BASED_SEGMENT, IN_SEGMENT } from '../../../utils/constants'; +import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants'; import { setToArray } from '../../../utils/lang/sets'; import { SPLIT_UPDATE } from '../../streaming/constants'; @@ -35,7 +35,7 @@ export function parseSegments(ruleEntity: ISplit | IRBSegment, matcherType: type 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)) { + if ((type === STANDARD_SEGMENT && matcherType === IN_SEGMENT) || (type === RULE_BASED_SEGMENT && matcherType === IN_RULE_BASED_SEGMENT)) { segments.add(name); } }); diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index eab99bbb..6686c68e 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -110,3 +110,7 @@ export const FLAG_SPEC_VERSION = '1.3'; export const IN_SEGMENT = 'IN_SEGMENT'; export const IN_LARGE_SEGMENT = 'IN_LARGE_SEGMENT'; export const IN_RULE_BASED_SEGMENT = 'IN_RULE_BASED_SEGMENT'; + +export const STANDARD_SEGMENT = 'standard'; +export const LARGE_SEGMENT = 'large'; +export const RULE_BASED_SEGMENT = 'rule-based'; From 5fc787cb9e607f8b004ac2a5cf01a263861595e2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 14:54:30 -0300 Subject: [PATCH 53/62] refactor: optimize rule-based segment matcher --- src/dtos/types.ts | 4 +-- src/evaluator/matchers/rbsegment.ts | 38 +++++++++++++++-------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index b0109f96..e38d125a 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -199,7 +199,7 @@ export interface ISplitCondition { conditionType?: 'ROLLOUT' | 'WHITELIST' } -export interface IExcludedSegments { +export interface IExcludedSegment { type: 'standard' | 'large' | 'rule-based', name: string, } @@ -211,7 +211,7 @@ export interface IRBSegment { conditions?: ISplitCondition[], excluded?: { keys?: string[], - segments?: IExcludedSegments[] + segments?: IExcludedSegment[] } } diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 22036f6f..3e974fde 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -1,4 +1,4 @@ -import { IRBSegment, MaybeThenable } from '../../dtos/types'; +import { IExcludedSegment, IRBSegment, MaybeThenable } from '../../dtos/types'; import { IStorageAsync, IStorageSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { IDependencyMatcherValue, ISplitEvaluator } from '../types'; @@ -11,6 +11,7 @@ import { STANDARD_SEGMENT, RULE_BASED_SEGMENT, LARGE_SEGMENT } from '../../utils export function ruleBasedSegmentMatcherContext(segmentName: string, storage: IStorageSync | IStorageAsync, log: ILogger) { return function ruleBasedSegmentMatcher({ key, attributes }: IDependencyMatcherValue, splitEvaluator: ISplitEvaluator): MaybeThenable { + const matchingKey = getMatching(key); function matchConditions(rbsegment: IRBSegment) { const conditions = rbsegment.conditions || []; @@ -30,28 +31,29 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt evaluation ? true : false; } + function isInExcludedSegment({ type, name }: IExcludedSegment) { + return type === STANDARD_SEGMENT ? + storage.segments.isInSegment(name, matchingKey) : + type === RULE_BASED_SEGMENT ? + ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) : + type === LARGE_SEGMENT && (storage as IStorageSync).largeSegments ? + (storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) : + false; + } + function isExcluded(rbSegment: IRBSegment) { - const matchingKey = getMatching(key); const excluded = rbSegment.excluded || {}; if (excluded.keys && excluded.keys.indexOf(matchingKey) !== -1) return true; - const isInSegment = (excluded.segments || []).map(({ type, name }) => { - return type === STANDARD_SEGMENT ? - storage.segments.isInSegment(name, matchingKey) : - type === RULE_BASED_SEGMENT ? - ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) : - type === LARGE_SEGMENT && (storage as IStorageSync).largeSegments ? - (storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) : - false; - }); - - return isInSegment.length && thenable(isInSegment[0]) ? - Promise.all(isInSegment).then(results => results.some(result => result)) : - isInSegment.some(result => result); + return (excluded.segments || []).reduce>((result, excludedSegment) => { + return thenable(result) ? + result.then(result => result || isInExcludedSegment(excludedSegment)) : + result || isInExcludedSegment(excludedSegment); + }, false); } - function isInSegment(rbSegment: IRBSegment | null) { + function isInRBSegment(rbSegment: IRBSegment | null) { if (!rbSegment) return false; const excluded = isExcluded(rbSegment); @@ -63,7 +65,7 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt const rbSegment = storage.rbSegments.get(segmentName); return thenable(rbSegment) ? - rbSegment.then(isInSegment) : - isInSegment(rbSegment); + rbSegment.then(isInRBSegment) : + isInRBSegment(rbSegment); }; } From bfcd0c425b811e9b10aee62b20e259f620b5480c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 15:15:12 -0300 Subject: [PATCH 54/62] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50a01b82..79260caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.3", + "version": "2.2.1-rc.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.3", + "version": "2.2.1-rc.4", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index f7fa5898..92eaab4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.3", + "version": "2.2.1-rc.4", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From f44e307ed6abd4f35a3aab0f8425d3528fbd5da3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 18:47:25 -0300 Subject: [PATCH 55/62] Bug fixes --- CHANGES.txt | 2 +- .../matchers/__tests__/rbsegment.spec.ts | 18 ++++++++++++++++-- src/evaluator/matchers/rbsegment.ts | 3 +++ src/services/splitHttpClient.ts | 4 +++- .../polling/fetchers/splitChangesFetcher.ts | 6 +++--- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index ebe50d6b..24a9471e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.3.0 (May XXX, 2025) +2.3.0 (May 12, 2025) - Added support for targeting rules based on rule-based segments. - Updated the Redis storage to: - Avoid lazy require of the `ioredis` dependency when the SDK is initialized, and diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts index 32b8f843..4ab02438 100644 --- a/src/evaluator/matchers/__tests__/rbsegment.spec.ts +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -94,8 +94,8 @@ const STORED_RBSEGMENTS: Record = { name: 'depend_on_always_on', changeNumber: 123, status: 'ACTIVE', - excluded: { - keys: [], + excluded: { // @ts-ignore + keys: null, segments: [] }, conditions: [{ @@ -167,6 +167,12 @@ const STORED_RBSEGMENTS: Record = { } } ], + }, + 'rule_based_segment_without_conditions': { + name: 'rule_based_segment_without_conditions', + changeNumber: 123, + status: 'ACTIVE', + conditions: [] } }; @@ -291,6 +297,14 @@ describe.each([ // should support feature flag dependency matcher expect(await matcherTrueAlwaysOn({ key: 'a-key' }, evaluateFeature)).toBe(true); // Parent split returns one of the expected treatments, so the matcher returns true + + const matcherTrueRuleBasedSegmentWithoutConditions = matcherFactory(loggerMock, { + type: matcherTypes.IN_RULE_BASED_SEGMENT, + value: 'rule_based_segment_without_conditions' + } as IMatcherDto, mockStorageSync)!; + + // should support rule-based segment without conditions + expect(await matcherTrueRuleBasedSegmentWithoutConditions({ key: 'a-key' }, evaluateFeature)).toBe(false); }); }); diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 3e974fde..2bab7388 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -15,6 +15,9 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt function matchConditions(rbsegment: IRBSegment) { const conditions = rbsegment.conditions || []; + + if (!conditions.length) return false; + const evaluator = parser(log, conditions, storage); const evaluation = evaluator( diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index 19329dc2..3a6b3594 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -4,6 +4,7 @@ import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants'; import { ISettings } from '../types'; import { IPlatform } from '../sdkFactory/types'; import { decorateHeaders, removeNonISO88591 } from './decorateHeaders'; +import { timeout } from '../utils/promise/timeout'; const messageNoFetch = 'Global fetch API is not available.'; @@ -45,7 +46,8 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful .then(response => { if (!response.ok) { - return response.text().then(message => Promise.reject({ response, message })); + // `text()` promise might not settle in some implementations and cases (e.g. no content) + return timeout(100, response.text()).then(message => Promise.reject({ response, message }), () => Promise.reject({ response })); } latencyTracker(); return response; diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index c0fb9816..8f7ab143 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -10,7 +10,7 @@ import { LOG_PREFIX_SYNC_SPLITS } from '../../../logger/constants'; const PROXY_CHECK_INTERVAL_MILLIS_CS = 60 * 60 * 1000; // 1 hour in Client Side const PROXY_CHECK_INTERVAL_MILLIS_SS = 24 * PROXY_CHECK_INTERVAL_MILLIS_CS; // 24 hours in Server Side -function sdkEndpointOverriden(settings: ISettings) { +function sdkEndpointOverridden(settings: ISettings) { return settings.urls.sdk !== base.urls.sdk; } @@ -42,8 +42,8 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges let splitsPromise = fetchSplitChanges(since, noCache, till, settings.sync.flagSpecVersion === FLAG_SPEC_VERSION ? rbSince : undefined) // Handle proxy error with spec 1.3 .catch((err) => { - if (err.statusCode === 400 && sdkEndpointOverriden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { - log.error(LOG_PREFIX_SYNC_SPLITS + 'Proxy error detected. If you are using Split Proxy, please upgrade to latest version'); + if (err.statusCode === 400 && sdkEndpointOverridden(settings) && settings.sync.flagSpecVersion === FLAG_SPEC_VERSION) { + log.error(LOG_PREFIX_SYNC_SPLITS + 'Proxy error detected. Retrying with spec 1.2. If you are using Split Proxy, please upgrade to latest version'); lastProxyCheckTimestamp = Date.now(); settings.sync.flagSpecVersion = '1.2'; // fallback to 1.2 spec return fetchSplitChanges(since, noCache, till); // retry request without rbSince From 2a0851c30861ff13b936bd40e26ec5f36926b8c8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 18:55:34 -0300 Subject: [PATCH 56/62] Polishing --- src/evaluator/matchers/__tests__/rbsegment.spec.ts | 4 ++-- src/services/splitHttpClient.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts index 4ab02438..9c991ad7 100644 --- a/src/evaluator/matchers/__tests__/rbsegment.spec.ts +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -95,8 +95,8 @@ const STORED_RBSEGMENTS: Record = { changeNumber: 123, status: 'ACTIVE', excluded: { // @ts-ignore - keys: null, - segments: [] + keys: null, // @ts-ignore + segments: null, }, conditions: [{ matcherGroup: { diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index 3a6b3594..c81644e7 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -46,7 +46,7 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful .then(response => { if (!response.ok) { - // `text()` promise might not settle in some implementations and cases (e.g. no content) + // timeout after 100ms because `text()` promise doesn't settle in some implementations and cases (e.g. no content) return timeout(100, response.text()).then(message => Promise.reject({ response, message }), () => Promise.reject({ response })); } latencyTracker(); From b8bb5df972ba2d0447bc0628578f59a18e2d3b69 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 18:57:29 -0300 Subject: [PATCH 57/62] Polishing --- src/dtos/types.ts | 4 ++-- src/evaluator/matchers/__tests__/rbsegment.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index e38d125a..077bdd0e 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -210,8 +210,8 @@ export interface IRBSegment { status: 'ACTIVE' | 'ARCHIVED', conditions?: ISplitCondition[], excluded?: { - keys?: string[], - segments?: IExcludedSegment[] + keys?: string[] | null, + segments?: IExcludedSegment[] | null } } diff --git a/src/evaluator/matchers/__tests__/rbsegment.spec.ts b/src/evaluator/matchers/__tests__/rbsegment.spec.ts index 9c991ad7..db597738 100644 --- a/src/evaluator/matchers/__tests__/rbsegment.spec.ts +++ b/src/evaluator/matchers/__tests__/rbsegment.spec.ts @@ -94,8 +94,8 @@ const STORED_RBSEGMENTS: Record = { name: 'depend_on_always_on', changeNumber: 123, status: 'ACTIVE', - excluded: { // @ts-ignore - keys: null, // @ts-ignore + excluded: { + keys: null, segments: null, }, conditions: [{ From 53a46999a538f4e866d0a30dbaaec2b1e055f025 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 9 May 2025 18:58:36 -0300 Subject: [PATCH 58/62] rc --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79260caa..603183e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.4", + "version": "2.2.1-rc.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.4", + "version": "2.2.1-rc.5", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 92eaab4f..3d57da52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.4", + "version": "2.2.1-rc.5", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 287d94ecab93b2d4fd7298b104e81448d1d5de38 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 14 May 2025 11:15:45 -0300 Subject: [PATCH 59/62] stable version --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/logger/messages/debug.ts | 4 ++-- src/sync/polling/fetchers/splitChangesFetcher.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 24a9471e..f4b35455 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.3.0 (May 12, 2025) +2.3.0 (May 14, 2025) - Added support for targeting rules based on rule-based segments. - Updated the Redis storage to: - Avoid lazy require of the `ioredis` dependency when the SDK is initialized, and diff --git a/package-lock.json b/package-lock.json index 603183e6..f098b430 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.5", + "version": "2.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.5", + "version": "2.3.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 3d57da52..3dc299c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.2.1-rc.5", + "version": "2.3.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index d9bce2ab..6d44ac96 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -21,8 +21,8 @@ export const codesDebug: [number, string][] = codesInfo.concat([ // synchronizer [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Feature flags data: \n%s'], [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up feature flags update using since = %s and rbSince = %s.'], - [c.SYNC_SPLITS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags %s. Removed feature flags %s.'], - [c.SYNC_RBS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New rule-based segments %s. Removed rule-based segments %s.'], + [c.SYNC_SPLITS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New feature flags: %s. Removed feature flags: %s.'], + [c.SYNC_RBS_UPDATE, c.LOG_PREFIX_SYNC_SPLITS + 'New rule-based segments: %s. Removed rule-based segments: %s.'], [c.STREAMING_NEW_MESSAGE, c.LOG_PREFIX_SYNC_STREAMING + 'New SSE message received, with data: %s.'], [c.SYNC_TASK_START, c.LOG_PREFIX_SYNC + ': Starting %s. Running each %s millis'], [c.SYNC_TASK_EXECUTE, c.LOG_PREFIX_SYNC + ': Running %s'], diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index 8f7ab143..79fd971c 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -56,7 +56,7 @@ export function splitChangesFetcherFactory(fetchSplitChanges: IFetchSplitChanges return splitsPromise .then(resp => resp.json()) .then(data => { - // Using flag spec version 1.2 + // Using flag spec version 1.2 or below if (data.splits) { return { ff: { From e660afe5df41ef85234164e6784b727eb8250023 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 19 May 2025 18:51:16 -0300 Subject: [PATCH 60/62] Remove type assertion --- package-lock.json | 4 ++-- package.json | 2 +- src/evaluator/matchers/rbsegment.ts | 4 ++-- src/storages/types.ts | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f098b430..2aa5d335 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.3.1-rc.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.3.1-rc.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 3dc299c1..cd22d2ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.0", + "version": "2.3.1-rc.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/evaluator/matchers/rbsegment.ts b/src/evaluator/matchers/rbsegment.ts index 2bab7388..f9cc12e4 100644 --- a/src/evaluator/matchers/rbsegment.ts +++ b/src/evaluator/matchers/rbsegment.ts @@ -39,8 +39,8 @@ export function ruleBasedSegmentMatcherContext(segmentName: string, storage: ISt storage.segments.isInSegment(name, matchingKey) : type === RULE_BASED_SEGMENT ? ruleBasedSegmentMatcherContext(name, storage, log)({ key, attributes }, splitEvaluator) : - type === LARGE_SEGMENT && (storage as IStorageSync).largeSegments ? - (storage as IStorageSync).largeSegments!.isInSegment(name, matchingKey) : + type === LARGE_SEGMENT && storage.largeSegments ? + storage.largeSegments.isInSegment(name, matchingKey) : false; } diff --git a/src/storages/types.ts b/src/storages/types.ts index 451ec700..8e93daca 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -458,6 +458,7 @@ export interface IStorageBase< splits: TSplitsCache, rbSegments: TRBSegmentsCache, segments: TSegmentsCache, + largeSegments?: TSegmentsCache, impressions: TImpressionsCache, impressionCounts: TImpressionsCountCache, events: TEventsCache, From 68590fa654a8d4ec859c520c5df90f502fa3cb0c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 May 2025 15:50:58 -0300 Subject: [PATCH 61/62] Stable version --- CHANGES.txt | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/sdkManager/__tests__/mocks/input.json | 2 +- src/sdkManager/__tests__/mocks/output.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 517af5d2..1f48c5fa 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -2.4.0 (May 20, 2025) +2.4.0 (May 27, 2025) - Added support for targeting rules based on rule-based segments. - Added support for feature flag prerequisites. diff --git a/package-lock.json b/package-lock.json index 2aa5d335..afd5917c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.1-rc.0", + "version": "2.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.3.1-rc.0", + "version": "2.4.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index cd22d2ba..60d02afa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.3.1-rc.0", + "version": "2.4.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkManager/__tests__/mocks/input.json b/src/sdkManager/__tests__/mocks/input.json index 2e951c2c..313dd38e 100644 --- a/src/sdkManager/__tests__/mocks/input.json +++ b/src/sdkManager/__tests__/mocks/input.json @@ -52,4 +52,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/sdkManager/__tests__/mocks/output.json b/src/sdkManager/__tests__/mocks/output.json index 7a4f72ef..67948bf9 100644 --- a/src/sdkManager/__tests__/mocks/output.json +++ b/src/sdkManager/__tests__/mocks/output.json @@ -14,4 +14,4 @@ "flagName": "some_flag", "treatments": ["on"] }] -} \ No newline at end of file +} From 2d87fbc2d0da7e2b9030673b11fb32a52366471b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 May 2025 17:29:54 -0300 Subject: [PATCH 62/62] Update changelog entry --- CHANGES.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1f48c5fa..499e296f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,6 @@ 2.4.0 (May 27, 2025) - - Added support for targeting rules based on rule-based segments. - - Added support for feature flag prerequisites. + - Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK. + - Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules. 2.3.0 (May 16, 2025) - Updated the Redis storage to: