Skip to content

Commit

Permalink
Sameeran/ff 981 use obfuscated rac (#24)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sameerank authored Oct 5, 2023
1 parent 1c48c33 commit b6f2fe6
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 49 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ test-data:
mkdir -p $(tempDir)
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
cp ${gitDataDir}rac-experiments-v3.json ${testDataDir}
cp ${gitDataDir}rac-experiments-v3-obfuscated.json ${testDataDir}
cp -r ${gitDataDir}assignment-v2 ${testDataDir}
rm -rf ${tempDir}

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "1.5.2",
"version": "1.6.0",
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
"main": "dist/index.js",
"files": [
Expand All @@ -21,7 +21,8 @@
"pre-commit": "lint-staged && tsc",
"typecheck": "tsc",
"test": "yarn test:unit",
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'"
"test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
"obfuscate-mock-rac": "ts-node test/writeObfuscatedMockRac"
},
"jsdelivr": "dist/eppo-sdk.js",
"repository": {
Expand Down Expand Up @@ -52,6 +53,7 @@
"testdouble": "^3.16.6",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
Expand Down
135 changes: 134 additions & 1 deletion src/client/eppo-client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import mock from 'xhr-mock';

import {
IAssignmentTestCase,
MOCK_RAC_RESPONSE_FILE,
OBFUSCATED_MOCK_RAC_RESPONSE_FILE,
ValueTestType,
readAssignmentTestData,
readMockRacResponse,
Expand Down Expand Up @@ -69,7 +71,7 @@ describe('EppoClient E2E test', () => {
beforeAll(async () => {
mock.setup();
mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
const rac = readMockRacResponse();
const rac = readMockRacResponse(MOCK_RAC_RESPONSE_FILE);
return res.status(200).body(JSON.stringify(rac));
});

Expand Down Expand Up @@ -403,6 +405,7 @@ describe('EppoClient E2E test', () => {
}[],
experiment: string,
valueTestType: ValueTestType = ValueTestType.StringType,
obfuscated = false,
): (EppoValue | null)[] {
return subjectsWithAttributes.map((subject) => {
switch (valueTestType) {
Expand All @@ -411,6 +414,8 @@ describe('EppoClient E2E test', () => {
subject.subjectKey,
experiment,
subject.subjectAttributes,
undefined,
obfuscated,
);
if (ba === null) return null;
return EppoValue.Bool(ba);
Expand Down Expand Up @@ -542,3 +547,131 @@ describe('EppoClient E2E test', () => {
});
});
});

describe(' EppoClient getAssignment From Obfuscated RAC', () => {
const storage = new TestConfigurationStore();
const globalClient = new EppoClient(storage);

beforeAll(async () => {
mock.setup();
mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
const rac = readMockRacResponse(OBFUSCATED_MOCK_RAC_RESPONSE_FILE);
return res.status(200).body(JSON.stringify(rac));
});
await init(storage);
});

afterAll(() => {
mock.teardown();
});

it.each(readAssignmentTestData())(
'test variation assignment splits',
async ({
experiment,
valueType = ValueTestType.StringType,
subjects,
subjectsWithAttributes,
expectedAssignments,
}: IAssignmentTestCase) => {
`---- Test Case for ${experiment} Experiment ----`;

const assignments = getAssignmentsWithSubjectAttributes(
subjectsWithAttributes
? subjectsWithAttributes
: subjects.map((subject) => ({ subjectKey: subject })),
experiment,
valueType,
);

switch (valueType) {
case ValueTestType.BoolType: {
const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
expect(boolAssignments).toEqual(expectedAssignments);
break;
}
case ValueTestType.NumericType: {
const numericAssignments = assignments.map((a) => a?.numericValue ?? null);
expect(numericAssignments).toEqual(expectedAssignments);
break;
}
case ValueTestType.StringType: {
const stringAssignments = assignments.map((a) => a?.stringValue ?? null);
expect(stringAssignments).toEqual(expectedAssignments);
break;
}
case ValueTestType.JSONType: {
const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
expect(jsonStringAssignments).toEqual(expectedAssignments);
break;
}
}
},
);

function getAssignmentsWithSubjectAttributes(
subjectsWithAttributes: {
subjectKey: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes?: Record<string, any>;
}[],
experiment: string,
valueTestType: ValueTestType = ValueTestType.StringType,
): (EppoValue | null)[] {
return subjectsWithAttributes.map((subject) => {
switch (valueTestType) {
case ValueTestType.BoolType: {
const ba = globalClient.getBoolAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
undefined,
true,
);
if (ba === null) return null;
return EppoValue.Bool(ba);
}
case ValueTestType.NumericType: {
const na = globalClient.getNumericAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
undefined,
true,
);
if (na === null) return null;
return EppoValue.Numeric(na);
}
case ValueTestType.StringType: {
const sa = globalClient.getStringAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
undefined,
true,
);
if (sa === null) return null;
return EppoValue.String(sa);
}
case ValueTestType.JSONType: {
const sa = globalClient.getJSONStringAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
undefined,
true,
);
const oa = globalClient.getParsedJSONAssignment(
subject.subjectKey,
experiment,
subject.subjectAttributes,
undefined,
true,
);
if (oa == null || sa === null) return null;
return EppoValue.JSON(sa, oa);
}
}
});
}
});
34 changes: 30 additions & 4 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { MAX_EVENT_QUEUE_SIZE } from '../constants';
import { IAllocation } from '../dto/allocation-dto';
import { IExperimentConfiguration } from '../dto/experiment-configuration-dto';
import { EppoValue, ValueType } from '../eppo_value';
import { getMD5Hash } from '../obfuscation';
import { findMatchingRule } from '../rule_evaluator';
import { getShard, isShardInRange } from '../shard';
import { validateNotBlank } from '../validation';
Expand Down Expand Up @@ -100,11 +101,17 @@ export default class EppoClient implements IEppoClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes: Record<string, any> = {},
assignmentHooks?: IAssignmentHooks | undefined,
obfuscated = false,
): string | null {
try {
return (
this.getAssignmentVariation(subjectKey, flagKey, subjectAttributes, assignmentHooks)
.stringValue ?? null
this.getAssignmentVariation(
subjectKey,
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
).stringValue ?? null
);
} catch (error) {
return this.rethrowIfNotGraceful(error);
Expand All @@ -117,6 +124,7 @@ export default class EppoClient implements IEppoClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes: Record<string, any> = {},
assignmentHooks?: IAssignmentHooks | undefined,
obfuscated = false,
): string | null {
try {
return (
Expand All @@ -125,6 +133,7 @@ export default class EppoClient implements IEppoClient {
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
ValueType.StringType,
).stringValue ?? null
);
Expand All @@ -139,6 +148,7 @@ export default class EppoClient implements IEppoClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes: Record<string, any> = {},
assignmentHooks?: IAssignmentHooks | undefined,
obfuscated = false,
): boolean | null {
try {
return (
Expand All @@ -147,6 +157,7 @@ export default class EppoClient implements IEppoClient {
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
ValueType.BoolType,
).boolValue ?? null
);
Expand All @@ -160,6 +171,7 @@ export default class EppoClient implements IEppoClient {
flagKey: string,
subjectAttributes?: Record<string, EppoValue>,
assignmentHooks?: IAssignmentHooks | undefined,
obfuscated = false,
): number | null {
try {
return (
Expand All @@ -168,6 +180,7 @@ export default class EppoClient implements IEppoClient {
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
ValueType.NumericType,
).numericValue ?? null
);
Expand All @@ -182,6 +195,7 @@ export default class EppoClient implements IEppoClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes: Record<string, any> = {},
assignmentHooks?: IAssignmentHooks | undefined,
obfuscated = false,
): string | null {
try {
return (
Expand All @@ -190,6 +204,7 @@ export default class EppoClient implements IEppoClient {
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
ValueType.JSONType,
).stringValue ?? null
);
Expand All @@ -204,6 +219,7 @@ export default class EppoClient implements IEppoClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes: Record<string, any> = {},
assignmentHooks?: IAssignmentHooks | undefined,
obfuscated = false,
): object | null {
try {
return (
Expand All @@ -212,6 +228,7 @@ export default class EppoClient implements IEppoClient {
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
ValueType.JSONType,
).objectValue ?? null
);
Expand All @@ -234,13 +251,15 @@ export default class EppoClient implements IEppoClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subjectAttributes: Record<string, any> = {},
assignmentHooks: IAssignmentHooks | undefined,
obfuscated: boolean,
valueType?: ValueType,
): EppoValue {
const { allocationKey, assignment } = this.getAssignmentInternal(
subjectKey,
flagKey,
subjectAttributes,
assignmentHooks,
obfuscated,
valueType,
);
assignmentHooks?.onPostAssignment(flagKey, subjectKey, assignment, allocationKey);
Expand All @@ -256,14 +275,17 @@ export default class EppoClient implements IEppoClient {
flagKey: string,
subjectAttributes = {},
assignmentHooks: IAssignmentHooks | undefined,
obfuscated: boolean,
expectedValueType?: ValueType,
): { allocationKey: string | null; assignment: EppoValue } {
validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');

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

const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(flagKey);
const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(
obfuscated ? getMD5Hash(flagKey) : flagKey,
);
const allowListOverride = this.getSubjectVariationOverride(
subjectKey,
experimentConfig,
Expand All @@ -288,7 +310,11 @@ export default class EppoClient implements IEppoClient {
}

// Attempt to match a rule from the list.
const matchedRule = findMatchingRule(subjectAttributes || {}, experimentConfig.rules);
const matchedRule = findMatchingRule(
subjectAttributes || {},
experimentConfig.rules,
obfuscated,
);
if (!matchedRule) return nullAssignment;

// Check if subject is in allocation sample.
Expand Down
11 changes: 11 additions & 0 deletions src/obfuscation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { decodeBase64, encodeBase64 } from './obfuscation';

describe('obfuscation', () => {
it('encodes strings to base64', () => {
expect(encodeBase64('5.0')).toEqual('NS4w');
});

it('decodes base64 to string', () => {
expect(Number(decodeBase64('NS4w'))).toEqual(5);
});
});
13 changes: 13 additions & 0 deletions src/obfuscation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as md5 from 'md5';

export function getMD5Hash(input: string): string {
return md5(input);
}

export function encodeBase64(input: string): string {
return Buffer.from(input, 'utf8').toString('base64');
}

export function decodeBase64(input: string): string {
return Buffer.from(input, 'base64').toString('utf8');
}
Loading

0 comments on commit b6f2fe6

Please sign in to comment.