33
44import { TimeWindowFilter } from "./filter/TimeWindowFilter" ;
55import { IFeatureFilter } from "./filter/FeatureFilter" ;
6- import { RequirementType } from "./model" ;
6+ import { FeatureFlag , RequirementType , VariantDefinition } from "./model" ;
77import { IFeatureFlagProvider } from "./featureProvider" ;
88import { TargetingFilter } from "./filter/TargetingFilter" ;
9+ import { Variant } from "./variant/Variant" ;
10+ import { IFeatureManager } from "./IFeatureManager" ;
11+ import { ITargetingContext } from "./common/ITargetingContext" ;
12+ import { isTargetedGroup , isTargetedPercentile , isTargetedUser } from "./common/targetingEvaluator" ;
913
10- export class FeatureManager {
14+ export class FeatureManager implements IFeatureManager {
1115 #provider: IFeatureFlagProvider ;
1216 #featureFilters: Map < string , IFeatureFilter > = new Map ( ) ;
1317
@@ -30,15 +34,48 @@ export class FeatureManager {
3034
3135 // If multiple feature flags are found, the first one takes precedence.
3236 async isEnabled ( featureName : string , context ?: unknown ) : Promise < boolean > {
33- const featureFlag = await this . #provider. getFeatureFlag ( featureName ) ;
34- if ( featureFlag === undefined ) {
35- // If the feature is not found, then it is disabled.
36- return false ;
37+ const result = await this . #evaluateFeature( featureName , context ) ;
38+ return result . enabled ;
39+ }
40+
41+ async getVariant ( featureName : string , context ?: ITargetingContext ) : Promise < Variant | undefined > {
42+ const result = await this . #evaluateFeature( featureName , context ) ;
43+ return result . variant ;
44+ }
45+
46+ async #assignVariant( featureFlag : FeatureFlag , context : ITargetingContext ) : Promise < VariantAssignment > {
47+ // user allocation
48+ if ( featureFlag . allocation ?. user !== undefined ) {
49+ for ( const userAllocation of featureFlag . allocation . user ) {
50+ if ( isTargetedUser ( context . userId , userAllocation . users ) ) {
51+ return getVariantAssignment ( featureFlag , userAllocation . variant , VariantAssignmentReason . User ) ;
52+ }
53+ }
3754 }
3855
39- // Ensure that the feature flag is in the correct format. Feature providers should validate the feature flags, but we do it here as a safeguard.
40- validateFeatureFlagFormat ( featureFlag ) ;
56+ // group allocation
57+ if ( featureFlag . allocation ?. group !== undefined ) {
58+ for ( const groupAllocation of featureFlag . allocation . group ) {
59+ if ( isTargetedGroup ( context . groups , groupAllocation . groups ) ) {
60+ return getVariantAssignment ( featureFlag , groupAllocation . variant , VariantAssignmentReason . Group ) ;
61+ }
62+ }
63+ }
4164
65+ // percentile allocation
66+ if ( featureFlag . allocation ?. percentile !== undefined ) {
67+ for ( const percentileAllocation of featureFlag . allocation . percentile ) {
68+ const hint = featureFlag . allocation . seed ?? `allocation\n${ featureFlag . id } ` ;
69+ if ( isTargetedPercentile ( context . userId , hint , percentileAllocation . from , percentileAllocation . to ) ) {
70+ return getVariantAssignment ( featureFlag , percentileAllocation . variant , VariantAssignmentReason . Percentile ) ;
71+ }
72+ }
73+ }
74+
75+ return { variant : undefined , reason : VariantAssignmentReason . None } ;
76+ }
77+
78+ async #isEnabled( featureFlag : FeatureFlag , context ?: unknown ) : Promise < boolean > {
4279 if ( featureFlag . enabled !== true ) {
4380 // If the feature is not explicitly enabled, then it is disabled by default.
4481 return false ;
@@ -61,7 +98,7 @@ export class FeatureManager {
6198
6299 for ( const clientFilter of clientFilters ) {
63100 const matchedFeatureFilter = this . #featureFilters. get ( clientFilter . name ) ;
64- const contextWithFeatureName = { featureName, parameters : clientFilter . parameters } ;
101+ const contextWithFeatureName = { featureName : featureFlag . id , parameters : clientFilter . parameters } ;
65102 if ( matchedFeatureFilter === undefined ) {
66103 console . warn ( `Feature filter ${ clientFilter . name } is not found.` ) ;
67104 return false ;
@@ -75,14 +112,166 @@ export class FeatureManager {
75112 return ! shortCircuitEvaluationResult ;
76113 }
77114
115+ async #evaluateFeature( featureName : string , context : unknown ) : Promise < EvaluationResult > {
116+ const featureFlag = await this . #provider. getFeatureFlag ( featureName ) ;
117+ const result = new EvaluationResult ( featureFlag ) ;
118+
119+ if ( featureFlag === undefined ) {
120+ return result ;
121+ }
122+
123+ // Ensure that the feature flag is in the correct format. Feature providers should validate the feature flags, but we do it here as a safeguard.
124+ // TODO: move to the feature flag provider implementation.
125+ validateFeatureFlagFormat ( featureFlag ) ;
126+
127+ // Evaluate if the feature is enabled.
128+ result . enabled = await this . #isEnabled( featureFlag , context ) ;
129+
130+ // Determine Variant
131+ let variantDef : VariantDefinition | undefined ;
132+ let reason : VariantAssignmentReason = VariantAssignmentReason . None ;
133+
134+ // featureFlag.variant not empty
135+ if ( featureFlag . variants !== undefined && featureFlag . variants . length > 0 ) {
136+ if ( ! result . enabled ) {
137+ // not enabled, assign default if specified
138+ if ( featureFlag . allocation ?. default_when_disabled !== undefined ) {
139+ variantDef = featureFlag . variants . find ( v => v . name == featureFlag . allocation ?. default_when_disabled ) ;
140+ reason = VariantAssignmentReason . DefaultWhenDisabled ;
141+ } else {
142+ // no default specified
143+ variantDef = undefined ;
144+ reason = VariantAssignmentReason . DefaultWhenDisabled ;
145+ }
146+ } else {
147+ // enabled, assign based on allocation
148+ if ( context !== undefined && featureFlag . allocation !== undefined ) {
149+ const variantAndReason = await this . #assignVariant( featureFlag , context as ITargetingContext ) ;
150+ variantDef = variantAndReason . variant ;
151+ reason = variantAndReason . reason ;
152+ }
153+
154+ // allocation failed, assign default if specified
155+ if ( variantDef === undefined && reason === VariantAssignmentReason . None ) {
156+ if ( featureFlag . allocation ?. default_when_enabled !== undefined ) {
157+ variantDef = featureFlag . variants . find ( v => v . name == featureFlag . allocation ?. default_when_enabled ) ;
158+ reason = VariantAssignmentReason . DefaultWhenEnabled ;
159+ } else {
160+ variantDef = undefined ;
161+ reason = VariantAssignmentReason . DefaultWhenEnabled ;
162+ }
163+ }
164+ }
165+ }
166+
167+ // TODO: send telemetry for variant assignment reason in the future.
168+ console . log ( `Variant assignment for feature ${ featureName } : ${ variantDef ?. name ?? "default" } (${ reason } )` ) ;
169+
170+ if ( variantDef ?. configuration_reference !== undefined ) {
171+ console . warn ( "Configuration reference is not supported yet." ) ;
172+ }
173+
174+ result . variant = variantDef !== undefined ? new Variant ( variantDef . name , variantDef . configuration_value ) : undefined ;
175+ result . variantAssignmentReason = reason ;
176+
177+ // Status override for isEnabled
178+ if ( variantDef !== undefined && featureFlag . enabled ) {
179+ if ( variantDef . status_override === "Enabled" ) {
180+ result . enabled = true ;
181+ } else if ( variantDef . status_override === "Disabled" ) {
182+ result . enabled = false ;
183+ }
184+ }
185+
186+ return result ;
187+ }
78188}
79189
80190interface FeatureManagerOptions {
81191 customFilters ?: IFeatureFilter [ ] ;
82192}
83193
194+ /**
195+ * Validates the format of the feature flag definition.
196+ *
197+ * FeatureFlag data objects are from IFeatureFlagProvider, depending on the implementation.
198+ * Thus the properties are not guaranteed to have the expected types.
199+ *
200+ * @param featureFlag The feature flag definition to validate.
201+ */
84202function validateFeatureFlagFormat ( featureFlag : any ) : void {
85203 if ( featureFlag . enabled !== undefined && typeof featureFlag . enabled !== "boolean" ) {
86204 throw new Error ( `Feature flag ${ featureFlag . id } has an invalid 'enabled' value.` ) ;
87205 }
206+ // TODO: add more validations.
207+ // TODO: should be moved to the feature flag provider.
208+ }
209+
210+ /**
211+ * Try to get the variant assignment for the given variant name. If the variant is not found, override the reason with VariantAssignmentReason.None.
212+ *
213+ * @param featureFlag feature flag definition
214+ * @param variantName variant name
215+ * @param reason variant assignment reason
216+ * @returns variant assignment containing the variant definition and the reason
217+ */
218+ function getVariantAssignment ( featureFlag : FeatureFlag , variantName : string , reason : VariantAssignmentReason ) : VariantAssignment {
219+ const variant = featureFlag . variants ?. find ( v => v . name == variantName ) ;
220+ if ( variant !== undefined ) {
221+ return { variant, reason } ;
222+ } else {
223+ console . warn ( `Variant ${ variantName } not found for feature ${ featureFlag . id } .` ) ;
224+ return { variant : undefined , reason : VariantAssignmentReason . None } ;
225+ }
226+ }
227+
228+ type VariantAssignment = {
229+ variant : VariantDefinition | undefined ;
230+ reason : VariantAssignmentReason ;
231+ } ;
232+
233+ enum VariantAssignmentReason {
234+ /**
235+ * Variant allocation did not happen. No variant is assigned.
236+ */
237+ None ,
238+
239+ /**
240+ * The default variant is assigned when a feature flag is disabled.
241+ */
242+ DefaultWhenDisabled ,
243+
244+ /**
245+ * The default variant is assigned because of no applicable user/group/percentile allocation when a feature flag is enabled.
246+ */
247+ DefaultWhenEnabled ,
248+
249+ /**
250+ * The variant is assigned because of the user allocation when a feature flag is enabled.
251+ */
252+ User ,
253+
254+ /**
255+ * The variant is assigned because of the group allocation when a feature flag is enabled.
256+ */
257+ Group ,
258+
259+ /**
260+ * The variant is assigned because of the percentile allocation when a feature flag is enabled.
261+ */
262+ Percentile
263+ }
264+
265+ class EvaluationResult {
266+ constructor (
267+ // feature flag definition
268+ public readonly feature : FeatureFlag | undefined ,
269+
270+ // enabled state
271+ public enabled : boolean = false ,
272+
273+ // variant assignment
274+ public variant : Variant | undefined = undefined ,
275+ public variantAssignmentReason : VariantAssignmentReason = VariantAssignmentReason . None
276+ ) { }
88277}
0 commit comments