diff --git a/src/Evaluator.ts b/src/Evaluator.ts index 4a95cae..820484d 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -37,6 +37,10 @@ export default class Evaluator { string, Record> >; + private layerOverrides: Record< + string, + Record> + >; private initialized: boolean = false; private store: SpecStore; @@ -45,6 +49,7 @@ export default class Evaluator { this.store = new SpecStore(fetcher, options); this.gateOverrides = {}; this.configOverrides = {}; + this.layerOverrides = {}; } public async init(): Promise { @@ -77,53 +82,14 @@ export default class Evaluator { this.configOverrides[configName] = overrides; } - public lookupGateOverride( - user: StatsigUser, - gateName: string, - ): ConfigEvaluation | null { - const overrides = this.gateOverrides[gateName]; - if (overrides == null) { - return null; - } - if (user.userID != null) { - // check for a user level override - const userOverride = overrides[user.userID]; - if (userOverride != null) { - return new ConfigEvaluation(userOverride, 'override'); - } - } - - // check if there is a global override - const allOverride = overrides['']; - if (allOverride != null) { - return new ConfigEvaluation(allOverride, 'override'); - } - return null; - } - - public lookupConfigOverride( - user: StatsigUser, - configName: string, - ): ConfigEvaluation | null { - const overrides = this.configOverrides[configName]; - if (overrides == null) { - return null; - } - - if (user.userID != null) { - // check for a user level override - const userOverride = overrides[user.userID]; - if (userOverride != null) { - return new ConfigEvaluation(true, 'override', [], userOverride); - } - } - - // check if there is a global override - const allOverride = overrides['']; - if (allOverride != null) { - return new ConfigEvaluation(true, 'override', [], allOverride); - } - return null; + public overrideLayer( + layerName: string, + value: Record, + userID: string | null = '', + ): void { + let overrides = this.layerOverrides[layerName] ?? {}; + overrides[userID == null ? '' : userID] = value; + this.layerOverrides[layerName] = overrides; } public checkGate(user: StatsigUser, gateName: string): ConfigEvaluation { @@ -161,6 +127,13 @@ export default class Evaluator { } public getLayer(user: StatsigUser, layerName: string): ConfigEvaluation { + const override = this.lookupLayerOverride(user, layerName); + if (override) { + return override.withEvaluationDetails( + EvaluationDetails.make(this.store, 'LocalOverride'), + ); + } + if (this.store.getInitReason() === 'Uninitialized') { return new ConfigEvaluation(false).withEvaluationDetails( EvaluationDetails.uninitialized(), @@ -283,6 +256,70 @@ export default class Evaluator { return this.store.resetSyncTimerIfExited(); } + private lookupGateOverride( + user: StatsigUser, + gateName: string, + ): ConfigEvaluation | null { + const overrides = this.gateOverrides[gateName]; + if (overrides == null) { + return null; + } + if (user.userID != null) { + // check for a user level override + const userOverride = overrides[user.userID]; + if (userOverride != null) { + return new ConfigEvaluation(userOverride, 'override'); + } + } + + // check if there is a global override + const allOverride = overrides['']; + if (allOverride != null) { + return new ConfigEvaluation(allOverride, 'override'); + } + return null; + } + + private lookupConfigOverride( + user: StatsigUser, + configName: string, + ): ConfigEvaluation | null { + const overrides = this.configOverrides[configName]; + return this.lookupConfigBasedOverride(user, overrides); + } + + private lookupLayerOverride( + user: StatsigUser, + layerName: string, + ): ConfigEvaluation | null { + const overrides = this.layerOverrides[layerName]; + return this.lookupConfigBasedOverride(user, overrides); + } + + private lookupConfigBasedOverride( + user: StatsigUser, + overrides: Record>, + ): ConfigEvaluation | null { + if (overrides == null) { + return null; + } + + if (user.userID != null) { + // check for a user level override + const userOverride = overrides[user.userID]; + if (userOverride != null) { + return new ConfigEvaluation(true, 'override', [], userOverride); + } + } + + // check if there is a global override + const allOverride = overrides['']; + if (allOverride != null) { + return new ConfigEvaluation(true, 'override', [], allOverride); + } + return null; + } + private _specToInitializeResponse( spec: ConfigSpec, res: ConfigEvaluation, diff --git a/src/StatsigServer.ts b/src/StatsigServer.ts index 652ece4..107a1b7 100644 --- a/src/StatsigServer.ts +++ b/src/StatsigServer.ts @@ -453,6 +453,22 @@ export default class StatsigServer { }); } + public overrideLayer( + layerName: string, + value: Record, + userID: string | null = '', + ) { + this._errorBoundary.swallow(() => { + if (typeof value !== 'object') { + console.warn( + 'statsigSDK> Attempted to override a layer with a non object value', + ); + return; + } + this._evaluator.overrideLayer(layerName, value, userID); + }); + } + // // PRIVATE // diff --git a/src/__tests__/LocalModeOverride.test.ts b/src/__tests__/LocalModeOverride.test.ts index 2561aff..160261c 100644 --- a/src/__tests__/LocalModeOverride.test.ts +++ b/src/__tests__/LocalModeOverride.test.ts @@ -124,4 +124,39 @@ describe('Test local mode with overrides', () => { statsig.shutdown(); expect(hitNetwork).toEqual(false); }); + + describe('Layer overrides', () => { + beforeEach(async () => { + await statsig.initialize('secret-key', { localMode: true }); + expect(hitNetwork).toEqual(false); + }); + + it('returns fallback values when there are no overrides', async () => { + let layer = await statsig.getLayer({ userID: 'a-user' }, 'a_layer'); + expect(layer.get('a_param', 'fallback')).toEqual('fallback'); + + layer = await statsig.getLayer({ userID: 'b-user' }, 'a_layer'); + expect(layer.get('a_param', 'fallback')).toEqual('fallback'); + }); + + it('returns override values for a specific user', async () => { + statsig.overrideLayer('a_layer', { a_param: 'a_value' }, 'a-user'); + + let layer = await statsig.getLayer({ userID: 'a-user' }, 'a_layer'); + expect(layer.get('a_param', 'fallback')).toEqual('a_value'); + + layer = await statsig.getLayer({ userID: 'b-user' }, 'a_layer'); + expect(layer.get('a_param', 'fallback')).toEqual('fallback'); + }); + + it('returns override values for all users', async () => { + statsig.overrideLayer('a_layer', { a_param: 'a_value' }); + + let layer = await statsig.getLayer({ userID: 'a-user' }, 'a_layer'); + expect(layer.get('a_param', 'fallback')).toEqual('a_value'); + + layer = await statsig.getLayer({ userID: 'b-user' }, 'a_layer'); + expect(layer.get('a_param', 'fallback')).toEqual('a_value'); + }); + }); }); diff --git a/src/index.ts b/src/index.ts index 1d57138..78f4340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -160,6 +160,14 @@ const Statsig = { this._enforceServer().overrideConfig(configName, value, userID); }, + overrideLayer( + layerName: string, + value: Record, + userID = '', + ) { + this._enforceServer().overrideLayer(layerName, value, userID); + }, + flush(): Promise { const inst = Statsig._instance; if (inst == null) {