Repository: Unleash/unleash-client-go
Pull request #175 (2024-07-26 10:43:24)

feat: New Metrics metadata
 is included in client registration and client metrics


chriswk: > The code looks cool but what happened to `specVersion`?

Don't you come here with your facts :1st_place_medal: . Added specVersion...

Patch:
diff --git a/client.go b/client.go
index f45c385..2cb564f 100644
--- a/client.go
+++ b/client.go
@@ -18,6 +18,7 @@ const (
 	deprecatedSuffix = "/features"
 	clientName       = "unleash-client-go"
 	clientVersion    = "4.1.1"
+	specVersion      = "4.3.1"
 )
 
 var defaultStrategies = []strategy.Strategy{
diff --git a/config.go b/config.go
index 9ed7ddc..8fb1a40 100644
--- a/config.go
+++ b/config.go
@@ -265,4 +265,5 @@ type metricsOptions struct {
 	disableMetrics  bool
 	httpClient      *http.Client
 	customHeaders   http.Header
+	started         *time.Time
 }
diff --git a/go.mod b/go.mod
index b4b9962..6c6deb3 100644
--- a/go.mod
+++ b/go.mod
@@ -3,12 +3,12 @@ module github.com/Unleash/unleash-client-go/v4
 require (
 	github.com/Masterminds/semver/v3 v3.1.1
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
+	github.com/h2non/gock v1.2.0
+	github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/stretchr/objx v0.1.1 // indirect
 	github.com/stretchr/testify v1.2.2
 	github.com/twmb/murmur3 v1.1.8
-	gopkg.in/h2non/gock.v1 v1.0.10
 )
 
 go 1.13
diff --git a/go.sum b/go.sum
index 61d9218..f593884 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,10 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
 github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
+github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -12,5 +16,3 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
 github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
-gopkg.in/h2non/gock.v1 v1.0.10 h1:D4j796HhgidcxF0LnDyFXcoEbEZWoLEWf0kRh61p22w=
-gopkg.in/h2non/gock.v1 v1.0.10/go.mod h1:KHI4Z1sxDW6P4N3DfTWSEza07YpkQP7KJBfglRMEjKY=
diff --git a/metrics.go b/metrics.go
index 94eb178..3788eb8 100644
--- a/metrics.go
+++ b/metrics.go
@@ -8,6 +8,7 @@ import (
 	"math"
 	"net/http"
 	"net/url"
+	"runtime"
 	"sync"
 	"time"
 
@@ -24,6 +25,21 @@ type MetricsData struct {
 
 	// Bucket is the payload data sent to the server.
 	Bucket api.Bucket `json:"bucket"`
+
+	// The runtime version of our Platform
+	PlatformVersion string `json:"platformVersion"`
+
+	// The runtime name of our Platform
+	PlatformName string `json:"platformName"`
+
+	// Which version of Yggdrasil is being used
+	YggdrasilVersion *string `json:"yggdrasilVersion"`
+
+	// Optional field that describes the sdk version (name:version)
+	SDKVersion string `json:"sdkVersion"`
+
+	// Which version of the Unleash-Client-Spec is this SDK validated against
+	SpecVersion string `json:"specVersion"`
 }
 
 // ClientData represents the data sent to the unleash during registration.
@@ -46,6 +62,15 @@ type ClientData struct {
 	// Interval specifies the time interval (in ms) that the client is using for refreshing
 	// feature toggles.
 	Interval int64 `json:"interval"`
+
+	PlatformVersion string `json:"platformVersion"`
+
+	PlatformName string `json:"platformName"`
+
+	YggdrasilVersion *string `json:"yggdrasilVersion"`
+
+	// Which version of the Unleash-Client-Spec is this SDK validated against
+	SpecVersion string `json:"specVersion"`
 }
 
 type metric struct {
@@ -174,9 +199,14 @@ func (m *metrics) sendMetrics() {
 	}
 	bucket.Stop = time.Now()
 	payload := MetricsData{
-		AppName:    m.options.appName,
-		InstanceID: m.options.instanceId,
-		Bucket:     bucket,
+		AppName:          m.options.appName,
+		InstanceID:       m.options.instanceId,
+		Bucket:           bucket,
+		SDKVersion:       fmt.Sprintf("%s:%s", clientName, clientVersion),
+		PlatformName:     "go",
+		PlatformVersion:  runtime.Version(),
+		YggdrasilVersion: nil,
+		SpecVersion:      specVersion,
 	}
 
 	u, _ := m.options.url.Parse("./client/metrics")
@@ -305,5 +335,9 @@ func (m *metrics) getClientData() ClientData {
 		m.options.strategies,
 		m.started,
 		int64(m.options.metricsInterval.Seconds()),
+		runtime.Version(),
+		"go",
+		nil,
+		specVersion,
 	}
 }


Test patch:
diff --git a/client_test.go b/client_test.go
index 64f50b8..4e3ba26 100644
--- a/client_test.go
+++ b/client_test.go
@@ -10,8 +10,8 @@ import (
 
 	"testing"
 
+	"github.com/h2non/gock"
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/h2non/gock.v1"
 )
 
 func TestClientWithoutListener(t *testing.T) {
@@ -1399,12 +1399,12 @@ func TestGetVariant_FallbackVariantFeatureEnabledSettingIsLeftUnchanged(t *testi
 	client.WaitForReady()
 
 	fallbackVariantFeatureEnabled := api.Variant{
-		Name: "fallback-variant-feature-enabled",
-                FeatureEnabled: true,
+		Name:           "fallback-variant-feature-enabled",
+		FeatureEnabled: true,
 	}
 	fallbackVariantFeatureDisabled := api.Variant{
-		Name: "fallback-variant-feature-disabled",
-                FeatureEnabled: false,
+		Name:           "fallback-variant-feature-disabled",
+		FeatureEnabled: false,
 	}
 
 	variantForEnabledFeatureNoVariants := client.GetVariant(enabledFeatureNoVariants, WithVariantFallback(&fallbackVariantFeatureDisabled))
diff --git a/example_bootstrap_from_file_test.go b/example_bootstrap_from_file_test.go
index 8dcf3f6..02a38cb 100644
--- a/example_bootstrap_from_file_test.go
+++ b/example_bootstrap_from_file_test.go
@@ -8,8 +8,8 @@ import (
 
 	"github.com/Unleash/unleash-client-go/v4"
 	"github.com/Unleash/unleash-client-go/v4/context"
+	"github.com/h2non/gock"
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/h2non/gock.v1"
 )
 
 func Test_bootstrapFromFile(t *testing.T) {
diff --git a/internal/strategies/gradual_rollout_random_test.go b/internal/strategies/gradual_rollout_random_test.go
index a0e9790..1d3a78f 100644
--- a/internal/strategies/gradual_rollout_random_test.go
+++ b/internal/strategies/gradual_rollout_random_test.go
@@ -1,3 +1,4 @@
+//go:build norace
 // +build norace
 
 package strategies
diff --git a/internal/strategies/gradual_rollout_session_id_test.go b/internal/strategies/gradual_rollout_session_id_test.go
index 0d9ef0c..949e760 100644
--- a/internal/strategies/gradual_rollout_session_id_test.go
+++ b/internal/strategies/gradual_rollout_session_id_test.go
@@ -1,3 +1,4 @@
+//go:build norace
 // +build norace
 
 package strategies
diff --git a/internal/strategies/gradual_rollout_user_id_test.go b/internal/strategies/gradual_rollout_user_id_test.go
index d14d58e..c21fdc9 100644
--- a/internal/strategies/gradual_rollout_user_id_test.go
+++ b/internal/strategies/gradual_rollout_user_id_test.go
@@ -1,3 +1,4 @@
+//go:build norace
 // +build norace
 
 package strategies
diff --git a/metrics_test.go b/metrics_test.go
index 82fca5d..d69298b 100644
--- a/metrics_test.go
+++ b/metrics_test.go
@@ -2,11 +2,13 @@ package unleash
 
 import (
 	"encoding/json"
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"reflect"
+	"runtime"
 	"strings"
 	"sync/atomic"
 	"testing"
@@ -14,9 +16,10 @@ import (
 
 	"github.com/Unleash/unleash-client-go/v4/api"
 	internalapi "github.com/Unleash/unleash-client-go/v4/internal/api"
+	"github.com/h2non/gock"
+	"github.com/nbio/st"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
-	"gopkg.in/h2non/gock.v1"
 )
 
 func TestMetrics_RegisterInstance(t *testing.T) {
@@ -426,3 +429,166 @@ func TestMetrics_ErrorCountShouldDecreaseIfSuccessful(t *testing.T) {
 	assert.Equal(float64(0), client.metrics.errors)
 	assert.Nil(err, "Client should close without a problem")
 }
+
+func clientDataMatcher() func(req *http.Request, ereq *gock.Request) (bool, error) {
+	defaultStrategies := []string{
+		"default",
+		"applicationHostname",
+		"gradualRolloutRandom",
+		"gradualRolloutSessionId",
+		"gradualRolloutUserId",
+		"remoteAddress",
+		"userWithId",
+		"flexibleRollout",
+	}
+	return func(req *http.Request, ereq *gock.Request) (bool, error) {
+		var data ClientData
+		err := json.NewDecoder(req.Body).Decode(&data)
+		if err != nil {
+			return false, err
+		}
+
+		if data.Started.IsZero() {
+			return false, nil
+		}
+
+		expectedData := ClientData{
+			AppName:          mockAppName,
+			InstanceID:       mockInstanceId,
+			SDKVersion:       fmt.Sprintf("%s:%s", clientName, clientVersion),
+			Strategies:       defaultStrategies,
+			Interval:         0,
+			PlatformVersion:  runtime.Version(),
+			PlatformName:     "go",
+			YggdrasilVersion: nil,
+			SpecVersion:      specVersion,
+		}
+
+		return data.AppName == expectedData.AppName &&
+			data.InstanceID == expectedData.InstanceID &&
+			data.SDKVersion == expectedData.SDKVersion &&
+			compareStringSlices(data.Strategies, expectedData.Strategies) &&
+			data.Interval == expectedData.Interval &&
+			data.PlatformVersion == expectedData.PlatformVersion &&
+			data.PlatformName == expectedData.PlatformName &&
+			data.YggdrasilVersion == expectedData.YggdrasilVersion &&
+			data.SpecVersion == expectedData.SpecVersion, nil
+	}
+}
+
+func compareStringSlices(a, b []string) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for i := range a {
+		if a[i] != b[i] {
+			return false
+		}
+	}
+	return true
+}
+
+func TestMetrics_ClientDataIncludesNewMetadata(t *testing.T) {
+	assert := assert.New(t)
+	defer gock.OffAll()
+
+	gock.New(mockerServer).
+		Post("/client/register").
+		AddMatcher(clientDataMatcher()).
+		Reply(200)
+
+	client, err := NewClient(
+		WithUrl(mockerServer),
+		WithMetricsInterval(50*time.Millisecond),
+		WithAppName(mockAppName),
+		WithInstanceId(mockInstanceId),
+	)
+
+	assert.Nil(err, "Client should open without a problem")
+
+	time.Sleep(100 * time.Millisecond)
+
+	err = client.Close()
+
+	assert.Nil(err, "Client should close without errors")
+
+	st.Expect(t, gock.IsDone(), true)
+}
+
+func TestMetrics_metricsData_includes_new_metadata(t *testing.T) {
+	assert := assert.New(t)
+	defer gock.OffAll()
+
+	gock.Observe(gock.DumpRequest)
+
+	gock.New(mockerServer).Post("/client/register").Reply(200)
+	gock.New(mockerServer).
+		Post("/client/metrics").
+		BodyString(`.*platformVersion.*`).
+		Reply(200)
+	gock.New(mockerServer).
+		Get("/client/features").
+		Reply(200).
+		JSON(api.FeatureResponse{
+			Features: []api.Feature{
+				{
+					Name:        "parent",
+					Enabled:     true,
+					Description: "parent toggle",
+					Strategies: []api.Strategy{
+						{
+							Id:          1,
+							Name:        "flexibleRollout",
+							Constraints: []api.Constraint{},
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+						},
+					},
+				},
+				{
+					Name:        "child",
+					Enabled:     true,
+					Description: "parent toggle",
+					Strategies: []api.Strategy{
+						{
+							Id:          1,
+							Name:        "flexibleRollout",
+							Constraints: []api.Constraint{},
+							Parameters: map[string]interface{}{
+								"rollout":    100,
+								"stickiness": "default",
+							},
+						},
+					},
+					Dependencies: &[]api.Dependency{
+						{
+							Feature: "parent",
+						},
+					},
+				},
+			},
+		})
+
+	client, err := NewClient(
+		WithUrl(mockerServer),
+		WithMetricsInterval(50*time.Millisecond),
+		WithAppName(mockAppName),
+		WithInstanceId(mockInstanceId),
+		WithDisableMetrics(false),
+	)
+	assert.Nil(err, "Client should open without a problem")
+
+	client.WaitForReady()
+	client.IsEnabled("foo")
+	client.IsEnabled("bar")
+	client.IsEnabled("baz")
+
+	time.Sleep(320 * time.Millisecond)
+	err = client.Close()
+
+	assert.Nil(err, "Client should close without errors")
+
+	st.Expect(t, gock.IsDone(), true)
+}
diff --git a/repository_test.go b/repository_test.go
index 2854730..7f26fc9 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -3,7 +3,6 @@ package unleash
 import (
 	"bytes"
 	"encoding/json"
-	"gopkg.in/h2non/gock.v1"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -11,6 +10,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/h2non/gock"
+
 	"github.com/Unleash/unleash-client-go/v4/api"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -129,20 +130,19 @@ func TestRepository_ParseAPIResponse(t *testing.T) {
 	assert.Equal(0, len(response.Segments))
 }
 
-
 func TestRepository_backs_off_on_http_statuses(t *testing.T) {
 	a := assert.New(t)
 	testCases := []struct {
 		statusCode int
 		errorCount float64
 	}{
-		{ 401, 10},
-		{ 403, 10},
-		{ 404, 10},
-		{ 429, 1},
-		{ 500, 1},
-		{ 502, 1},
-		{ 503, 1},
+		{401, 10},
+		{403, 10},
+		{404, 10},
+		{429, 1},
+		{500, 1},
+		{502, 1},
+		{503, 1},
 	}
 	defer gock.Off()
 	for _, tc := range testCases {
@@ -154,7 +154,7 @@ func TestRepository_backs_off_on_http_statuses(t *testing.T) {
 			WithAppName(mockAppName),
 			WithDisableMetrics(true),
 			WithInstanceId(mockInstanceId),
-			WithRefreshInterval(time.Millisecond * 15),
+			WithRefreshInterval(time.Millisecond*15),
 		)
 		a.Nil(err)
 		time.Sleep(20 * time.Millisecond)
@@ -167,8 +167,8 @@ func TestRepository_back_offs_are_gradually_reduced_on_success(t *testing.T) {
 	a := assert.New(t)
 	defer gock.Off()
 	gock.New(mockerServer).
-	    Get("/client/features").
-	    Times(4).
+		Get("/client/features").
+		Times(4).
 		Reply(429)
 	gock.New(mockerServer).
 		Get("/client/features").
@@ -179,11 +179,11 @@ func TestRepository_back_offs_are_gradually_reduced_on_success(t *testing.T) {
 		WithAppName(mockAppName),
 		WithDisableMetrics(true),
 		WithInstanceId(mockInstanceId),
-		WithRefreshInterval(time.Millisecond * 10),
+		WithRefreshInterval(time.Millisecond*10),
 	)
 	a.Nil(err)
 	client.WaitForReady()
 	err = client.Close()
 	a.Equal(float64(3), client.repository.errors) // 4 failures, and then one success, should reduce error count to 3
 	a.Nil(err)
-}
\ No newline at end of file
+}
diff --git a/spec_test.go b/spec_test.go
index 7fb566a..55f1078 100644
--- a/spec_test.go
+++ b/spec_test.go
@@ -12,10 +12,10 @@ import (
 
 	"github.com/Unleash/unleash-client-go/v4/api"
 	"github.com/Unleash/unleash-client-go/v4/context"
+	"github.com/h2non/gock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/suite"
-	"gopkg.in/h2non/gock.v1"
 )
 
 const mockHost = "http://unleash-apu"
diff --git a/unleash_test.go b/unleash_test.go
index 1e96730..bf5653c 100644
--- a/unleash_test.go
+++ b/unleash_test.go
@@ -7,8 +7,8 @@ import (
 	"time"
 
 	"github.com/Unleash/unleash-client-go/v4"
+	"github.com/h2non/gock"
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/h2non/gock.v1"
 )
 
 func Test_withVariants(t *testing.T) {