Skip to content

Commit

Permalink
Add v4 endpoints (#58)
Browse files Browse the repository at this point in the history
* add v4 endpoints for visitor_config and split_registry

* add more tests

* bump version to 1.2.0

new features -> new minor version

or at least that's my read of semver today :)
  • Loading branch information
samandmoore authored Jan 20, 2021
1 parent 2fb6aaf commit 48302ef
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 8 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SHELL = /bin/sh

VERSION=1.1.3
VERSION=1.2.0
BUILD=`git rev-parse HEAD`

LDFLAGS=-ldflags "-w -s \
Expand Down
119 changes: 116 additions & 3 deletions fakeserver/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ type v1Visitor struct {
Assignments []v1Assignment `json:"assignments"`
}

// v4Visitor is the JSON output type for V4 visitor_config endpoints
type v4Visitor struct {
ID string `json:"id"`
Assignments []v4Assignment `json:"assignments"`
}

// v1Assignment is the JSON input/output type for V1 visitor endpoints
type v1Assignment struct {
SplitName string `json:"split_name"`
Expand All @@ -26,6 +32,12 @@ type v1Assignment struct {
Unsynced bool `json:"unsynced"`
}

// v4Assignment is the JSON input/output type for V4 visitor_config endpoints
type v4Assignment struct {
SplitName string `json:"split_name"`
Variant string `json:"variant"`
}

// v2AssignmentOverrideRequestBody is the JSON input for the V2 assignment override endpoint
type v2AssignmentOverrideRequestBody struct {
Assignments []v1Assignment `json:"assignments"`
Expand All @@ -44,18 +56,44 @@ type v2VisitorConfig struct {
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
}

// v4VisitorConfig is the JSON output type for V4 visitor_config endpoints
type v4VisitorConfig struct {
Splits []v4Split `json:"splits"`
Visitor v4Visitor `json:"visitor"`
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
}

// v2SplitRegistry is the JSON output type for V2 split_registry endpoint
type v2SplitRegistry struct {
Splits map[string]*v2Split `json:"splits"`
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
}

// v2SplitRegistry is the JSON output type for V2 split_registry endpoint
// v4SplitRegistry is the JSON output type for V4 split_registry endpoint
type v4SplitRegistry struct {
Splits []v4Split `json:"splits"`
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
}

// v2Split is the JSON output type for V2 split_registry endpoint
type v2Split struct {
Weights map[string]int `json:"weights"`
FeatureGate bool `json:"feature_gate"`
}

// v4Split is the JSON output type for V4 split_registry endpoint
type v4Split struct {
Name string `json:"name"`
Variants []v4Variant `json:"variants"`
FeatureGate bool `json:"feature_gate"`
}

// v4Split is the JSON output type for V4 split_registry endpoint
type v4Variant struct {
Name string `json:"name"`
Weight int `json:"weight"`
}

// v1SplitDetail is the JSON output type for the V1 split detail endpoint
type v1SplitDetail struct {
Name string `json:"name"`
Expand Down Expand Up @@ -126,13 +164,17 @@ func (s *server) routes() {
getV1AppVisitorConfig,
)
s.handleGet(
"/api/v2/apps/{a}/versions/{v}/builds/{b}/visitors/{id}/config",
getV2AppVisitorConfig,
"/api/v4/apps/{a}/versions/{v}/builds/{b}/visitors/{id}/config",
getV4AppVisitorConfig,
)
s.handleGet(
"/api/v1/apps/{a}/versions/{v}/builds/{b}/identifier_types/{t}/identifiers/{i}/visitor_config",
getV1AppVisitorConfig,
)
s.handleGet(
"/api/v4/apps/{a}/versions/{v}/builds/{b}/identifier_types/{t}/identifiers/{i}/visitor_config",
getV4AppVisitorConfig,
)
s.handleGet(
"/api/v1/split_details/{id}",
getV1SplitDetail,
Expand All @@ -141,6 +183,10 @@ func (s *server) routes() {
"/api/v3/builds/{b}/split_registry",
getV2PlusSplitRegistry,
)
s.handleGet(
"/api/v4/builds/{b}/split_registry",
getV4SplitRegistry,
)
}

func getV1SplitRegistry() (interface{}, error) {
Expand Down Expand Up @@ -181,6 +227,37 @@ func getV2PlusSplitRegistry() (interface{}, error) {
}, nil
}

func getV4SplitRegistry() (interface{}, error) {
schema, err := schema.ReadMerged()
if err != nil {
return nil, err
}
v4Splits := make([]v4Split, 0, len(schema.Splits))
for _, split := range schema.Splits {
isFeatureGate := splits.IsFeatureGateFromName(split.Name)
weights, err := splits.WeightsFromYAML(split.Weights)
if err != nil {
return nil, err
}
v4Variants := make([]v4Variant, 0, len(*weights))
for variantName, weight := range *weights {
v4Variants = append(v4Variants, v4Variant{
Name: variantName,
Weight: weight,
})
}
v4Splits = append(v4Splits, v4Split{
Name: split.Name,
Variants: v4Variants,
FeatureGate: isFeatureGate,
})
}
return v4SplitRegistry{
Splits: v4Splits,
ExperienceSamplingWeight: 1,
}, nil
}

func postNoop(*http.Request) error {
return nil
}
Expand Down Expand Up @@ -214,6 +291,24 @@ func getV1Visitor() (interface{}, error) {
}, nil
}

func getV4Visitor() (interface{}, error) {
assignments, err := fakeassignments.Read()
if err != nil {
return nil, err
}
v4Assignments := make([]v4Assignment, 0, len(*assignments))
for split, variant := range *assignments {
v4Assignments = append(v4Assignments, v4Assignment{
SplitName: split,
Variant: variant,
})
}
return v4Visitor{
ID: "00000000-0000-0000-0000-000000000000",
Assignments: v4Assignments,
}, nil
}

func getV1VisitorDetail() (interface{}, error) {
assignments, err := fakeassignments.Read()
if err != nil {
Expand Down Expand Up @@ -312,6 +407,24 @@ func getV1AppVisitorConfig() (interface{}, error) {
}, nil
}

func getV4AppVisitorConfig() (interface{}, error) {
isplitRegistry, err := getV4SplitRegistry()
splitRegistry := isplitRegistry.(v4SplitRegistry)
if err != nil {
return nil, err
}
ivisitor, err := getV4Visitor()
visitor := ivisitor.(v4Visitor)
if err != nil {
return nil, err
}
return v4VisitorConfig{
Splits: splitRegistry.Splits,
Visitor: visitor,
ExperienceSamplingWeight: splitRegistry.ExperienceSamplingWeight,
}, nil
}

func getV2AppVisitorConfig() (interface{}, error) {
isplitRegistry, err := getV2PlusSplitRegistry()
splitRegistry := isplitRegistry.(v2SplitRegistry)
Expand Down
80 changes: 76 additions & 4 deletions fakeserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ splits:
treatment: 40
`

var testAssignments = `
something_something_enabled: "true"
`

func TestMain(m *testing.M) {
current, exists := os.LookupEnv("TESTTRACK_FAKE_SERVER_CONFIG_DIR")

Expand All @@ -53,6 +57,11 @@ func TestMain(m *testing.M) {
log.Fatal(err)
}

assignmentsContent := []byte(testAssignments)
if err := ioutil.WriteFile(filepath.Join(dir, "assignments.yml"), assignmentsContent, 0644); err != nil {
log.Fatal(err)
}

os.Setenv("TESTTRACK_FAKE_SERVER_CONFIG_DIR", dir)
exitCode := m.Run()
if exists {
Expand Down Expand Up @@ -97,6 +106,73 @@ func TestSplitRegistry(t *testing.T) {
require.Equal(t, 40, registry.Splits["test.test_experiment"].Weights["treatment"])
require.Equal(t, false, registry.Splits["test.test_experiment"].FeatureGate)
})

t.Run("it loads split registry v4", func(t *testing.T) {
w := httptest.NewRecorder()
h := createHandler()

h.ServeHTTP(w, httptest.NewRequest("GET", "/api/v4/builds/2020-01-02T03:04:05/split_registry", nil))

require.Equal(t, http.StatusOK, w.Code)

registry := v4SplitRegistry{}
err := json.Unmarshal(w.Body.Bytes(), &registry)
require.Nil(t, err)

require.Equal(t, 1, registry.ExperienceSamplingWeight)
require.Equal(t, "test.test_experiment", registry.Splits[0].Name)
require.Equal(t, 60, registry.Splits[0].Variants[0].Weight)
require.Equal(t, 40, registry.Splits[0].Variants[1].Weight)
require.Equal(t, false, registry.Splits[0].FeatureGate)
})
}

func TestVisitorConfig(t *testing.T) {
t.Run("it loads visitor config v4", func(t *testing.T) {
w := httptest.NewRecorder()
h := createHandler()

h.ServeHTTP(w, httptest.NewRequest("GET", "/api/v4/apps/foo/versions/1/builds/2020-01-02T03:04:05/visitors/00000000-0000-0000-0000-000000000000/config", nil))

require.Equal(t, http.StatusOK, w.Code)

visitorConfig := v4VisitorConfig{}
err := json.Unmarshal(w.Body.Bytes(), &visitorConfig)
require.Nil(t, err)

require.Equal(t, 1, visitorConfig.ExperienceSamplingWeight)
require.Equal(t, "test.test_experiment", visitorConfig.Splits[0].Name)
require.Equal(t, 60, visitorConfig.Splits[0].Variants[0].Weight)
require.Equal(t, 40, visitorConfig.Splits[0].Variants[1].Weight)
require.Equal(t, false, visitorConfig.Splits[0].FeatureGate)
require.Equal(t, "00000000-0000-0000-0000-000000000000", visitorConfig.Visitor.ID)
require.Equal(t, "something_something_enabled", visitorConfig.Visitor.Assignments[0].SplitName)
require.Equal(t, "true", visitorConfig.Visitor.Assignments[0].Variant)
})
}

func TestIdentifierVisitorConfig(t *testing.T) {
t.Run("it loads visitor config v4", func(t *testing.T) {
w := httptest.NewRecorder()
h := createHandler()

h.ServeHTTP(w, httptest.NewRequest("GET", "/api/v4/apps/foo/versions/1/builds/2020-01-02T03:04:05/identifier_types/user_id/identifiers/123/visitor_config", nil))

require.Equal(t, http.StatusOK, w.Code)

visitorConfig := v4VisitorConfig{}
err := json.Unmarshal(w.Body.Bytes(), &visitorConfig)
require.Nil(t, err)

require.Equal(t, 1, visitorConfig.ExperienceSamplingWeight)
require.Equal(t, "test.test_experiment", visitorConfig.Splits[0].Name)
require.Equal(t, 60, visitorConfig.Splits[0].Variants[0].Weight)
require.Equal(t, 40, visitorConfig.Splits[0].Variants[1].Weight)
require.Equal(t, false, visitorConfig.Splits[0].FeatureGate)
require.Equal(t, "00000000-0000-0000-0000-000000000000", visitorConfig.Visitor.ID)
require.Equal(t, "something_something_enabled", visitorConfig.Visitor.Assignments[0].SplitName)
require.Equal(t, "true", visitorConfig.Visitor.Assignments[0].Variant)
})
}

func TestCors(t *testing.T) {
Expand Down Expand Up @@ -132,8 +208,6 @@ func TestCors(t *testing.T) {
}

func TestPersistAssignment(t *testing.T) {
os.Remove("testdata/assignments.yml")

t.Run("it persists assignments to yaml", func(t *testing.T) {
w := httptest.NewRecorder()
h := createHandler()
Expand All @@ -157,8 +231,6 @@ func TestPersistAssignment(t *testing.T) {
}

func TestPersistAssignmentV2(t *testing.T) {
os.Remove("testdata/assignments.yml")

t.Run("it persists assignments to yaml", func(t *testing.T) {
w := httptest.NewRecorder()
h := createHandler()
Expand Down

0 comments on commit 48302ef

Please sign in to comment.