Skip to content

Commit

Permalink
feat: add support for client-side prerequisite events (#201)
Browse files Browse the repository at this point in the history
This commit updates `AllFlagsState` to track prerequisite evaluations. 

This didn't require modifying the eval module, as it already exposes a
`PrerequisiteEventRecorder` interface with the necessary info.

The `AllFlagsState` public API allows for fetching a flag's details
(`FlagState`) from the top-level `AllFlags` object. This struct now has
prerequisite information exposed.

Additionally when the `AllFlags` is marshaled to JSON, it will now
contain the prerequisite relationships for each flag.
  • Loading branch information
cwaldren-ld authored Oct 22, 2024
1 parent ece9cab commit d9804ec
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/common_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: make workspace
- name: Start test service in background
run: make start-contract-test-service-bg
- uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.0.0
- uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.1.0
continue-on-error: true
with:
test_service_port: ${{ env.TEST_SERVICE_PORT }}
Expand Down
11 changes: 11 additions & 0 deletions interfaces/flagstate/flags_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ type FlagState struct {
// OmitDetails is true if, based on the options passed to AllFlagsState and the flag state, some of the
// metadata can be left out of the JSON representation.
OmitDetails bool

// Prerequisites is an ordered list of direct prerequisites that were evaluated in the process of evaluating this
// flag.
Prerequisites []string
}

// Option is the interface for optional parameters that can be passed to LDClient.AllFlagsState.
Expand Down Expand Up @@ -156,6 +160,13 @@ func (a AllFlags) MarshalJSON() ([]byte, error) {
flagObj.Maybe("trackEvents", flag.TrackEvents).Bool(flag.TrackEvents)
flagObj.Maybe("trackReason", flag.TrackReason).Bool(flag.TrackReason)
flagObj.Maybe("debugEventsUntilDate", flag.DebugEventsUntilDate > 0).Float64(float64(flag.DebugEventsUntilDate))
if len(flag.Prerequisites) > 0 {
prerequisites := flagObj.Name("prerequisites").Array()
for _, p := range flag.Prerequisites {
prerequisites.String(p)
}
prerequisites.End()
}
flagObj.End()
}
stateObj.End()
Expand Down
63 changes: 61 additions & 2 deletions interfaces/flagstate/flags_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func TestAllFlagsJSON(t *testing.T) {
Reason: ldreason.NewEvalReasonFallthrough(),
TrackEvents: true,
DebugEventsUntilDate: ldtime.UnixMillisecondTime(100000),
Prerequisites: []string{"flag2", "flag3", "flag4"},
},
},
}
Expand All @@ -109,7 +110,8 @@ func TestAllFlagsJSON(t *testing.T) {
"$valid":true,
"flag1": "value1",
"$flagsState":{
"flag1": {"variation":1,"version":1000,"reason":{"kind":"FALLTHROUGH"},"trackEvents":true,"debugEventsUntilDate":100000}
"flag1": {"variation":1,"version":1000,"reason":{"kind":"FALLTHROUGH"},"trackEvents":true,"debugEventsUntilDate":100000,
"prerequisites": ["flag2","flag3","flag4"]}
}
}`, string(bytes))
})
Expand Down Expand Up @@ -140,7 +142,7 @@ func TestAllFlagsJSON(t *testing.T) {
}`, string(bytes))
})

t.Run("omitting details", func(t *testing.T) {
t.Run("omitting details, no prerequisites present", func(t *testing.T) {
a := AllFlags{
valid: true,
flags: map[string]FlagState{
Expand All @@ -162,6 +164,32 @@ func TestAllFlagsJSON(t *testing.T) {
"$flagsState":{
"flag1": {"variation":1}
}
}`, string(bytes))
})

t.Run("omitting details, prerequisites present", func(t *testing.T) {
a := AllFlags{
valid: true,
flags: map[string]FlagState{
"flag1": {
Value: ldvalue.String("value1"),
Variation: ldvalue.NewOptionalInt(1),
Version: 1000,
Reason: ldreason.NewEvalReasonFallthrough(),
OmitDetails: true,
Prerequisites: []string{"flag2", "flag3", "flag4"},
},
},
}
bytes, err := a.MarshalJSON()
assert.NoError(t, err)
assert.JSONEq(t,
`{
"$valid":true,
"flag1": "value1",
"$flagsState":{
"flag1": {"variation":1, "prerequisites": ["flag2","flag3","flag4"]}
}
}`, string(bytes))
})
}
Expand Down Expand Up @@ -295,6 +323,37 @@ func TestAllFlagsBuilder(t *testing.T) {
"flag5": flag5,
}, a.flags)
})

t.Run("add flags with prerequisites", func(t *testing.T) {
b := NewAllFlagsBuilder()

flag1 := FlagState{
Value: ldvalue.String("value1"),
Variation: ldvalue.NewOptionalInt(1),
Version: 1000,
Prerequisites: []string{"flag2"},
}
flag2 := FlagState{
Value: ldvalue.String("value2"),
Version: 2000,
Prerequisites: []string{"flag3"},
}
flag3 := FlagState{
Value: ldvalue.String("value3"),
Version: 3000,
}

b.AddFlag("flag1", flag1)
b.AddFlag("flag2", flag2)
b.AddFlag("flag3", flag3)

a := b.Build()
assert.Equal(t, map[string]FlagState{
"flag1": flag1,
"flag2": flag2,
"flag3": flag3,
}, a.flags)
})
}

func TestAllFlagsOptions(t *testing.T) {
Expand Down
8 changes: 7 additions & 1 deletion ldclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,12 @@ func (client *LDClient) AllFlagsState(context ldcontext.Context, options ...flag
continue
}

result := client.evaluator.Evaluate(flag, context, nil)
var prerequisites []string
result := client.evaluator.Evaluate(flag, context, func(event ldeval.PrerequisiteFlagEvent) {
if event.TargetFlagKey == flag.Key {
prerequisites = append(prerequisites, event.PrerequisiteFlag.Key)
}
})

state.AddFlag(
item.Key,
Expand All @@ -696,6 +701,7 @@ func (client *LDClient) AllFlagsState(context ldcontext.Context, options ...flag
TrackEvents: flag.TrackEvents || result.IsExperiment,
TrackReason: result.IsExperiment,
DebugEventsUntilDate: flag.DebugEventsUntilDate,
Prerequisites: prerequisites,
},
)
}
Expand Down
Loading

0 comments on commit d9804ec

Please sign in to comment.