Skip to content

Commit 5b79a35

Browse files
authored
Implement the new User Agent sent to the update service (#1072)
Signed-off-by: lujunsan <luisjuncaldev@gmail.com>
1 parent 2554e78 commit 5b79a35

File tree

4 files changed

+102
-34
lines changed

4 files changed

+102
-34
lines changed

pkg/api/server.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,23 +94,39 @@ func updateCheckMiddleware() func(next http.Handler) http.Handler {
9494
return func(next http.Handler) http.Handler {
9595
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
9696
go func() {
97-
versionClient := updates.NewVersionClient()
97+
component, version, uiReleaseBuild := getComponentAndVersionFromRequest(r)
98+
versionClient := updates.NewVersionClientForComponent(component, version, uiReleaseBuild)
99+
98100
updateChecker, err := updates.NewUpdateChecker(versionClient)
99101
if err != nil {
100-
logger.Warnf("unable to create update client for API: %s", err)
102+
logger.Warnf("unable to create update client for %s: %s", component, err)
101103
return
102104
}
103105

104106
err = updateChecker.CheckLatestVersion()
105107
if err != nil {
106-
logger.Warnf("could not check for updates for API: %s", err)
108+
logger.Warnf("could not check for updates for %s: %s", component, err)
107109
}
108110
}()
109111
next.ServeHTTP(w, r)
110112
})
111113
}
112114
}
113115

116+
// getComponentAndVersionFromRequest determines the component name, version, and ui release build from the request
117+
func getComponentAndVersionFromRequest(r *http.Request) (string, string, bool) {
118+
clientType := r.Header.Get("X-Client-Type")
119+
120+
if clientType == "toolhive-studio" {
121+
version := r.Header.Get("X-Client-Version")
122+
// Checks if the UI is calling from an official release
123+
uiReleaseBuild := r.Header.Get("X-Client-Release-Build") == "true"
124+
return "UI", version, uiReleaseBuild
125+
}
126+
127+
return "API", "", false
128+
}
129+
114130
// Serve starts the server on the given address and serves the API.
115131
// It is assumed that the caller sets up appropriate signal handling.
116132
// If isUnixSocket is true, address is treated as a UNIX socket path.

pkg/operator/telemetry/telemetry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func NewService(k8sClient client.Client, namespace string) *Service {
8484
}
8585
return &Service{
8686
client: k8sClient,
87-
versionClient: updates.NewVersionClientWithSuffix("operator"),
87+
versionClient: updates.NewVersionClientForComponent("operator", "", false),
8888
namespace: namespace,
8989
}
9090
}

pkg/updates/client.go

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"runtime"
9+
"strings"
810
"time"
911

