|
| 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