diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts index 5d331e5..8a56e79 100644 --- a/src/client/eppo-client.ts +++ b/src/client/eppo-client.ts @@ -24,6 +24,7 @@ import { FlagEvaluationDetailsBuilder, IFlagEvaluationDetails, } from '../flag-evaluation-details-builder'; +import { FlagEvaluationError } from '../flag-evaluation-error'; import FetchHttpClient from '../http-client'; import { BanditParameters, @@ -693,16 +694,23 @@ export default class EppoClient { }; } catch (error) { const eppoValue = this.rethrowIfNotGraceful(error, defaultValue); - const flagEvaluationDetails = new FlagEvaluationDetailsBuilder( - '', - [], - '', - '', - ).buildForNoneResult('ASSIGNMENT_ERROR', `Assignment Error: ${error.message}`); - return { - eppoValue, - flagEvaluationDetails, - }; + if (error instanceof FlagEvaluationError && error.flagEvaluationDetails) { + return { + eppoValue, + flagEvaluationDetails: error.flagEvaluationDetails, + }; + } else { + const flagEvaluationDetails = new FlagEvaluationDetailsBuilder( + '', + [], + '', + '', + ).buildForNoneResult('ASSIGNMENT_ERROR', `Assignment Error: ${error.message}`); + return { + eppoValue, + flagEvaluationDetails, + }; + } } } diff --git a/src/evaluator.ts b/src/evaluator.ts index 6c8d9ea..d0845b2 100644 --- a/src/evaluator.ts +++ b/src/evaluator.ts @@ -1,11 +1,11 @@ import { checkValueTypeMatch } from './client/eppo-client'; import { - AllocationEvaluation, AllocationEvaluationCode, IFlagEvaluationDetails, FlagEvaluationDetailsBuilder, FlagEvaluationCode, } from './flag-evaluation-details-builder'; +import { FlagEvaluationError } from './flag-evaluation-error'; import { Flag, Shard, @@ -52,97 +52,99 @@ export class Evaluator { configDetails.configFetchedAt, configDetails.configPublishedAt, ); + try { + if (!flag.enabled) { + return noneResult( + flag.key, + subjectKey, + subjectAttributes, + flagEvaluationDetailsBuilder.buildForNoneResult( + 'FLAG_UNRECOGNIZED_OR_DISABLED', + `Unrecognized or disabled flag: ${flag.key}`, + ), + ); + } + + const now = new Date(); + for (let i = 0; i < flag.allocations.length; i++) { + const allocation = flag.allocations[i]; + const addUnmatchedAllocation = (code: AllocationEvaluationCode) => { + flagEvaluationDetailsBuilder.addUnmatchedAllocation({ + key: allocation.key, + allocationEvaluationCode: code, + orderPosition: i + 1, + }); + }; - if (!flag.enabled) { + if (allocation.startAt && now < new Date(allocation.startAt)) { + addUnmatchedAllocation(AllocationEvaluationCode.BEFORE_START_TIME); + continue; + } + if (allocation.endAt && now > new Date(allocation.endAt)) { + addUnmatchedAllocation(AllocationEvaluationCode.AFTER_END_TIME); + continue; + } + const { matched, matchedRule } = matchesRules( + allocation?.rules ?? [], + { id: subjectKey, ...subjectAttributes }, + obfuscated, + ); + if (matched) { + for (const split of allocation.splits) { + if ( + split.shards.every((shard) => this.matchesShard(shard, subjectKey, flag.totalShards)) + ) { + const variation = flag.variations[split.variationKey]; + const { flagEvaluationCode, flagEvaluationDescription } = + this.getMatchedEvaluationCodeAndDescription( + variation, + allocation, + split, + subjectKey, + expectedVariationType, + ); + const flagEvaluationDetails = flagEvaluationDetailsBuilder + .setMatch(i, variation, allocation, matchedRule, expectedVariationType) + .build(flagEvaluationCode, flagEvaluationDescription); + return { + flagKey: flag.key, + subjectKey, + subjectAttributes, + allocationKey: allocation.key, + variation, + extraLogging: split.extraLogging ?? {}, + doLog: allocation.doLog, + flagEvaluationDetails, + }; + } + } + // matched, but does not fall within split range + addUnmatchedAllocation(AllocationEvaluationCode.TRAFFIC_EXPOSURE_MISS); + } else { + addUnmatchedAllocation(AllocationEvaluationCode.FAILING_RULE); + } + } return noneResult( flag.key, subjectKey, subjectAttributes, flagEvaluationDetailsBuilder.buildForNoneResult( - 'FLAG_UNRECOGNIZED_OR_DISABLED', - `Unrecognized or disabled flag: ${flag.key}`, + 'DEFAULT_ALLOCATION_NULL', + 'No allocations matched. Falling back to "Default Allocation", serving NULL', ), ); - } - - const now = new Date(); - const unmatchedAllocations: Array = []; - for (let i = 0; i < flag.allocations.length; i++) { - const allocation = flag.allocations[i]; - const addUnmatchedAllocation = (code: AllocationEvaluationCode) => { - unmatchedAllocations.push({ - key: allocation.key, - allocationEvaluationCode: code, - orderPosition: i + 1, - }); - }; - - if (allocation.startAt && now < new Date(allocation.startAt)) { - addUnmatchedAllocation(AllocationEvaluationCode.BEFORE_START_TIME); - continue; - } - if (allocation.endAt && now > new Date(allocation.endAt)) { - addUnmatchedAllocation(AllocationEvaluationCode.AFTER_END_TIME); - continue; - } - const { matched, matchedRule } = matchesRules( - allocation?.rules ?? [], - { id: subjectKey, ...subjectAttributes }, - obfuscated, + } catch (err) { + const flagEvaluationDetails = flagEvaluationDetailsBuilder.gracefulBuild( + 'ASSIGNMENT_ERROR', + `Assignment Error: ${err.message}`, ); - if (matched) { - for (const split of allocation.splits) { - if ( - split.shards.every((shard) => this.matchesShard(shard, subjectKey, flag.totalShards)) - ) { - const variation = flag.variations[split.variationKey]; - const { flagEvaluationCode, flagEvaluationDescription } = - this.getMatchedEvaluationCodeAndDescription( - variation, - allocation, - split, - subjectKey, - expectedVariationType, - ); - const flagEvaluationDetails = flagEvaluationDetailsBuilder - .setMatch( - i, - variation, - allocation, - matchedRule, - unmatchedAllocations, - expectedVariationType, - ) - .build(flagEvaluationCode, flagEvaluationDescription); - return { - flagKey: flag.key, - subjectKey, - subjectAttributes, - allocationKey: allocation.key, - variation, - extraLogging: split.extraLogging ?? {}, - doLog: allocation.doLog, - flagEvaluationDetails, - }; - } - } - // matched, but does not fall within split range - addUnmatchedAllocation(AllocationEvaluationCode.TRAFFIC_EXPOSURE_MISS); - } else { - addUnmatchedAllocation(AllocationEvaluationCode.FAILING_RULE); + if (flagEvaluationDetails) { + const flagEvaluationError = new FlagEvaluationError(err.message); + flagEvaluationError.flagEvaluationDetails = flagEvaluationDetails; + throw flagEvaluationError; } + throw err; } - return noneResult( - flag.key, - subjectKey, - subjectAttributes, - flagEvaluationDetailsBuilder - .setNoMatchFound(unmatchedAllocations) - .build( - 'DEFAULT_ALLOCATION_NULL', - 'No allocations matched. Falling back to "Default Allocation", serving NULL', - ), - ); } matchesShard(shard: Shard, subjectKey: string, totalShards: number): boolean { diff --git a/src/flag-evaluation-details-builder.ts b/src/flag-evaluation-details-builder.ts index 310a9bb..31c6b30 100644 --- a/src/flag-evaluation-details-builder.ts +++ b/src/flag-evaluation-details-builder.ts @@ -49,8 +49,8 @@ export class FlagEvaluationDetailsBuilder { private variationValue: IFlagEvaluationDetails['variationValue'] = null; private matchedRule: IFlagEvaluationDetails['matchedRule'] = null; private matchedAllocation: IFlagEvaluationDetails['matchedAllocation'] = null; - private unmatchedAllocations: IFlagEvaluationDetails['unmatchedAllocations'] = []; - private unevaluatedAllocations: IFlagEvaluationDetails['unevaluatedAllocations'] = []; + private readonly unmatchedAllocations: IFlagEvaluationDetails['unmatchedAllocations'] = []; + private readonly unevaluatedAllocations: IFlagEvaluationDetails['unevaluatedAllocations'] = []; constructor( private readonly environmentName: string, @@ -61,31 +61,15 @@ export class FlagEvaluationDetailsBuilder { this.setNone(); } - setNone = (): FlagEvaluationDetailsBuilder => { - this.variationKey = null; - this.variationValue = null; - this.matchedRule = null; - this.matchedAllocation = null; - this.unmatchedAllocations = []; - this.unevaluatedAllocations = this.allocations.map( - (allocation, i): AllocationEvaluation => ({ - key: allocation.key, - allocationEvaluationCode: AllocationEvaluationCode.UNEVALUATED, - orderPosition: i + 1, - }), - ); - return this; + addUnmatchedAllocation = (allocationEvaluation: AllocationEvaluation) => { + this.unmatchedAllocations.push(allocationEvaluation); }; - setNoMatchFound = ( - unmatchedAllocations: Array = [], - ): FlagEvaluationDetailsBuilder => { + setNone = (): FlagEvaluationDetailsBuilder => { this.variationKey = null; this.variationValue = null; - this.matchedAllocation = null; this.matchedRule = null; - this.unmatchedAllocations = unmatchedAllocations; - this.unevaluatedAllocations = []; + this.matchedAllocation = null; return this; }; @@ -94,7 +78,6 @@ export class FlagEvaluationDetailsBuilder { variation: Variation, allocation: Allocation, matchedRule: Rule | null, - unmatchedAllocations: Array, expectedVariationType: VariationType | undefined, ): FlagEvaluationDetailsBuilder => { this.variationKey = variation.key; @@ -110,17 +93,6 @@ export class FlagEvaluationDetailsBuilder { allocationEvaluationCode: AllocationEvaluationCode.MATCH, orderPosition: indexPosition + 1, // orderPosition is 1-indexed to match UI }; - this.unmatchedAllocations = unmatchedAllocations; - const unevaluatedStartIndex = indexPosition + 1; - const unevaluatedStartOrderPosition = unevaluatedStartIndex + 1; // orderPosition is 1-indexed to match UI - this.unevaluatedAllocations = this.allocations.slice(unevaluatedStartIndex).map( - (allocation, i) => - ({ - key: allocation.key, - allocationEvaluationCode: AllocationEvaluationCode.UNEVALUATED, - orderPosition: unevaluatedStartOrderPosition + i, - } as AllocationEvaluation), - ); return this; }; @@ -145,6 +117,32 @@ export class FlagEvaluationDetailsBuilder { matchedRule: this.matchedRule, matchedAllocation: this.matchedAllocation, unmatchedAllocations: this.unmatchedAllocations, - unevaluatedAllocations: this.unevaluatedAllocations, + unevaluatedAllocations: this.calculateUnevaluatedAllocations(), }); + + gracefulBuild = ( + flagEvaluationCode: FlagEvaluationCode, + flagEvaluationDescription: string, + ): IFlagEvaluationDetails | null => { + try { + return this.build(flagEvaluationCode, flagEvaluationDescription); + } catch (err) { + return null; + } + }; + + private calculateUnevaluatedAllocations = (): Array => { + const unevaluatedStartIndex = this.matchedAllocation + ? this.unmatchedAllocations.length + 1 + : this.unmatchedAllocations.length; + const unevaluatedStartOrderPosition = unevaluatedStartIndex + 1; // orderPosition is 1-indexed to match UI + return this.allocations.slice(unevaluatedStartIndex).map( + (allocation, i) => + ({ + key: allocation.key, + allocationEvaluationCode: AllocationEvaluationCode.UNEVALUATED, + orderPosition: unevaluatedStartOrderPosition + i, + } as AllocationEvaluation), + ); + }; } diff --git a/src/flag-evaluation-error.ts b/src/flag-evaluation-error.ts new file mode 100644 index 0000000..412eabd --- /dev/null +++ b/src/flag-evaluation-error.ts @@ -0,0 +1,5 @@ +import { IFlagEvaluationDetails } from './flag-evaluation-details-builder'; + +export class FlagEvaluationError extends Error { + flagEvaluationDetails: IFlagEvaluationDetails | undefined; +}