Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: evaluate features in CLI #71

Merged
merged 2 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,18 @@ To benchmark evaluating a feature's variable via SDKs's `.getVariable()` method:
-n 100
```

### Evaluate
To learn why certain values (like feature and its variation or variables) are evaluated as they are against provided [context](https://featurevisor.com/docs/sdks/javascript/#context):

```bash
FeaturevisorTestRunner evaluate \
--environment staging \
--feature feature_key \
--context '{"user_id":"123"}' \
```
This will show you full [evaluation details](https://featurevisor.com/docs/sdks/javascript/#evaluation-details) helping you debug better in case of any confusion.
It is similar to logging in SDKs with debug level. But here instead, we are doing it at CLI directly in our Featurevisor project without having to involve our application(s).

## License

[MIT](./LICENSE)
95 changes: 65 additions & 30 deletions Sources/FeaturevisorSDK/Instance+Evaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ extension FeaturevisorInstance {
let finalContext = interceptContext != nil ? interceptContext!(context) : context

// forced
if let force = findForceFromFeature(
let forceResult = findForceFromFeature(
feature,
context: context,
datafileReader: datafileReader
) {
)

if let force = forceResult.force {
let variation = feature.variations.first(where: { variation in
return variation.value == force.variation
})
Expand All @@ -102,12 +104,12 @@ extension FeaturevisorInstance {
}

// bucketing
let bucketValue = getBucketValue(feature: feature, context: finalContext)
let bucketResult = getBucketValue(feature: feature, context: finalContext)

let matchedTrafficAndAllocation = getMatchedTrafficAndAllocation(
traffic: feature.traffic,
context: finalContext,
bucketValue: bucketValue,
bucketValue: bucketResult.bucketValue,
datafileReader: datafileReader,
logger: logger
)
Expand All @@ -125,7 +127,8 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .rule,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
variation: variation
)
Expand All @@ -147,7 +150,10 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .allocated,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
traffic: matchedTraffic,
variation: variation
)

Expand All @@ -162,7 +168,8 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .error,
bucketValue: bucketValue
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue
)

logger.debug("no matched variation", evaluation.toDictionary())
Expand Down Expand Up @@ -225,13 +232,19 @@ extension FeaturevisorInstance {
let finalContext = interceptContext != nil ? interceptContext!(context) : context

// forced
let force = findForceFromFeature(feature, context: context, datafileReader: datafileReader)
let forceResult = findForceFromFeature(
feature,
context: context,
datafileReader: datafileReader
)

if let force, force.enabled != nil {
if let force = forceResult.force, force.enabled != nil {
evaluation = Evaluation(
featureKey: featureKey,
reason: .forced,
enabled: force.enabled
enabled: force.enabled,
forceIndex: forceResult.forceIndex,
force: force
)

logger.debug("forced enabled found", evaluation.toDictionary())
Expand Down Expand Up @@ -284,7 +297,7 @@ extension FeaturevisorInstance {
}

// bucketing
let bucketValue = getBucketValue(feature: feature, context: finalContext)
let bucketResult = getBucketValue(feature: feature, context: finalContext)

let matchedTraffic = getMatchedTraffic(
traffic: feature.traffic,
Expand All @@ -297,15 +310,17 @@ extension FeaturevisorInstance {
if !feature.ranges.isEmpty {

let matchedRange = feature.ranges.first(where: { range in
return bucketValue >= range.start && bucketValue < range.end
return bucketResult.bucketValue >= range.start
&& bucketResult.bucketValue < range.end
})

// matched
if matchedRange != nil {
evaluation = Evaluation(
featureKey: feature.key,
reason: .allocated,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
enabled: matchedTraffic.enabled ?? true
)

Expand All @@ -316,7 +331,8 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .outOfRange,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
enabled: false
)

Expand All @@ -330,7 +346,8 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .override,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
enabled: matchedTrafficEnabled,
traffic: matchedTraffic
Expand All @@ -342,11 +359,12 @@ extension FeaturevisorInstance {
}

// treated as enabled because of matched traffic
if bucketValue <= matchedTraffic.percentage {
if bucketResult.bucketValue <= matchedTraffic.percentage {
evaluation = Evaluation(
featureKey: feature.key,
reason: .rule,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
enabled: true,
traffic: matchedTraffic
Expand All @@ -360,7 +378,8 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .error,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
enabled: false
)

Expand Down Expand Up @@ -451,12 +470,18 @@ extension FeaturevisorInstance {
let finalContext = interceptContext != nil ? interceptContext!(context) : context

// forced
let force = findForceFromFeature(feature, context: context, datafileReader: datafileReader)
let forceResult = findForceFromFeature(
feature,
context: context,
datafileReader: datafileReader
)

if let force, let variableValue = force.variables?[variableKey] {
if let force = forceResult.force, let variableValue = force.variables?[variableKey] {
evaluation = Evaluation(
featureKey: feature.key,
reason: .forced,
forceIndex: forceResult.forceIndex,
force: force,
variableKey: variableKey,
variableValue: variableValue,
variableSchema: variableSchema
Expand All @@ -468,12 +493,12 @@ extension FeaturevisorInstance {
}

// bucketing
let bucketValue = getBucketValue(feature: feature, context: finalContext)
let bucketResult = getBucketValue(feature: feature, context: finalContext)

let matchedTrafficAndAllocation = getMatchedTrafficAndAllocation(
traffic: feature.traffic,
context: finalContext,
bucketValue: bucketValue,
bucketValue: bucketResult.bucketValue,
datafileReader: datafileReader,
logger: logger
)
Expand All @@ -484,8 +509,10 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .rule,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
traffic: matchedTraffic,
variableKey: variableKey,
variableValue: variableValue,
variableSchema: variableSchema
Expand All @@ -499,7 +526,7 @@ extension FeaturevisorInstance {
// regular allocation
var variationValue: VariationValue? = nil

if let forceVariation = force?.variation {
if let forceVariation = forceResult.force?.variation {
variationValue = forceVariation
}
else if let matchedAllocationVariation = matchedTrafficAndAllocation.matchedAllocation?
Expand Down Expand Up @@ -543,8 +570,10 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .override,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
traffic: matchedTraffic,
variableKey: variableKey,
variableValue: override.value,
variableSchema: variableSchema
Expand All @@ -560,8 +589,10 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .allocated,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
ruleKey: matchedTraffic.key,
traffic: matchedTraffic,
variableKey: variableKey,
variableValue: variableFromVariationValue,
variableSchema: variableSchema
Expand All @@ -579,7 +610,8 @@ extension FeaturevisorInstance {
evaluation = Evaluation(
featureKey: feature.key,
reason: .defaulted,
bucketValue: bucketValue,
bucketKey: bucketResult.bucketKey,
bucketValue: bucketResult.bucketValue,
variableKey: variableKey,
variableValue: variableSchema.defaultValue,
variableSchema: variableSchema
Expand Down Expand Up @@ -645,15 +677,18 @@ extension FeaturevisorInstance {
return result
}

fileprivate func getBucketValue(feature: Feature, context: Context) -> BucketValue {
public typealias BucketResult = (bucketKey: BucketKey, bucketValue: BucketValue)

fileprivate func getBucketValue(feature: Feature, context: Context) -> BucketResult {

let bucketKey = getBucketKey(feature: feature, context: context)
let value = Bucket.resolveNumber(forKey: bucketKey)

if let configureBucketValue = self.configureBucketValue {
return configureBucketValue(feature, context, value)
let configuredValue = configureBucketValue(feature, context, value)
return (bucketKey: bucketKey, bucketValue: configuredValue)
}

return value
return (bucketKey: bucketKey, bucketValue: value)
}
}
28 changes: 20 additions & 8 deletions Sources/FeaturevisorSDK/Instance+Feature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,35 @@ extension FeaturevisorInstance {
_ feature: Feature,
context: Context,
datafileReader: DatafileReader
) -> Force? {
) -> ForceResult {

return feature.force.first(where: { force in
if let conditions = force.conditions {
return allConditionsAreMatched(condition: conditions, context: context)
var force: Force?
var forceIndex: Int?

for (index, currentForce) in feature.force.enumerated() {
if let condition = currentForce.conditions,
allConditionsAreMatched(condition: condition, context: context)
{

force = currentForce
forceIndex = index
break
}

if let segments = force.segments {
return allGroupSegmentsAreMatched(
if let segments = currentForce.segments,
allGroupSegmentsAreMatched(
groupSegments: segments,
context: context,
datafileReader: datafileReader
)
{
force = currentForce
forceIndex = index
break
}
}

return false
})
return .init(force: force, forceIndex: forceIndex)
}

func getMatchedTraffic(
Expand Down
Loading
Loading