1012
"github.com/stacklok/toolhive/pkg/versions"
@@ -17,27 +19,34 @@ type VersionClient interface {
1719

1820
// NewVersionClient creates a new instance of VersionClient.
1921
func NewVersionClient() VersionClient {
20-
return NewVersionClientWithSuffix("")
22+
return NewVersionClientForComponent("CLI", "", false)
2123
}
2224

23-
// NewVersionClientWithSuffix creates a new instance of VersionClient with an optional user agent suffix.
24-
func NewVersionClientWithSuffix(suffix string) VersionClient {
25+
// NewVersionClientForComponent creates a new instance of VersionClient for a specific component.
26+
func NewVersionClientForComponent(component, version string, uiReleaseBuild bool) VersionClient {
2527
return &defaultVersionClient{
2628
versionEndpoint: defaultVersionAPI,
27-
userAgentSuffix: suffix,
29+
component: component,
30+
customVersion: version,
31+
uiReleaseBuild: uiReleaseBuild,
2832
}
2933
}
3034

3135
type defaultVersionClient struct {
3236
versionEndpoint string
33-
userAgentSuffix string
37+
component string
38+
customVersion string
39+
uiReleaseBuild bool // For the UI component, tracks if the UI is calling from a release build, false otherwise
3440
}
3541

3642
const (
3743
instanceIDHeader = "X-Instance-ID"
3844
userAgentHeader = "User-Agent"
3945
defaultVersionAPI = "https://updates.codegate.ai/api/v1/version"
4046
defaultTimeout = 3 * time.Second
47+
48+
buildTypeRelease = "release"
49+
buildTypeLocalBuild = "local_build"
4150
)
4251

4352
// GetLatestVersion sends a GET request to the update API endpoint and returns the version from the response.
@@ -49,19 +58,38 @@ func (d *defaultVersionClient) GetLatestVersion(instanceID string, currentVersio
4958
return "", fmt.Errorf("failed to create request: %w", err)
5059
}
5160

52-
// Set headers
53-
// Determine user agent based on build type
54-
var userAgent string
55-
if versions.BuildType == "release" {
56-
userAgent = fmt.Sprintf("toolhive/%s", currentVersion)
57-
} else {
58-
userAgent = fmt.Sprintf("toolhive/development-%s", currentVersion)
61+
// Generate user agent in format: toolhive/[component] [vXX] [release/local_build] ([operating_system])
62+
63+
// Use custom version if set, otherwise use the passed currentVersion
64+
version := currentVersion
65+
if d.customVersion != "" {
66+
version = d.customVersion
5967
}
6068

61-
// Add suffix
62-
if d.userAgentSuffix != "" {
63-
userAgent += " " + d.userAgentSuffix
69+
// Format version with 'v' prefix if it doesn't start with 'v'
70+
if !strings.HasPrefix(version, "v") {
71+
version = "v" + version
6472
}
73+
74+
buildType := buildTypeLocalBuild
75+
if d.component == "UI" {
76+
// For UI: only use "release" if both server and UI are release builds
77+
if versions.BuildType == buildTypeRelease && d.uiReleaseBuild {
78+
buildType = buildTypeRelease
79+
}
80+
} else {
81+
// For other components: use server build type
82+
if versions.BuildType == buildTypeRelease {
83+
buildType = buildTypeRelease
84+
}
85+
}
86+
87+
// Get platform info as OperatingSystem/Architecture
88+
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
89+
90+
// Format: toolhive/[component] [vXX] [release/local_build] ([operating_system])
91+
userAgent := fmt.Sprintf("toolhive/%s %s %s (%s)", d.component, version, buildType, platform)
92+
6593
req.Header.Set(instanceIDHeader, instanceID)
6694
req.Header.Set(userAgentHeader, userAgent)
6795

pkg/updates/client_test.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,61 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9-
func TestNewVersionClientWithSuffix(t *testing.T) {
9+
func TestNewVersionClientForComponent(t *testing.T) {
1010
t.Parallel()
1111
tests := []struct {
12-
name string
13-
suffix string
14-
expected string
12+
name string
13+
component string
14+
version string
15+
uiReleaseBuild bool
16+
expected string
1517
}{
1618
{
17-
name: "no suffix",
18-
suffix: "",
19-
expected: "",
19+
name: "CLI component",
20+
component: "CLI",
21+
version: "",
22+
uiReleaseBuild: false,
23+
expected: "CLI",
2024
},
2125
{
22-
name: "operator suffix",
23-
suffix: "operator",
24-
expected: "operator",
26+
name: "operator component",
27+
component: "operator",
28+
version: "",
29+
uiReleaseBuild: false,
30+
expected: "operator",
2531
},
2632
{
27-
name: "custom suffix",
28-
suffix: "custom",
29-
expected: "custom",
33+
name: "UI component with version and release build",
34+
component: "UI",
35+
version: "2.0.0",
36+
uiReleaseBuild: true,
37+
expected: "UI",
38+
},
39+
{
40+
name: "UI component with version and local build",
41+
component: "UI",
42+
version: "2.0.0",
43+
uiReleaseBuild: false,
44+
expected: "UI",
45+
},
46+
{
47+
name: "API component",
48+
component: "API",
49+
version: "",
50+
uiReleaseBuild: false,
51+
expected: "API",
3052
},
3153
}
3254

3355
for _, tt := range tests {
3456
t.Run(tt.name, func(t *testing.T) {
3557
t.Parallel()
36-
client := NewVersionClientWithSuffix(tt.suffix)
58+
client := NewVersionClientForComponent(tt.component, tt.version, tt.uiReleaseBuild)
3759
defaultClient, ok := client.(*defaultVersionClient)
3860
assert.True(t, ok, "Expected defaultVersionClient type")
39-
assert.Equal(t, tt.expected, defaultClient.userAgentSuffix)
61+
assert.Equal(t, tt.expected, defaultClient.component)
62+
assert.Equal(t, tt.version, defaultClient.customVersion)
63+
assert.Equal(t, tt.uiReleaseBuild, defaultClient.uiReleaseBuild)
4064
})
4165
}
4266
}

0 commit comments

Comments
 (0)