Skip to content

Commit 48302ef

Browse files
authored
Add v4 endpoints (#58)
* 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 :)
1 parent 2fb6aaf commit 48302ef

File tree

3 files changed

+193
-8
lines changed

3 files changed

+193
-8
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
SHELL = /bin/sh
22

3-
VERSION=1.1.3
3+
VERSION=1.2.0
44
BUILD=`git rev-parse HEAD`
55

66
LDFLAGS=-ldflags "-w -s \

fakeserver/routes.go

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ type v1Visitor struct {
1818
Assignments []v1Assignment `json:"assignments"`
1919
}
2020

21+
// v4Visitor is the JSON output type for V4 visitor_config endpoints
22+
type v4Visitor struct {
23+
ID string `json:"id"`
24+
Assignments []v4Assignment `json:"assignments"`
25+
}
26+
2127
// v1Assignment is the JSON input/output type for V1 visitor endpoints
2228
type v1Assignment struct {
2329
SplitName string `json:"split_name"`
@@ -26,6 +32,12 @@ type v1Assignment struct {
2632
Unsynced bool `json:"unsynced"`
2733
}
2834

35+
// v4Assignment is the JSON input/output type for V4 visitor_config endpoints
36+
type v4Assignment struct {
37+
SplitName string `json:"split_name"`
38+
Variant string `json:"variant"`
39+
}
40+
2941
// v2AssignmentOverrideRequestBody is the JSON input for the V2 assignment override endpoint
3042
type v2AssignmentOverrideRequestBody struct {
3143
Assignments []v1Assignment `json:"assignments"`
@@ -44,18 +56,44 @@ type v2VisitorConfig struct {
4456
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
4557
}
4658

59+
// v4VisitorConfig is the JSON output type for V4 visitor_config endpoints
60+
type v4VisitorConfig struct {
61+
Splits []v4Split `json:"splits"`
62+
Visitor v4Visitor `json:"visitor"`
63+
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
64+
}
65+
4766
// v2SplitRegistry is the JSON output type for V2 split_registry endpoint
4867
type v2SplitRegistry struct {
4968
Splits map[string]*v2Split `json:"splits"`
5069
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
5170
}
5271

53-
// v2SplitRegistry is the JSON output type for V2 split_registry endpoint
72+
// v4SplitRegistry is the JSON output type for V4 split_registry endpoint
73+
type v4SplitRegistry struct {
74+
Splits []v4Split `json:"splits"`
75+
ExperienceSamplingWeight int `json:"experience_sampling_weight"`
76+
}
77+
78+
// v2Split is the JSON output type for V2 split_registry endpoint
5479
type v2Split struct {
5580
Weights map[string]int `json:"weights"`
5681
FeatureGate bool `json:"feature_gate"`
5782
}
5883

84+
// v4Split is the JSON output type for V4 split_registry endpoint
85+
type v4Split struct {
86+
Name string `json:"name"`
87+
Variants []v4Variant `json:"variants"`
88+
FeatureGate bool `json:"feature_gate"`
89+
}
90+
91+
// v4Split is the JSON output type for V4 split_registry endpoint
92+
type v4Variant struct {
93+
Name string `json:"name"`
94+
Weight int `json:"weight"`
95+
}
96+
5997
// v1SplitDetail is the JSON output type for the V1 split detail endpoint
6098
type v1SplitDetail struct {
6199
Name string `json:"name"`
@@ -126,13 +164,17 @@ func (s *server) routes() {
126164
getV1AppVisitorConfig,
127165
)
128166
s.handleGet(
129-
"/api/v2/apps/{a}/versions/{v}/builds/{b}/visitors/{id}/config",
130-
getV2AppVisitorConfig,
167+
"/api/v4/apps/{a}/versions/{v}/builds/{b}/visitors/{id}/config",
168+
getV4AppVisitorConfig,
131169
)
132170
s.handleGet(
133171
"/api/v1/apps/{a}/versions/{v}/builds/{b}/identifier_types/{t}/identifiers/{i}/visitor_config",
134172
getV1AppVisitorConfig,
135173
)
174+
s.handleGet(
175+
"/api/v4/apps/{a}/versions/{v}/builds/{b}/identifier_types/{t}/identifiers/{i}/visitor_config",
176+
getV4AppVisitorConfig,
177+
)
136178
s.handleGet(
137179
"/api/v1/split_details/{id}",
138180
getV1SplitDetail,
@@ -141,6 +183,10 @@ func (s *server) routes() {
141183
"/api/v3/builds/{b}/split_registry",
142184
getV2PlusSplitRegistry,
143185
)
186+
s.handleGet(
187+
"/api/v4/builds/{b}/split_registry",
188+
getV4SplitRegistry,
189+
)
144190
}
145191

146192
func getV1SplitRegistry() (interface{}, error) {
@@ -181,6 +227,37 @@ func getV2PlusSplitRegistry() (interface{}, error) {
181227
}, nil
182228
}
183229

230+
func getV4SplitRegistry() (interface{}, error) {
231+
schema, err := schema.ReadMerged()
232+
if err != nil {
233+
return nil, err
234+
}
235+
v4Splits := make([]v4Split, 0, len(schema.Splits))
236+
for _, split := range schema.Splits {
237+
isFeatureGate := splits.IsFeatureGateFromName(split.Name)
238+
weights, err := splits.WeightsFromYAML(split.Weights)
239+
if err != nil {
240+
return nil, err
241+
}
242+
v4Variants := make([]v4Variant, 0, len(*weights))
243+
for variantName, weight := range *weights {
244+
v4Variants = append(v4Variants, v4Variant{
245+
Name: variantName,
246+
Weight: weight,
247+
})
248+
}
249+
v4Splits = append(v4Splits, v4Split{
250+
Name: split.Name,
251+
Variants: v4Variants,
252+
FeatureGate: isFeatureGate,
253+
})
254+
}
255+
return v4SplitRegistry{
256+
Splits: v4Splits,
257+
ExperienceSamplingWeight: 1,
258+
}, nil
259+
}
260+
184261
func postNoop(*http.Request) error {
185262
return nil
186263
}
@@ -214,6 +291,24 @@ func getV1Visitor() (interface{}, error) {
214291
}, nil
215292
}
216293

294+
func getV4Visitor() (interface{}, error) {
295+
assignments, err := fakeassignments.Read()
296+
if err != nil {
297+
return nil, err
298+
}
299+
v4Assignments := make([]v4Assignment, 0, len(*assignments))
300+
for split, variant := range *assignments {
301+
v4Assignments = append(v4Assignments, v4Assignment{
302+
SplitName: split,
303+
Variant: variant,
304+
})
305+
}
306+
return v4Visitor{
307+
ID: "00000000-0000-0000-0000-000000000000",
308+
Assignments: v4Assignments,
309+
}, nil
310+
}
311+
217312
func getV1VisitorDetail() (interface{}, error) {
218313
assignments, err := fakeassignments.Read()
219314
if err != nil {
@@ -312,6 +407,24 @@ func getV1AppVisitorConfig() (interface{}, error) {
312407
}, nil
313408
}
314409

410+
func getV4AppVisitorConfig() (interface{}, error) {
411+
isplitRegistry, err := getV4SplitRegistry()
412+
splitRegistry := isplitRegistry.(v4SplitRegistry)
413+
if err != nil {
414+
return nil, err
415+
}
416+
ivisitor, err := getV4Visitor()
417+
visitor := ivisitor.(v4Visitor)
418+
if err != nil {
419+
return nil, err
420+
}
421+
return v4VisitorConfig{
422+
Splits: splitRegistry.Splits,
423+
Visitor: visitor,
424+
ExperienceSamplingWeight: splitRegistry.ExperienceSamplingWeight,
425+
}, nil
426+
}
427+
315428
func getV2AppVisitorConfig() (interface{}, error) {
316429
isplitRegistry, err := getV2PlusSplitRegistry()
317430
splitRegistry := isplitRegistry.(v2SplitRegistry)

fakeserver/server_test.go

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ splits:
3434
treatment: 40
3535
`
3636

37+
var testAssignments = `
38+
something_something_enabled: "true"
39+
`
40+
3741
func TestMain(m *testing.M) {
3842
current, exists := os.LookupEnv("TESTTRACK_FAKE_SERVER_CONFIG_DIR")
3943

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

60+
assignmentsContent := []byte(testAssignments)
61+
if err := ioutil.WriteFile(filepath.Join(dir, "assignments.yml"), assignmentsContent, 0644); err != nil {
62+
log.Fatal(err)
63+
}
64+
5665
os.Setenv("TESTTRACK_FAKE_SERVER_CONFIG_DIR", dir)
5766
exitCode := m.Run()
5867
if exists {
@@ -97,6 +106,73 @@ func TestSplitRegistry(t *testing.T) {
97106
require.Equal(t, 40, registry.Splits["test.test_experiment"].Weights["treatment"])
98107
require.Equal(t, false, registry.Splits["test.test_experiment"].FeatureGate)
99108
})
109+
110+
t.Run("it loads split registry v4", func(t *testing.T) {
111+
w := httptest.NewRecorder()
112+
h := createHandler()
113+
114+
h.ServeHTTP(w, httptest.NewRequest("GET", "/api/v4/builds/2020-01-02T03:04:05/split_registry", nil))
115+
116+
require.Equal(t, http.StatusOK, w.Code)
117+
118+
registry := v4SplitRegistry{}
119+
err := json.Unmarshal(w.Body.Bytes(), &registry)
120+
require.Nil(t, err)
121+
122+
require.Equal(t, 1, registry.ExperienceSamplingWeight)
123+
require.Equal(t, "test.test_experiment", registry.Splits[0].Name)
124+
require.Equal(t, 60, registry.Splits[0].Variants[0].Weight)
125+
require.Equal(t, 40, registry.Splits[0].Variants[1].Weight)
126+
require.Equal(t, false, registry.Splits[0].FeatureGate)
127+
})
128+
}
129+
130+
func TestVisitorConfig(t *testing.T) {
131+
t.Run("it loads visitor config v4", func(t *testing.T) {
132+
w := httptest.NewRecorder()
133+
h := createHandler()
134+
135+
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))
136+
137+
require.Equal(t, http.StatusOK, w.Code)
138+
139+
visitorConfig := v4VisitorConfig{}
140+
err := json.Unmarshal(w.Body.Bytes(), &visitorConfig)
141+
require.Nil(t, err)
142+
143+
require.Equal(t, 1, visitorConfig.ExperienceSamplingWeight)
144+
require.Equal(t, "test.test_experiment", visitorConfig.Splits[0].Name)
145+
require.Equal(t, 60, visitorConfig.Splits[0].Variants[0].Weight)
146+
require.Equal(t, 40, visitorConfig.Splits[0].Variants[1].Weight)
147+
require.Equal(t, false, visitorConfig.Splits[0].FeatureGate)
148+
require.Equal(t, "00000000-0000-0000-0000-000000000000", visitorConfig.Visitor.ID)
149+
require.Equal(t, "something_something_enabled", visitorConfig.Visitor.Assignments[0].SplitName)
150+
require.Equal(t, "true", visitorConfig.Visitor.Assignments[0].Variant)
151+
})
152+
}
153+
154+
func TestIdentifierVisitorConfig(t *testing.T) {
155+
t.Run("it loads visitor config v4", func(t *testing.T) {
156+
w := httptest.NewRecorder()
157+
h := createHandler()
158+
159+
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))
160+
161+
require.Equal(t, http.StatusOK, w.Code)
162+
163+
visitorConfig := v4VisitorConfig{}
164+
err := json.Unmarshal(w.Body.Bytes(), &visitorConfig)
165+
require.Nil(t, err)
166+
167+
require.Equal(t, 1, visitorConfig.ExperienceSamplingWeight)
168+
require.Equal(t, "test.test_experiment", visitorConfig.Splits[0].Name)
169+
require.Equal(t, 60, visitorConfig.Splits[0].Variants[0].Weight)
170+
require.Equal(t, 40, visitorConfig.Splits[0].Variants[1].Weight)
171+
require.Equal(t, false, visitorConfig.Splits[0].FeatureGate)
172+
require.Equal(t, "00000000-0000-0000-0000-000000000000", visitorConfig.Visitor.ID)
173+
require.Equal(t, "something_something_enabled", visitorConfig.Visitor.Assignments[0].SplitName)
174+
require.Equal(t, "true", visitorConfig.Visitor.Assignments[0].Variant)
175+
})
100176
}
101177

102178
func TestCors(t *testing.T) {
@@ -132,8 +208,6 @@ func TestCors(t *testing.T) {
132208
}
133209

134210
func TestPersistAssignment(t *testing.T) {
135-
os.Remove("testdata/assignments.yml")
136-
137211
t.Run("it persists assignments to yaml", func(t *testing.T) {
138212
w := httptest.NewRecorder()
139213
h := createHandler()
@@ -157,8 +231,6 @@ func TestPersistAssignment(t *testing.T) {
157231
}
158232

159233
func TestPersistAssignmentV2(t *testing.T) {
160-
os.Remove("testdata/assignments.yml")
161-
162234
t.Run("it persists assignments to yaml", func(t *testing.T) {
163235
w := httptest.NewRecorder()
164236
h := createHandler()

0 commit comments

Comments
 (0)