Skip to content

Commit a15bc1d

Browse files
author
jianggang
authored
Add Track Event (#12)
* ✨ feat: add track api
1 parent bac088d commit a15bc1d

File tree

5 files changed

+201
-123
lines changed

5 files changed

+201
-123
lines changed

evaluate.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ type Toggles struct {
2424
}
2525

2626
type Toggle struct {
27-
Key string `json:"key"`
28-
Enabled bool `json:"enabled"`
29-
Version uint64 `json:"version"`
30-
ForClient bool `json:"forClient"`
31-
DisabledServe Serve `json:"disabledServe"`
32-
DefaultServe Serve `json:"defaultServe"`
33-
Rules []Rule `json:"rules"`
34-
Variations []interface{} `json:"variations"`
27+
Key string `json:"key"`
28+
Enabled bool `json:"enabled"`
29+
TrackAccessEvents bool `json:"trackAccessEvents"`
30+
LastModified uint64 `json:"lastModified"`
31+
Version uint64 `json:"version"`
32+
ForClient bool `json:"forClient"`
33+
DisabledServe Serve `json:"disabledServe"`
34+
DefaultServe Serve `json:"defaultServe"`
35+
Rules []Rule `json:"rules"`
36+
Variations []interface{} `json:"variations"`
3537
}
3638

3739
type Segment struct {

event.go

Lines changed: 67 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ type EventRecorder struct {
1313
auth string
1414
eventsUrl string
1515
flushInterval time.Duration
16-
incomingEvents []AccessEvent
17-
packedData []PackedData
16+
incomingEvents []interface{}
17+
access Access
1818
httpClient http.Client
1919
mu sync.Mutex
2020
wg sync.WaitGroup
@@ -25,16 +25,27 @@ type EventRecorder struct {
2525
}
2626

2727
type AccessEvent struct {
28-
Time int64 `json:"time"`
29-
Key string `json:"key"`
30-
Value interface{} `json:"value"`
31-
Index *int `json:"index"`
32-
Version *uint64 `json:"version"`
33-
Reason string `json:"reason"`
28+
Kind string `json:"kind"`
29+
Time int64 `json:"time"`
30+
User string `json:"user"`
31+
Key string `json:"key"`
32+
Value interface{} `json:"value"`
33+
VariationIndex *int `json:"variationIndex"`
34+
RuleIndex *int `json:"ruleIndex"`
35+
Version *uint64 `json:"version"`
36+
Reason string `json:"reason"`
37+
}
38+
39+
type CustomEvent struct {
40+
Kind string `json:"kind"`
41+
Time int64 `json:"time"`
42+
User string `json:"user"`
43+
Name string `json:"name"`
44+
Value *float64 `json:"value"`
3445
}
3546

3647
type PackedData struct {
37-
Events []AccessEvent `json:"events"`
48+
Events []interface{} `json:"events"`
3849
Access Access `json:"access"`
3950
}
4051

@@ -67,17 +78,32 @@ func NewEventRecorder(eventsUrl string, flushInterval time.Duration, auth string
6778
auth: auth,
6879
eventsUrl: eventsUrl,
6980
flushInterval: flushInterval,
70-
incomingEvents: []AccessEvent{},
71-
packedData: []PackedData{},
81+
incomingEvents: []interface{}{},
82+
access: newAccess(),
7283
httpClient: newHttpClient(flushInterval),
7384
stopChan: make(chan struct{}),
7485
}
7586
}
7687

88+
func newAccess() Access {
89+
return Access{
90+
Counters: make(map[string][]ToggleCounter),
91+
}
92+
}
93+
94+
func nowToggleCounter(value interface{}, version *uint64, index *int) ToggleCounter {
95+
return ToggleCounter{
96+
value,
97+
version,
98+
index,
99+
1,
100+
}
101+
}
102+
77103
func (e *EventRecorder) Start() {
78104
e.wg.Add(1)
79105
e.startOnce.Do(func() {
80-
e.ticker = time.NewTicker(e.flushInterval * time.Millisecond)
106+
e.ticker = time.NewTicker(e.flushInterval)
81107
go func() {
82108
for {
83109
select {
@@ -94,14 +120,15 @@ func (e *EventRecorder) Start() {
94120
}
95121

96122
func (e *EventRecorder) doFlush() {
97-
events := make([]AccessEvent, 0)
123+
events := make([]interface{}, 0)
98124
e.mu.Lock()
99125
events, e.incomingEvents = e.incomingEvents, events
126+
packedData := e.buildPackedData(events)
127+
e.access = newAccess()
100128
e.mu.Unlock()
101-
if len(events) == 0 {
129+
if len(events) == 0 && len(packedData[0].Access.Counters) == 0 {
102130
return
103131
}
104-
packedData := e.buildPackedData(events)
105132
body, _ := json.Marshal(packedData)
106133
req, err := http.NewRequest(http.MethodPost, e.eventsUrl, bytes.NewBuffer(body))
107134
if err != nil {
@@ -117,62 +144,42 @@ func (e *EventRecorder) doFlush() {
117144
}
118145
}
119146

120-
func (e *EventRecorder) buildPackedData(events []AccessEvent) []PackedData {
121-
access := e.buildAccess(events)
122-
p := PackedData{Access: access, Events: events}
147+
func (e *EventRecorder) buildPackedData(events []interface{}) []PackedData {
148+
e.access.EndTime = time.Now().UnixNano() / 1e6
149+
p := PackedData{Access: e.access, Events: events}
123150
return []PackedData{p}
124151
}
125152

126-
func (e *EventRecorder) buildAccess(events []AccessEvent) Access {
127-
counters, startTime, endTime := e.buildCounters(events)
128-
access := Access{
129-
StartTime: startTime,
130-
EndTime: endTime,
131-
Counters: map[string][]ToggleCounter{},
153+
func (e *EventRecorder) addAccess(event AccessEvent) {
154+
if len(e.access.Counters) == 0 {
155+
e.access.StartTime = time.Now().UnixNano() / 1e6
132156
}
133-
134-
for k, v := range counters {
135-
counter := ToggleCounter{
136-
Index: k.Index,
137-
Version: k.Version,
138-
Count: v.Count,
139-
Value: v.Value,
140-
}
141-
c, ok := access.Counters[k.Key]
142-
if !ok {
143-
access.Counters[k.Key] = []ToggleCounter{counter}
144-
} else {
145-
access.Counters[k.Key] = append(c, counter)
157+
counters, ok := e.access.Counters[event.Key]
158+
if ok {
159+
for index, counter := range counters {
160+
if *counter.Version == *event.Version && *counter.Index == *event.VariationIndex {
161+
counters[index].Count = counter.Count + 1
162+
return
163+
}
146164
}
165+
e.access.Counters[event.Key] = append(counters,
166+
nowToggleCounter(event.Value, event.Version, event.VariationIndex))
167+
} else {
168+
e.access.Counters[event.Key] = []ToggleCounter{
169+
nowToggleCounter(event.Value, event.Version, event.VariationIndex)}
147170
}
148-
return access
149171
}
150172

151-
func (e *EventRecorder) buildCounters(events []AccessEvent) (map[Variation]CountValue, int64, int64) {
152-
var startTime *int64 = nil
153-
var endTime *int64 = nil
154-
counters := map[Variation]CountValue{}
155-
156-
for _, event := range events {
157-
if startTime == nil || *startTime < event.Time {
158-
startTime = &event.Time
159-
}
160-
if endTime == nil || *endTime > event.Time {
161-
endTime = &event.Time
162-
}
163-
164-
v := Variation{Key: event.Key, Version: event.Version, Index: event.Index}
165-
c, ok := counters[v]
166-
if !ok {
167-
counters[v] = CountValue{Count: 1, Value: event.Value}
168-
} else {
169-
c.Count += 1
170-
}
173+
func (e *EventRecorder) RecordAccess(event AccessEvent, trackAccessEvents bool) {
174+
e.mu.Lock()
175+
if trackAccessEvents {
176+
e.incomingEvents = append(e.incomingEvents, event)
171177
}
172-
return counters, *startTime, *endTime
178+
e.addAccess(event)
179+
e.mu.Unlock()
173180
}
174181

175-
func (e *EventRecorder) RecordAccess(event AccessEvent) {
182+
func (e *EventRecorder) RecordCustom(event CustomEvent) {
176183
e.mu.Lock()
177184
e.incomingEvents = append(e.incomingEvents, event)
178185
e.mu.Unlock()

event_test.go

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,38 @@ import (
1010

1111
func TestEventFlush(t *testing.T) {
1212
recorder := NewEventRecorder("https://featureprobe.com/api/events", 1000, "sdk_key")
13-
version1 := uint64(1)
14-
version2 := uint64(2)
15-
recorder.RecordAccess(AccessEvent{
16-
Time: time.Now().Unix(),
17-
Key: "some_toggle",
18-
Value: "some_value",
19-
Version: &version1,
20-
Reason: "default",
21-
})
22-
recorder.RecordAccess(AccessEvent{
23-
Time: time.Now().Unix(),
24-
Key: "some_toggle",
25-
Value: "some_value",
26-
Version: &version1,
27-
Reason: "default",
28-
})
29-
recorder.RecordAccess(AccessEvent{
30-
Time: time.Now().Unix(),
31-
Key: "some_toggle",
32-
Value: "some_value",
33-
Version: &version2,
34-
Reason: "default",
13+
ruleIndex := 0
14+
versions := []uint64{1, 1, 1, 1, 2}
15+
variations := []int{0, 0, 0, 1, 1}
16+
trackAccessEvents := []bool{true, true, false, true, true}
17+
keys := []string{"some_toggle", "some_toggle", "some_toggle", "some_toggle", "some_toggle2"}
18+
for index, _ := range versions {
19+
recorder.RecordAccess(AccessEvent{
20+
Kind: "access",
21+
Time: time.Now().Unix(),
22+
User: "some_user",
23+
Key: keys[index],
24+
Value: "some_value",
25+
VariationIndex: &variations[index],
26+
RuleIndex: &ruleIndex,
27+
Version: &versions[index],
28+
Reason: "default",
29+
}, trackAccessEvents[index])
30+
}
31+
32+
recorder.RecordCustom(CustomEvent{
33+
Kind: "custom",
34+
Time: time.Now().Unix(),
35+
User: "some_user",
36+
Name: "some_event",
37+
Value: nil,
3538
})
3639

40+
assert.True(t, len(recorder.access.Counters) == 2)
41+
assert.True(t, recorder.access.Counters["some_toggle"][0].Count == 3)
42+
assert.True(t, len(recorder.access.Counters["some_toggle"]) == 2)
43+
assert.True(t, len(recorder.incomingEvents) == 5)
44+
3745
httpmock.ActivateNonDefault(&recorder.httpClient)
3846
httpmock.RegisterResponder("POST", "https://featureprobe.com/api/events",
3947
httpmock.NewStringResponder(200, "{}"))
@@ -47,19 +55,32 @@ func TestEventFlush(t *testing.T) {
4755
}
4856

4957
func TestEventFlushInvalidUrl(t *testing.T) {
58+
version := uint64(1)
59+
variationIndex := 0
60+
ruleIndex := 0
5061
recorder := NewEventRecorder(string([]byte{1, 2, 3}), 1000, "sdk_key")
5162
recorder.RecordAccess(AccessEvent{
52-
Time: time.Now().Unix(),
53-
Key: "some_toggle",
54-
Value: "some_value",
55-
Reason: "default",
56-
})
63+
Kind: "access",
64+
Time: time.Now().Unix(),
65+
User: "some_user",
66+
Key: "some_toggle",
67+
Value: "some_value",
68+
VariationIndex: &variationIndex,
69+
RuleIndex: &ruleIndex,
70+
Version: &version,
71+
Reason: "default",
72+
}, true)
5773
recorder.RecordAccess(AccessEvent{
58-
Time: time.Now().Unix(),
59-
Key: "some_toggle",
60-
Value: "some_value",
61-
Reason: "default",
62-
})
74+
Kind: "access",
75+
Time: time.Now().Unix(),
76+
User: "some_user",
77+
Key: "some_toggle",
78+
Value: "some_value",
79+
VariationIndex: &variationIndex,
80+
RuleIndex: &ruleIndex,
81+
Version: &version,
82+
Reason: "default",
83+
}, true)
6384

6485
httpmock.ActivateNonDefault(&recorder.httpClient)
6586
httpmock.RegisterResponder("POST", "https://featureprobe.com/api/events",
@@ -74,19 +95,32 @@ func TestEventFlushInvalidUrl(t *testing.T) {
7495
}
7596

7697
func TestEventFlushInvalidResp(t *testing.T) {
98+
version := uint64(1)
99+
variationIndex := 0
100+
ruleIndex := 0
77101
recorder := NewEventRecorder("https://featureprobe.com/api/events", 1000, "sdk_key")
78102
recorder.RecordAccess(AccessEvent{
79-
Time: time.Now().Unix(),
80-
Key: "some_toggle",
81-
Value: "some_value",
82-
Reason: "default",
83-
})
103+
Kind: "access",
104+
Time: time.Now().Unix(),
105+
User: "some_user",
106+
Key: "some_toggle",
107+
Value: "some_value",
108+
VariationIndex: &variationIndex,
109+
RuleIndex: &ruleIndex,
110+
Version: &version,
111+
Reason: "default",
112+
}, true)
84113
recorder.RecordAccess(AccessEvent{
85-
Time: time.Now().Unix(),
86-
Key: "some_toggle",
87-
Value: "some_value",
88-
Reason: "default",
89-
})
114+
Kind: "access",
115+
Time: time.Now().Unix(),
116+
User: "some_user",
117+
Key: "some_toggle",
118+
Value: "some_value",
119+
VariationIndex: &variationIndex,
120+
RuleIndex: &ruleIndex,
121+
Version: &version,
122+
Reason: "default",
123+
}, true)
90124

91125
httpmock.ActivateNonDefault(&recorder.httpClient)
92126
httpmock.RegisterResponder("POST", "https://featureprobe.com/api/events",
@@ -101,14 +135,22 @@ func TestEventFlushInvalidResp(t *testing.T) {
101135
}
102136

103137
func TestCloseEvent(t *testing.T) {
138+
version := uint64(1)
139+
variationIndex := 0
140+
ruleIndex := 0
104141
recorder := NewEventRecorder("https://featureprobe.com/api/events", 5000, "sdk_key")
105142
recorder.Start()
106143
recorder.RecordAccess(AccessEvent{
107-
Time: time.Now().Unix(),
108-
Key: "some_toggle",
109-
Value: "some_value",
110-
Reason: "default",
111-
})
144+
Kind: "access",
145+
Time: time.Now().Unix(),
146+
User: "some_user",
147+
Key: "some_toggle",
148+
Value: "some_value",
149+
VariationIndex: &variationIndex,
150+
RuleIndex: &ruleIndex,
151+
Version: &version,
152+
Reason: "default",
153+
}, true)
112154
httpmock.ActivateNonDefault(&recorder.httpClient)
113155
httpmock.RegisterResponder("POST", "https://featureprobe.com/api/events",
114156
httpmock.NewStringResponder(200, "{"))

0 commit comments

Comments
 (0)