Skip to content

Commit 45944c2

Browse files
lpusokofalvai
andauthored
Added sync analytics (#161)
* Added analytics that do not use goroutines to help debug a scheduling issue (golang/go#54061). Co-authored-by: Olivér Falvai <[email protected]>
1 parent a47ab04 commit 45944c2

File tree

5 files changed

+164
-11
lines changed

5 files changed

+164
-11
lines changed

analytics/client.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package analytics
22

33
import (
44
"bytes"
5+
"context"
56
"net/http"
67
"time"
78

@@ -10,7 +11,6 @@ import (
1011
)
1112

1213
const trackEndpoint = "https://bitrise-step-analytics.herokuapp.com/track"
13-
const timeOut = 30 * time.Second
1414

1515
// Client ...
1616
type Client interface {
@@ -19,29 +19,47 @@ type Client interface {
1919

2020
type client struct {
2121
httpClient *http.Client
22+
timeout time.Duration
2223
endpoint string
2324
logger log.Logger
2425
}
2526

2627
// NewDefaultClient ...
27-
func NewDefaultClient(logger log.Logger) Client {
28+
func NewDefaultClient(logger log.Logger, timeout time.Duration) Client {
2829
httpClient := retry.NewHTTPClient().StandardClient()
29-
httpClient.Timeout = timeOut
30-
return NewClient(httpClient, trackEndpoint, logger)
30+
httpClient.Timeout = timeout
31+
return NewClient(httpClient, trackEndpoint, logger, timeout)
3132
}
3233

3334
// NewClient ...
34-
func NewClient(httpClient *http.Client, endpoint string, logger log.Logger) Client {
35-
return client{httpClient: httpClient, endpoint: endpoint, logger: logger}
35+
func NewClient(httpClient *http.Client, endpoint string, logger log.Logger, timeout time.Duration) Client {
36+
return client{httpClient: httpClient, endpoint: endpoint, logger: logger, timeout: timeout}
3637
}
3738

3839
// Send ...
3940
func (t client) Send(buffer *bytes.Buffer) {
40-
res, err := t.httpClient.Post(t.endpoint, "application/json", buffer)
41+
ctx, cancel := context.WithTimeout(context.Background(), t.timeout)
42+
defer cancel()
43+
44+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.endpoint, buffer)
4145
if err != nil {
42-
t.logger.Debugf("Couldn't send analytics event: %s", err.Error())
46+
t.logger.Warnf("Couldn't create analytics request: %s", err)
47+
}
48+
49+
req.Header.Set("Content-Type", "application/json")
50+
51+
res, err := t.httpClient.Do(req)
52+
if err != nil {
53+
t.logger.Debugf("Couldn't send analytics event: %s", err)
4354
return
4455
}
56+
57+
defer func() {
58+
if err := res.Body.Close(); err != nil {
59+
t.logger.Debugf("Couldn't close anaytics body: %s", err)
60+
}
61+
}()
62+
4563
if statusOK := res.StatusCode >= 200 && res.StatusCode < 300; !statusOK {
4664
t.logger.Debugf("Couldn't send analytics event, status code: %d", res.StatusCode)
4765
}

analytics/client_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"testing"
9+
"time"
910

1011
"github.com/bitrise-io/go-utils/v2/analytics/mocks"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/mock"
1314
)
1415

16+
const cientTimeout = 10 * time.Second
17+
1518
func Test_trackerClient_send_success(t *testing.T) {
1619
mockLogger := new(mocks.Logger)
1720
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
@@ -25,7 +28,7 @@ func Test_trackerClient_send_success(t *testing.T) {
2528
assert.NoError(t, err)
2629
}))
2730
defer func() { testServer.Close() }()
28-
client := NewClient(http.DefaultClient, testServer.URL, mockLogger)
31+
client := NewClient(http.DefaultClient, testServer.URL, mockLogger, cientTimeout)
2932
client.Send(bytes.NewBufferString("{}"))
3033
mockLogger.AssertNotCalled(t, "Debugf", mock.Anything, mock.Anything)
3134
}
@@ -44,7 +47,7 @@ func Test_trackerClient_send_failure(t *testing.T) {
4447
assert.NoError(t, err)
4548
}))
4649
defer func() { testServer.Close() }()
47-
client := NewClient(http.DefaultClient, testServer.URL, mockLogger)
50+
client := NewClient(http.DefaultClient, testServer.URL, mockLogger, cientTimeout)
4851
client.Send(bytes.NewBufferString("{}"))
4952
mockLogger.AssertCalled(t, "Debugf", "Couldn't send analytics event, status code: %d", 500)
5053
}

analytics/sync_track.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package analytics
2+
3+
import (
4+
"bytes"
5+
"time"
6+
7+
"github.com/bitrise-io/go-utils/v2/log"
8+
)
9+
10+
const syncClientTimeout = 10 * time.Second
11+
12+
type syncTracker struct {
13+
client Client
14+
properties []Properties
15+
}
16+
17+
// NewDefaultSyncTracker ...
18+
func NewDefaultSyncTracker(logger log.Logger, properties ...Properties) Tracker {
19+
return NewSyncTracker(NewDefaultClient(logger, syncClientTimeout), properties...)
20+
}
21+
22+
// NewSyncTracker ...
23+
func NewSyncTracker(client Client, properties ...Properties) Tracker {
24+
t := syncTracker{client: client, properties: properties}
25+
return &t
26+
}
27+
28+
// Enqueue ...
29+
func (t syncTracker) Enqueue(eventName string, properties ...Properties) {
30+
var b bytes.Buffer
31+
32+
newEvent(eventName, append(t.properties, properties...)).toJSON(&b)
33+
t.client.Send(&b)
34+
}
35+
36+
// Wait ...
37+
func (t syncTracker) Wait() {
38+
// no-op in sync tracker
39+
}

analytics/sync_track_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package analytics
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/bitrise-io/go-utils/v2/analytics/mocks"
9+
"github.com/stretchr/testify/mock"
10+
)
11+
12+
func Test_syncTracker_SendIsCalledWithExpectedData(t *testing.T) {
13+
mockClient := new(mocks.Client)
14+
mockClient.On("Send", mock.Anything).Return()
15+
16+
tracker := NewSyncTracker(mockClient)
17+
baseProperties := Properties{"session": "id"}
18+
tracker.Enqueue(
19+
"first",
20+
baseProperties, Properties{
21+
"property": "value",
22+
"intproperty": 42,
23+
"longproperty": 42,
24+
"floatproperty": 3.14,
25+
"boolproperty": true,
26+
"property2": Properties{"foo": "bar"},
27+
},
28+
)
29+
tracker.Wait()
30+
31+
matcher := mock.MatchedBy(func(buffer *bytes.Buffer) bool {
32+
var event event
33+
err := json.Unmarshal(buffer.Bytes(), &event)
34+
if err != nil {
35+
return false
36+
}
37+
if event.EventName != "first" {
38+
return false
39+
}
40+
if len(event.Properties) != 7 ||
41+
event.Properties["property"] != "value" ||
42+
event.Properties["intproperty"].(float64) != 42 ||
43+
event.Properties["longproperty"].(float64) != 42 ||
44+
event.Properties["floatproperty"].(float64) != 3.14 ||
45+
event.Properties["boolproperty"].(bool) != true ||
46+
event.Properties["session"] != "id" ||
47+
event.Properties["property2"].(map[string]interface{})["foo"] != "bar" {
48+
return false
49+
}
50+
if event.ID == "" || event.Timestamp == 0 {
51+
return false
52+
}
53+
return true
54+
})
55+
mockClient.AssertNumberOfCalls(t, "Send", 1)
56+
mockClient.AssertCalled(t, "Send", matcher)
57+
}
58+
59+
func Test_syncTracker_MergingPropertiesWork(t *testing.T) {
60+
mockClient := new(mocks.Client)
61+
mockClient.On("Send", mock.Anything).Return()
62+
63+
tracker := NewSyncTracker(mockClient, Properties{"base": "base"})
64+
baseProperties := Properties{"first": "first"}
65+
tracker.Enqueue("event", baseProperties)
66+
newBaseProperties := baseProperties.Merge(Properties{"second": "second"})
67+
tracker.Enqueue("event2", newBaseProperties)
68+
tracker.Wait()
69+
70+
mockClient.AssertNumberOfCalls(t, "Send", 2)
71+
matcher := mock.MatchedBy(func(buffer *bytes.Buffer) bool {
72+
var event event
73+
err := json.Unmarshal(buffer.Bytes(), &event)
74+
if err != nil {
75+
return false
76+
}
77+
if event.EventName == "event" {
78+
if len(event.Properties) != 2 || event.Properties["base"] != "base" || event.Properties["first"] != "first" {
79+
return false
80+
}
81+
return true
82+
}
83+
if event.EventName == "event2" {
84+
if len(event.Properties) != 3 || event.Properties["base"] != "base" || event.Properties["first"] != "first" || event.Properties["second"] != "second" {
85+
return false
86+
}
87+
return true
88+
}
89+
return false
90+
})
91+
mockClient.AssertCalled(t, "Send", matcher)
92+
}

analytics/track.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
const poolSize = 10
1212
const bufferSize = 100
1313
const timeout = 30 * time.Second
14+
const asyncClientTimeout = 30 * time.Second
1415

1516
// Tracker ...
1617
type Tracker interface {
@@ -28,7 +29,7 @@ type tracker struct {
2829

2930
// NewDefaultTracker ...
3031
func NewDefaultTracker(logger log.Logger, properties ...Properties) Tracker {
31-
return NewTracker(NewDefaultClient(logger), timeout, properties...)
32+
return NewTracker(NewDefaultClient(logger, asyncClientTimeout), timeout, properties...)
3233
}
3334

3435
// NewTracker ...

0 commit comments

Comments
 (0)