Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: obfuscated precomputed assignments #164

Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a1cf3bf
make getMD5HashWithSalt
sameerank Dec 9, 2024
22538e1
add tests
sameerank Dec 9, 2024
7cd544c
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Dec 9, 2024
1ff6d8b
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Dec 12, 2024
b8a2d9d
Export saltedHasher
sameerank Dec 12, 2024
567e66d
Fix tests
sameerank Dec 12, 2024
c8f5171
Alter test to use obfuscated file
sameerank Dec 12, 2024
c0288fe
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Dec 14, 2024
ed3f0b3
Change branch name for test data
sameerank Dec 14, 2024
47333ec
Get all the tests to pass
sameerank Dec 15, 2024
a3ed52a
Make more obvious that the salt was decoded
sameerank Dec 15, 2024
25c6f8c
Switch to using appendBinary for the salt
sameerank Dec 16, 2024
a169e29
Clean up
sameerank Dec 16, 2024
d366925
Include salt in convenience method for setting precomputed flag store
sameerank Dec 16, 2024
8fe80dc
Add a helper to convert context attributes to subject attributes
sameerank Dec 16, 2024
b1048a3
Change default to isObfuscated since we expect the precomputed api to…
sameerank Dec 16, 2024
b81175f
v4.7.1-alpha.0
sameerank Dec 16, 2024
9f36640
Revert "v4.7.1-alpha.0"
sameerank Dec 16, 2024
aee17ce
v4.7.0-alpha.0
sameerank Dec 16, 2024
67e9446
Switch to initializing the client with an options object
sameerank Dec 16, 2024
ed8027d
Make response data not optional
sameerank Dec 16, 2024
4e8aa63
precomputedFlag variable casing
sameerank Dec 16, 2024
6040e96
update hashing
typotter Dec 17, 2024
dd6b1e9
fix lint
leoromanovsky Dec 17, 2024
b178a6c
handoff and address comments
typotter Dec 18, 2024
749e8c1
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
typotter Dec 18, 2024
e99306d
bump version
typotter Dec 18, 2024
f036edc
Merge branch 'main' into sameeran/ff-3682-use-salt-in-flag-key-hash-t…
sameerank Jan 6, 2025
173b3a3
Inf is a numeric attribute too
sameerank Jan 7, 2025
d43e9be
Remove unnecessary public methods
sameerank Jan 8, 2025
bba9863
Remove more unnecessary functions
sameerank Jan 8, 2025
0d1f341
Add to exported interfaces
sameerank Jan 8, 2025
bb03341
Update src/interfaces.ts
sameerank Jan 8, 2025
ad86727
Update src/attributes.ts attributes is ContextAttributes
sameerank Jan 8, 2025
b15f488
Remove redundant 'subjectAttributes as ContextAttributes'
sameerank Jan 8, 2025
3a322a7
Also print error if store is missing salt
sameerank Jan 8, 2025
928f58d
Remove buildContextAttributes
sameerank Jan 8, 2025
5241bf7
v4.8.0-alpha.0
sameerank Jan 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 -r ${gitDataDir}ufc ${testDataDir}
cp -r ${gitDataDir}configuration-wire ${testDataDir}
sameerank marked this conversation as resolved.
Show resolved Hide resolved
rm -rf ${tempDir}

## prepare
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eppo/js-client-sdk-common",
"version": "4.7.0",
"version": "4.7.0-alpha.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"version": "4.7.0-alpha.1",
"version": "5.0.0-alpha.1",

removing some public methods that are used by customers (see below)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my other comment — we are not removing used methods. All changes are for the new precomputed client, not the regular one

"description": "Common library for Eppo JavaScript SDKs (web, react native, and node)",
"main": "dist/index.js",
"files": [
Expand Down
80 changes: 80 additions & 0 deletions src/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
Attributes,
AttributeType,
BanditActions,
BanditSubjectAttributes,
ContextAttributes,
} from './types';

export function isInstanceOfContextualAttributes(attributes: unknown): boolean {
sameerank marked this conversation as resolved.
Show resolved Hide resolved
return Boolean(
typeof attributes === 'object' &&
attributes && // exclude null
'numericAttributes' in attributes &&
'categoricalAttributes' in attributes,
);
}

