|
| 1 | +import { checkValueTypeMatch } from './client/eppo-client'; |
1 | 2 | import {
|
2 |
| - AllocationEvaluation, |
3 | 3 | AllocationEvaluationCode,
|
4 | 4 | IFlagEvaluationDetails,
|
5 | 5 | FlagEvaluationDetailsBuilder,
|
| 6 | + FlagEvaluationCode, |
6 | 7 | } from './flag-evaluation-details-builder';
|
| 8 | +import { FlagEvaluationError } from './flag-evaluation-error'; |
7 | 9 | import {
|
8 | 10 | Flag,
|
9 | 11 | Shard,
|
@@ -50,116 +52,141 @@ export class Evaluator {
|
50 | 52 | configDetails.configFetchedAt,
|
51 | 53 | configDetails.configPublishedAt,
|
52 | 54 | );
|
| 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 | + }; |
53 | 78 |
|
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 | + } |
55 | 127 | return noneResult(
|
56 | 128 | flag.key,
|
57 | 129 | subjectKey,
|
58 | 130 | subjectAttributes,
|
59 | 131 | 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', |
62 | 134 | ),
|
63 | 135 | );
|
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}`, |
90 | 140 | );
|
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; |
126 | 145 | }
|
| 146 | + throw err; |
127 | 147 | }
|
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 |
| - ); |
139 | 148 | }
|
140 | 149 |
|
141 | 150 | matchesShard(shard: Shard, subjectKey: string, totalShards: number): boolean {
|
142 | 151 | const assignedShard = this.sharder.getShard(hashKey(shard.salt, subjectKey), totalShards);
|
143 | 152 | return shard.ranges.some((range) => isInShardRange(assignedShard, range));
|
144 | 153 | }
|
145 | 154 |
|
146 |
| - private getMatchedEvaluationDetailsMessage = ( |
| 155 | + private getMatchedEvaluationCodeAndDescription = ( |
| 156 | + variation: Variation, |
147 | 157 | allocation: Allocation,
|
148 | 158 | split: Split,
|
149 | 159 | 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 | + } |
151 | 169 | const hasDefinedRules = !!allocation.rules?.length;
|
152 | 170 | const isExperiment = allocation.splits.length > 1;
|
153 | 171 | const isPartialRollout = split.shards.length > 1;
|
154 | 172 | const isExperimentOrPartialRollout = isExperiment || isPartialRollout;
|
155 | 173 |
|
156 | 174 | 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 | + }; |
158 | 179 | }
|
159 | 180 | 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 | + }; |
161 | 185 | }
|
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 | + }; |
163 | 190 | };
|
164 | 191 | }
|
165 | 192 |
|
|
0 commit comments