Skip to content

Commit ae6bfde

Browse files
authored
fix: preserve allocation evaluation details for ASSIGNMENT_ERROR (#118)
* fix: preserve allocation evaluation details for ASSIGNMENT_ERROR * CR changes * ensure json parsing preserves evaluation details
1 parent f76cae8 commit ae6bfde

File tree

4 files changed

+177
-135
lines changed

4 files changed

+177
-135
lines changed

src/client/eppo-client.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
FlagEvaluationDetailsBuilder,
2525
IFlagEvaluationDetails,
2626
} from '../flag-evaluation-details-builder';
27+
import { FlagEvaluationError } from '../flag-evaluation-error';
2728
import FetchHttpClient from '../http-client';
2829
import {
2930
BanditParameters,
@@ -679,29 +680,50 @@ export default class EppoClient {
679680
subjectAttributes,
680681
expectedVariationType,
681682
);
683+
return this.parseVariationWithDetails(result, defaultValue, expectedVariationType);
684+
} catch (error) {
685+
const eppoValue = this.rethrowIfNotGraceful(error, defaultValue);
686+
if (error instanceof FlagEvaluationError && error.flagEvaluationDetails) {
687+
return {
688+
eppoValue,
689+
flagEvaluationDetails: error.flagEvaluationDetails,
690+
};
691+
} else {
692+
const flagEvaluationDetails = new FlagEvaluationDetailsBuilder(
693+
'',
694+
[],
695+
'',
696+
'',
697+
).buildForNoneResult('ASSIGNMENT_ERROR', `Assignment Error: ${error.message}`);
698+
return {
699+
eppoValue,
700+
flagEvaluationDetails,
701+
};
702+
}
703+
}
704+
}
682705