export function ensureNonContextualSubjectAttributes(
subjectAttributes: BanditSubjectAttributes,
): Attributes {
let result: Attributes;
if (isInstanceOfContextualAttributes(subjectAttributes)) {
const contextualSubjectAttributes = subjectAttributes as ContextAttributes;
result = {
...contextualSubjectAttributes.numericAttributes,
...contextualSubjectAttributes.categoricalAttributes,
};
} else {
// Attributes are non-contextual
result = subjectAttributes as Attributes;
}
return result;
}

export function ensureContextualSubjectAttributes(
subjectAttributes: BanditSubjectAttributes,
): ContextAttributes {
if (isInstanceOfContextualAttributes(subjectAttributes)) {
return subjectAttributes as ContextAttributes;
} else {
return deduceAttributeContext(subjectAttributes as Attributes);
}
}

export function deduceAttributeContext(attributes: Attributes): ContextAttributes {
const contextualAttributes: ContextAttributes = {
numericAttributes: {},
categoricalAttributes: {},
};
Object.entries(attributes).forEach(([attribute, value]) => {
const isNumeric = typeof value === 'number' && isFinite(value);
if (isNumeric) {
contextualAttributes.numericAttributes[attribute] = value;
} else {
contextualAttributes.categoricalAttributes[attribute] = value as AttributeType;
sameerank marked this conversation as resolved.
Show resolved Hide resolved
}
sameerank marked this conversation as resolved.
Show resolved Hide resolved
});
return contextualAttributes;
}

