Skip to content

Commit 7e2ea37

Browse files
authored
feat: add User-Agent header with SDK version (#181)
* feat: add User-Agent header with SDK version Implements #175 - Add getUserAgent() function that uses runtime/debug.ReadBuildInfo() to detect SDK version - User-Agent format: flagsmith-go-sdk/<version> or flagsmith-go-sdk/unknown in development - Added comprehensive unit and integration tests - Exported getUserAgent() via export_test.go for testing purposes * don't generate eval context * fix version when used as dep * fixup * fixup; test
1 parent 4e53c08 commit 7e2ea37

File tree

6 files changed

+130
-3
lines changed

6 files changed

+130
-3
lines changed

.github/workflows/go.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ jobs:
3030
with:
3131
submodules: recursive
3232

33-
- name: Build evaluation context struct
34-
run: make generate-evaluation-context
35-
3633
- name: Get dependencies
3734
run: |
3835
go get -v -t -d ./...

client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func NewClient(apiKey string, options ...Option) *Client {
9797

9898
c.client.SetHeaders(map[string]string{
9999
"Accept": "application/json",
100+
"User-Agent": getUserAgent(),
100101
EnvironmentKeyHeader: c.apiKey,
101102
})
102103

client_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,38 @@ func TestClientErrorsIfLocalEvaluationModeAndOfflineHandlerAreBothSet(t *testing
107107
flagsmith.WithLocalEvaluation(context.Background()))
108108
}
109109

110+
func TestUserAgentHeaderIsSent(t *testing.T) {
111+
// Given
112+
userAgentReceived := ""
113+
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
114+
userAgentReceived = req.Header.Get("User-Agent")
115+
rw.Header().Set("Content-Type", "application/json")
116+
rw.WriteHeader(http.StatusOK)
117+
_, err := io.WriteString(rw, fixtures.EnvironmentJson)
118+
if err != nil {
119+
panic(err)
120+
}
121+
}))
122+
defer server.Close()
123+
124+
// When
125+
client := flagsmith.NewClient(fixtures.EnvironmentAPIKey,
126+
flagsmith.WithBaseURL(server.URL+"/api/v1/"))
127+
_, _ = client.GetEnvironmentFlags(context.Background())
128+
129+
// Then
130+
// Get the expected User-Agent value from the SDK's getUserAgent() function
131+
expectedUserAgent := flagsmith.GetUserAgentForTest()
132+
133+
assert.NotEmpty(t, userAgentReceived, "User-Agent header should be sent")
134+
assert.Equal(t, expectedUserAgent, userAgentReceived,
135+
"User-Agent header should match the value returned by getUserAgent()")
136+
137+
// Verify basic format requirements
138+
assert.True(t, strings.HasPrefix(userAgentReceived, "flagsmith-go-sdk/"),
139+
"User-Agent should start with 'flagsmith-go-sdk/', got: %s", userAgentReceived)
140+
}
141+
110142
func TestClientUpdatesEnvironmentOnStartForLocalEvaluation(t *testing.T) {
111143
// Given
112144
ctx := context.Background()

export_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package flagsmith
2+
3+
// This file exports internal functions for testing purposes only.
4+
// It is compiled only when running tests (no build tags needed).
5+
6+
// GetUserAgentForTest exposes the getUserAgent function for external tests.
7+
func GetUserAgentForTest() string {
8+
return getUserAgent()
9+
}

version.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package flagsmith
2+
3+
import (
4+
"fmt"
5+
"runtime/debug"
6+
"strings"
7+
)
8+
9+
// getUserAgent returns the User-Agent header value in the format "flagsmith-go-sdk/<version>".
10+
// If the version cannot be determined (e.g., during development), it returns "flagsmith-go-sdk/unknown".
11+
func getUserAgent() string {
12+
const sdkName = "flagsmith-go-sdk"
13+
const unknownVersion = "unknown"
14+
const modulePrefix = "github.com/Flagsmith/flagsmith-go-client"
15+
16+
info, ok := debug.ReadBuildInfo()
17+
if !ok {
18+
return fmt.Sprintf("%s/%s", sdkName, unknownVersion)
19+
}
20+
21+
// Check if SDK module path matches (supports any major version: v4, v5, etc.)
22+
isSDKModule := func(path string) bool {
23+
return strings.HasPrefix(path, modulePrefix+"/")
24+
}
25+
26+
// If this is the main module (running tests or examples from within the SDK repo),
27+
// use the main module version
28+
if isSDKModule(info.Main.Path) {
29+
version := info.Main.Version
30+
if version != "" && version != "(devel)" {
31+
return fmt.Sprintf("%s/%s", sdkName, version)
32+
}
33+
}
34+
35+
// Otherwise, look for the SDK in the dependencies (when used as a library)
36+
for _, dep := range info.Deps {
37+
if isSDKModule(dep.Path) {
38+
version := dep.Version
39+
if version != "" && version != "(devel)" {
40+
return fmt.Sprintf("%s/%s", sdkName, version)
41+
}
42+
break
43+
}
44+
}
45+
46+
return fmt.Sprintf("%s/%s", sdkName, unknownVersion)
47+
}

version_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package flagsmith
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGetUserAgent(t *testing.T) {
11+
// Given/When
12+
userAgent := getUserAgent()
13+
14+
// Then - should return a non-empty string
15+
assert.NotEmpty(t, userAgent, "User-Agent should not be empty")
16+
}
17+
18+
func TestGetUserAgentFormat(t *testing.T) {
19+
// Given/When
20+
userAgent := getUserAgent()
21+
22+
// Then - should start with "flagsmith-go-sdk/"
23+
assert.True(t, strings.HasPrefix(userAgent, "flagsmith-go-sdk/"),
24+
"User-Agent should start with 'flagsmith-go-sdk/', got: %s", userAgent)
25+
}
26+
27+
func TestGetUserAgentValidFormats(t *testing.T) {
28+
// Given/When
29+
userAgent := getUserAgent()
30+
31+
// Then - should be either a valid version or "unknown"
32+
parts := strings.Split(userAgent, "/")
33+
assert.Equal(t, 2, len(parts), "User-Agent should have exactly two parts separated by '/'")
34+
assert.Equal(t, "flagsmith-go-sdk", parts[0], "First part should be 'flagsmith-go-sdk'")
35+
36+
// Version part should be either a version string (starting with 'v') or "unknown"
37+
versionPart := parts[1]
38+
isValid := versionPart == "unknown" || strings.HasPrefix(versionPart, "v")
39+
assert.True(t, isValid,
40+
"Version should be 'unknown' or start with 'v', got: %s", versionPart)
41+
}

0 commit comments

Comments
 (0)