Skip to content

Commit 7455f38

Browse files
API override for retry requests (#208)
Allow falling back to Statsig default API for retries See next PR
1 parent d6add30 commit 7455f38

10 files changed

+80
-70
lines changed

api_override_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ type counters struct {
1313

1414
func setupTestServer(counts *counters) *httptest.Server {
1515
return getTestServer(testServerOptions{
16-
dcsOnline: true,
1716
onLogEvent: func(events []map[string]interface{}) {
1817
counts.logEvent += 1
1918
},

data_adapter_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ func TestBootstrapWithAdapter(t *testing.T) {
1919
idlists_bytes, _ := os.ReadFile("test_data/get_id_lists.json")
2020
idlist_bytes, _ := os.ReadFile("test_data/list_1.txt")
2121
testServer := getTestServer(testServerOptions{
22-
dcsOnline: true,
2322
onLogEvent: func(newEvents []map[string]interface{}) {
2423
for _, newEvent := range newEvents {
2524
eventTyped := convertToExposureEvent(newEvent)
@@ -217,7 +216,6 @@ func TestAdapterWithPolling(t *testing.T) {
217216
func TestIncorrectlyImplementedAdapter(t *testing.T) {
218217
events := []Event{}
219218
testServer := getTestServer(testServerOptions{
220-
dcsOnline: true,
221219
onLogEvent: func(newEvents []map[string]interface{}) {
222220
for _, newEvent := range newEvents {
223221
eventTyped := convertToExposureEvent(newEvent)

diagnostics_test.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ type Pair struct {
1515
func TestInitDiagnostics(t *testing.T) {
1616
var events events
1717
testServer := getTestServer(testServerOptions{
18-
dcsOnline: true,
1918
onLogEvent: func(newEvents []map[string]interface{}) {
2019
events = newEvents
2120
},
@@ -60,7 +59,6 @@ func TestInitDiagnostics(t *testing.T) {
6059
func TestInitTimeoutDiagnostics(t *testing.T) {
6160
var events events
6261
testServer := getTestServer(testServerOptions{
63-
dcsOnline: true,
6462
onLogEvent: func(newEvents []map[string]interface{}) {
6563
events = newEvents
6664
},
@@ -93,7 +91,6 @@ func TestConfigSyncDiagnostics(t *testing.T) {
9391
count := 0
9492
testServer := getTestServer(
9593
testServerOptions{
96-
dcsOnline: true,
9794
onLogEvent: func(events []map[string]interface{}) {
9895
mu.Lock()
9996
defer mu.Unlock()
@@ -154,7 +151,6 @@ func TestApiCallDiagnostics(t *testing.T) {
154151
var events events
155152
testServer := getTestServer(
156153
testServerOptions{
157-
dcsOnline: true,
158154
onLogEvent: func(newEvents []map[string]interface{}) {
159155
events = newEvents
160156
},
@@ -200,7 +196,6 @@ func TestBootstrapDiagnostics(t *testing.T) {
200196
var events events
201197
testServer := getTestServer(
202198
testServerOptions{
203-
dcsOnline: true,
204199
onLogEvent: func(newEvents []map[string]interface{}) {
205200
events = newEvents
206201
},
@@ -252,7 +247,6 @@ func TestDiagnosticsGetCleared(t *testing.T) {
252247
var mu sync.Mutex
253248
count := 0
254249
testServer := getTestServer(testServerOptions{
255-
dcsOnline: true,
256250
onLogEvent: func(events []map[string]interface{}) {
257251
mu.Lock()
258252
defer mu.Unlock()
@@ -327,7 +321,6 @@ func TestDiagnosticsSampling(t *testing.T) {
327321
var mu sync.RWMutex
328322

329323
testServer := getTestServer(testServerOptions{
330-
dcsOnline: true,
331324
onLogEvent: func(newEvents []map[string]interface{}) {
332325
mu.Lock()
333326
events = append(events, newEvents...)
@@ -383,7 +376,6 @@ func TestDiagnosticsClearMarkers(t *testing.T) {
383376
var events events
384377
testServer := getTestServer(
385378
testServerOptions{
386-
dcsOnline: true,
387379
onLogEvent: func(newEvents []map[string]interface{}) {
388380
events = append(events, newEvents...)
389381
},
@@ -426,7 +418,6 @@ func TestDiagnosticsMaxMarkers(t *testing.T) {
426418
var events events
427419
testServer := getTestServer(
428420
testServerOptions{
429-
dcsOnline: true,
430421
onLogEvent: func(newEvents []map[string]interface{}) {
431422
events = newEvents
432423
},
@@ -465,7 +456,6 @@ func TestDiagnosticsMaxMarkers(t *testing.T) {
465456
func TestDisableDiagnostics(t *testing.T) {
466457
var events events
467458
testServer := getTestServer(testServerOptions{
468-
dcsOnline: true,
469459
onLogEvent: func(newEvents []map[string]interface{}) {
470460
events = newEvents
471461

evaluation_callbacks_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ func TestExposureCallback(t *testing.T) {
4242
},
4343
}
4444

45-
testServer := getTestServer(testServerOptions{
46-
dcsOnline: true,
47-
})
45+
testServer := getTestServer(testServerOptions{})
4846

4947
var opt *Options
5048
var user User

exposure_logging_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ func TestExposureLogging(t *testing.T) {
88
events := []Event{}
99

1010
testServer := getTestServer(testServerOptions{
11-
dcsOnline: true,
1211
onLogEvent: func(newEvents []map[string]interface{}) {
1312
for _, newEvent := range newEvents {
1413
eventTyped := convertToExposureEvent(newEvent)

layer_exposure_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ func TestLayerExposure(t *testing.T) {
88
events := []Event{}
99

1010
testServer := getTestServer(testServerOptions{
11-
dcsOnline: true,
1211
onLogEvent: func(newEvents []map[string]interface{}) {
1312
for _, newEvent := range newEvents {
1413
eventTyped := convertToExposureEvent(newEvent)

statsig.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func Initialize(sdkKey string) {
2525
type Options struct {
2626
API string `json:"api"`
2727
APIOverrides APIOverrides `json:"api_overrides"`
28+
FallbackToStatsigAPI bool
2829
Transport http.RoundTripper
2930
Environment Environment `json:"environment"`
3031
LocalMode bool `json:"localMode"`

statsig_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ func TestRulesUpdatedCallback(t *testing.T) {
9292
func TestLogImmediate(t *testing.T) {
9393
env := ""
9494
testServer := getTestServer(testServerOptions{
95-
dcsOnline: true,
9695
onLogEvent: func(newEvents []map[string]interface{}) {
9796
eventTyped := convertToExposureEvent(newEvents[0])
9897
env = eventTyped.User.StatsigEnvironment["tier"]

test_utils.go

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
type events []map[string]interface{}
1616

1717
type testServerOptions struct {
18-
dcsOnline bool
18+
status int
1919
onLogEvent func(events []map[string]interface{})
2020
onDCS func()
2121
onGetIDLists func()
@@ -26,29 +26,30 @@ type testServerOptions struct {
2626
func getTestServer(opts testServerOptions) *httptest.Server {
2727
return httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
2828
res.Header().Add("x-statsig-region", "az-westus-2")
29+
var status int
30+
if opts.status == 0 {
31+
status = http.StatusOK
32+
} else {
33+
status = opts.status
34+
}
35+
res.WriteHeader(status)
36+
if status < 200 || status >= 300 {
37+
return
38+
}
2939
if strings.Contains(req.URL.Path, "download_config_specs") {
30-
if !opts.dcsOnline {
31-
res.WriteHeader(http.StatusInternalServerError)
32-
} else {
33-
dcsFile := "download_config_specs.json"
34-
if opts.withSampling {
35-
dcsFile = "download_config_specs_with_diagnostics_sampling.json"
36-
}
37-
if opts.isLayerExposure {
38-
dcsFile = "layer_exposure_download_config_specs.json"
39-
}
40-
bytes, _ := os.ReadFile(dcsFile)
41-
res.WriteHeader(http.StatusOK)
42-
_, _ = res.Write(bytes)
43-
if opts.onDCS != nil {
44-
opts.onDCS()
45-
}
40+
dcsFile := "download_config_specs.json"
41+
if opts.withSampling {
42+
dcsFile = "download_config_specs_with_diagnostics_sampling.json"
4643
}
44+
if opts.isLayerExposure {
45+
dcsFile = "layer_exposure_download_config_specs.json"
46+
}
47+
bytes, _ := os.ReadFile(dcsFile)
48+
_, _ = res.Write(bytes)
4749
if opts.onDCS != nil {
4850
opts.onDCS()
4951
}
5052
} else if strings.Contains(req.URL.Path, "log_event") {
51-
res.WriteHeader(http.StatusOK)
5253
type requestInput struct {
5354
Events []map[string]interface{} `json:"events"`
5455
StatsigMetadata statsigMetadata `json:"statsigMetadata"`
@@ -71,7 +72,6 @@ func getTestServer(opts testServerOptions) *httptest.Server {
7172
opts.onLogEvent(input.Events)
7273
}
7374
} else if strings.Contains(req.URL.Path, "get_id_lists") {
74-
res.WriteHeader(http.StatusOK)
7575
response, _ := json.Marshal(map[string]map[string]interface{}{
7676
"my_id_list": {
7777
"name": "my_id_list",

transport.go

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9-
"io/ioutil"
109
"net/http"
10+
"net/url"
1111
"strconv"
1212
"strings"
1313
"time"
@@ -23,43 +23,21 @@ const (
2323
backoffMultiplier = 10
2424
)
2525

26-
type apis struct {
27-
downloadConfigSpecs string
28-
getIDLists string
29-
logEvent string
30-
}
31-
3226
type transport struct {
33-
api apis
3427
sdkKey string
3528
metadata statsigMetadata // Safe to read from but not thread safe to write into. If value needs to change, please ensure thread safety.
3629
client *http.Client
3730
options *Options
3831
}
3932

4033
func newTransport(secret string, options *Options) *transport {
41-
api := apis{
42-
downloadConfigSpecs: strings.TrimSuffix(defaultString(
43-
options.APIOverrides.DownloadConfigSpecs,
44-
defaultString(options.API, StatsigCDN),
45-
), "/"),
46-
getIDLists: strings.TrimSuffix(defaultString(
47-
options.APIOverrides.GetIDLists,
48-
defaultString(options.API, StatsigAPI),
49-
), "/"),
50-
logEvent: strings.TrimSuffix(defaultString(
51-
options.APIOverrides.LogEvent,
52-
defaultString(options.API, StatsigAPI),
53-
), "/"),
54-
}
5534
defer func() {
5635
if err := recover(); err != nil {
5736
Logger().LogError(err)
5837
}
5938
}()
6039

6140
return &transport{
62-
api: api,
6341
metadata: getStatsigMetadata(),
6442
sdkKey: secret,
6543
client: &http.Client{
@@ -89,11 +67,19 @@ func (transport *transport) download_config_specs(sinceTime int64, responseBody
8967
} else {
9068
endpoint = fmt.Sprintf("/download_config_specs/%s.json?sinceTime=%d", transport.sdkKey, sinceTime)
9169
}
92-
return transport.get(endpoint, responseBody, RequestOptions{})
70+
options := RequestOptions{}
71+
if transport.options.FallbackToStatsigAPI {
72+
options.retries = 1
73+
}
74+
return transport.get(endpoint, responseBody, options)
9375
}
9476

9577
func (transport *transport) get_id_lists(responseBody interface{}) (*http.Response, error) {
96-
return transport.post("/get_id_lists", nil, responseBody, RequestOptions{})
78+
options := RequestOptions{}
79+
if transport.options.FallbackToStatsigAPI {
80+
options.retries = 1
81+
}
82+
return transport.post("/get_id_lists", nil, responseBody, options)
9783
}
9884

9985
func (transport *transport) get_id_list(url string, headers map[string]string) (*http.Response, error) {
@@ -171,7 +157,11 @@ func (transport *transport) buildRequest(method, endpoint string, body interface
171157
bodyBuf = bytes.NewBufferString("{}")
172158
}
173159
}
174-
req, err := http.NewRequest(method, transport.buildURL(endpoint), bodyBuf)
160+
url, err := transport.buildURL(endpoint, false)
161+
if err != nil {
162+
return nil, err
163+
}
164+
req, err := http.NewRequest(method, url.String(), bodyBuf)
175165
if err != nil {
176166
return nil, err
177167
}
@@ -193,16 +183,47 @@ func (transport *transport) buildRequest(method, endpoint string, body interface
193183
return req, nil
194184
}
195185

196-
func (transport *transport) buildURL(endpoint string) string {
186+
func (t *transport) buildURL(path string, isRetry bool) (*url.URL, error) {
187+
var api string
188+
useDefaultAPI := isRetry && t.options.FallbackToStatsigAPI
189+
endpoint := strings.TrimPrefix(path, "/v1")
197190
if strings.Contains(endpoint, "download_config_specs") {
198-
return transport.api.downloadConfigSpecs + endpoint
191+
if useDefaultAPI {
192+
api = StatsigCDN
193+
} else {
194+
api = defaultString(t.options.APIOverrides.DownloadConfigSpecs, defaultString(t.options.API, StatsigCDN))
195+
}
199196
} else if strings.Contains(endpoint, "get_id_list") {
200-
return transport.api.getIDLists + endpoint
197+
if useDefaultAPI {
198+
api = StatsigAPI
199+
} else {
200+
api = defaultString(t.options.APIOverrides.GetIDLists, defaultString(t.options.API, StatsigAPI))
201+
}
201202
} else if strings.Contains(endpoint, "log_event") {
202-
return transport.api.logEvent + endpoint
203+
if useDefaultAPI {
204+
api = StatsigAPI
205+
} else {
206+
api = defaultString(t.options.APIOverrides.LogEvent, defaultString(t.options.API, StatsigAPI))
207+
}
203208
} else {
204-
return defaultString(transport.options.API, StatsigAPI) + endpoint
209+
if useDefaultAPI {
210+
api = StatsigAPI
211+
} else {
212+
api = defaultString(t.options.API, StatsigAPI)
213+
}
205214
}
215+
return url.Parse(strings.TrimSuffix(api, "/") + endpoint)
216+
}
217+
218+
func (t *transport) updateRequestForRetry(r *http.Request) *http.Request {
219+
retryURL, err := t.buildURL(r.URL.Path, true)
220+
if err == nil && strings.Compare(r.URL.Host, retryURL.Host) != 0 {
221+
retryRequest, err := http.NewRequest(r.Method, retryURL.String(), r.Body)
222+
if err == nil {
223+
return retryRequest
224+
}
225+
}
226+
return nil
206227
}
207228

208229
func (transport *transport) doRequest(
@@ -225,10 +246,16 @@ func (transport *transport) doRequest(
225246
if err != nil {
226247
return response, response != nil, err
227248
}
249+
250+
retryRequest := transport.updateRequestForRetry(request)
251+
if retryRequest != nil {
252+
request = retryRequest
253+
}
254+
228255
drainAndCloseBody := func() {
229256
if response.Body != nil {
230257
// Drain body to re-use the same connection
231-
_, _ = io.Copy(ioutil.Discard, response.Body)
258+
_, _ = io.Copy(io.Discard, response.Body)
232259
response.Body.Close()
233260
}
234261
}

0 commit comments

Comments
 (0)