export function ensureActionsWithContextualAttributes(
actions: BanditActions,
): Record<string, ContextAttributes> {
let result: Record<string, ContextAttributes> = {};
if (Array.isArray(actions)) {
// no context
actions.forEach((action) => {
result[action] = { numericAttributes: {}, categoricalAttributes: {} };
});
} else if (!Object.values(actions).every(isInstanceOfContextualAttributes)) {
// Actions have non-contextual attributes; bucket based on number or not
Object.entries(actions).forEach(([action, attributes]) => {
result[action] = deduceAttributeContext(attributes);
});
} else {
// Actions already have contextual attributes
result = actions as Record<string, ContextAttributes>;
}
return result;
}
94 changes: 13 additions & 81 deletions src/client/eppo-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { v4 as randomUUID } from 'uuid';
import ApiEndpoints from '../api-endpoints';
import { logger } from '../application-logger';
import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
import {
ensureActionsWithContextualAttributes,
ensureContextualSubjectAttributes,
ensureNonContextualSubjectAttributes,
} from '../attributes';
import { BanditEvaluator } from '../bandit-evaluator';
import { IBanditEvent, IBanditLogger } from '../bandit-logger';
import { AssignmentCache } from '../cache/abstract-assignment-cache';
Expand Down Expand Up @@ -535,9 +540,8 @@ export default class EppoClient {
if (banditKey) {
const banditParameters = this.banditModelConfigurationStore?.get(banditKey);
if (banditParameters) {
const contextualSubjectAttributes =
this.ensureContextualSubjectAttributes(subjectAttributes);
const actionsWithContextualAttributes = this.ensureActionsWithContextualAttributes(actions);
const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes);
const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions);

result = this.banditEvaluator.evaluateBestBanditAction(
contextualSubjectAttributes,
Expand Down Expand Up @@ -571,7 +575,7 @@ export default class EppoClient {
// Get the assigned variation for the flag with a possible bandit
// Note for getting assignments, we don't care about context
const nonContextualSubjectAttributes =
this.ensureNonContextualSubjectAttributes(subjectAttributes);
ensureNonContextualSubjectAttributes(subjectAttributes);
const { variation: assignedVariation, evaluationDetails: assignmentEvaluationDetails } =
this.getStringAssignmentDetails(
flagKey,
Expand Down Expand Up @@ -683,8 +687,8 @@ export default class EppoClient {
}

const banditModelData = banditParameters.modelData;
const contextualSubjectAttributes = this.ensureContextualSubjectAttributes(subjectAttributes);
const actionsWithContextualAttributes = this.ensureActionsWithContextualAttributes(actions);
const contextualSubjectAttributes = ensureContextualSubjectAttributes(subjectAttributes);
const actionsWithContextualAttributes = ensureActionsWithContextualAttributes(actions);
const banditEvaluation = this.banditEvaluator.evaluateBandit(
flagKey,
subjectKey,
Expand Down Expand Up @@ -715,79 +719,6 @@ export default class EppoClient {
return action;
}

private ensureNonContextualSubjectAttributes(
subjectAttributes: BanditSubjectAttributes,
): Attributes {
let result: Attributes;
if (this.isInstanceOfContextualAttributes(subjectAttributes)) {
const contextualSubjectAttributes = subjectAttributes as ContextAttributes;
result = {
...contextualSubjectAttributes.numericAttributes,
...contextualSubjectAttributes.categoricalAttributes,
};
} else {
// Attributes are non-contextual
result = subjectAttributes as Attributes;
}
return result;
}

private ensureContextualSubjectAttributes(
subjectAttributes: BanditSubjectAttributes,
): ContextAttributes {
if (this.isInstanceOfContextualAttributes(subjectAttributes)) {
return subjectAttributes as ContextAttributes;
} else {
return this.deduceAttributeContext(subjectAttributes as Attributes);
}
}

private ensureActionsWithContextualAttributes(
actions: BanditActions,
): Record<string, ContextAttributes> {
let result: Record<string, ContextAttributes> = {};
if (Array.isArray(actions)) {
// no context
actions.forEach((action) => {
result[action] = { numericAttributes: {}, categoricalAttributes: {} };
});
} else if (!Object.values(actions).every(this.isInstanceOfContextualAttributes)) {
// Actions have non-contextual attributes; bucket based on number or not
Object.entries(actions).forEach(([action, attributes]) => {
result[action] = this.deduceAttributeContext(attributes);
});
} else {
// Actions already have contextual attributes
result = actions as Record<string, ContextAttributes>;
}
return result;
}

private isInstanceOfContextualAttributes(attributes: unknown): boolean {
return Boolean(
typeof attributes === 'object' &&
attributes && // exclude null
'numericAttributes' in attributes &&
'categoricalAttributes' in attributes,
);
}

private deduceAttributeContext(attributes: Attributes): ContextAttributes {
const contextualAttributes: ContextAttributes = {
numericAttributes: {},
categoricalAttributes: {},
};
Object.entries(attributes).forEach(([attribute, value]) => {
const isNumeric = typeof value === 'number' && isFinite(value);
if (isNumeric) {
contextualAttributes.numericAttributes[attribute] = value;
} else {
contextualAttributes.categoricalAttributes[attribute] = value as AttributeType;
}
});
return contextualAttributes;
}

private logBanditAction(banditEvent: IBanditEvent): void {
// First we check if this bandit action has been logged before
const subjectKey = banditEvent.subject;
Expand Down Expand Up @@ -944,8 +875,8 @@ export default class EppoClient {
): string {
const configDetails = this.getConfigDetails();

const subjectContextualAttributes = this.ensureContextualSubjectAttributes(subjectAttributes);
const subjectFlatAttributes = this.ensureNonContextualSubjectAttributes(subjectAttributes);
const subjectContextualAttributes = ensureContextualSubjectAttributes(subjectAttributes);
const subjectFlatAttributes = ensureNonContextualSubjectAttributes(subjectAttributes);
const flags = this.getAllAssignments(subjectKey, subjectFlatAttributes);

const precomputedConfig: IPrecomputedConfiguration = obfuscated
Expand Down Expand Up @@ -1095,6 +1026,7 @@ export default class EppoClient {
configPublishedAt: this.flagConfigurationStore.getConfigPublishedAt() ?? '',
configEnvironment: this.flagConfigurationStore.getEnvironment() ?? { name: '' },
configFormat: this.flagConfigurationStore.getFormat() ?? '',
salt: this.flagConfigurationStore.salt,
};
}

Expand Down
Loading
Loading