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 support for client-side prerequisite events #452

Merged
merged 16 commits into from
Oct 17, 2024
Merged
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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This little bug (the error is a pointer) was causing response bodies not to be logged.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a whole new builder scheme just for these tests - we need a simple way to create flags via REST API. The existing methods were single-purpose.


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