Skip to content

Commit 9b443b1

Browse files
decode obfuscated bandit (#202)
* decode obfuscated bandit * dont decode the banditKey * base64 bandit key * decode * ok * populate variation * 4.8.3-alpha.3 * 4.8.3 * decode to number, tests, nit (#203) Co-authored-by: Ty Potter <[email protected]> --------- Co-authored-by: Ty Potter <[email protected]>
1 parent 8ce8737 commit 9b443b1

File tree

6 files changed

+127
-14
lines changed

6 files changed

+127
-14
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "4.8.2",
3+
"version": "4.8.3",
44
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
55
"main": "dist/index.js",
66
"files": [
@@ -78,4 +78,4 @@
7878
"uuid": "^11.0.5"
7979
},
8080
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
81-
}
81+
}

src/client/eppo-precomputed-client.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
MAX_EVENT_QUEUE_SIZE,
1919
PRECOMPUTED_BASE_URL,
2020
} from '../constants';
21-
import { decodePrecomputedFlag } from '../decoding';
21+
import { decodePrecomputedBandit, decodePrecomputedFlag } from '../decoding';
2222
import { FlagEvaluationWithoutDetails } from '../evaluator';
2323
import FetchHttpClient from '../http-client';
2424
import {
@@ -307,7 +307,7 @@ export default class EppoPrecomputedClient {
307307
);
308308
}
309309

