Skip to content

Commit b178a6c

Browse files
committed
handoff and address comments
1 parent dd6b1e9 commit b178a6c

14 files changed

+265
-248
lines changed

src/attributes.ts

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

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)