Skip to content

Commit

Permalink
Add layer overrides (#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-statsig authored Nov 28, 2022
1 parent a824223 commit 673a917
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 47 deletions.
131 changes: 84 additions & 47 deletions src/Evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default class Evaluator {
string,
Record<string, Record<string, unknown>>
>;
private layerOverrides: Record<
string,
Record<string, Record<string, unknown>>
>;
private initialized: boolean = false;

private store: SpecStore;
Expand All @@ -45,6 +49,7 @@ export default class Evaluator {
this.store = new SpecStore(fetcher, options);
this.gateOverrides = {};
this.configOverrides = {};
this.layerOverrides = {};
}

public async init(): Promise<void> {
Expand Down Expand Up @@ -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<string, unknown>,
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 {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<string, Record<string, unknown>>,
): 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,
Expand Down
16 changes: 16 additions & 0 deletions src/StatsigServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,22 @@ export default class StatsigServer {
});
}

public overrideLayer(
layerName: string,
value: Record<string, unknown>,
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
//
Expand Down
35 changes: 35 additions & 0 deletions src/__tests__/LocalModeOverride.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ const Statsig = {
this._enforceServer().overrideConfig(configName, value, userID);
},

overrideLayer(
layerName: string,
value: Record<string, unknown>,
userID = '',
) {
this._enforceServer().overrideLayer(layerName, value, userID);
},

flush(): Promise<void> {
const inst = Statsig._instance;
if (inst == null) {
Expand Down

0 comments on commit 673a917

Please sign in to comment.