Skip to content

Commit 65986fe

Browse files
sameeranktypotterleoromanovskyrasendubi
authored
feat: obfuscated precomputed assignments (#164)
* make getMD5HashWithSalt * add tests * Export saltedHasher * Fix tests * Alter test to use obfuscated file * Change branch name for test data * Get all the tests to pass * Make more obvious that the salt was decoded * Switch to using appendBinary for the salt * Clean up * Include salt in convenience method for setting precomputed flag store * Add a helper to convert context attributes to subject attributes * Change default to isObfuscated since we expect the precomputed api to mainly be used by clients * v4.7.1-alpha.0 * Revert "v4.7.1-alpha.0" This reverts commit b81175f. * v4.7.0-alpha.0 * Switch to initializing the client with an options object * Make response data not optional * precomputedFlag variable casing * update hashing * fix lint * handoff and address comments * bump version * Inf is a numeric attribute too * Remove unnecessary public methods * Remove more unnecessary functions * Add to exported interfaces * Update src/interfaces.ts Co-authored-by: Oleksii Shmalko <[email protected]> * Update src/attributes.ts attributes is ContextAttributes Co-authored-by: Oleksii Shmalko <[email protected]> * Remove redundant 'subjectAttributes as ContextAttributes' * Also print error if store is missing salt * Remove buildContextAttributes * v4.8.0-alpha.0 --------- Co-authored-by: Ty Potter <[email protected]> Co-authored-by: Leo Romanovsky <[email protected]> Co-authored-by: Oleksii Shmalko <[email protected]>
1 parent 4e979df commit 65986fe

16 files changed

+472
-334
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 -r ${gitDataDir}ufc ${testDataDir}
39+
cp -r ${gitDataDir}configuration-wire ${testDataDir}
3940
rm -rf ${tempDir}
4041

4142
## prepare

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "4.7.1",
3+
"version": "4.8.0-alpha.0",
44
"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
55
"main": "dist/index.js",
66
"files": [

src/attributes.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Attributes, BanditActions, BanditSubjectAttributes, ContextAttributes } from './types';
2+
3+
export function isInstanceOfContextualAttributes(
4+
attributes: unknown,
5+
): attributes is ContextAttributes {
6+
return Boolean(
7+
typeof attributes === 'object' &&
8+
attributes && // exclude null
9+
'numericAttributes' in attributes &&
10+
'categoricalAttributes' in attributes,
11+
);
12+
}
13+
14+
export function ensureNonContextualSubjectAttributes(
15+
subjectAttributes: BanditSubjectAttributes,
16+
): Attributes {
17+
let result: Attributes;
18+
if (isInstanceOfContextualAttributes(subjectAttributes)) {
19+
const contextualSubjectAttributes = subjectAttributes;
20+
result = {
21+
...contextualSubjectAttributes.numericAttributes,
22+
...contextualSubjectAttributes.categoricalAttributes,
23+
};
24+
} else {
25+
// Attributes are non-contextual
26+
result = subjectAttributes as Attributes;
27+
}
28+
return result;
29+
}
30+
31+
export function ensureContextualSubjectAttributes(
32+
subjectAttributes: BanditSubjectAttributes,
33+
): ContextAttributes {
34+
if (isInstanceOfContextualAttributes(subjectAttributes)) {
35+
return subjectAttributes;
36+
} else {
37+
return deduceAttributeContext(subjectAttributes as Attributes);
38+
}
39+
}
40+
41+
export function deduceAttributeContext(attributes: Attributes): ContextAttributes {
42+
const contextualAttributes: ContextAttributes = {
43+
numericAttributes: {},
44+
categoricalAttributes: {},
45+
};
46+
Object.entries(attributes).forEach(([attribute, value]) => {
47+
const isNumeric = typeof value === 'number';
48+
if (isNumeric) {
49+
contextualAttributes.numericAttributes[attribute] = value;
50+
} else {
51+
contextualAttributes.categoricalAttributes[attribute] = value;
52+
}
53+
});
54+
return contextualAttributes;
55+
}
56+
57+
export function ensureActionsWithContextualAttributes(
58+
actions: BanditActions,
59+
): Record<string, ContextAttributes> {
60+
let result: Record<string, ContextAttributes> = {};
61+
if (Array.isArray(actions)) {
62+
// no context
63+
actions.forEach((action) => {
64+
result[action] = { numericAttributes: {}, categoricalAttributes: {} };
65+
});
66+
} else if (!Object.values(actions).every(isInstanceOfContextualAttributes)) {
67+
// Actions have non-contextual attributes; bucket based on number or not
68+
Object.entries(actions).forEach(([action, attributes]) => {
69+
result[action] = deduceAttributeContext(attributes);
70+
});
71+
} else {
72+
// Actions already have contextual attributes
73+
result = actions as Record<string, ContextAttributes>;
74+
}
75+
return result;
76+
}

src/client/eppo-client.ts

Lines changed: 13 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { v4 as randomUUID } from 'uuid';
33
import ApiEndpoints from '../api-endpoints';
44
import { logger } from '../application-logger';
55
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
6+
import {
7+
ensureActionsWithContextualAttributes,
8+
ensureContextualSubjectAttributes,
9+
ensureNonContextualSubjectAttributes,
10+
} from '../attributes';
611
import { BanditEvaluator } from '../bandit-evaluator';
712
import { IBanditEvent, IBanditLogger } from '../bandit-logger';
813
import { AssignmentCache } from '../cache/abstract-assignment-cache';
@@ -535,9 +540,8 @@ export default class EppoClient {
535540
if (banditKey) {
536541
const banditParameters = this.banditModelConfigurationStore?.get(banditKey);
537542
if (banditParameters) {
538-
const contextualSubjectAttributes =
539-
this.ensureContextualSubjectAttributes(subjectAttributes);
540-
const actionsWithContextualAttributes = this.ensureActionsWithContextualAttributes(actions);
543+
const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes);
544+
const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions);
541545

542546
result = this.banditEvaluator.evaluateBestBanditAction(
543547
contextualSubjectAttributes,
@@ -571,7 +575,7 @@ export default class EppoClient {
571575
// Get the assigned variation for the flag with a possible bandit
572576
// Note for getting assignments, we don't care about context
573577
const nonContextualSubjectAttributes =
574-
this.ensureNonContextualSubjectAttributes(subjectAttributes);
578+
ensureNonContextualSubjectAttributes(subjectAttributes);
575579
const { variation: assignedVariation, evaluationDetails: assignmentEvaluationDetails } =
576580
this.getStringAssignmentDetails(
577581
flagKey,
@@ -683,8 +687,8 @@ export default class EppoClient {
683687
}
684688

685689
const banditModelData = banditParameters.modelData;
686-
const contextualSubjectAttributes = this.ensureContextualSubjectAttributes(subjectAttributes);
687-
const actionsWithContextualAttributes = this.ensureActionsWithContextualAttributes(actions);
690+
const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes);
691+
const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions);
688692
const banditEvaluation = this.banditEvaluator.evaluateBandit(
689693
flagKey,
690694
subjectKey,
@@ -715,79 +719,6 @@ export default class EppoClient {
715719
return action;
716720
}
717721

718-
private ensureNonContextualSubjectAttributes(
719-
subjectAttributes: BanditSubjectAttributes,
720-
): Attributes {
721-
let result: Attributes;
722-
if (this.isInstanceOfContextualAttributes(subjectAttributes)) {
723-
const contextualSubjectAttributes = subjectAttributes as ContextAttributes;
724-
result = {
725-
...contextualSubjectAttributes.numericAttributes,
726-
...contextualSubjectAttributes.categoricalAttributes,
727-
};
728-
} else {
729-
// Attributes are non-contextual
730-
result = subjectAttributes as Attributes;
731-
}
732-
return result;
733-
}
734-
735-
private ensureContextualSubjectAttributes(
736-
subjectAttributes: BanditSubjectAttributes,
737-
): ContextAttributes {
738-
if (this.isInstanceOfContextualAttributes(subjectAttributes)) {
739-
return subjectAttributes as ContextAttributes;
740-
} else {
741-
return this.deduceAttributeContext(subjectAttributes as Attributes);
742-
}
743-
}
744-
745-
private ensureActionsWithContextualAttributes(
746-
actions: BanditActions,
747-
): Record<string, ContextAttributes> {
748-
let result: Record<string, ContextAttributes> = {};
749-
if (Array.isArray(actions)) {
750-
// no context
751-
actions.forEach((action) => {
752-
result[action] = { numericAttributes: {}, categoricalAttributes: {} };
753-
});
754-
} else if (!Object.values(actions).every(this.isInstanceOfContextualAttributes)) {
755-
// Actions have non-contextual attributes; bucket based on number or not
756-
Object.entries(actions).forEach(([action, attributes]) => {
757-
result[action] = this.deduceAttributeContext(attributes);
758-
});
759-
} else {
760-
// Actions already have contextual attributes
761-
result = actions as Record<string, ContextAttributes>;
762-
}
763-
return result;
764-
}
765-
766-
private isInstanceOfContextualAttributes(attributes: unknown): boolean {
767-
return Boolean(
768-
typeof attributes === 'object' &&
769-
attributes && // exclude null
770-
'numericAttributes' in attributes &&
771-
'categoricalAttributes' in attributes,
772-
);
773-
}
774-
775-
private deduceAttributeContext(attributes: Attributes): ContextAttributes {
776-
const contextualAttributes: ContextAttributes = {
777-
numericAttributes: {},
778-
categoricalAttributes: {},
779-
};
780-
Object.entries(attributes).forEach(([attribute, value]) => {
781-
const isNumeric = typeof value === 'number' && isFinite(value);
782-
if (isNumeric) {
783-
contextualAttributes.numericAttributes[attribute] = value;
784-
} else {
785-
contextualAttributes.categoricalAttributes[attribute] = value as AttributeType;
786-
}
787-
});
788-
return contextualAttributes;
789-
}
790-
791722
private logBanditAction(banditEvent: IBanditEvent): void {
792723
// First we check if this bandit action has been logged before
793724
const subjectKey = banditEvent.subject;
@@ -944,8 +875,8 @@ export default class EppoClient {
944875
): string {
945876
const configDetails = this.getConfigDetails();
946877

947-
const subjectContextualAttributes = this.ensureContextualSubjectAttributes(subjectAttributes);
948-
const subjectFlatAttributes = this.ensureNonContextualSubjectAttributes(subjectAttributes);
878+
const subjectContextualAttributes = ensureContextualSubjectAttributes(subjectAttributes);
879+
const subjectFlatAttributes = ensureNonContextualSubjectAttributes(subjectAttributes);
949880
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes);
950881

951882
const precomputedConfig: IPrecomputedConfiguration = obfuscated
@@ -1095,6 +1026,7 @@ export default class EppoClient {
10951026
configPublishedAt: this.flagConfigurationStore.getConfigPublishedAt() ?? '',
10961027
configEnvironment: this.flagConfigurationStore.getEnvironment() ?? { name: '' },
10971028
configFormat: this.flagConfigurationStore.getFormat() ?? '',
1029+
salt: this.flagConfigurationStore.salt,
10981030
};
10991031
}
11001032

0 commit comments

Comments
 (0)