Skip to content

Commit

Permalink
feat: add support for client-side prerequisite events (#452)
Browse files Browse the repository at this point in the history
This PR updates Relay's client-side evaluation logic to track the
top-level prerequisites associated with a flag.

These prerequisites are then passed back in the response as a
`prerequisites` key, allowing client-side SDKs to properly emit
prerequisite events.
  • Loading branch information
cwaldren-ld authored Oct 17, 2024
1 parent 57277e3 commit 9dea4b5
Show file tree
Hide file tree
Showing 6 changed files with 565 additions and 2 deletions.
8 changes: 8 additions & 0 deletions integrationtests/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ func TestEndToEnd(t *testing.T) {
testStandardMode(t, manager)
})

t.Run("standard mode with prerequisites", func(t *testing.T) {
// The standard tests above use simple flags that don't contain prerequisite relationships. This
// test explicitly configures flags with prerequisites, and verifies that client side flag evals contain
// prerequisite metadata for each flag. This information needs to be passed to client-side SDKs so that they
// can generate prerequisite events to power LaunchDarkly SaaS features.
testStandardModeWithPrerequisites(t, manager)
})

t.Run("standard mode with payload filters", func(t *testing.T) {
t.Run("default filters", func(t *testing.T) {
// This case is similar to the "standard mode" test above, except with payload filtering in the picture.
Expand Down
4 changes: 3 additions & 1 deletion integrationtests/api_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package integrationtests
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
Expand Down Expand Up @@ -32,7 +33,8 @@ func (a *apiHelper) logResult(desc string, err error) error {
return nil
}
addInfo := ""
if gse, ok := err.(ldapi.GenericOpenAPIError); ok {
var gse *ldapi.GenericOpenAPIError
if errors.As(err, &gse) {
body := string(gse.Body())
addInfo = " - " + string(body)
}
Expand Down
137 changes: 137 additions & 0 deletions integrationtests/flag_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//go:build integrationtests

package integrationtests

import (
"fmt"

ldapi "github.com/launchdarkly/api-client-go/v13"
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
)

type flagBuilder struct {
key string
projectKey string
envKey string
offVariation int
fallthroughVariation int
on bool
variations []ldapi.Variation
prerequisites []ldapi.Prerequisite
clientSide ldapi.ClientSideAvailabilityPost
helper *apiHelper
}

// newFlagBuilder creates a builder for a flag which will be created in the specified project and environment.
// By default, the flag has two variations: off = false, and on = true. The flag is on by default.
// Additionally, the flag is available to both mobile and client SDKs.
func newFlagBuilder(helper *apiHelper, flagKey string, projectKey string, envKey string) *flagBuilder {
builder := &flagBuilder{
key: flagKey,
projectKey: projectKey,
envKey: envKey,
on: true,
offVariation: 0,
fallthroughVariation: 1,
helper: helper,
clientSide: ldapi.ClientSideAvailabilityPost{
UsingMobileKey: true,
UsingEnvironmentId: true,
},
}
return builder.Variations(ldvalue.Bool(false), ldvalue.Bool(true))
}

// Variations overwrites the flag's variations. A valid flag has two or more variations.
func (f *flagBuilder) Variations(variation1 ldvalue.Value, variations ...ldvalue.Value) *flagBuilder {
f.variations = nil
for _, value := range append([]ldvalue.Value{variation1}, variations...) {
valueAsInterface := value.AsArbitraryValue()
f.variations = append(f.variations, ldapi.Variation{Value: &valueAsInterface})
}
return f
}

// ClientSideUsingEnvironmentID enables the flag for clients that use environment ID for auth.
func (f *flagBuilder) ClientSideUsingEnvironmentID(usingEnvID bool) *flagBuilder {
f.clientSide.UsingEnvironmentId = usingEnvID
return f
}

// ClientSideUsingMobileKey enables the flag for clients that use mobile keys for auth.
func (f *flagBuilder) ClientSideUsingMobileKey(usingMobileKey bool) *flagBuilder {
f.clientSide.UsingMobileKey = usingMobileKey
return f
}

// Prerequisites overwrites the flag's prerequisites.
func (f *flagBuilder) Prerequisites(prerequisites []ldapi.Prerequisite) *flagBuilder {
f.prerequisites = prerequisites
return f
}

// Prerequisite is a helper that calls Prerequisites with a single value.
func (f *flagBuilder) Prerequisite(prerequisiteKey string, variation int32) *flagBuilder {
return f.Prerequisites([]ldapi.Prerequisite{{Key: prerequisiteKey, Variation: variation}})
}

// OffVariation sets the flag's off variation.
func (f *flagBuilder) OffVariation(v int) *flagBuilder {
f.offVariation = v
return f
}

// FallthroughVariation sets the flag's fallthrough variation.
func (f *flagBuilder) FallthroughVariation(v int) *flagBuilder {
f.fallthroughVariation = v
return f
}

// On enables or disables flag targeting.
func (f *flagBuilder) On(on bool) *flagBuilder {
f.on = on
return f
}

// Create creates the flag using the LD REST API.
func (f *flagBuilder) Create() error {
flagPost := ldapi.FeatureFlagBody{
Name: f.key,
Key: f.key,
ClientSideAvailability: &f.clientSide,
}

_, _, err := f.helper.apiClient.FeatureFlagsApi.
PostFeatureFlag(f.helper.apiContext, f.projectKey).
FeatureFlagBody(flagPost).
Execute()

if err := f.logAPIResult("create flag", err); err != nil {
return err
}

envPrefix := fmt.Sprintf("/environments/%s", f.envKey)
patch := ldapi.PatchWithComment{
Patch: []ldapi.PatchOperation{
makePatch("replace", envPrefix+"/offVariation", f.offVariation),
makePatch("replace", envPrefix+"/fallthrough/variation", f.fallthroughVariation),
makePatch("replace", envPrefix+"/on", f.on),
makePatch("replace", envPrefix+"/prerequisites", f.prerequisites),
},
}

_, _, err = f.helper.apiClient.FeatureFlagsApi.
PatchFeatureFlag(f.helper.apiContext, f.projectKey, f.key).
PatchWithComment(patch).
Execute()

return f.logAPIResult("patch flag", err)
}

func (f *flagBuilder) logAPIResult(desc string, err error) error {
return f.helper.logResult(f.scopedOp(desc), err)
}

func (f *flagBuilder) scopedOp(desc string) string {
return fmt.Sprintf("%s %s in %s/%s", desc, f.key, f.projectKey, f.envKey)
}
Loading

0 comments on commit 9dea4b5

Please sign in to comment.