Skip to content

Conversation

gagantrivedi
Copy link
Member

No description provided.

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements an engine evaluation context feature that introduces a new engine evaluation system to replace the existing feature state evaluation approach. The changes modernize the evaluation architecture by introducing JSON-based context handling and new evaluation models.

Key Changes

  • Added new engine evaluation context system with JSON-based models
  • Replaced old feature state evaluation with the new engine evaluation system
  • Updated client to use the new evaluation context and mappers

Reviewed Changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
models.go Added functions to convert engine evaluation results to Flag objects
models_test.go Added comprehensive tests for the new conversion functions
flagengine/engine_eval/ New package with evaluation context, mappers, and evaluators
client.go Updated to use new engine evaluation context instead of old models
flagengine/engine.go Added new evaluation result generation function
flagengine/segments/ Simplified segment evaluation by removing complex logic
go.mod Added new dependency for JSON processing

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +52 to +54
if id, err := strconv.Atoi(flagResult.FeatureKey); err == nil {
featureID = id
}
Copy link
Preview

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FeatureKey to FeatureID conversion assumes FeatureKey is always a string representation of an integer. If FeatureKey is not numeric, this silently defaults to 0, which could mask data issues. Consider adding error handling or validation.

Copilot uses AI. Check for mistakes.

Comment on lines +313 to +316
if err := json.Unmarshal(jsonBytes, &traitList); err != nil {
// Log error or handle gracefully - for now, continue with empty list
traitList = nil
}
Copy link
Preview

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent error handling with only a comment is problematic. Consider logging the error or providing a more explicit error handling mechanism to aid debugging when trait conversion fails.

Copilot uses AI. Check for mistakes.

return nil
}

return fmt.Errorf("unable to unmarshal FlexibleString from %s", string(data))
Copy link
Preview

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message could expose sensitive data by including the raw JSON data. Consider sanitizing or limiting the exposed content in error messages.

Suggested change
return fmt.Errorf("unable to unmarshal FlexibleString from %s", string(data))
return fmt.Errorf("unable to unmarshal FlexibleString: invalid input")

Copilot uses AI. Check for mistakes.

return nil
}

return fmt.Errorf("unable to unmarshal ValueUnion from %s", string(data))
Copy link
Preview

Copilot AI Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above - the error message could expose sensitive data by including the raw JSON data. Consider sanitizing the exposed content.

Suggested change
return fmt.Errorf("unable to unmarshal ValueUnion from %s", string(data))
return fmt.Errorf("unable to unmarshal ValueUnion: invalid format")

Copilot uses AI. Check for mistakes.

@gagantrivedi gagantrivedi changed the title feat: engine evlauation context feat: engine evaluation context Oct 2, 2025
Copy link
Contributor

@Zaimwa9 Zaimwa9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great progress. I'll re-review once 100% finalized but here are some early comments.

  • pinned some error handling
  • rest is mostly about abstracting.
    Given we're implementing across 15 languages and this is core/critical logic, I'm in favor of extracting where it makes sense to ease future context-switching between SDKs and lowering the techno expertise barrier

featureKey := override.FeatureKey

// Get priority, defaulting to 0 if not set
overridePriority := defaultPriority
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that should be the other way around and defaulting to Infinity

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, done

// Check if we have a segment override for this feature
if segmentFeatureCtx, exists := segmentFeatureContexts[featureContext.FeatureKey]; exists {
// Use segment override
fc := segmentFeatureCtx.featureContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth checking it's not nil, even though it shouldn't be

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be a pointer... I have updated it


// Convert FeatureKey (string ID) to integer FeatureID
featureID := 0
if id, err := strconv.Atoi(flagResult.FeatureKey); err == nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might want to throw if it errors

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the feature ID isn’t critical for the user, I’d prefer not to throw an error to the client due to a misconfiguration on our side. Let me know if you disagree — I don’t have a strong opinion on this.

}

// Variants
if len(fs.MultivariateFeatureStateValues) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see this part in its own function to clearly isolate it

f := float64(v)
return &Value{Double: &f}
case string:
if v == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just extract this string converter into its own function for clarity

if segmentCondition.Property != "" {
contextValue = getContextValue(ec, segmentCondition.Property)
}
if segmentCondition.Operator == PercentageSplit {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be its own handler

return matchRegex(traitValue, conditionValue)
}

