Skip to content

Commit b6f2fe6

Browse files
authored
Sameeran/ff 981 use obfuscated rac (#24)
* Add obfuscation helpers and test for boolean type in RAC * Include numeric in test * Extend obfuscation logic to rules matching * convert to yarn script to write obfuscated mock rac * move generateObfuscatedMockRac to script file * Generate obfuscated file as part of makefile * Increment minor version * Run all test cases and consolidate readMockRacResponse * Fix makefile * Copy obfuscated rac from data repo instead of generating it * Remove default arg in findMatchingRule * Obfuscation tests
1 parent 1c48c33 commit b6f2fe6

12 files changed

+415
-49
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ test-data:
3636
mkdir -p $(tempDir)
3737
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
3838
cp ${gitDataDir}rac-experiments-v3.json ${testDataDir}
39+
cp ${gitDataDir}rac-experiments-v3-obfuscated.json ${testDataDir}
3940
cp -r ${gitDataDir}assignment-v2 ${testDataDir}
4041
rm -rf ${tempDir}
4142

package.json

Lines changed: 4 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": "1.5.2",
3+
"version": "1.6.0",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [
@@ -21,7 +21,8 @@
2121
"pre-commit": "lint-staged && tsc",
2222
"typecheck": "tsc",
2323
"test": "yarn test:unit",
24-
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'"
24+
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
25+
"obfuscate-mock-rac": "ts-node test/writeObfuscatedMockRac"
2526
},
2627
"jsdelivr": "dist/eppo-sdk.js",
2728
"repository": {
@@ -52,6 +53,7 @@
5253
"testdouble": "^3.16.6",
5354
"ts-jest": "^28.0.5",
5455
"ts-loader": "^9.3.1",
56+
"ts-node": "^10.9.1",
5557
"typescript": "^4.7.4",
5658
"webpack": "^5.73.0",
5759
"webpack-cli": "^4.10.0",

src/client/eppo-client.spec.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import mock from 'xhr-mock';
77

88
import {
99
IAssignmentTestCase,
10+
MOCK_RAC_RESPONSE_FILE,
11+
OBFUSCATED_MOCK_RAC_RESPONSE_FILE,
1012
ValueTestType,
1113
readAssignmentTestData,
1214
readMockRacResponse,
@@ -69,7 +71,7 @@ describe('EppoClient E2E test', () => {
6971
beforeAll(async () => {
7072
mock.setup();
7173
mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
72-
const rac = readMockRacResponse();
74+
const rac = readMockRacResponse(MOCK_RAC_RESPONSE_FILE);
7375
return res.status(200).body(JSON.stringify(rac));
7476
});
7577

@@ -403,6 +405,7 @@ describe('EppoClient E2E test', () => {
403405
}[],
404406
experiment: string,
405407
valueTestType: ValueTestType = ValueTestType.StringType,
408+
obfuscated = false,
406409
): (EppoValue | null)[] {
407410
return subjectsWithAttributes.map((subject) => {
408411
switch (valueTestType) {
@@ -411,6 +414,8 @@ describe('EppoClient E2E test', () => {
411414
subject.subjectKey,
412415
experiment,
413416
subject.subjectAttributes,
417+
undefined,
418+
obfuscated,
414419
);
415420
if (ba === null) return null;
416421
return EppoValue.Bool(ba);
@@ -542,3 +547,131 @@ describe('EppoClient E2E test', () => {
542547
});
543548
});
544549
});
550+
551+
describe(' EppoClient getAssignment From Obfuscated RAC', () => {
552+
const storage = new TestConfigurationStore();
553+
const globalClient = new EppoClient(storage);
554+
555+
beforeAll(async () => {
556+
mock.setup();
557+
mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
558+
const rac = readMockRacResponse(OBFUSCATED_MOCK_RAC_RESPONSE_FILE);
559+
return res.status(200).body(JSON.stringify(rac));
560+
});
561+
await init(storage);
562+
});
563+
564+
afterAll(() => {
565+
mock.teardown();
566+
});
567+
568+
it.each(readAssignmentTestData())(
569+
'test variation assignment splits',
570+
async ({
571+
experiment,
572+
valueType = ValueTestType.StringType,
573+
subjects,
574+
subjectsWithAttributes,
575+
expectedAssignments,
576+
}: IAssignmentTestCase) => {
577+
`---- Test Case for ${experiment} Experiment ----`;
578+
579+
const assignments = getAssignmentsWithSubjectAttributes(
580+
subjectsWithAttributes
581+
? subjectsWithAttributes
582+
: subjects.map((subject) => ({ subjectKey: subject })),
583+
experiment,
584+
valueType,
585+
);
586+
587+
switch (valueType) {
588+
case ValueTestType.BoolType: {
589+
const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
590+
expect(boolAssignments).toEqual(expectedAssignments);
591+
break;
592+
}
593+
case ValueTestType.NumericType: {
594+
const numericAssignments = assignments.map((a) => a?.numericValue ?? null);
595+
expect(numericAssignments).toEqual(expectedAssignments);
596+
break;
597+
}
598+
case ValueTestType.StringType: {
599+
const stringAssignments = assignments.map((a) => a?.stringValue ?? null);
600+
expect(stringAssignments).toEqual(expectedAssignments);
601+
break;
602+
}
603+
case ValueTestType.JSONType: {
604+
const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
605+
expect(jsonStringAssignments).toEqual(expectedAssignments);
606+
break;
607+
}
608+
}
609+
},
610+
);
611+
612+
function getAssignmentsWithSubjectAttributes(
613+
subjectsWithAttributes: {
614+
subjectKey: string;
615+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
616+
subjectAttributes?: Record<string, any>;
617+
}[],
618+
experiment: string,
619+
valueTestType: ValueTestType = ValueTestType.StringType,
620+
): (EppoValue | null)[] {
621+
return subjectsWithAttributes.map((subject) => {
622+
switch (valueTestType) {
623+
case ValueTestType.BoolType: {
624+
const ba = globalClient.getBoolAssignment(
625+
subject.subjectKey,
626+
experiment,
627+
subject.subjectAttributes,
628+
undefined,
629+
true,
630+
);
631+
if (ba === null) return null;
632+
return EppoValue.Bool(ba);
633+
}
634+
case ValueTestType.NumericType: {
635+
const na = globalClient.getNumericAssignment(
636+
subject.subjectKey,
637+
experiment,
638+
subject.subjectAttributes,
639+
undefined,
640+
true,
641+
);
642+
if (na === null) return null;
643+
return EppoValue.Numeric(na);
644+
}
645+
case ValueTestType.StringType: {
646+
const sa = globalClient.getStringAssignment(
647+
subject.subjectKey,
648+
experiment,
649+
subject.subjectAttributes,
650+
undefined,
651+
true,
652+
);
653+
if (sa === null) return null;
654+
return EppoValue.String(sa);
655+
}
656+
case ValueTestType.JSONType: {
657+
const sa = globalClient.getJSONStringAssignment(
658+
subject.subjectKey,
659+
experiment,
660+
subject.subjectAttributes,
661+
undefined,
662+
true,
663+
);
664+
const oa = globalClient.getParsedJSONAssignment(
665+
subject.subjectKey,
666+
experiment,
667+
subject.subjectAttributes,
668+
undefined,
669+
true,
670+
);
671+
if (oa == null || sa === null) return null;
672+
return EppoValue.JSON(sa, oa);
673+
}
674+
}
675+
});
676+
}
677+
});

src/client/eppo-client.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MAX_EVENT_QUEUE_SIZE } from '../constants';
77
import { IAllocation } from '../dto/allocation-dto';
88
import { IExperimentConfiguration } from '../dto/experiment-configuration-dto';
99
import { EppoValue, ValueType } from '../eppo_value';
10+
import { getMD5Hash } from '../obfuscation';
1011
import { findMatchingRule } from '../rule_evaluator';
1112
import { getShard, isShardInRange } from '../shard';
1213
import { validateNotBlank } from '../validation';
@@ -100,11 +101,17 @@ export default class EppoClient implements IEppoClient {
100101
// eslint-disable-next-line @typescript-eslint/no-explicit-any
101102
subjectAttributes: Record<string, any> = {},
102103
assignmentHooks?: IAssignmentHooks | undefined,
104+
obfuscated = false,
103105
): string | null {
104106
try {
105107
return (
106-
this.getAssignmentVariation(subjectKey, flagKey, subjectAttributes, assignmentHooks)
107-
.stringValue ?? null
108+
this.getAssignmentVariation(
109+
subjectKey,
110+
flagKey,
111+
subjectAttributes,
112+
assignmentHooks,
113+
obfuscated,
114+
).stringValue ?? null
108115
);
109116
} catch (error) {
110117
return this.rethrowIfNotGraceful(error);
@@ -117,6 +124,7 @@ export default class EppoClient implements IEppoClient {
117124
// eslint-disable-next-line @typescript-eslint/no-explicit-any
118125
subjectAttributes: Record<string, any> = {},
119126
assignmentHooks?: IAssignmentHooks | undefined,
127+
obfuscated = false,
120128
): string | null {
121129
try {
122130
return (
@@ -125,6 +133,7 @@ export default class EppoClient implements IEppoClient {
125133
flagKey,
126134
subjectAttributes,
127135
assignmentHooks,
136+
obfuscated,
128137
ValueType.StringType,
129138
).stringValue ?? null
130139
);
@@ -139,6 +148,7 @@ export default class EppoClient implements IEppoClient {
139148
// eslint-disable-next-line @typescript-eslint/no-explicit-any
140149
subjectAttributes: Record<string, any> = {},
141150
assignmentHooks?: IAssignmentHooks | undefined,
151+
obfuscated = false,
142152
): boolean | null {
143153
try {
144154
return (
@@ -147,6 +157,7 @@ export default class EppoClient implements IEppoClient {
147157
flagKey,
148158
subjectAttributes,
149159
assignmentHooks,
160+
obfuscated,
150161
ValueType.BoolType,
151162
).boolValue ?? null
152163
);
@@ -160,6 +171,7 @@ export default class EppoClient implements IEppoClient {
160171
flagKey: string,
161172
subjectAttributes?: Record<string, EppoValue>,
162173
assignmentHooks?: IAssignmentHooks | undefined,
174+
obfuscated = false,
163175
): number | null {
164176
try {
165177
return (
@@ -168,6 +180,7 @@ export default class EppoClient implements IEppoClient {
168180
flagKey,
169181
subjectAttributes,
170182
assignmentHooks,
183+
obfuscated,
171184
ValueType.NumericType,
172185
).numericValue ?? null
173186
);
@@ -182,6 +195,7 @@ export default class EppoClient implements IEppoClient {
182195
// eslint-disable-next-line @typescript-eslint/no-explicit-any
183196
subjectAttributes: Record<string, any> = {},
184197
assignmentHooks?: IAssignmentHooks | undefined,
198+
obfuscated = false,
185199
): string | null {
186200
try {
187201
return (
@@ -190,6 +204,7 @@ export default class EppoClient implements IEppoClient {
190204
flagKey,
191205
subjectAttributes,
192206
assignmentHooks,
207+
obfuscated,
193208
ValueType.JSONType,
194209
).stringValue ?? null
195210
);
@@ -204,6 +219,7 @@ export default class EppoClient implements IEppoClient {
204219
// eslint-disable-next-line @typescript-eslint/no-explicit-any
205220
subjectAttributes: Record<string, any> = {},
206221
assignmentHooks?: IAssignmentHooks | undefined,
222+
obfuscated = false,
207223
): object | null {
208224
try {
209225
return (
@@ -212,6 +228,7 @@ export default class EppoClient implements IEppoClient {
212228
flagKey,
213229
subjectAttributes,
214230
assignmentHooks,
231+
obfuscated,
215232
ValueType.JSONType,
216233
).objectValue ?? null
217234
);
@@ -234,13 +251,15 @@ export default class EppoClient implements IEppoClient {
234251
// eslint-disable-next-line @typescript-eslint/no-explicit-any
235252
subjectAttributes: Record<string, any> = {},
236253
assignmentHooks: IAssignmentHooks | undefined,
254+
obfuscated: boolean,
237255
valueType?: ValueType,
238256
): EppoValue {
239257
const { allocationKey, assignment } = this.getAssignmentInternal(
240258
subjectKey,
241259
flagKey,
242260
subjectAttributes,
243261
assignmentHooks,
262+
obfuscated,
244263
valueType,
245264
);
246265
assignmentHooks?.onPostAssignment(flagKey, subjectKey, assignment, allocationKey);
@@ -256,14 +275,17 @@ export default class EppoClient implements IEppoClient {
256275
flagKey: string,
257276
subjectAttributes = {},
258277
assignmentHooks: IAssignmentHooks | undefined,
278+
obfuscated: boolean,
259279
expectedValueType?: ValueType,
260280
): { allocationKey: string | null; assignment: EppoValue } {
261281
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
262282
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
263283

264284
const nullAssignment = { allocationKey: null, assignment: EppoValue.Null() };
265285

266-
const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(flagKey);
286+
const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(
287+
obfuscated ? getMD5Hash(flagKey) : flagKey,
288+
);
267289
const allowListOverride = this.getSubjectVariationOverride(
268290
subjectKey,
269291
experimentConfig,
@@ -288,7 +310,11 @@ export default class EppoClient implements IEppoClient {
288310
}
289311

290312
// Attempt to match a rule from the list.
291-
const matchedRule = findMatchingRule(subjectAttributes || {}, experimentConfig.rules);
313+
const matchedRule = findMatchingRule(
314+
subjectAttributes || {},
315+
experimentConfig.rules,
316+
obfuscated,
317+
);
292318
if (!matchedRule) return nullAssignment;
293319

294320
// Check if subject is in allocation sample.

src/obfuscation.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { decodeBase64, encodeBase64 } from './obfuscation';
2+
3+
describe('obfuscation', () => {
4+
it('encodes strings to base64', () => {
5+
expect(encodeBase64('5.0')).toEqual('NS4w');
6+
});
7+
8+
it('decodes base64 to string', () => {
9+
expect(Number(decodeBase64('NS4w'))).toEqual(5);
10+
});
11+
});

src/obfuscation.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as md5 from 'md5';
2+
3+
export function getMD5Hash(input: string): string {
4+
return md5(input);
5+
}
6+
7+
export function encodeBase64(input: string): string {
8+
return Buffer.from(input, 'utf8').toString('base64');
9+
}
10+
11+
export function decodeBase64(input: string): string {
12+
return Buffer.from(input, 'base64').toString('utf8');
13+
}

0 commit comments

Comments
 (0)