Skip to content

fix: resolve missing dependencies in incremental feature flag evaluation - #2135

Merged
cre8ivejp merged 4 commits into
mainfrom
fix-flag-not-found
Sep 26, 2025
Merged

fix: resolve missing dependencies in incremental feature flag evaluation#2135
cre8ivejp merged 4 commits into
mainfrom
fix-flag-not-found

Conversation

@cre8ivejp

@cre8ivejp cre8ivejp commented Sep 25, 2025

Copy link
Copy Markdown
Member

Fix #2136

Problem

Incremental evaluation failed with "feature not found" errors when features had multiple dependencies (2+ prerequisites or FEATURE_FLAG rules). Single dependencies worked fine, which is why this bug went undetected.

Root Cause: The GetFeaturesDependsOnTargets function found direct dependents but missed their transitive dependencies. For example:

  • Target C is updated → finds dependent A
  • But A depends on both B and D → D was missing, causing evaluation failure

Solution

Implemented complete transitive closure in GetFeaturesDependsOnTargets:

  1. Find all features that depend on targets (existing DFS)
  2. NEW: Iteratively find dependencies of discovered dependents until no new dependencies are found

Changes

  • Go: Fixed pkg/feature/domain/feature.go - added iterative dependency resolution
  • TypeScript: Fixed evaluation/typescript/src/evaluation.ts - same algorithm for Node.js SDK consistency
  • Tests: Added comprehensive test coverage for multiple dependency scenarios

Impact

  • Fixes production "feature not found" errors
  • Scales to any number of dependencies (3+, 10+, etc.)
  • Maintains backward compatibility
  • Both backend and Node.js SDK are fixed

Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
@cre8ivejp
cre8ivejp requested a review from Copilot September 25, 2025 23:15

Copilot AI left a comment

Copy link
Copy Markdown

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 fixes a critical issue where incremental feature flag evaluation failed with "feature not found" errors when features had multiple dependencies. The root cause was that the dependency resolution logic found direct dependents but missed their transitive dependencies.

  • Fixed transitive dependency resolution by implementing complete closure in both Go and TypeScript
  • Enhanced API layer to pass all features instead of filtering out archived ones during evaluation
  • Added comprehensive test coverage for multi-dependency scenarios and edge cases

Reviewed Changes

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

Show a summary per file
File Description
pkg/feature/domain/feature.go Implements iterative transitive closure for complete dependency resolution and removes debug logging
pkg/api/api/api_grpc.go Removes archived feature filtering and adds enhanced error logging for debugging dependency issues
evaluation/typescript/src/evaluation.ts Mirrors Go implementation with transitive closure and fixes archived feature evaluation order
evaluation/typescript/src/tests/evaluator/get_eval_features_test.ts Updates test expectations and improves assertion logic for dependency resolution
evaluation/go/evaluation_test.go Adds comprehensive test coverage for multiple dependency scenarios and edge cases
evaluation/go/evaluation.go Fixes evaluation order to set flag variations for all features before filtering archived ones

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

Comment thread pkg/feature/domain/feature.go Outdated
Comment thread evaluation/typescript/src/evaluation.ts Outdated
Comment thread evaluation/typescript/src/evaluation.ts Outdated
Comment thread pkg/feature/domain/feature.go Outdated
Comment thread evaluation/typescript/src/evaluation.ts Outdated
…essing

Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
@cre8ivejp
cre8ivejp requested a review from Copilot September 25, 2025 23:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.


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

@cre8ivejp
cre8ivejp marked this pull request as ready for review September 25, 2025 23:35
Comment thread pkg/api/api/api_grpc.go
}
spanGetFeatures.End()
features := f.([]*featureproto.Feature)
activeFeatures := s.filterOutArchivedFeatures(features)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We can't filter out all archived flags because we must know what has been archived in the last 30 days, so in the SDK response, the client SDK can know what flag can be deleted from the local cache.

Comment on lines +1456 to +1461
// Check if the dependency exists in the all map
if dep, ok := all[fid]; ok {
if dfs(dep) {
evals[f.Id] = f
return true
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This logic prevents a panic error in case the flag is missing for some edge case.

Comment on lines +1472 to +1505
// Step 2: Ensure complete transitive closure
// The DFS above finds dependents (who depends on targets), but misses dependencies of those dependents.
// Example: If target C → dependent A → dependency D, we found A but missed D.
// Efficiently process only newly discovered features in each iteration.
processed := make(map[string]struct{})
queue := make([]*feature.Feature, 0, len(evals))
for _, f := range evals {
queue = append(queue, f)
}

const maxIterations = 100 // Prevent infinite loops in case of circular dependencies
iteration := 0
for len(queue) > 0 && iteration < maxIterations {
iteration++
nextQueue := make([]*feature.Feature, 0)
for _, f := range queue {
if _, ok := processed[f.Id]; ok {
continue
}
processed[f.Id] = struct{}{}

// Find dependencies of f
dmn := &Feature{Feature: f}
for _, depID := range dmn.FeatureIDsDependsOn() {
if dep, ok := all[depID]; ok {
if _, exists := evals[depID]; !exists {
evals[depID] = dep
nextQueue = append(nextQueue, dep)
}
}
}
}
queue = nextQueue
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Here is the fix for our error.
I will keep adding the dependent flags until I add all.

@cre8ivejp
cre8ivejp merged commit 7dc9b2b into main Sep 26, 2025
21 checks passed
@cre8ivejp
cre8ivejp deleted the fix-flag-not-found branch September 26, 2025 00:00
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.

fix: resolve missing dependencies in incremental feature flag evaluation

2 participants