Skip to content

Commit

Permalink
feat: Add client-prereq-events capability (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
keelerm84 authored Oct 15, 2024
1 parent 3d96276 commit 3172672
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 0 deletions.
8 changes: 8 additions & 0 deletions docs/service_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ This means that the SDK supports Big Segments and can be configured with a custo

For tests that involve Big Segments, the test harness will provide parameters in the `bigSegments` property of the configuration object, including a `callbackUri` that points to one of the test harness's callback services (see [Callback endpoints](#callback-endpoints)). The test service should configure the SDK with its own implementation of a Big Segment store, where every method of the store delegates to a corresponding endpoint in the callback service.

#### Capability `"client-prereq-events"`

This means that the SDK supports client-side prerequisite events.

In short, this means that a client SDK emits prerequisite evaluation events along side the main evaluation event. This requires an updated understanding of the flag evaluation model.

For server side SDKs, this means `allFlagData` will reflect that updated flag evaluation model.

#### Capability `"context-type"`

This means that the SDK has its own type for evaluation contexts (as opposed to just representing them as a JSON-equivalent generic data structure) and convert that type to and from JSON.
Expand Down
1 change: 1 addition & 0 deletions mockld/sdk_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type ClientSDKFlag struct {
TrackEvents bool `json:"trackEvents"`
TrackReason bool `json:"trackReason"`
DebugEventsUntilDate o.Maybe[ldtime.UnixMillisecondTime] `json:"debugEventsUntilDate"`
Prerequisites []string `json:"prerequisites"`
}

// ClientSDKFlagWithKey is used only in stream updates, where the key is within the same object.
Expand Down
3 changes: 3 additions & 0 deletions sdktests/client_side_events_all.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func doClientSideEventTests(t *ldtest.T) {
t.Run("context properties", doClientSideEventContextTests)
t.Run("event capacity", doClientSideEventBufferTests)
t.Run("disabling", doClientSideEventDisableTests)

t.RequireCapability(servicedef.CapabilityClientPrereqEvents)
t.Run("prerequisite events emit in order", doClientSideInOrderPrereqEventTests)
}

func doClientSideEventRequestTests(t *ldtest.T) {
Expand Down
74 changes: 74 additions & 0 deletions sdktests/client_side_events_eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/launchdarkly/sdk-test-harness/v2/data"
h "github.com/launchdarkly/sdk-test-harness/v2/framework/helpers"
"github.com/launchdarkly/sdk-test-harness/v2/framework/ldtest"
o "github.com/launchdarkly/sdk-test-harness/v2/framework/opt"
"github.com/launchdarkly/sdk-test-harness/v2/mockld"
"github.com/launchdarkly/sdk-test-harness/v2/servicedef"

Expand Down Expand Up @@ -253,6 +254,79 @@ func doClientSideFeatureEventTests(t *ldtest.T) {
})
}

func doClientSideInOrderPrereqEventTests(t *ldtest.T) {
dataBuilder := mockld.NewClientSDKDataBuilder()
dataBuilder.
Flag("topLevel", mockld.ClientSDKFlag{
Value: ldvalue.String("value1"),
Variation: o.Some(0),
TrackEvents: true,
Prerequisites: []string{"prereq1", "preqreq2"},
}).
Flag("prereq1", mockld.ClientSDKFlag{
Value: ldvalue.String("value2"),
TrackEvents: true,
Variation: o.Some(0),
Prerequisites: []string{"prereq2"},
}).
Flag("prereq2", mockld.ClientSDKFlag{
Value: ldvalue.String("value3"),
TrackEvents: true,
Variation: o.Some(0),
})
dataSource := NewSDKDataSource(t, dataBuilder.Build())
context := ldcontext.New("user")

events := NewSDKEventSink(t)
client := NewSDKClient(t,
WithClientSideInitialContext(context),
dataSource, events)

_ = client.EvaluateFlag(t, servicedef.EvaluateFlagParams{
FlagKey: "topLevel",
DefaultValue: ldvalue.Null(),
ValueType: servicedef.ValueTypeAny,
})

client.FlushEvents(t)
payload := events.ExpectAnalyticsEvents(t, defaultEventTimeout)

prereq2FeatureEvent := IsValidFeatureEventWithConditions(
t, false, context,
m.JSONProperty("key").Should(m.Equal("prereq2")),
m.JSONProperty("version").Should(m.Equal(0)),
m.JSONProperty("value").Should(m.JSONEqual(ldvalue.String("value3"))),
m.JSONOptProperty("variation").Should(m.JSONEqual(0)),
JSONPropertyNullOrAbsent("prereqOf"),
)

prereq1FeatureEvent := IsValidFeatureEventWithConditions(
t, false, context,
m.JSONProperty("key").Should(m.Equal("prereq1")),
m.JSONProperty("version").Should(m.Equal(0)),
m.JSONProperty("value").Should(m.JSONEqual(ldvalue.String("value2"))),
m.JSONOptProperty("variation").Should(m.JSONEqual(0)),
JSONPropertyNullOrAbsent("prereqOf"),
)

topLevelFeatureEvent := IsValidFeatureEventWithConditions(
t, false, context,
m.JSONProperty("key").Should(m.Equal("topLevel")),
m.JSONProperty("version").Should(m.Equal(0)),
m.JSONProperty("value").Should(m.JSONEqual(ldvalue.String("value1"))),
m.JSONOptProperty("variation").Should(m.JSONEqual(0)),
JSONPropertyNullOrAbsent("prereqOf"),
)

m.In(t).Assert(payload, m.ItemsInAnyOrder(
IsIdentifyEventForContext(context),
prereq2FeatureEvent,
prereq1FeatureEvent,
topLevelFeatureEvent,
IsSummaryEvent(),
))
}

