Repository: hetznercloud/hcloud-go
Pull request #96 (2018-09-24 09:39:11)

Allow to set application name/version for user agent
Allows us to gather better statistics on which integrations are popular.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index e30e144..4c70b56 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,10 @@
 # Changes
 
+## master
+
+* Add `WithApplication()` client option to provide an application name and version
+  that will be included in the `User-Agent` HTTP header
+
 ## v1.9.0
 
 * Add `AllWithOpts()` to server, Floating IP, image, and SSH key client
diff --git a/hcloud/client.go b/hcloud/client.go
index c578da2..0b55531 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -20,7 +20,8 @@ import (
 // Endpoint is the base URL of the API.
 const Endpoint = "https://api.hetzner.cloud/v1"
 
-// UserAgent is the value for the User-Agent header sent with each request.
+// UserAgent is the value for the library part of the User-Agent header
+// that is sent with each request.
 const UserAgent = "hcloud-go/" + Version
 
 // A BackoffFunc returns the duration to wait before performing the
@@ -46,11 +47,14 @@ func ExponentialBackoff(b float64, d time.Duration) BackoffFunc {
 
 // Client is a client for the Hetzner Cloud API.
 type Client struct {
-	endpoint     string
-	token        string
-	pollInterval time.Duration
-	backoffFunc  BackoffFunc
-	httpClient   *http.Client
+	endpoint           string
+	token              string
+	pollInterval       time.Duration
+	backoffFunc        BackoffFunc
+	httpClient         *http.Client
+	applicationName    string
+	applicationVersion string
+	userAgent          string
 
 	Action     ActionClient
 	Datacenter DatacenterClient
@@ -96,6 +100,16 @@ func WithBackoffFunc(f BackoffFunc) ClientOption {
 	}
 }
 
+// WithApplication configures a Client with the given application name and
+// application version. The version may be blank. Programs are encouraged
+// to at least set an application name.
+func WithApplication(name, version string) ClientOption {
+	return func(client *Client) {
+		client.applicationName = name
+		client.applicationVersion = version
+	}
+}
+
 // NewClient creates a new client.
 func NewClient(options ...ClientOption) *Client {
 	client := &Client{
@@ -109,6 +123,8 @@ func NewClient(options ...ClientOption) *Client {
 		option(client)
 	}
 
+	client.buildUserAgent()
+
 	client.Action = ActionClient{client: client}
 	client.Datacenter = DatacenterClient{client: client}
 	client.FloatingIP = FloatingIPClient{client: client}
@@ -131,7 +147,7 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re
 	if err != nil {
 		return nil, err
 	}
-	req.Header.Set("User-Agent", UserAgent)
+	req.Header.Set("User-Agent", c.userAgent)
 	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
 	if body != nil {
 		req.Header.Set("Content-Type", "application/json")
@@ -207,6 +223,17 @@ func (c *Client) all(f func(int) (*Response, error)) (*Response, error) {
 	}
 }
 
+func (c *Client) buildUserAgent() {
+	switch {
+	case c.applicationName != "" && c.applicationVersion != "":
+		c.userAgent = c.applicationName + "/" + c.applicationVersion + " " + UserAgent
+	case c.applicationName != "" && c.applicationVersion == "":
+		c.userAgent = c.applicationName + " " + UserAgent
+	default:
+		c.userAgent = UserAgent
+	}
+}
+
 func errorFromResponse(resp *http.Response, body []byte) error {
 	if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
 		return nil


Test patch:
diff --git a/hcloud/client_test.go b/hcloud/client_test.go
index a291911..99ddfc4 100644
--- a/hcloud/client_test.go
+++ b/hcloud/client_test.go
@@ -262,3 +262,25 @@ func TestClientDo(t *testing.T) {
 		t.Fatalf("unexpected error: %v", err)
 	}
 }
+
+func TestBuildUserAgent(t *testing.T) {
+	testCases := []struct {
+		name               string
+		applicationName    string
+		applicationVersion string
+		userAgent          string
+	}{
+		{"with application name and version", "test", "1.0", "test/1.0 " + UserAgent},
+		{"with application name but no version", "test", "", "test " + UserAgent},
+		{"without application name and version", "", "", UserAgent},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			client := NewClient(WithApplication(testCase.applicationName, testCase.applicationVersion))
+			if client.userAgent != testCase.userAgent {
+				t.Errorf("unexpected user agent: %v", client.userAgent)
+			}
+		})
+	}
+}