Skip to content

Commit 52d67c3

Browse files
authored
feat: Add reason, variant to flagsmith eval output (#74)
1 parent 9ad9979 commit 52d67c3

4 files changed

Lines changed: 175 additions & 9 deletions

File tree

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/Flagsmith/flagsmith-cli/v2
33
go 1.26
44

55
require (
6-
github.com/Flagsmith/flagsmith-go-client/v5 v5.1.0
6+
github.com/Flagsmith/flagsmith-go-client/v5 v5.2.0
77
github.com/blang/semver/v4 v4.0.0
88
github.com/charmbracelet/huh v1.0.0
99
github.com/fatih/color v1.19.0

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
github.com/Flagsmith/flagsmith-go-client/v5 v5.1.0 h1:/1KMxWHfzo8I7FnZ0mnMw+ICM4HVZvCEYTJ4idM2VEU=
2-
github.com/Flagsmith/flagsmith-go-client/v5 v5.1.0/go.mod h1:CjTs0XAIJXG+08lf4h51W9cjvrNaK92TLVYtjPrnI3w=
1+
github.com/Flagsmith/flagsmith-go-client/v5 v5.2.0 h1:Mqs3nEceWQixRcnIdqnju1Kqo5HvTlhcR9HTdiOsjc4=
2+
github.com/Flagsmith/flagsmith-go-client/v5 v5.2.0/go.mod h1:CjTs0XAIJXG+08lf4h51W9cjvrNaK92TLVYtjPrnI3w=
33
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
44
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
55
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=

‎internal/cmd/cmd_test.go‎

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6938,7 +6938,6 @@ func TestEvaluate(t *testing.T) {
69386938
if items["value"] != float64(25) || items["enabled"] != false {
69396939
t.Errorf("flag = %+v, want the resolved value", items)
69406940
}
6941-
// Omitted until the SDK API returns them: absent, never faked.
69426941
for _, absent := range []string{"reason", "variant"} {
69436942
if _, ok := items[absent]; ok {
69446943
t.Errorf("flag = %+v, want %q omitted rather than invented", items, absent)
@@ -6949,6 +6948,86 @@ func TestEvaluate(t *testing.T) {
69496948
}
69506949
})
69516950

6951+
t.Run("json carries the reason and variant the SDK API returned", func(t *testing.T) {
6952+
// Given
6953+
f := evalEnv(t)
6954+
flags := sdkFlagsFrom(defaultFeatures())
6955+
flags[0]["reason"] = "DEFAULT"
6956+
flags[1]["reason"] = "SPLIT; weight=30"
6957+
flags[1]["variant"] = "treatment"
6958+
f.sdkEnvFlags["WqXhZk8sVY3dGgTqZ9pJmN"] = flags
6959+
6960+
// When
6961+
out, err := run("", "evaluate", "--json")
6962+
6963+
// Then
6964+
if err != nil {
6965+
t.Fatalf("evaluate --json: %v\noutput: %s", err, out)
6966+
}
6967+
resolved, _ := evalDoc(t, out)["flags"].(map[string]any)
6968+
items, _ := resolved["max_items"].(map[string]any)
6969+
if items["reason"] != "SPLIT; weight=30" || items["variant"] != "treatment" {
6970+
t.Errorf("flag = %+v, want the reason and variant it resolved by", items)
6971+
}
6972+
// A standard feature has no variant to report; the schema calls that null,
6973+
// and an EvaluationResult that is already a subset omits it.
6974+
banner, _ := resolved["onboarding_banner"].(map[string]any)
6975+
if banner["reason"] != "DEFAULT" {
6976+
t.Errorf("flag = %+v, want the reason it resolved by", banner)
6977+
}
6978+
if _, ok := banner["variant"]; ok {
6979+
t.Errorf("flag = %+v, want no variant where none applied", banner)
6980+
}
6981+
})
6982+
6983+
t.Run("the table adds VARIANT when SDK API returns a variant", func(t *testing.T) {
6984+
// Given
6985+
f := evalEnv(t)
6986+
flags := sdkFlagsFrom(defaultFeatures())
6987+
flags[0]["reason"] = "DEFAULT"
6988+
flags[1]["reason"] = "TARGETING_MATCH; segment=power users"
6989+
flags[1]["variant"] = "treatment"
6990+
f.sdkEnvFlags["WqXhZk8sVY3dGgTqZ9pJmN"] = flags
6991+
6992+
// When
6993+
out, err := run("", "evaluate")
6994+
6995+
// Then
6996+
if err != nil {
6997+
t.Fatalf("evaluate: %v\noutput: %s", err, out)
6998+
}
6999+
for _, want := range []string{
7000+
"VARIANT", "REASON",
7001+
"TARGETING_MATCH; segment=power users", "treatment", "DEFAULT",
7002+
} {
7003+
if !strings.Contains(out, want) {
7004+
t.Errorf("output = %q, want it to contain %q", out, want)
7005+
}
7006+
}
7007+
})
7008+
7009+
t.Run("the table drops the columns an SDK API left empty", func(t *testing.T) {
7010+
// Given
7011+
f := evalEnv(t)
7012+
flags := sdkFlagsFrom(defaultFeatures())
7013+
flags[0]["reason"], flags[1]["reason"] = "DEFAULT", "DEFAULT"
7014+
f.sdkEnvFlags["WqXhZk8sVY3dGgTqZ9pJmN"] = flags
7015+
7016+
// When
7017+
out, err := run("", "evaluate")
7018+
7019+
// Then
7020+
if !strings.Contains(out, "REASON") {
7021+
t.Errorf("output = %q, want the reason column", out)
7022+
}
7023+
if strings.Contains(out, "VARIANT") {
7024+
t.Errorf("output = %q, want no variant column where nothing was bucketed", out)
7025+
}
7026+
if err != nil {
7027+
t.Fatalf("evaluate: %v\noutput: %s", err, out)
7028+
}
7029+
})
7030+
69527031
t.Run("--js is the frontend SDK's hydration state", func(t *testing.T) {
69537032
// Given
69547033
f := evalEnv(t)
@@ -7172,6 +7251,32 @@ func TestEvaluate(t *testing.T) {
71727251
if strings.Contains(out, "onboarding_banner") {
71737252
t.Errorf("output = %q, want only the named feature", out)
71747253
}
7254+
for _, absent := range []string{"Reason", "Variant"} {
7255+
if strings.Contains(out, absent) {
7256+
t.Errorf("output = %q, want no %q the SDK API never reported", out, absent)
7257+
}
7258+
}
7259+
})
7260+
7261+
t.Run("a single feature's detail view says why it resolved", func(t *testing.T) {
7262+
// Given
7263+
f := evalEnv(t)
7264+
flags := sdkFlagsFrom(defaultFeatures())
7265+
flags[1]["reason"], flags[1]["variant"] = "SPLIT; weight=30", "treatment"
7266+
f.sdkEnvFlags["WqXhZk8sVY3dGgTqZ9pJmN"] = flags
7267+
7268+
// When
7269+
out, err := run("", "evaluate", "max_items")
7270+
7271+
// Then
7272+
if err != nil {
7273+
t.Fatalf("evaluate max_items: %v\noutput: %s", err, out)
7274+
}
7275+
for _, want := range []string{"Variant", "treatment", "Reason", "SPLIT; weight=30"} {
7276+
if !strings.Contains(out, want) {
7277+
t.Errorf("output = %q, want %q", out, want)
7278+
}
7279+
}
71757280
})
71767281

71777282
t.Run("an unknown feature errors with a hint", func(t *testing.T) {
@@ -7451,6 +7556,26 @@ func TestEvaluateIdentity(t *testing.T) {
74517556
}
74527557
})
74537558

7559+
t.Run("an identity's variant and reason survive the identify call", func(t *testing.T) {
7560+
// Given
7561+
f := evalEnv(t)
7562+
flags := overridden()
7563+
flags[1]["reason"], flags[1]["variant"] = "SPLIT; weight=50", "treatment"
7564+
f.sdkIdentityFlags["user-123"] = flags
7565+
7566+
// When
7567+
out, err := run("", "evaluate", "max_items", "--identity", "user-123", "--json")
7568+
7569+
// Then
7570+
if err != nil {
7571+
t.Fatalf("evaluate --identity --json: %v\noutput: %s", err, out)
7572+
}
7573+
doc := evalDoc(t, out)
7574+
if doc["reason"] != "SPLIT; weight=50" || doc["variant"] != "treatment" {
7575+
t.Errorf("doc = %+v, want the identity's reason and variant", doc)
7576+
}
7577+
})
7578+
74547579
// The flag commands name this --identifier; eval names it --identity. Both
74557580
// spellings work here, so nobody has to remember which command uses which.
74567581
for _, spelling := range []string{"--identity", "--identifier", "-i"} {

‎internal/cmd/evaluate.go‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,18 @@ type evalView struct {
2323
Feature string
2424
Enabled bool
2525
Value any
26+
Reason string
27+
Variant string
2628
}
2729

2830
func newEvalView(f flagsmith.Flag) evalView {
29-
return evalView{Feature: f.FeatureName, Enabled: f.Enabled, Value: f.Value}
31+
return evalView{
32+
Feature: f.FeatureName,
33+
Enabled: f.Enabled,
34+
Value: f.Value,
35+
Reason: f.Reason,
36+
Variant: f.Variant,
37+
}
3038
}
3139

3240
const evaluationResultSchema = "https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json"
@@ -44,10 +52,18 @@ type evalFlag struct {
4452
Name string `json:"name"`
4553
Enabled bool `json:"enabled"`
4654
Value any `json:"value"`
55+
Reason string `json:"reason,omitempty"`
56+
Variant string `json:"variant,omitempty"`
4757
}
4858

4959
func newEvalFlag(v evalView) evalFlag {
50-
return evalFlag{Name: v.Feature, Enabled: v.Enabled, Value: v.Value}
60+
return evalFlag{
61+
Name: v.Feature,
62+
Enabled: v.Enabled,
63+
Value: v.Value,
64+
Reason: v.Reason,
65+
Variant: v.Variant,
66+
}
5167
}
5268

5369
func newEvalResult(views []evalView) evalResult {
@@ -190,26 +206,51 @@ func renderEvaluation(cmd *cobra.Command, sdkURL string, views []evalView, singl
190206
"Warning: no flags to hydrate from — an SDK given this state will wait for a fetch")
191207
}
192208
}
209+
var anyReason, anyVariant bool
210+
for _, v := range views {
211+
anyReason = anyReason || v.Reason != ""
212+
anyVariant = anyVariant || v.Variant != ""
213+
}
193214
opts := outputOpts()
194215
opts.JSON = opts.JSON || evalJSFlag
195216
return output.Render(cmd.OutOrStdout(), doc, opts, func(w io.Writer) error {
196217
if single {
197218
v := views[0]
198-
return output.Detail(w, []output.Field{
219+
fields := []output.Field{
199220
{Label: "Feature", Value: v.Feature},
200221
{Label: "Enabled", Value: boolState(v.Enabled)},
201222
{Label: "Value", Value: valueDisplay(v.Value)},
202-
})
223+
}
224+
if v.Variant != "" {
225+
fields = append(fields, output.Field{Label: "Variant", Value: v.Variant})
226+
}
227+
if v.Reason != "" {
228+
fields = append(fields, output.Field{Label: "Reason", Value: v.Reason})
229+
}
230+
return output.Detail(w, fields)
203231
}
204232
if len(views) == 0 {
205233
fmt.Fprintln(w, "No flags.")
206234
return nil
207235
}
236+
headers := []string{"FEATURE", "ENABLED", "VALUE"}
237+
if anyVariant {
238+
headers = append(headers, "VARIANT")
239+
}
240+
if anyReason {
241+
headers = append(headers, "REASON")
242+
}
208243
rows := make([][]string, len(views))
209244
for i, v := range views {
210245
rows[i] = []string{v.Feature, boolState(v.Enabled), truncateValue(valueDisplay(v.Value))}
246+
if anyVariant {
247+
rows[i] = append(rows[i], v.Variant)
248+
}
249+
if anyReason {
250+
rows[i] = append(rows[i], v.Reason)
251+
}
211252
}
212-
if err := output.Table(w, []string{"FEATURE", "ENABLED", "VALUE"}, rows); err != nil {
253+
if err := output.Table(w, headers, rows); err != nil {
213254
return err
214255
}
215256
fmt.Fprintf(w, "\n%d %s\n", len(views), plural(len(views), "flag", "flags"))

0 commit comments

Comments
 (0)