func doClientSideDebugEventTests(t *ldtest.T) {
// These tests could misbehave if the system clocks of the host that's running the test harness
// and the host that's running the test service are out of sync by at least an hour. However,
Expand Down
151 changes: 151 additions & 0 deletions sdktests/client_side_events_summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ func doClientSideSummaryEventTests(t *ldtest.T) {
t.Run("context kinds", doClientSideSummaryEventContextKindsTest)
t.Run("unknown flag", doClientSideSummaryEventUnknownFlagTest)
t.Run("reset after each flush", doClientSideSummaryEventResetTest)

t.Run("prerequisites", func(t *ldtest.T) {
t.RequireCapability(servicedef.CapabilityClientPrereqEvents)
t.Run("basic behavior", doClientSideSummaryBasicPrereqTest)
t.Run("emits unknown event", doClientSideSummaryPrereqUnknownFlagTest)
})
}

func doClientSideSummaryEventBasicTest(t *ldtest.T) {
Expand Down Expand Up @@ -276,3 +282,148 @@ func doClientSideSummaryEventResetTest(t *ldtest.T) {
)),
)
}

func doClientSideSummaryBasicPrereqTest(t *ldtest.T) {
topLevelKey := "flag1"
topLevelResult := mockld.ClientSDKFlag{
Value: ldvalue.String("value1-a"),
Variation: o.Some(0),
FlagVersion: o.Some(1),
Version: 11,
Prerequisites: []string{"prereq1", "prereq2"},
}

prereq1Key := "prereq1"
prereq1Result := mockld.ClientSDKFlag{
Value: ldvalue.String("prereq1"),
Variation: o.Some(0),
FlagVersion: o.Some(1),
Version: 11,
Prerequisites: []string{"prereq3"},
}

prereq2Key := "prereq2"
prereq2Result := mockld.ClientSDKFlag{
Value: ldvalue.String("prereq2"),
Variation: o.Some(0),
FlagVersion: o.Some(1),
Version: 11,
}

prereq3Key := "prereq3"
prereq3Result := mockld.ClientSDKFlag{
Value: ldvalue.String("prereq3"),
Variation: o.Some(0),
FlagVersion: o.Some(1),
Version: 11,
}

contextA := ldcontext.New("user-a")
default1 := ldvalue.String("default1")
default2 := ldvalue.String("default2")
default3 := ldvalue.String("default3")

dataBuilder := mockld.NewClientSDKDataBuilder()
dataBuilder.Flag(topLevelKey, topLevelResult).
Flag(prereq1Key, prereq1Result).
Flag(prereq2Key, prereq2Result).
Flag(prereq3Key, prereq3Result)

dataSource := NewSDKDataSource(t, dataBuilder.Build())
events := NewSDKEventSinkWithGzip(t, t.Capabilities().Has(servicedef.CapabilityEventGzip))
client := NewSDKClient(t,
WithClientSideInitialContext(contextA),
dataSource, events)

_ = client.EvaluateFlag(t, servicedef.EvaluateFlagParams{FlagKey: prereq1Key, DefaultValue: default1})
_ = client.EvaluateFlag(t, servicedef.EvaluateFlagParams{FlagKey: topLevelKey, DefaultValue: default2})
_ = client.EvaluateFlag(t, servicedef.EvaluateFlagParams{FlagKey: prereq2Key, DefaultValue: default3})

client.FlushEvents(t)
payload := events.ExpectAnalyticsEvents(t, defaultEventTimeout)

m.In(t).Assert(payload, m.ItemsInAnyOrder(
IsIdentifyEventForContext(contextA),
IsValidSummaryEventWithFlags(
m.KV(topLevelKey, m.MapOf(
// Was first evaluated through the EvaluateFlag call, so it has a default value.
m.KV("default", m.JSONEqual(default2)),
m.KV("counters", m.ItemsInAnyOrder(
flagCounter(topLevelResult.Value, topLevelResult.Variation.Value(), topLevelResult.FlagVersion.Value(), 1),
)),
m.KV("contextKinds", anyContextKindsList()),
)),
m.KV(prereq1Key, m.MapOf(
// Was first evaluated through the EvaluateFlag call, so it has a default value.
m.KV("default", m.JSONEqual(default1)),
m.KV("counters", m.ItemsInAnyOrder(
flagCounter(prereq1Result.Value, prereq1Result.Variation.Value(), prereq1Result.FlagVersion.Value(), 2),
)),
m.KV("contextKinds", anyContextKindsList()),
)),
m.KV(prereq2Key, m.MapOf(
m.KV("counters", m.ItemsInAnyOrder(
flagCounter(prereq2Result.Value, prereq2Result.Variation.Value(), prereq2Result.FlagVersion.Value(), 2),
)),
m.KV("contextKinds", anyContextKindsList()),
)),
m.KV(prereq3Key, m.MapOf(
m.KV("counters", m.ItemsInAnyOrder(
flagCounter(prereq3Result.Value, prereq3Result.Variation.Value(), prereq3Result.FlagVersion.Value(), 2),
)),
m.KV("contextKinds", anyContextKindsList()),
)),
)),
)
}

func doClientSideSummaryPrereqUnknownFlagTest(t *ldtest.T) {
topLevelKey := "flag1"
topLevelResult := mockld.ClientSDKFlag{
Value: ldvalue.String("value1-a"),
Variation: o.Some(0),
FlagVersion: o.Some(1),
Version: 11,
Prerequisites: []string{"unknown"},
}

contextA := ldcontext.New("user-a")
default1 := ldvalue.String("default1")

dataBuilder := mockld.NewClientSDKDataBuilder()
dataBuilder.Flag(topLevelKey, topLevelResult)

dataSource := NewSDKDataSource(t, dataBuilder.Build())
events := NewSDKEventSinkWithGzip(t, t.Capabilities().Has(servicedef.CapabilityEventGzip))
client := NewSDKClient(t,
WithClientSideInitialContext(contextA),
dataSource, events)

_ = client.EvaluateFlag(t, servicedef.EvaluateFlagParams{FlagKey: topLevelKey, DefaultValue: default1})

client.FlushEvents(t)
payload := events.ExpectAnalyticsEvents(t, defaultEventTimeout)

m.In(t).Assert(payload, m.ItemsInAnyOrder(
IsIdentifyEventForContext(contextA),
IsValidSummaryEventWithFlags(
m.KV(topLevelKey, m.MapOf(
// Was first evaluated through the EvaluateFlag call, so it has a default value.
m.KV("default", m.JSONEqual(default1)),
m.KV("counters", m.ItemsInAnyOrder(
flagCounter(topLevelResult.Value, topLevelResult.Variation.Value(), topLevelResult.FlagVersion.Value(), 1),
)),
m.KV("contextKinds", anyContextKindsList()),
)),
m.KV("unknown", m.AllOf(
m.JSONOptProperty("default").Should(m.BeNil()),
m.MapIncluding(
m.KV("counters", m.ItemsInAnyOrder(
unknownFlagCounter(ldvalue.Null(), 1),
)),
m.KV("contextKinds", anyContextKindsList()),
),
)),
)),
)
}
Loading

0 comments on commit 3172672

Please sign in to comment.