b1, e1 := strconv.ParseBool(traitValue)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here I think it could be extracted

}
// GetEvaluationResult computes flags and matched segments given a context and a segment matcher.
// The matcher should return true when the provided segment applies to the provided context.
func GetEvaluationResult(ec *engine_eval.EngineEvaluationContext) engine_eval.EvaluationResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semi NIT:
Overall I think we could abstract this one in all the SDKs, nothing changing functionally but it's basically doing 2 things:

  • processSegments
  • evaluate/process features.

Basically following your comments, imo it could be worth extracting them for better maintainability and unit tests. I know it's not a big impact either so your call

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean abstract them inside GetEvaluationResult?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes to have 2 separate functions and it becomes just (exaggerating)

func GetEvaluationResult(ec *engine_eval.EngineEvaluationContext) engine_eval.EvaluationResult {
     segments := processSegments(xxx)
     flags := processFeatures(yyy)
      return engine_eval.EvaluationResult{
		Flags:    flags,
		Segments: segments,
	}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was going to do that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, should look a little better now

Copy link

@emyller emyller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Only a handful things to fix/improve.

P.S. I have only reviewed the client work to the extent of what I have full context so far. My PHP project is still a WIP, so I'm hoping to review this again by the time I submit my own work for review.

Comment on lines +32 to +35
conditions := make([]bool, len(segmentRule.Conditions))
for i := range segmentRule.Conditions {
conditions[i] = contextMatchesCondition(ec, &segmentRule.Conditions[i], segmentKey)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We likely don't need to evaluate all conditions here. Can we short-circuit this logic depending on segmentRule.Type? See example.

Comment on lines 41 to 42
default:
matchesConditions = utils.None(conditions)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For explicitness.

Suggested change
default:
matchesConditions = utils.None(conditions)
case None:
matchesConditions = utils.None(conditions)
default:
return false

Comment on lines 271 to 272
case In:
return slices.Contains(strings.Split(v2, ","), v1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where in the code we account for the case when the context value is a list of values rather than an explodable string.

Related: Flagsmith/engine-test-data#16


// Create overrides for each feature
for _, override := range overrides {
priority := math.Inf(-1) // Highest possible priority
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
priority := math.Inf(-1) // Highest possible priority
priority := math.Inf(-1) // Strongest possible priority

Comment on lines 339 to 350
var environmentKey string
if newContext.Environment.Key != "" {
environmentKey = newContext.Environment.Key
} else {
environmentKey = newContext.Environment.Name
}

identity := IdentityContext{
Identifier: identifier,
Key: fmt.Sprintf("%s_%s", environmentKey, identifier),
Traits: identityTraits,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var environmentKey string
if newContext.Environment.Key != "" {
environmentKey = newContext.Environment.Key
} else {
environmentKey = newContext.Environment.Name
}
identity := IdentityContext{
Identifier: identifier,
Key: fmt.Sprintf("%s_%s", environmentKey, identifier),
Traits: identityTraits,
}
identity := IdentityContext{
Identifier: identifier,
Key: fmt.Sprintf("%s_%s", newContext.Environment.Key, identifier),
Traits: identityTraits,
}

Key should never be empty, right?

return *priority
}
return nil
return math.Inf(1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return math.Inf(1)
return math.Inf(1) // Weakest possible priority

Comment on lines +109 to +120
func GetEvaluationResult(ec *engine_eval.EngineEvaluationContext) engine_eval.EvaluationResult {
// Process segments
segments, segmentFeatureContexts := processSegments(ec)

// Process features
flags := processFeatures(ec, segmentFeatureContexts)

return engine_eval.EvaluationResult{
Flags: flags,
Segments: segments,
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat ✨

Comment on lines 43 to 52
// Note: Flags are now a map, so no need to sort them
// The comparison will be done by comparing the map contents directly

require.Len(actual.Flags, len(expected.Flags))
for featureName, expectedFlag := range expected.Flags {
actualFlag, exists := actual.Flags[featureName]
require.True(exists, "Expected flag %s not found in actual result", featureName)
assert.Equal(expectedFlag.Value, actualFlag.Value)
assert.Equal(expectedFlag.Enabled, actualFlag.Enabled)
assert.Equal(expectedFlag.FeatureKey, actualFlag.FeatureKey)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! 🎉 Looking forward to see less code here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants