Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add client-prereq-events capability #242

Merged
merged 2 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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})
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 @kinyoklion check out this setup I have.

  • I explicitly evaluate prereq1Key. This gets added to the summary event, including the default value I provided.
  • I then evaluate top level, which has a prereq on prereqs 1 and 2. This results in prereq2 getting added to the summary event without a default value.
  • We then evaluate prereq 2 explicitly, which just increments that value, but does not add the default value to the summary counter.

_ = 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"},
Copy link
Member Author

Choose a reason for hiding this comment

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

And yet here we have a prereq on an unknown flag. Once we explicitly evaluate the top level key here, the unknown flag is added to the summary event WITH a default value. What's up with that?

}

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
Loading