3
3
4
4
import { TimeWindowFilter } from "./filter/TimeWindowFilter" ;
5
5
import { IFeatureFilter } from "./filter/FeatureFilter" ;
6
- import { RequirementType } from "./model" ;
6
+ import { FeatureFlag , RequirementType , VariantDefinition } from "./model" ;
7
7
import { IFeatureFlagProvider } from "./featureProvider" ;
8
8
import { 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" ;
9
13
10
- export class FeatureManager {
14
+ export class FeatureManager implements IFeatureManager {
11
15
#provider: IFeatureFlagProvider ;
12
16
#featureFilters: Map < string , IFeatureFilter > = new Map ( ) ;
13
17
@@ -30,15 +34,48 @@ export class FeatureManager {
30
34
31
35
// If multiple feature flags are found, the first one takes precedence.
32
36
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
+ }
37
54
}
38
55
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
+ }
41
64
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 > {
42
79
if ( featureFlag . enabled !== true ) {
43
80
// If the feature is not explicitly enabled, then it is disabled by default.
44
81
return false ;
@@ -61,7 +98,7 @@ export class FeatureManager {
61
98
62
99
for ( const clientFilter of clientFilters ) {
63
100
const matchedFeatureFilter = this . #featureFilters. get ( clientFilter . name ) ;
64
- const contextWithFeatureName = { featureName, parameters : clientFilter . parameters } ;
101
+ const contextWithFeatureName = { featureName : featureFlag . id , parameters : clientFilter . parameters } ;
65
102
if ( matchedFeatureFilter === undefined ) {
66
103
console . warn ( `Feature filter ${ clientFilter . name } is not found.` ) ;
67
104
return false ;
@@ -75,14 +112,166 @@ export class FeatureManager {
75
112
return ! shortCircuitEvaluationResult ;
76
113
}
77
114
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
+ }
78
188
}
79
189
80
190
interface FeatureManagerOptions {
81
191
customFilters ?: IFeatureFilter [ ] ;
82
192
}
83
193
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
+ */
84
202
function validateFeatureFlagFormat ( featureFlag : any ) : void {
85
203
if ( featureFlag . enabled !== undefined && typeof featureFlag . enabled !== "boolean" ) {
86
204
throw new Error ( `Feature flag ${ featureFlag . id } has an invalid 'enabled' value.` ) ;
87
205
}
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
+ ) { }
88
277
}
0 commit comments