From 5bd5fd270c2b105516076712be832fbf372f1d31 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 13:43:35 -0800 Subject: [PATCH 1/9] decode obfuscated bandit --- package.json | 2 +- src/client/eppo-precomputed-client.ts | 9 ++++++--- src/decoding.ts | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d62261a..3a5e60f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.8.2", + "version": "4.8.3-alpha.1", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ diff --git a/src/client/eppo-precomputed-client.ts b/src/client/eppo-precomputed-client.ts index b2e19ff..5c93377 100644 --- a/src/client/eppo-precomputed-client.ts +++ b/src/client/eppo-precomputed-client.ts @@ -18,7 +18,7 @@ import { MAX_EVENT_QUEUE_SIZE, PRECOMPUTED_BASE_URL, } from '../constants'; -import { decodePrecomputedFlag } from '../decoding'; +import { decodePrecomputedBandit, decodePrecomputedFlag } from '../decoding'; import { FlagEvaluationWithoutDetails } from '../evaluator'; import FetchHttpClient from '../http-client'; import { @@ -361,10 +361,13 @@ export default class EppoPrecomputedClient { return this.getObfuscatedPrecomputedBandit(banditKey); } - private getObfuscatedPrecomputedBandit(banditKey: string): IObfuscatedPrecomputedBandit | null { + private getObfuscatedPrecomputedBandit(banditKey: string): IPrecomputedBandit | null { const salt = this.precomputedBanditStore?.salt; const saltedAndHashedBanditKey = getMD5Hash(banditKey, salt); - return this.precomputedBanditStore?.get(saltedAndHashedBanditKey) ?? null; + const precomputedBandit: IObfuscatedPrecomputedBandit | null = this.precomputedBanditStore?.get( + saltedAndHashedBanditKey, + ) as IObfuscatedPrecomputedBandit; + return precomputedBandit ? decodePrecomputedBandit(precomputedBandit) : null; } public isInitialized() { diff --git a/src/decoding.ts b/src/decoding.ts index fdb43e7..b2935cd 100644 --- a/src/decoding.ts +++ b/src/decoding.ts @@ -11,6 +11,8 @@ import { ObfuscatedSplit, PrecomputedFlag, DecodedPrecomputedFlag, + IPrecomputedBandit, + IObfuscatedPrecomputedBandit, } from './interfaces'; import { decodeBase64 } from './obfuscation'; @@ -88,3 +90,16 @@ export function decodePrecomputedFlag(precomputedFlag: PrecomputedFlag): Decoded extraLogging: decodeObject(precomputedFlag.extraLogging ?? {}), }; } + +export function decodePrecomputedBandit( + precomputedBandit: IObfuscatedPrecomputedBandit, +): IPrecomputedBandit { + return { + ...precomputedBandit, + banditKey: decodeBase64(precomputedBandit.banditKey), + action: decodeBase64(precomputedBandit.action), + modelVersion: decodeBase64(precomputedBandit.modelVersion), + actionNumericAttributes: decodeObject(precomputedBandit.actionNumericAttributes ?? {}), + actionCategoricalAttributes: decodeObject(precomputedBandit.actionCategoricalAttributes ?? {}), + }; +} From 9e82f49d5f1a21c66f00155674bb6c61f5b4512d Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 14:05:52 -0800 Subject: [PATCH 2/9] dont decode the banditKey --- src/decoding.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/decoding.ts b/src/decoding.ts index b2935cd..83b6f42 100644 --- a/src/decoding.ts +++ b/src/decoding.ts @@ -96,7 +96,6 @@ export function decodePrecomputedBandit( ): IPrecomputedBandit { return { ...precomputedBandit, - banditKey: decodeBase64(precomputedBandit.banditKey), action: decodeBase64(precomputedBandit.action), modelVersion: decodeBase64(precomputedBandit.modelVersion), actionNumericAttributes: decodeObject(precomputedBandit.actionNumericAttributes ?? {}), From e29c47ccc92d8e42d30d46486b6e1e84d512ad71 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 15:17:35 -0800 Subject: [PATCH 3/9] base64 bandit key --- package.json | 4 ++-- src/obfuscation.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3a5e60f..1a2bb4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.8.3-alpha.1", + "version": "4.8.3-alpha.3", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ @@ -78,4 +78,4 @@ "uuid": "^11.0.5" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" -} \ No newline at end of file +} diff --git a/src/obfuscation.ts b/src/obfuscation.ts index cd71728..671d2b7 100644 --- a/src/obfuscation.ts +++ b/src/obfuscation.ts @@ -29,17 +29,16 @@ export function obfuscatePrecomputedBanditMap( return Object.fromEntries( Object.entries(bandits).map(([variationValue, bandit]) => { const hashedKey = getMD5Hash(variationValue, salt); - return [hashedKey, obfuscatePrecomputedBandit(salt, bandit)]; + return [hashedKey, obfuscatePrecomputedBandit(bandit)]; }), ); } function obfuscatePrecomputedBandit( - salt: string, banditResult: IPrecomputedBandit, ): IObfuscatedPrecomputedBandit { return { - banditKey: getMD5Hash(banditResult.banditKey, salt), + banditKey: encodeBase64(banditResult.banditKey), action: encodeBase64(banditResult.action), actionProbability: banditResult.actionProbability, optimalityGap: banditResult.optimalityGap, From e397bfc0a6efa9f91e060f5d3e87c997aaec9338 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 21:29:40 -0800 Subject: [PATCH 4/9] decode --- src/decoding.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decoding.ts b/src/decoding.ts index 83b6f42..b2935cd 100644 --- a/src/decoding.ts +++ b/src/decoding.ts @@ -96,6 +96,7 @@ export function decodePrecomputedBandit( ): IPrecomputedBandit { return { ...precomputedBandit, + banditKey: decodeBase64(precomputedBandit.banditKey), action: decodeBase64(precomputedBandit.action), modelVersion: decodeBase64(precomputedBandit.modelVersion), actionNumericAttributes: decodeObject(precomputedBandit.actionNumericAttributes ?? {}), From 2e2d51d245f5457f86529faf6544948f7beae902 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 21:37:58 -0800 Subject: [PATCH 5/9] ok --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1a2bb4a..7ecb65b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.8.3-alpha.3", + "version": "4.8.3-alpha.4", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ From 786cd083a4918d5145f8f9aee909d0d8e69fdc8e Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 21:44:16 -0800 Subject: [PATCH 6/9] populate variation --- src/client/eppo-precomputed-client.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/eppo-precomputed-client.ts b/src/client/eppo-precomputed-client.ts index 5c93377..4f35f5a 100644 --- a/src/client/eppo-precomputed-client.ts +++ b/src/client/eppo-precomputed-client.ts @@ -307,7 +307,7 @@ export default class EppoPrecomputedClient { ); } - getBanditAction( + public getBanditAction( flagKey: string, defaultValue: string, ): Omit, 'evaluationDetails'> { @@ -318,6 +318,8 @@ export default class EppoPrecomputedClient { return { variation: defaultValue, action: null }; } + const assignedVariation = this.getStringAssignment(flagKey, defaultValue); + const banditEvent: IBanditEvent = { timestamp: new Date().toISOString(), featureFlag: flagKey, @@ -341,7 +343,7 @@ export default class EppoPrecomputedClient { logger.error(`${loggerPrefix} Error logging bandit action: ${error}`); } - return { variation: defaultValue, action: banditEvent.action }; + return { variation: assignedVariation, action: banditEvent.action }; } private getPrecomputedFlag(flagKey: string): DecodedPrecomputedFlag | null { From a0ec137daed8ce593767bd6f2afe2f7f9b420232 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 22:03:39 -0800 Subject: [PATCH 7/9] 4.8.3-alpha.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ecb65b..3454cbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.8.3-alpha.4", + "version": "4.8.3-alpha.5", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ From afdd429e1ba63435be05d444299786ec2806edba Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 16 Jan 2025 23:44:21 -0800 Subject: [PATCH 8/9] 4.8.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3454cbd..454c66f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eppo/js-client-sdk-common", - "version": "4.8.3-alpha.5", + "version": "4.8.3", "description": "Common library for Eppo JavaScript SDKs (web, react native, and node)", "main": "dist/index.js", "files": [ From 0f515c2d002f821878142556dbdf47f12934a329 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Fri, 17 Jan 2025 09:17:07 -0800 Subject: [PATCH 9/9] decode to number, tests, nit (#203) Co-authored-by: Ty Potter --- src/client/eppo-precomputed-client.ts | 7 ++-- src/decoding.spec.ts | 49 +++++++++++++++++++++++++-- src/decoding.ts | 13 +++++-- src/obfuscation.spec.ts | 41 +++++++++++++++++++++- 4 files changed, 102 insertions(+), 8 deletions(-) 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, + }, + }); + }); + }); });