683-
if (!result.variation) {
706+
private parseVariationWithDetails(
707+
result: FlagEvaluation,
708+
defaultValue: EppoValue,
709+
expectedVariationType: VariationType,
710+
): { eppoValue: EppoValue; flagEvaluationDetails: IFlagEvaluationDetails } {
711+
try {
712+
if (!result.variation || result.flagEvaluationDetails.flagEvaluationCode !== 'MATCH') {
684713
return {
685714
eppoValue: defaultValue,
686715
flagEvaluationDetails: result.flagEvaluationDetails,
687716
};
688717
}
689-
690718
return {
691719
eppoValue: EppoValue.valueOf(result.variation.value, expectedVariationType),
692720
flagEvaluationDetails: result.flagEvaluationDetails,
693721
};
694722
} catch (error) {
695723
const eppoValue = this.rethrowIfNotGraceful(error, defaultValue);
696-
const flagEvaluationDetails = new FlagEvaluationDetailsBuilder(
697-
'',
698-
[],
699-
'',
700-
'',
701-
).buildForNoneResult('ASSIGNMENT_ERROR', `Assignment Error: ${error.message}`);
702724
return {
703725
eppoValue,
704-
flagEvaluationDetails,
726+
flagEvaluationDetails: result.flagEvaluationDetails,
705727
};
706728
}
707729
}
@@ -784,16 +806,6 @@ export default class EppoClient {
784806
result.flagKey = flagKey;
785807
}
786808

787-
if (result?.variation && !checkValueTypeMatch(expectedVariationType, result.variation.value)) {
788-
const { key: vKey, value: vValue } = result.variation;
789-
const reason = `Variation (${vKey}) is configured for type ${expectedVariationType}, but is set to incompatible value (${vValue})`;
790-
const flagEvaluationDetails = flagEvaluationDetailsBuilder.buildForNoneResult(
791-
'ASSIGNMENT_ERROR',
792-
reason,
793-
);
794-
return noneResult(flagKey, subjectKey, subjectAttributes, flagEvaluationDetails);
795-
}
796-
797809
try {
798810
if (result?.doLog) {
799811
this.logAssignment(result);

src/evaluator.ts

Lines changed: 108 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { checkValueTypeMatch } from './client/eppo-client';
12
import {
2-
AllocationEvaluation,
33
AllocationEvaluationCode,
44
IFlagEvaluationDetails,
55
FlagEvaluationDetailsBuilder,
6+
FlagEvaluationCode,
67
} from './flag-evaluation-details-builder';
8+
import { FlagEvaluationError } from './flag-evaluation-error';
79
import {
810
Flag,
911
Shard,
@@ -50,116 +52,141 @@ export class Evaluator {
5052
configDetails.configFetchedAt,
5153
configDetails.configPublishedAt,
5254
);
55+
try {
56+
if (!flag.enabled) {
57+
return noneResult(
58+
flag.key,
59+
subjectKey,
60+
subjectAttributes,
61+
flagEvaluationDetailsBuilder.buildForNoneResult(
62+
'FLAG_UNRECOGNIZED_OR_DISABLED',
63+
`Unrecognized or disabled flag: ${flag.key}`,
64+
),
65+
);
66+
}
67+
68+
const now = new Date();
69+
for (let i = 0; i < flag.allocations.length; i++) {
70+
const allocation = flag.allocations[i];
71+
const addUnmatchedAllocation = (code: AllocationEvaluationCode) => {
72+
flagEvaluationDetailsBuilder.addUnmatchedAllocation({
73+
key: allocation.key,
74+
allocationEvaluationCode: code,
75+
orderPosition: i + 1,
76+
});
77+
};
5378

54-
if (!flag.enabled) {
79+
if (allocation.startAt && now < new Date(allocation.startAt)) {
80+
addUnmatchedAllocation(AllocationEvaluationCode.BEFORE_START_TIME);
81+
continue;
82+
}
83+
if (allocation.endAt && now > new Date(allocation.endAt)) {
84+
addUnmatchedAllocation(AllocationEvaluationCode.AFTER_END_TIME);
85+
continue;
86+
}
87+
const { matched, matchedRule } = matchesRules(
88+
allocation?.rules ?? [],
89+
{ id: subjectKey, ...subjectAttributes },
90+
obfuscated,
91+
);
92+
if (matched) {
93+
for (const split of allocation.splits) {
94+
if (
95+
split.shards.every((shard) => this.matchesShard(shard, subjectKey, flag.totalShards))
96+
) {
97+
const variation = flag.variations[split.variationKey];
98+
const { flagEvaluationCode, flagEvaluationDescription } =
99+
this.getMatchedEvaluationCodeAndDescription(
100+
variation,
101+
allocation,
102+
split,
103+
subjectKey,
104+
expectedVariationType,
105+
);
106+
const flagEvaluationDetails = flagEvaluationDetailsBuilder
107+
.setMatch(i, variation, allocation, matchedRule, expectedVariationType)
108+
.build(flagEvaluationCode, flagEvaluationDescription);
109+
return {
110+
flagKey: flag.key,
111+
subjectKey,
112+
subjectAttributes,
113+
allocationKey: allocation.key,
114+
variation,
115+
extraLogging: split.extraLogging ?? {},
116+
doLog: allocation.doLog,
117+
flagEvaluationDetails,
118+
};
119+
}
120+
}
121+
// matched, but does not fall within split range
122+
addUnmatchedAllocation(AllocationEvaluationCode.TRAFFIC_EXPOSURE_MISS);
123+
} else {
124+
addUnmatchedAllocation(AllocationEvaluationCode.FAILING_RULE);
125+
}
126+
}
55127
return noneResult(
56128
flag.key,
57129
subjectKey,
58130
subjectAttributes,
59131
flagEvaluationDetailsBuilder.buildForNoneResult(
60-
'FLAG_UNRECOGNIZED_OR_DISABLED',
61-
`Unrecognized or disabled flag: ${flag.key}`,
132+
'DEFAULT_ALLOCATION_NULL',
133+
'No allocations matched. Falling back to "Default Allocation", serving NULL',
62134
),
63135
);
64-
}
65-
66-
const now = new Date();
67-
const unmatchedAllocations: Array<AllocationEvaluation> = [];
68-
for (let i = 0; i < flag.allocations.length; i++) {
69-
const allocation = flag.allocations[i];
70-
const addUnmatchedAllocation = (code: AllocationEvaluationCode) => {
71-
unmatchedAllocations.push({
72-
key: allocation.key,
73-
allocationEvaluationCode: code,
74-
orderPosition: i + 1,
75-
});
76-
};
77-
78-
if (allocation.startAt && now < new Date(allocation.startAt)) {
79-
addUnmatchedAllocation(AllocationEvaluationCode.BEFORE_START_TIME);
80-
continue;
81-
}
82-
if (allocation.endAt && now > new Date(allocation.endAt)) {
83-
addUnmatchedAllocation(AllocationEvaluationCode.AFTER_END_TIME);
84-
continue;
85-
}
86-
const { matched, matchedRule } = matchesRules(
87-
allocation?.rules ?? [],
88-
{ id: subjectKey, ...subjectAttributes },
89-
obfuscated,
136+
} catch (err) {
137+
const flagEvaluationDetails = flagEvaluationDetailsBuilder.gracefulBuild(
138+
'ASSIGNMENT_ERROR',
139+
`Assignment Error: ${err.message}`,
90140
);
91-
if (matched) {
92-
for (const split of allocation.splits) {
93-
if (
94-
split.shards.every((shard) => this.matchesShard(shard, subjectKey, flag.totalShards))
95-
) {
96-
const variation = flag.variations[split.variationKey];
97-
const flagEvaluationDetails = flagEvaluationDetailsBuilder
98-
.setMatch(
99-
i,
100-
variation,
101-
allocation,
102-
matchedRule,
103-
unmatchedAllocations,
104-
expectedVariationType,
105-
)
106-
.build(
107-
'MATCH',
108-
this.getMatchedEvaluationDetailsMessage(allocation, split, subjectKey),
109-
);
110-
return {
111-
flagKey: flag.key,
112-
subjectKey,
113-
subjectAttributes,
114-
allocationKey: allocation.key,
115-
variation,
116-
extraLogging: split.extraLogging ?? {},
117-
doLog: allocation.doLog,
118-
flagEvaluationDetails,
119-
};
120-
}
121-
}
122-
// matched, but does not fall within split range
123-
addUnmatchedAllocation(AllocationEvaluationCode.TRAFFIC_EXPOSURE_MISS);
124-
} else {
125-
addUnmatchedAllocation(AllocationEvaluationCode.FAILING_RULE);
141+
if (flagEvaluationDetails) {
142+
const flagEvaluationError = new FlagEvaluationError(err.message);
143+
flagEvaluationError.flagEvaluationDetails = flagEvaluationDetails;
144+
throw flagEvaluationError;
126145
}
146+
throw err;
127147
}
128-
return noneResult(
129-
flag.key,
130-
subjectKey,
131-
subjectAttributes,
132-
flagEvaluationDetailsBuilder
133-
.setNoMatchFound(unmatchedAllocations)
134-
.build(
135-
'DEFAULT_ALLOCATION_NULL',
136-
'No allocations matched. Falling back to "Default Allocation", serving NULL',
137-
),
138-
);
139148
}
140149

141150
matchesShard(shard: Shard, subjectKey: string, totalShards: number): boolean {
142151
const assignedShard = this.sharder.getShard(hashKey(shard.salt, subjectKey), totalShards);
143152
return shard.ranges.some((range) => isInShardRange(assignedShard, range));
144153
}
145154

146-
private getMatchedEvaluationDetailsMessage = (
155+
private getMatchedEvaluationCodeAndDescription = (
156+
variation: Variation,
147157
allocation: Allocation,
148158
split: Split,
149159
subjectKey: string,
150-
): string => {
160+
expectedVariationType: VariationType | undefined,
161+
): { flagEvaluationCode: FlagEvaluationCode; flagEvaluationDescription: string } => {
162+
if (!checkValueTypeMatch(expectedVariationType, variation.value)) {
163+
const { key: vKey, value: vValue } = variation;
164+
return {
165+
flagEvaluationCode: 'ASSIGNMENT_ERROR',
166+
flagEvaluationDescription: `Variation (${vKey}) is configured for type ${expectedVariationType}, but is set to incompatible value (${vValue})`,
167+
};
168+
}
151169
const hasDefinedRules = !!allocation.rules?.length;
152170
const isExperiment = allocation.splits.length > 1;
153171
const isPartialRollout = split.shards.length > 1;
154172
const isExperimentOrPartialRollout = isExperiment || isPartialRollout;
155173

156174
if (hasDefinedRules && isExperimentOrPartialRollout) {
157-
return `Supplied attributes match rules defined in allocation "${allocation.key}" and ${subjectKey} belongs to the range of traffic assigned to "${split.variationKey}".`;
175+
return {
176+
flagEvaluationCode: 'MATCH',
177+
flagEvaluationDescription: `Supplied attributes match rules defined in allocation "${allocation.key}" and ${subjectKey} belongs to the range of traffic assigned to "${split.variationKey}".`,
178+
};
158179
}
159180
if (hasDefinedRules && !isExperimentOrPartialRollout) {
160-
return `Supplied attributes match rules defined in allocation "${allocation.key}".`;
181+
return {
182+
flagEvaluationCode: 'MATCH',
183+
flagEvaluationDescription: `Supplied attributes match rules defined in allocation "${allocation.key}".`,
184+
};
161185
}
162-
return `${subjectKey} belongs to the range of traffic assigned to "${split.variationKey}" defined in allocation "${allocation.key}".`;
186+
return {
187+
flagEvaluationCode: 'MATCH',
188+
flagEvaluationDescription: `${subjectKey} belongs to the range of traffic assigned to "${split.variationKey}" defined in allocation "${allocation.key}".`,
189+
};
163190
};
164191
}
165192

0 commit comments

Comments
 (0)