From 395a9d4bd5a54873c446a0b23fc00d1185cc16ff Mon Sep 17 00:00:00 2001 From: kenny-statsig <111380336+kenny-statsig@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:27:22 -0700 Subject: [PATCH] Evaluation context (#484) Replacing some arguments with an `EvaluationContext` to set up for persistent assignment --- src/ErrorBoundary.ts | 2 +- src/Evaluator.ts | 84 +++-- src/LogEventProcessor.ts | 2 +- src/StatsigServer.ts | 2 +- src/__tests__/ErrorBoundary.test.ts | 2 +- src/__tests__/Evaluator.test.ts | 291 +++++++++++++----- src/__tests__/RulesetsEvalConsistency.test.ts | 2 +- src/utils/StatsigContext.ts | 75 ++++- src/utils/StatsigFetcher.ts | 2 +- 9 files changed, 344 insertions(+), 118 deletions(-) diff --git a/src/ErrorBoundary.ts b/src/ErrorBoundary.ts index c44225d..63c71a7 100644 --- a/src/ErrorBoundary.ts +++ b/src/ErrorBoundary.ts @@ -9,7 +9,7 @@ import OutputLogger from './OutputLogger'; import { StatsigOptions } from './StatsigOptions'; import { getSDKType, getSDKVersion, getStatsigMetadata } from './utils/core'; import safeFetch from './utils/safeFetch'; -import StatsigContext from './utils/StatsigContext'; +import { StatsigContext } from './utils/StatsigContext'; export const ExceptionEndpoint = 'https://statsigapi.net/v1/sdk_exception'; diff --git a/src/Evaluator.ts b/src/Evaluator.ts index 6800012..a319beb 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -28,7 +28,7 @@ import { hashString, hashUnitIDForIDList, } from './utils/Hashing'; -import StatsigContext from './utils/StatsigContext'; +import { EvaluationContext, StatsigContext } from './utils/StatsigContext'; import StatsigFetcher from './utils/StatsigFetcher'; const CONDITION_SEGMENT_COUNT = 10 * 1000; @@ -162,7 +162,9 @@ export default class Evaluator { ); return this.getUnrecognizedEvaluation(); } - return this._evalSpec(user, gate, ctx); + return this._evalSpec( + EvaluationContext.get(ctx.getRequestContext(), { user, spec: gate }), + ); } public getConfig( @@ -194,7 +196,9 @@ export default class Evaluator { ); return this.getUnrecognizedEvaluation(); } - return this._evalSpec(user, config, ctx); + return this._evalSpec( + EvaluationContext.get(ctx.getRequestContext(), { user, spec: config }), + ); } public getLayer( @@ -226,12 +230,14 @@ export default class Evaluator { ); return this.getUnrecognizedEvaluation(); } - return this._evalSpec(user, layer, ctx); + return this._evalSpec( + EvaluationContext.get(ctx.getRequestContext(), { user, spec: layer }), + ); } public getClientInitializeResponse( inputUser: StatsigUser, - _ctx: StatsigContext, + ctx: StatsigContext, clientSDKKey?: string, options?: ClientInitializeResponseOptions, ): ClientInitializeResponse | null { @@ -251,7 +257,6 @@ export default class Evaluator { if (clientSDKKey != null && targetAppID == null) { targetAppID = clientKeyToAppMap[clientSDKKey] ?? null; } - const ctx = targetAppID ? _ctx.withTargetAppID(targetAppID) : _ctx; const filterTargetAppID = (spec: ConfigSpec) => { if ( @@ -286,7 +291,15 @@ export default class Evaluator { const localOverride = options?.includeLocalOverrides ? this.lookupGateOverride(user, spec.name) : null; - const res = localOverride ?? this._eval(user, spec, ctx); + const res = + localOverride ?? + this._eval( + EvaluationContext.get(ctx.getRequestContext(), { + user, + spec, + targetAppID: targetAppID ?? undefined, + }), + ); return { name: hashString(gate, options?.hash), value: res.unsupported ? false : res.value, @@ -304,7 +317,15 @@ export default class Evaluator { const localOverride = options?.includeLocalOverrides ? this.lookupConfigOverride(user, spec.name) : null; - const res = localOverride ?? this._eval(user, spec, ctx); + const res = + localOverride ?? + this._eval( + EvaluationContext.get(ctx.getRequestContext(), { + user, + spec, + targetAppID: targetAppID ?? undefined, + }), + ); const format = this._specToInitializeResponse(spec, res, options?.hash); if (spec.entity !== 'dynamic_config' && spec.entity !== 'autotune') { format.is_user_in_experiment = this._isUserAllocatedToExperiment( @@ -342,7 +363,15 @@ export default class Evaluator { const localOverride = options?.includeLocalOverrides ? this.lookupLayerOverride(user, spec.name) : null; - const res = localOverride ?? this._eval(user, spec, ctx); + const res = + localOverride ?? + this._eval( + EvaluationContext.get(ctx.getRequestContext(), { + user, + spec, + targetAppID: targetAppID ?? undefined, + }), + ); const format = this._specToInitializeResponse(spec, res, options?.hash); format.explicit_parameters = spec.explicitParameters ?? []; if (res.config_delegate != null && res.config_delegate !== '') { @@ -602,12 +631,8 @@ export default class Evaluator { ); } - _evalSpec( - user: StatsigUser, - config: ConfigSpec, - ctx: StatsigContext, - ): ConfigEvaluation { - const evaulation = this._eval(user, config, ctx); + _evalSpec(ctx: EvaluationContext): ConfigEvaluation { + const evaulation = this._eval(ctx); if (evaulation.evaluation_details) { return evaulation; } @@ -621,11 +646,8 @@ export default class Evaluator { ); } - _eval( - user: StatsigUser, - config: ConfigSpec, - ctx: StatsigContext, - ): ConfigEvaluation { + _eval(ctx: EvaluationContext): ConfigEvaluation { + const { user, spec: config } = ctx; if (!config.enabled) { return new ConfigEvaluation( false, @@ -654,7 +676,6 @@ export default class Evaluator { if (ruleResult.value === true) { const delegatedResult = this._evalDelegate( - user, rule, secondary_exposures, ctx, @@ -693,10 +714,9 @@ export default class Evaluator { } _evalDelegate( - user: StatsigUser, rule: ConfigRule, exposures: SecondaryExposure[], - ctx: StatsigContext, + ctx: EvaluationContext, ) { if (rule.configDelegate == null) { return null; @@ -706,7 +726,12 @@ export default class Evaluator { return null; } - const delegatedResult = this._eval(user, config, ctx); + const delegatedResult = this._eval( + EvaluationContext.get(ctx.getRequestContext(), { + user: ctx.user, + spec: config, + }), + ); delegatedResult.config_delegate = rule.configDelegate; delegatedResult.undelegated_secondary_exposures = exposures; delegatedResult.explicit_parameters = config.explicitParameters; @@ -729,7 +754,7 @@ export default class Evaluator { ); } - _evalRule(user: StatsigUser, rule: ConfigRule, ctx: StatsigContext) { + _evalRule(user: StatsigUser, rule: ConfigRule, ctx: EvaluationContext) { let secondaryExposures: SecondaryExposure[] = []; let pass = true; @@ -766,7 +791,7 @@ export default class Evaluator { _evalCondition( user: StatsigUser, condition: ConfigCondition, - ctx: StatsigContext, + ctx: EvaluationContext, ): { passes: boolean; unsupported?: boolean; @@ -1059,7 +1084,12 @@ export default class Evaluator { if (experimentConfig == null) { return false; } - const evalResult = this._eval(user, experimentConfig, ctx); + const evalResult = this._eval( + EvaluationContext.get(ctx.getRequestContext(), { + user, + spec: experimentConfig, + }), + ); return evalResult.is_experiment_group; } diff --git a/src/LogEventProcessor.ts b/src/LogEventProcessor.ts index 1daa731..93fa1f6 100644 --- a/src/LogEventProcessor.ts +++ b/src/LogEventProcessor.ts @@ -7,7 +7,7 @@ import OutputLogger from './OutputLogger'; import { ExplicitStatsigOptions, StatsigOptions } from './StatsigOptions'; import { StatsigUser } from './StatsigUser'; import { getStatsigMetadata, poll } from './utils/core'; -import StatsigContext from './utils/StatsigContext'; +import { StatsigContext } from './utils/StatsigContext'; import StatsigFetcher from './utils/StatsigFetcher'; const CONFIG_EXPOSURE_EVENT = 'config_exposure'; diff --git a/src/StatsigServer.ts b/src/StatsigServer.ts index e14053c..aad531e 100644 --- a/src/StatsigServer.ts +++ b/src/StatsigServer.ts @@ -29,7 +29,7 @@ import asyncify from './utils/asyncify'; import { isUserIdentifiable, notEmptyObject } from './utils/core'; import type { HashingAlgorithm } from './utils/Hashing'; import LogEventValidator from './utils/LogEventValidator'; -import StatsigContext from './utils/StatsigContext'; +import { StatsigContext } from './utils/StatsigContext'; import StatsigFetcher from './utils/StatsigFetcher'; let hasLoggedNoUserIdWarning = false; diff --git a/src/__tests__/ErrorBoundary.test.ts b/src/__tests__/ErrorBoundary.test.ts index 990bb49..b02ea6d 100644 --- a/src/__tests__/ErrorBoundary.test.ts +++ b/src/__tests__/ErrorBoundary.test.ts @@ -7,7 +7,7 @@ import { } from '../Errors'; import { InitStrategy, OptionsLoggingCopy } from '../StatsigOptions'; import { getStatsigMetadata } from '../utils/core'; -import StatsigContext from '../utils/StatsigContext'; +import { StatsigContext } from '../utils/StatsigContext'; import { getDecodedBody } from './StatsigTestUtils'; jest.mock('node-fetch', () => jest.fn()); const TestDataAdapter: IDataAdapter = { diff --git a/src/__tests__/Evaluator.test.ts b/src/__tests__/Evaluator.test.ts index 9ef48ec..5298740 100644 --- a/src/__tests__/Evaluator.test.ts +++ b/src/__tests__/Evaluator.test.ts @@ -5,17 +5,29 @@ import ErrorBoundary from '../ErrorBoundary'; import Evaluator from '../Evaluator'; import LogEventProcessor from '../LogEventProcessor'; import SpecStore from '../SpecStore'; -import { OptionsWithDefaults } from '../StatsigOptions'; +import { OptionsLoggingCopy, OptionsWithDefaults } from '../StatsigOptions'; +import { EvaluationContext, StatsigContext } from '../utils/StatsigContext'; import StatsigFetcher from '../utils/StatsigFetcher'; const exampleConfigSpecs = require('./jest.setup'); describe('Test condition evaluation', () => { const options = OptionsWithDefaults({ loggingMaxBufferSize: 1 }); const logger = new LogEventProcessor( - new StatsigFetcher('secret-asdf1234', options), - new ErrorBoundary('secret-asdf1234', options, "sessionid-a"), - options - ); + new StatsigFetcher( + 'secret-asdf1234', + options, + new ErrorBoundary( + 'secret-asdf1234', + OptionsLoggingCopy(options), + 'sessionid-a', + ), + 'sessionid-a', + ), + new ErrorBoundary('secret-asdf1234', options, 'sessionid-a'), + options, + OptionsLoggingCopy(options), + 'sessionid-a', + ); beforeEach(() => { Diagnostics.initialize({ logger }); }); @@ -267,7 +279,17 @@ describe('Test condition evaluation', () => { ['user_field', 'eq', null, 'nullable', { custom: { nullable: 'sth' } }, false], ] - const fetcher = new StatsigFetcher('secret-123', OptionsWithDefaults({})); + const defaultOptions = OptionsWithDefaults({}); + const fetcher = new StatsigFetcher( + 'secret-123', + defaultOptions, + new ErrorBoundary( + 'secret-123', + OptionsLoggingCopy(defaultOptions), + 'session-123', + ), + 'session-123', + ); const mockedEvaluator = new Evaluator(fetcher, OptionsWithDefaults({})); jest .spyOn(mockedEvaluator, 'checkGate') @@ -293,7 +315,17 @@ describe('Test condition evaluation', () => { const dynamicConfigSpec = new ConfigSpec(exampleConfigSpecs.config); it('works', () => { - const network = new StatsigFetcher('secret-123', OptionsWithDefaults({})); + const defaultOptions = OptionsWithDefaults({}); + const network = new StatsigFetcher( + 'secret-123', + defaultOptions, + new ErrorBoundary( + 'secret-123', + OptionsLoggingCopy(defaultOptions), + 'session-123', + ), + 'session-123', + ); const store = new SpecStore( network, OptionsWithDefaults({ api: 'https://statsigapi.net/v1' }), @@ -360,32 +392,84 @@ describe('Test condition evaluation', () => { }); it('evals gates correctly', () => { - expect(mockedEvaluator._eval({}, gateSpec)).toEqual( - new ConfigEvaluation(false, 'default', null, 'teamID', [], {}), - ); - expect(mockedEvaluator._eval({ userID: 'jkw' }, gateSpec)).toEqual( - new ConfigEvaluation(false, 'default', null, 'teamID', [], {}), - ); expect( - mockedEvaluator._eval({ email: 'tore@packers.com' }, gateSpec), + mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-ignore + user: {}, + spec: gateSpec, + }), + ), + ).toEqual(new ConfigEvaluation(false, 'default', null, 'teamID', [], {})); + expect( + mockedEvaluator._eval( + EvaluationContext.new({ + user: { userID: 'jkw' }, + spec: gateSpec, + }), + ), + ).toEqual(new ConfigEvaluation(false, 'default', null, 'teamID', [], {})); + expect( + mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-ignore + user: { email: 'tore@packers.com' }, + spec: gateSpec, + }), + ), ).toEqual( - new ConfigEvaluation(true, 'rule_id_gate', 'group_name_gate', 'teamID', [], {}), + new ConfigEvaluation( + true, + 'rule_id_gate', + 'group_name_gate', + 'teamID', + [], + {}, + ), ); expect( - mockedEvaluator._eval({ custom: { email: 'tore@nfl.com' } }, gateSpec), + mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-ignore + user: { custom: { email: 'tore@nfl.com' } }, + spec: gateSpec, + }), + ), ).toEqual( - new ConfigEvaluation(true, 'rule_id_gate', 'group_name_gate', 'teamID', [], {}), + new ConfigEvaluation( + true, + 'rule_id_gate', + 'group_name_gate', + 'teamID', + [], + {}, + ), ); expect( - mockedEvaluator._eval({ email: 'jkw@seahawks.com' }, gateSpec), + mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-ignore + user: { email: 'jkw@seahawks.com' }, + spec: gateSpec, + }), + ), ).toEqual(new ConfigEvaluation(false, 'default', null, 'teamID', [], {})); expect( - mockedEvaluator._eval({ email: 'tore@packers.com' }, disabledGateSpec), + mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-ignore + user: { custom: { email: 'tore@packers.com' } }, + spec: disabledGateSpec, + }), + ), ).toEqual(new ConfigEvaluation(false, 'disabled', null, 'teamID', [], {})); expect( mockedEvaluator._eval( - { custom: { email: 'tore@nfl.com' } }, - disabledGateSpec, + EvaluationContext.new({ + // @ts-ignore + user: { custom: { email: 'tore@nfl.com' } }, + spec: disabledGateSpec, + }), ), ).toEqual(new ConfigEvaluation(false, 'disabled', null, 'teamID', [], {})); }); @@ -395,12 +479,13 @@ describe('Test condition evaluation', () => { for (let i = 0; i < 1000; i++) { if ( mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - // @ts-ignore - }, - halfPassGateSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + }, + spec: halfPassGateSpec, + }), ).value ) { passCount++; @@ -412,36 +497,44 @@ describe('Test condition evaluation', () => { it('implements pass percentage correctly', () => { const valueID1 = mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - customIDs: { teamID: '3' }, - }, - halfPassGateCustomIDSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + customIDs: { teamID: '3' }, + }, + spec: halfPassGateCustomIDSpec, + }), ).value; const valueID2 = mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - customIDs: { teamid: '2' }, - }, - halfPassGateCustomIDSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + customIDs: { teamid: '2' }, + }, + spec: halfPassGateCustomIDSpec, + }), ).value; const valueID3 = mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - customIDs: { teamId: '3' }, - }, - halfPassGateCustomIDSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + customIDs: { teamId: '3' }, + }, + spec: halfPassGateCustomIDSpec, + }), ).value; const valueID4 = mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - customIDs: { TeamID: '2' }, - }, - halfPassGateCustomIDSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + customIDs: { TeamID: '2' }, + }, + spec: halfPassGateCustomIDSpec, + }), ).value; expect(valueID1).toEqual(true); expect(valueID2).toEqual(false); @@ -454,12 +547,13 @@ describe('Test condition evaluation', () => { return false; }); const failResult = mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - // @ts-ignore - }, - halfPassGateSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + }, + spec: halfPassGateSpec, + }), ); expect(failResult.rule_id).toEqual(halfPassGateSpec.rules[0].id); @@ -469,11 +563,13 @@ describe('Test condition evaluation', () => { return true; }); const passResult = mockedEvaluator._eval( - { - userID: Math.random() + '', - email: 'tore@packers.com', - }, - halfPassGateSpec, + EvaluationContext.new({ + user: { + userID: Math.random() + '', + email: 'tore@packers.com', + }, + spec: halfPassGateSpec, + }), ); expect(passResult.rule_id).toEqual(halfPassGateSpec.rules[0].id); @@ -482,11 +578,21 @@ describe('Test condition evaluation', () => { }); it('evals dynamic configs correctly', () => { - expect(mockedEvaluator._eval({}, dynamicConfigSpec).json_value).toEqual({}); expect( mockedEvaluator._eval( - { userID: 'jkw', custom: { level: 10 } }, - dynamicConfigSpec, + EvaluationContext.new({ + // @ts-ignore + user: {}, + spec: dynamicConfigSpec, + }), + ).json_value, + ).toEqual({}); + expect( + mockedEvaluator._eval( + EvaluationContext.new({ + user: { userID: 'jkw', custom: { level: 10 } }, + spec: dynamicConfigSpec, + }), ).json_value, ).toEqual({ packers: { @@ -500,19 +606,31 @@ describe('Test condition evaluation', () => { }); let res = mockedEvaluator._eval( - { userID: 'jkw', custom: { level: 10 } }, - dynamicConfigSpec, + EvaluationContext.new({ + user: { userID: 'jkw', custom: { level: 10 } }, + spec: dynamicConfigSpec, + }), ); expect(res.rule_id).toEqual('rule_id_config'); expect(res.group_name).toEqual('group_name_config'); expect( - // @ts-expect-error - mockedEvaluator._eval({ level: 5 }, dynamicConfigSpec).json_value, + mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-expect-error + user: { level: 5 }, + spec: dynamicConfigSpec, + }), + ).json_value, ).toEqual({}); - // @ts-expect-error - res = mockedEvaluator._eval({ level: 5 }, dynamicConfigSpec); + res = mockedEvaluator._eval( + EvaluationContext.new({ + // @ts-expect-error + user: { level: 5 }, + spec: dynamicConfigSpec, + }), + ); expect(res.rule_id).toEqual('rule_id_config_public'); expect(res.group_name).toEqual('group_name_config_public'); }); @@ -549,7 +667,16 @@ describe('testing checkGate and getConfig', () => { beforeEach(() => { jest.resetModules(); jest.restoreAllMocks(); - const network = new StatsigFetcher('secret-123', OptionsWithDefaults({})); + const network = new StatsigFetcher( + 'secret-123', + options, + new ErrorBoundary( + 'secret-123', + OptionsLoggingCopy(options), + 'session-123', + ), + 'session-123', + ); evaluator = new Evaluator( network, @@ -565,6 +692,7 @@ describe('testing checkGate and getConfig', () => { evaluator.checkGate( { userID: 'jkw', custom: { email: 'jkw@nfl.com' } }, exampleConfigSpecs.gate.name, + StatsigContext.new({}), ), ).toMatchObject({ value: false }); @@ -572,6 +700,7 @@ describe('testing checkGate and getConfig', () => { let result = evaluator.checkGate( { userID: 'jkw', custom: { email: 'jkw@nfl.com' } }, exampleConfigSpecs.gate.name, + StatsigContext.new({}), ); expect(result.value).toEqual(true); expect(result.rule_id).toEqual(exampleConfigSpecs.gate.rules[0].id); @@ -579,6 +708,7 @@ describe('testing checkGate and getConfig', () => { result = evaluator.checkGate( { userID: 'jkw', custom: { email: 'jkw@gmail.com' } }, exampleConfigSpecs.gate.name, + StatsigContext.new({}), ); expect(result.value).toEqual(false); expect(result.rule_id).toEqual('default'); @@ -588,6 +718,7 @@ describe('testing checkGate and getConfig', () => { evaluator.checkGate( { userID: 'jkw', custom: { email: 'jkw@gmail.com' } }, exampleConfigSpecs.gate.name + 'non-existent-gate', + StatsigContext.new({}), ), ).toMatchObject({ value: false }); }); @@ -597,6 +728,7 @@ describe('testing checkGate and getConfig', () => { evaluator.getConfig( { userID: 'jkw', custom: { email: 'jkw@nfl.com' } }, exampleConfigSpecs.config.name, + StatsigContext.new({}), ), ).toMatchObject({ json_value: {} }); @@ -606,6 +738,7 @@ describe('testing checkGate and getConfig', () => { const result = evaluator.getConfig( { userID: 'jkw', custom: { email: 'jkw@nfl.com', level: 10 } }, exampleConfigSpecs.config.name, + StatsigContext.new({}), ); expect(result.value).toEqual(true); expect(result.rule_id).toEqual(exampleConfigSpecs.config.rules[0].id); @@ -621,6 +754,7 @@ describe('testing checkGate and getConfig', () => { evaluator.getConfig( { userID: 'jkw', custom: { email: 'jkw@gmail.com' } }, exampleConfigSpecs.config.name + 'non-existent-config', + StatsigContext.new({}), ), ).toMatchObject({ json_value: {} }); }); @@ -628,7 +762,11 @@ describe('testing checkGate and getConfig', () => { describe('getLayer()', () => { it('returns {} when not initialized', () => { expect( - evaluator.getLayer({ userID: 'dloomb' }, 'unallocated_layer'), + evaluator.getLayer( + { userID: 'dloomb' }, + 'unallocated_layer', + StatsigContext.new({}), + ), ).toMatchObject({ json_value: {} }); }); @@ -639,7 +777,11 @@ describe('testing checkGate and getConfig', () => { it('returns {} when a non existant layer name is given', async () => { expect( - evaluator.getLayer({ userID: 'dloomb' }, 'not_a_layer'), + evaluator.getLayer( + { userID: 'dloomb' }, + 'not_a_layer', + StatsigContext.new({}), + ), ).toMatchObject({ json_value: {} }); }); @@ -647,6 +789,7 @@ describe('testing checkGate and getConfig', () => { const layer = evaluator.getLayer( { userID: 'dloomb' }, 'unallocated_layer', + StatsigContext.new({}), ); expect(layer.json_value).toEqual({ b_param: 'layer_default' }); expect(layer.rule_id).toEqual('default'); diff --git a/src/__tests__/RulesetsEvalConsistency.test.ts b/src/__tests__/RulesetsEvalConsistency.test.ts index 587bcb8..25aaa8a 100644 --- a/src/__tests__/RulesetsEvalConsistency.test.ts +++ b/src/__tests__/RulesetsEvalConsistency.test.ts @@ -2,7 +2,7 @@ import Evaluator, { ClientInitializeResponse } from '../Evaluator'; import Statsig from '../index'; import StatsigInstanceUtils from '../StatsigInstanceUtils'; import safeFetch from '../utils/safeFetch'; -import StatsigContext from '../utils/StatsigContext'; +import { StatsigContext } from '../utils/StatsigContext'; import StatsigTestUtils from './StatsigTestUtils'; const secret: string = process.env.test_api_key ?? ''; diff --git a/src/utils/StatsigContext.ts b/src/utils/StatsigContext.ts index 4342df1..6e10b87 100644 --- a/src/utils/StatsigContext.ts +++ b/src/utils/StatsigContext.ts @@ -1,4 +1,7 @@ -interface Context { +import { ConfigSpec } from '../ConfigSpec'; +import { StatsigUser } from '../StatsigUser'; + +type Context = { caller?: string; configName?: string; clientKey?: string; @@ -6,36 +9,32 @@ interface Context { eventCount?: number; bypassDedupe?: boolean; targetAppID?: string; -} + user?: StatsigUser; + spec?: ConfigSpec; +}; -export default class StatsigContext { +export class StatsigContext { readonly caller?: string; readonly eventCount?: number; readonly configName?: string; readonly clientKey?: string; readonly hash?: string; readonly bypassDedupe?: boolean; - readonly targetAppID?: string; - private constructor(protected ctx: Context) { + protected constructor(protected ctx: Context) { this.caller = ctx.caller; this.eventCount = ctx.eventCount; this.configName = ctx.configName; this.clientKey = ctx.clientKey; this.hash = ctx.clientKey; this.bypassDedupe = ctx.bypassDedupe; - this.targetAppID = ctx.targetAppID; } + // Create a new context to avoid modifying context up the stack static new(ctx: Context): StatsigContext { return new this(ctx); } - // Create a new context to avoid modifying context up the stack - public withTargetAppID(targetAppID: string): StatsigContext { - return StatsigContext.new({ ...this.ctx, targetAppID }); - } - getContextForLogging(): object { return { tag: this.caller, @@ -45,4 +44,58 @@ export default class StatsigContext { hash: this.clientKey, }; } + + getRequestContext(): Context { + return this.ctx; + } +} + +export class EvaluationContext extends StatsigContext { + readonly user: StatsigUser; + readonly spec: ConfigSpec; + readonly targetAppID?: string; + + protected constructor( + ctx: Context, + user: StatsigUser, + spec: ConfigSpec, + targetAppID?: string, + ) { + super(ctx); + this.user = user; + this.spec = spec; + this.targetAppID = targetAppID; + } + + public static new( + ctx: Context & Required>, + ): EvaluationContext { + const { user, spec, ...optionalCtx } = ctx; + return new this(optionalCtx, user, spec); + } + + public static get( + ctx: Context, + evalCtx: { + user: StatsigUser; + spec: ConfigSpec; + targetAppID?: string; + }, + ): EvaluationContext { + return new EvaluationContext( + ctx, + evalCtx.user, + evalCtx.spec, + evalCtx.targetAppID, + ); + } + + public withTargetAppID(targetAppID: string): EvaluationContext { + return new EvaluationContext( + this.getRequestContext(), + this.user, + this.spec, + targetAppID, + ); + } } diff --git a/src/utils/StatsigFetcher.ts b/src/utils/StatsigFetcher.ts index 5b7fe05..56f7e3a 100644 --- a/src/utils/StatsigFetcher.ts +++ b/src/utils/StatsigFetcher.ts @@ -11,7 +11,7 @@ import Dispatcher from './Dispatcher'; import getCompressionFunc from './getCompressionFunc'; import { djb2Hash } from './Hashing'; import safeFetch from './safeFetch'; -import StatsigContext from './StatsigContext'; +import { StatsigContext } from './StatsigContext'; const retryStatusCodes = [408, 500, 502, 503, 504, 522, 524, 599]; export const STATSIG_API = 'https://statsigapi.net/v1';