310-
getBanditAction(
310+
public getBanditAction(
311311
flagKey: string,
312312
defaultValue: string,
313313
): Omit<IAssignmentDetails<string>, 'evaluationDetails'> {
@@ -318,6 +318,8 @@ export default class EppoPrecomputedClient {
318318
return { variation: defaultValue, action: null };
319319
}
320320

321+
const assignedVariation = this.getStringAssignment(flagKey, defaultValue);
322+
321323
const banditEvent: IBanditEvent = {
322324
timestamp: new Date().toISOString(),
323325
featureFlag: flagKey,
@@ -341,7 +343,7 @@ export default class EppoPrecomputedClient {
341343
logger.error(`${loggerPrefix} Error logging bandit action: ${error}`);
342344
}
343345

344-
return { variation: defaultValue, action: banditEvent.action };
346+
return { variation: assignedVariation, action: banditEvent.action };
345347
}
346348

347349
private getPrecomputedFlag(flagKey: string): DecodedPrecomputedFlag | null {
@@ -358,13 +360,17 @@ export default class EppoPrecomputedClient {
358360
}
359361

360362
private getPrecomputedBandit(banditKey: string): IPrecomputedBandit | null {
361-
return this.getObfuscatedPrecomputedBandit(banditKey);
363+
const obfuscatedBandit = this.getObfuscatedPrecomputedBandit(banditKey);
364+
return obfuscatedBandit ? decodePrecomputedBandit(obfuscatedBandit) : null;
362365
}
363366

364367
private getObfuscatedPrecomputedBandit(banditKey: string): IObfuscatedPrecomputedBandit | null {
365368
const salt = this.precomputedBanditStore?.salt;
366369
const saltedAndHashedBanditKey = getMD5Hash(banditKey, salt);
367-
return this.precomputedBanditStore?.get(saltedAndHashedBanditKey) ?? null;
370+
const precomputedBandit: IObfuscatedPrecomputedBandit | null = this.precomputedBanditStore?.get(
371+
saltedAndHashedBanditKey,
372+
) as IObfuscatedPrecomputedBandit;
373+
return precomputedBandit ?? null;
368374
}
369375

370376
public isInitialized() {

src/decoding.spec.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
import { decodeAllocation, decodeSplit, decodeValue, decodeVariations } from './decoding';
2-
import { VariationType, ObfuscatedVariation, Variation } from './interfaces';
1+
import {
2+
decodeAllocation,
3+
decodePrecomputedBandit,
4+
decodeSplit,
5+
decodeValue,
6+
decodeVariations,
7+
} from './decoding';
8+
import {
9+
VariationType,
10+
ObfuscatedVariation,
11+
Variation,
12+
IObfuscatedPrecomputedBandit,
13+
} from './interfaces';
314

415
describe('decoding', () => {
516
describe('decodeVariations', () => {
@@ -175,4 +186,38 @@ describe('decoding', () => {
175186
expect(decodeAllocation(obfuscatedAllocation)).toEqual(expectedAllocation);
176187
});
177188
});
189+
190+
describe('decode bandit', () => {
191+
it('should correctly decode bandit', () => {
192+
const encodedBandit = {
193+
action: 'Z3JlZW5CYWNrZ3JvdW5k',
194+
actionCategoricalAttributes: {
195+
'Y29sb3I=': 'Z3JlZW4=',
196+
'dHlwZQ==': 'YmFja2dyb3VuZA==',
197+
},
198+
actionNumericAttributes: { Zm9udEhlaWdodEVt: 'MTA=' },
199+
actionProbability: 0.95,
200+
banditKey: 'bGF1bmNoLWJ1dHRvbi10cmVhdG1lbnQ=',
201+
modelVersion: 'MzI0OQ==',
202+
optimalityGap: 0,
203+
} as IObfuscatedPrecomputedBandit;
204+
205+
const decodedBandit = decodePrecomputedBandit(encodedBandit);
206+
207+
expect(decodedBandit).toEqual({
208+
action: 'greenBackground',
209+
actionCategoricalAttributes: {
210+
color: 'green',
211+
type: 'background',
212+
},
213+
actionNumericAttributes: {
214+
fontHeightEm: 10,
215+
},
216+
actionProbability: 0.95,
217+
banditKey: 'launch-button-treatment',
218+
modelVersion: '3249',
219+
optimalityGap: 0,
220+
});
221+
});
222+
});
178223
});

src/decoding.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
ObfuscatedSplit,
1212
PrecomputedFlag,
1313
DecodedPrecomputedFlag,
14+
IPrecomputedBandit,
15+
IObfuscatedPrecomputedBandit,
1416
} from './interfaces';
1517
import { decodeBase64 } from './obfuscation';
1618

@@ -74,8 +76,14 @@ export function decodeShard(shard: Shard): Shard {
7476
}
7577

7678
export function decodeObject(obj: Record<string, string>): Record<string, string> {
79+
return decodeObjectTo(obj, (v: string) => v);
80+
}
81+
export function decodeObjectTo<T>(
82+
obj: Record<string, string>,
83+
transform: (v: string) => T,
84+
): Record<string, T> {
7785
return Object.fromEntries(
78-
Object.entries(obj).map(([key, value]) => [decodeBase64(key), decodeBase64(value)]),
86+
Object.entries(obj).map(([key, value]) => [decodeBase64(key), transform(decodeBase64(value))]),
7987
);
8088
}
8189

@@ -88,3 +96,19 @@ export function decodePrecomputedFlag(precomputedFlag: PrecomputedFlag): Decoded
8896
extraLogging: decodeObject(precomputedFlag.extraLogging ?? {}),
8997
};
9098
}
99+
100+
export function decodePrecomputedBandit(
101+
precomputedBandit: IObfuscatedPrecomputedBandit,
102+
): IPrecomputedBandit {
103+
return {
104+
...precomputedBandit,
105+
banditKey: decodeBase64(precomputedBandit.banditKey),
106+
action: decodeBase64(precomputedBandit.action),
107+
modelVersion: decodeBase64(precomputedBandit.modelVersion),
108+
actionNumericAttributes: decodeObjectTo<number>(
109+
precomputedBandit.actionNumericAttributes ?? {},
110+
(v) => +v, // Convert to a number
111+
),
112+
actionCategoricalAttributes: decodeObject(precomputedBandit.actionCategoricalAttributes ?? {}),
113+
};
114+
}

src/obfuscation.spec.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { decodeBase64, encodeBase64 } from './obfuscation';
1+
import { IPrecomputedBandit } from './interfaces';
2+
import { decodeBase64, encodeBase64, obfuscatePrecomputedBanditMap } from './obfuscation';
23

34
describe('obfuscation', () => {
45
it('encodes strings to base64', () => {
@@ -27,4 +28,42 @@ describe('obfuscation', () => {
2728

2829
expect(decodeBase64('a8O8bW1lcnQ=')).toEqual('kümmert');
2930
});
31+
32+
describe('bandit obfuscation', () => {
33+
it('obfuscates precomputed bandits', () => {
34+
const bandit: IPrecomputedBandit = {
35+
action: 'greenBackground',
36+
actionCategoricalAttributes: {
37+
color: 'green',
38+
type: 'background',
39+
},
40+
actionNumericAttributes: {
41+
fontHeightEm: 10,
42+
},
43+
actionProbability: 0.95,
44+
banditKey: 'launch-button-treatment',
45+
modelVersion: '3249',
46+
optimalityGap: 0,
47+
};
48+
49+
const encodedBandit = obfuscatePrecomputedBanditMap('', {
50+
'launch-button-treatment': bandit,
51+
});
52+
53+
expect(encodedBandit).toEqual({
54+
'0ae2ece7bf09e40dd6b28a02574a4826': {
55+
action: 'Z3JlZW5CYWNrZ3JvdW5k',
56+
actionCategoricalAttributes: {
57+
'Y29sb3I=': 'Z3JlZW4=',
58+
'dHlwZQ==': 'YmFja2dyb3VuZA==',
59+
},
60+
actionNumericAttributes: { Zm9udEhlaWdodEVt: 'MTA=' },
61+
actionProbability: 0.95,
62+
banditKey: 'bGF1bmNoLWJ1dHRvbi10cmVhdG1lbnQ=',
63+
modelVersion: 'MzI0OQ==',
64+
optimalityGap: 0,
65+
},
66+
});
67+
});
68+
});
3069
});

src/obfuscation.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,16 @@ export function obfuscatePrecomputedBanditMap(
2929
return Object.fromEntries(
3030
Object.entries(bandits).map(([variationValue, bandit]) => {
3131
const hashedKey = getMD5Hash(variationValue, salt);
32-
return [hashedKey, obfuscatePrecomputedBandit(salt, bandit)];
32+
return [hashedKey, obfuscatePrecomputedBandit(bandit)];
3333
}),
3434
);
3535
}
3636

3737
function obfuscatePrecomputedBandit(
38-
salt: string,
3938
banditResult: IPrecomputedBandit,
4039
): IObfuscatedPrecomputedBandit {
4140
return {
42-
banditKey: getMD5Hash(banditResult.banditKey, salt),
41+
banditKey: encodeBase64(banditResult.banditKey),
4342
action: encodeBase64(banditResult.action),
4443
actionProbability: banditResult.actionProbability,
4544
optimalityGap: banditResult.optimalityGap,

0 commit comments

Comments
 (0)