diff --git a/src/client/eppo-precomputed-client.ts b/src/client/eppo-precomputed-client.ts index 4f35f5a..6415552 100644 --- a/src/client/eppo-precomputed-client.ts +++ b/src/client/eppo-precomputed-client.ts @@ -360,16 +360,17 @@ export default class EppoPrecomputedClient { } private getPrecomputedBandit(banditKey: string): IPrecomputedBandit | null { - return this.getObfuscatedPrecomputedBandit(banditKey); + const obfuscatedBandit = this.getObfuscatedPrecomputedBandit(banditKey); + return obfuscatedBandit ? decodePrecomputedBandit(obfuscatedBandit) : null; } - private getObfuscatedPrecomputedBandit(banditKey: string): IPrecomputedBandit | null { + private getObfuscatedPrecomputedBandit(banditKey: string): IObfuscatedPrecomputedBandit | null { const salt = this.precomputedBanditStore?.salt; const saltedAndHashedBanditKey = getMD5Hash(banditKey, salt); const precomputedBandit: IObfuscatedPrecomputedBandit | null = this.precomputedBanditStore?.get( saltedAndHashedBanditKey, ) as IObfuscatedPrecomputedBandit; - return precomputedBandit ? decodePrecomputedBandit(precomputedBandit) : null; + return precomputedBandit ?? null; } public isInitialized() { diff --git a/src/decoding.spec.ts b/src/decoding.spec.ts index 83bd666..81d2725 100644 --- a/src/decoding.spec.ts +++ b/src/decoding.spec.ts @@ -1,5 +1,16 @@ -import { decodeAllocation, decodeSplit, decodeValue, decodeVariations } from './decoding'; -import { VariationType, ObfuscatedVariation, Variation } from './interfaces'; +import { + decodeAllocation, + decodePrecomputedBandit, + decodeSplit, + decodeValue, + decodeVariations, +} from './decoding'; +import { + VariationType, + ObfuscatedVariation, + Variation, + IObfuscatedPrecomputedBandit, +} from './interfaces'; describe('decoding', () => { describe('decodeVariations', () => { @@ -175,4 +186,38 @@ describe('decoding', () => { expect(decodeAllocation(obfuscatedAllocation)).toEqual(expectedAllocation); }); }); + + describe('decode bandit', () => { + it('should correctly decode bandit', () => { + const encodedBandit = { + action: 'Z3JlZW5CYWNrZ3JvdW5k', + actionCategoricalAttributes: { + 'Y29sb3I=': 'Z3JlZW4=', + 'dHlwZQ==': 'YmFja2dyb3VuZA==', + }, + actionNumericAttributes: { Zm9udEhlaWdodEVt: 'MTA=' }, + actionProbability: 0.95, + banditKey: 'bGF1bmNoLWJ1dHRvbi10cmVhdG1lbnQ=', + modelVersion: 'MzI0OQ==', + optimalityGap: 0, + } as IObfuscatedPrecomputedBandit; + + const decodedBandit = decodePrecomputedBandit(encodedBandit); + + expect(decodedBandit).toEqual({ + action: 'greenBackground', + actionCategoricalAttributes: { + color: 'green', + type: 'background', + }, + actionNumericAttributes: { + fontHeightEm: 10, + }, + actionProbability: 0.95, + banditKey: 'launch-button-treatment', + modelVersion: '3249', + optimalityGap: 0, + }); + }); + }); }); diff --git a/src/decoding.ts b/src/decoding.ts index b2935cd..c2231bf 100644 --- a/src/decoding.ts +++ b/src/decoding.ts @@ -76,8 +76,14 @@ export function decodeShard(shard: Shard): Shard { } export function decodeObject(obj: Record): Record { + return decodeObjectTo(obj, (v: string) => v); +} +export function decodeObjectTo( + obj: Record, + transform: (v: string) => T, +): Record { return Object.fromEntries( - Object.entries(obj).map(([key, value]) => [decodeBase64(key), decodeBase64(value)]), + Object.entries(obj).map(([key, value]) => [decodeBase64(key), transform(decodeBase64(value))]), ); } @@ -99,7 +105,10 @@ export function decodePrecomputedBandit( banditKey: decodeBase64(precomputedBandit.banditKey), action: decodeBase64(precomputedBandit.action), modelVersion: decodeBase64(precomputedBandit.modelVersion), - actionNumericAttributes: decodeObject(precomputedBandit.actionNumericAttributes ?? {}), + actionNumericAttributes: decodeObjectTo( + precomputedBandit.actionNumericAttributes ?? {}, + (v) => +v, // Convert to a number + ), actionCategoricalAttributes: decodeObject(precomputedBandit.actionCategoricalAttributes ?? {}), }; } diff --git a/src/obfuscation.spec.ts b/src/obfuscation.spec.ts index fae7724..cb092dd 100644 --- a/src/obfuscation.spec.ts +++ b/src/obfuscation.spec.ts @@ -1,4 +1,5 @@ -import { decodeBase64, encodeBase64 } from './obfuscation'; +import { IPrecomputedBandit } from './interfaces'; +import { decodeBase64, encodeBase64, obfuscatePrecomputedBanditMap } from './obfuscation'; describe('obfuscation', () => { it('encodes strings to base64', () => { @@ -27,4 +28,42 @@ describe('obfuscation', () => { expect(decodeBase64('a8O8bW1lcnQ=')).toEqual('kümmert'); }); + + describe('bandit obfuscation', () => { + it('obfuscates precomputed bandits', () => { + const bandit: IPrecomputedBandit = { + action: 'greenBackground', + actionCategoricalAttributes: { + color: 'green', + type: 'background', + }, + actionNumericAttributes: { + fontHeightEm: 10, + }, + actionProbability: 0.95, + banditKey: 'launch-button-treatment', + modelVersion: '3249', + optimalityGap: 0, + }; + + const encodedBandit = obfuscatePrecomputedBanditMap('', { + 'launch-button-treatment': bandit, + }); + + expect(encodedBandit).toEqual({ + '0ae2ece7bf09e40dd6b28a02574a4826': { + action: 'Z3JlZW5CYWNrZ3JvdW5k', + actionCategoricalAttributes: { + 'Y29sb3I=': 'Z3JlZW4=', + 'dHlwZQ==': 'YmFja2dyb3VuZA==', + }, + actionNumericAttributes: { Zm9udEhlaWdodEVt: 'MTA=' }, + actionProbability: 0.95, + banditKey: 'bGF1bmNoLWJ1dHRvbi10cmVhdG1lbnQ=', + modelVersion: 'MzI0OQ==', + optimalityGap: 0, + }, + }); + }); + }); });