Skip to content

Commit aab8893

Browse files
authored
feat: client identification headers (#186)
1 parent e1e3ed0 commit aab8893

File tree

7 files changed

+101
-6
lines changed

7 files changed

+101
-6
lines changed

client.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package unleash
33
import (
44
"fmt"
55

6+
"net/http"
67
"net/url"
78
"strings"
89
"time"
@@ -164,6 +165,14 @@ func NewClient(options ...ConfigOption) (*Client, error) {
164165
uc.options.instanceId = generateInstanceId()
165166
}
166167

168+
headers := make(http.Header)
169+
if uc.options.customHeaders != nil {
170+
headers = uc.options.customHeaders
171+
}
172+
headers.Set("unleash-appname", uc.options.appName)
173+
headers.Set("unleash-sdk", fmt.Sprintf("%s:%s", clientName, clientVersion))
174+
headers.Set("unleash-connection-id", getConnectionId())
175+
167176
uc.repository = newRepository(
168177
repositoryOptions{
169178
backupPath: uc.options.backupPath,
@@ -174,7 +183,7 @@ func NewClient(options ...ConfigOption) (*Client, error) {
174183
refreshInterval: uc.options.refreshInterval,
175184
storage: uc.options.storage,
176185
httpClient: uc.options.httpClient,
177-
customHeaders: uc.options.customHeaders,
186+
headers: headers,
178187
},
179188
repositoryChannels{
180189
errorChannels: errChannels,
@@ -197,7 +206,7 @@ func NewClient(options ...ConfigOption) (*Client, error) {
197206
metricsInterval: uc.options.metricsInterval,
198207
url: *parsedUrl,
199208
httpClient: uc.options.httpClient,
200-
customHeaders: uc.options.customHeaders,
209+
headers: headers,
201210
disableMetrics: uc.options.disableMetrics,
202211
},
203212
metricsChannels{

client_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,3 +1417,36 @@ func TestGetVariant_FallbackVariantFeatureEnabledSettingIsLeftUnchanged(t *testi
14171417

14181418
assert.True(gock.IsDone(), "there should be no more mocks")
14191419
}
1420+
1421+
func TestSendIdentificationHeaders(t *testing.T) {
1422+
assert := assert.New(t)
1423+
defer gock.OffAll()
1424+
1425+
gock.New(mockerServer).
1426+
Post("/client/register").
1427+
MatchHeader("UNLEASH-APPNAME", mockAppName).
1428+
MatchHeader("UNLEASH-SDK", `unleash-client-go:\d+\.\d+\.\d+`).
1429+
MatchHeader("UNLEASH-CONNECTION-ID", `[0-9a-f\-]{36}`).
1430+
Reply(200)
1431+
1432+
gock.New(mockerServer).
1433+
Get("/client/features").
1434+
MatchHeader("UNLEASH-APPNAME", mockAppName).
1435+
MatchHeader("UNLEASH-SDK", `unleash-client-go:\d+\.\d+\.\d+`).
1436+
MatchHeader("UNLEASH-CONNECTION-ID", `[0-9a-f\-]{36}`).
1437+
Reply(200).
1438+
JSON(api.FeatureResponse{})
1439+
1440+
client, err := NewClient(
1441+
WithUrl(mockerServer),
1442+
WithAppName(mockAppName),
1443+
WithInstanceId(mockInstanceId),
1444+
WithListener(&NoopListener{}),
1445+
)
1446+
1447+
assert.NoError(err)
1448+
1449+
client.WaitForReady()
1450+
1451+
assert.True(gock.IsDone(), "there should be no more mocks")
1452+
}

config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ type repositoryOptions struct {
253253
refreshInterval time.Duration
254254
storage Storage
255255
httpClient *http.Client
256-
customHeaders http.Header
256+
headers http.Header
257257
}
258258

259259
type metricsOptions struct {
@@ -264,6 +264,6 @@ type metricsOptions struct {
264264
metricsInterval time.Duration
265265
disableMetrics bool
266266
httpClient *http.Client
267-
customHeaders http.Header
267+
headers http.Header
268268
started *time.Time
269269
}

metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func (m *metrics) doPost(url *url.URL, payload interface{}) (*http.Response, err
259259
req.Header.Add("UNLEASH-INSTANCEID", m.options.instanceId)
260260
req.Header.Add("User-Agent", m.options.appName)
261261

262-
for k, v := range m.options.customHeaders {
262+
for k, v := range m.options.headers {
263263
req.Header[k] = v
264264
}
265265

repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func (r *repository) fetch() error {
130130
// global segments
131131
req.Header.Add("Unleash-Client-Spec", SEGMENT_CLIENT_SPEC_VERSION)
132132

133-
for k, v := range r.options.customHeaders {
133+
for k, v := range r.options.headers {
134134
req.Header[k] = v
135135
}
136136

utils.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package unleash
22

33
import (
4+
cryptoRand "crypto/rand"
45
"fmt"
56
"math/rand"
67
"os"
@@ -30,6 +31,26 @@ func generateInstanceId() string {
3031
return prefix
3132
}
3233

34+
// https://github.com/google/uuid/blob/2d3c2a9cc518326daf99a383f07c4d3c44317e4d/version4.go#L47-L56
35+
// https://github.com/hprose/hprose-go/blob/83de97da5004027694d321ca38c80fca3fac98c2/uuid.go#L91-L98
36+
func getConnectionId() string {
37+
b := make([]byte, 16)
38+
cryptoRand.Read(b)
39+
40+
b[6] = (b[6] & 0x0F) | 0x40
41+
b[8] = (b[8] & 0x3F) | 0x80
42+
43+
uuid := fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
44+
b[0:4],
45+
b[4:6],
46+
b[6:8],
47+
b[8:10],
48+
b[10:16],
49+
)
50+
51+
return uuid
52+
}
53+
3354
func getFetchURLPath(projectName string) string {
3455
if projectName != "" {
3556
return fmt.Sprintf("./client/features?project=%s", projectName)

utils_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,35 @@ func TestContains(t *testing.T) {
116116
}
117117
})
118118
}
119+
120+
func TestGetConnectionId(t *testing.T) {
121+
for i := 0; i < 100; i++ {
122+
uuid := getConnectionId()
123+
124+
t.Run("Correct length", func(t *testing.T) {
125+
if len(uuid) != 36 {
126+
t.Errorf("Expected UUID length to be 36, but got %d in %s", len(uuid), uuid)
127+
}
128+
})
129+
130+
t.Run("UUIDv4 version", func(t *testing.T) {
131+
if uuid[14] != '4' {
132+
t.Errorf("Expected version 4, but got %c in %s", uuid[14], uuid)
133+
}
134+
})
135+
136+
t.Run("UUIDv4 variant", func(t *testing.T) {
137+
variant := uuid[19]
138+
if variant != '8' && variant != '9' && variant != 'a' && variant != 'b' {
139+
t.Errorf("Expected variant 10xx, but got %c in %s", variant, uuid)
140+
}
141+
})
142+
143+
t.Run("Uniqueness", func(t *testing.T) {
144+
uuid2 := getConnectionId()
145+
if uuid == uuid2 {
146+
t.Errorf("Generated UUIDs are not unique: '%s' and '%s'", uuid, uuid2)
147+
}
148+
})
149+
}
150+
}

0 commit comments

Comments
 (0)