Repository: rollbar/rollbar-go
Pull request #65 (2019-09-21 00:13:57)

Diagnostic info
1. Send diagnostic info in the payload:
* Go version
* Configured Options

This brings rollbar-go into sync with work done previously in other SDKs.

2. The travis matrix is updated to include recent Go versions, and evaluates older versions in the `allow_failures` key.

3. Updated tests for config options.



Patch:
diff --git a/.travis.yml b/.travis.yml
index 107e6d6..a7b8ca9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,15 @@
 language: go
 
 go:
+  - 1.7.x
   - 1.8.x
   - 1.9.x
+  - 1.10.x
+  - 1.11.x
+  - 1.12.x
+  - 1.13.x
   - master
+
+matrix:
+  allow_failures:
+    go: 1.7.x
diff --git a/client.go b/client.go
index 283fca6..189a13f 100644
--- a/client.go
+++ b/client.go
@@ -26,6 +26,7 @@ type Client struct {
 	// implementation of the Transport interface is used.
 	Transport     Transport
 	configuration configuration
+	diagnostic    diagnostic
 }
 
 // New returns the default implementation of a Client.
@@ -38,9 +39,11 @@ func New(token, environment, codeVersion, serverHost, serverRoot string) *Client
 func NewAsync(token, environment, codeVersion, serverHost, serverRoot string) *Client {
 	configuration := createConfiguration(token, environment, codeVersion, serverHost, serverRoot)
 	transport := NewTransport(token, configuration.endpoint)
+	diagnostic := createDiagnostic()
 	return &Client{
 		Transport:     transport,
 		configuration: configuration,
+		diagnostic:    diagnostic,
 	}
 }
 
@@ -48,9 +51,11 @@ func NewAsync(token, environment, codeVersion, serverHost, serverRoot string) *C
 func NewSync(token, environment, codeVersion, serverHost, serverRoot string) *Client {
 	configuration := createConfiguration(token, environment, codeVersion, serverHost, serverRoot)
 	transport := NewSyncTransport(token, configuration.endpoint)
+	diagnostic := createDiagnostic()
 	return &Client{
 		Transport:     transport,
 		configuration: configuration,
+		diagnostic:    diagnostic,
 	}
 }
 
@@ -73,33 +78,39 @@ func (c *Client) SetToken(token string) {
 
 // SetEnvironment sets the environment under which all errors and messages will be submitted.
 func (c *Client) SetEnvironment(environment string) {
+	c.diagnostic.configuredOptions["environment"] = environment
 	c.configuration.environment = environment
 }
 
 // SetEndpoint sets the endpoint to post items to. This also configures the underlying Transport.
 func (c *Client) SetEndpoint(endpoint string) {
+	c.diagnostic.configuredOptions["endpoint"] = endpoint
 	c.configuration.endpoint = endpoint
 	c.Transport.SetEndpoint(endpoint)
 }
 
 // SetPlatform sets the platform to be reported for all items.
 func (c *Client) SetPlatform(platform string) {
+	c.diagnostic.configuredOptions["platform"] = platform
 	c.configuration.platform = platform
 }
 
 // SetCodeVersion sets the string describing the running code version on the server.
 func (c *Client) SetCodeVersion(codeVersion string) {
+	c.diagnostic.configuredOptions["codeVersion"] = codeVersion
 	c.configuration.codeVersion = codeVersion
 }
 
 // SetServerHost sets the hostname sent with each item. This value will be indexed.
 func (c *Client) SetServerHost(serverHost string) {
+	c.diagnostic.configuredOptions["serverHost"] = serverHost
 	c.configuration.serverHost = serverHost
 }
 
 // SetServerRoot sets the path to the application code root, not including the final slash.
 // This is used to collapse non-project code when displaying tracebacks.
 func (c *Client) SetServerRoot(serverRoot string) {
+	c.diagnostic.configuredOptions["serverRoot"] = serverRoot
 	c.configuration.serverRoot = serverRoot
 }
 
@@ -112,22 +123,33 @@ func (c *Client) SetCustom(custom map[string]interface{}) {
 // any subsequent errors or messages. Only id is required to be
 // non-empty.
 func (c *Client) SetPerson(id, username, email string) {
-	c.configuration.person = Person{
+	person := Person{
 		Id:       id,
 		Username: username,
 		Email:    email,
 	}
+
+	c.diagnostic.configuredOptions["person"] = map[string]string{
+		"Id": id,
+		"Username": username,
+		"Email": email,
+	}
+	c.configuration.person = person
 }
 
 // ClearPerson clears any previously set person information. See `SetPerson` for more
 // information.
 func (c *Client) ClearPerson() {
-	c.configuration.person = Person{}
+	person := Person{}
+
+	c.diagnostic.configuredOptions["person"] = map[string]string{}
+	c.configuration.person = person
 }
 
 // SetFingerprint sets whether or not to use a custom client-side fingerprint. The default value is
 // false.
 func (c *Client) SetFingerprint(fingerprint bool) {
+	c.diagnostic.configuredOptions["fingerprint"] = fingerprint
 	c.configuration.fingerprint = fingerprint
 }
 
@@ -139,12 +161,14 @@ func (c *Client) SetLogger(logger ClientLogger) {
 // SetScrubHeaders sets the regular expression used to match headers for scrubbing.
 // The default value is regexp.MustCompile("Authorization")
 func (c *Client) SetScrubHeaders(headers *regexp.Regexp) {
+	c.diagnostic.configuredOptions["scrubHeaders"] = headers
 	c.configuration.scrubHeaders = headers
 }
 
 // SetScrubFields sets the regular expression to match keys in the item payload for scrubbing.
 // The default vlaue is regexp.MustCompile("password|secret|token"),
 func (c *Client) SetScrubFields(fields *regexp.Regexp) {
+	c.diagnostic.configuredOptions["scrubFields"] = fields
 	c.configuration.scrubFields = fields
 }
 
@@ -160,6 +184,7 @@ func (c *Client) SetScrubFields(fields *regexp.Regexp) {
 // make before it is finally sent. Be careful with the modifications you make as they could lead to
 // the payload being malformed from the perspective of the API.
 func (c *Client) SetTransform(transform func(map[string]interface{})) {
+	c.diagnostic.configuredOptions["transform"] = functionToString(transform)
 	c.configuration.transform = transform
 }
 
@@ -171,6 +196,7 @@ func (c *Client) SetTransform(transform func(map[string]interface{})) {
 // In order to preserve the default unwrapping behavior, callers of SetUnwrapper may wish to include
 // a call to DefaultUnwrapper in their custom unwrapper function. See the example on the SetUnwrapper function.
 func (c *Client) SetUnwrapper(unwrapper UnwrapperFunc) {
+	c.diagnostic.configuredOptions["unwrapper"] = functionToString(unwrapper)
 	c.configuration.unwrapper = unwrapper
 }
 
@@ -183,6 +209,7 @@ func (c *Client) SetUnwrapper(unwrapper UnwrapperFunc) {
 // to include a call to DefaultStackTracer in their custom tracing function. See the example
 // on the SetStackTracer function.
 func (c *Client) SetStackTracer(stackTracer StackTracerFunc) {
+	c.diagnostic.configuredOptions["stackTracer"] = functionToString(stackTracer)
 	c.configuration.stackTracer = stackTracer
 }
 
@@ -193,6 +220,7 @@ func (c *Client) SetStackTracer(stackTracer StackTracerFunc) {
 // this function is called with the result of calling Error(), otherwise
 // the string representation of the value is passed to this function.
 func (c *Client) SetCheckIgnore(checkIgnore func(string) bool) {
+	c.diagnostic.configuredOptions["checkIgnore"] = functionToString(checkIgnore)
 	c.configuration.checkIgnore = checkIgnore
 }
 
@@ -201,6 +229,7 @@ func (c *Client) SetCheckIgnore(checkIgnore func(string) bool) {
 // CaptureIpAnonymize means apply a pseudo-anonymization.
 // CaptureIpNone means do not capture anything.
 func (c *Client) SetCaptureIp(captureIp captureIp) {
+	c.diagnostic.configuredOptions["captureIp"] = captureIp
 	c.configuration.captureIp = captureIp
 }
 
@@ -560,7 +589,7 @@ func (c *Client) Close() error {
 }
 
 func (c *Client) buildBody(ctx context.Context, level, title string, extras map[string]interface{}) map[string]interface{} {
-	return buildBody(ctx, c.configuration, level, title, extras)
+	return buildBody(ctx, c.configuration, c.diagnostic, level, title, extras)
 }
 
 func (c *Client) requestDetails(r *http.Request) map[string]interface{} {
@@ -652,6 +681,18 @@ func createConfiguration(token, environment, codeVersion, serverHost, serverRoot
 	}
 }
 
+type diagnostic struct {
+	languageVersion string
+	configuredOptions map[string]interface{}
+}
+
+func createDiagnostic() diagnostic {
+	return diagnostic{
+		languageVersion: runtime.Version(),
+		configuredOptions: map[string]interface{}{},
+	}
+}
+
 // clientPost returns an error which indicates the type of error that occured while attempting to
 // send the body input to the endpoint given, or nil if no error occurred. If error is not nil, the
 // boolean return parameter indicates whether the error is temporary or not. If this boolean return
@@ -716,3 +757,7 @@ func isTemporary(err error) bool {
 	}
 	return false
 }
+
+func functionToString(function interface{}) string {
+	return runtime.FuncForPC(reflect.ValueOf(function).Pointer()).Name()
+}
diff --git a/transforms.go b/transforms.go
index f262cbd..d4e066f 100644
--- a/transforms.go
+++ b/transforms.go
@@ -13,7 +13,9 @@ import (
 
 // Build the main JSON structure that will be sent to Rollbar with the
 // appropriate metadata.
-func buildBody(ctx context.Context, configuration configuration, level, title string, extras map[string]interface{}) map[string]interface{} {
+func buildBody(ctx context.Context, configuration configuration, diagnostic diagnostic,
+	level, title string, extras map[string]interface{}) map[string]interface{} {
+
 	timestamp := time.Now().Unix()
 
 	data := map[string]interface{}{
@@ -31,6 +33,10 @@ func buildBody(ctx context.Context, configuration configuration, level, title st
 		"notifier": map[string]interface{}{
 			"name":    NAME,
 			"version": VERSION,
+			"diagnostic": map[string]interface{}{
+				"languageVersion": diagnostic.languageVersion,
+				"configuredOptions": diagnostic.configuredOptions,
+			},
 		},
 	}
 


Test patch:
diff --git a/client_test.go b/client_test.go
index 638f4c0..82453ae 100644
--- a/client_test.go
+++ b/client_test.go
@@ -7,6 +7,7 @@ import (
 	"context"
 	"reflect"
 	"testing"
+	"strings"
 )
 
 type TestTransport struct {
@@ -295,16 +296,19 @@ func TestLambdaWrapperWithMessage(t *testing.T) {
 
 func TestGettersAndSetters_Default(t *testing.T) {
 	c := testClient()
+	c.Transport = &TestTransport{}
 	testGettersAndSetters(c, t)
 }
 
 func TestGettersAndSetters_Async(t *testing.T) {
 	c := rollbar.NewAsync("", "", "", "", "")
+	c.Transport = &TestTransport{}
 	testGettersAndSetters(c, t)
 }
 
 func TestGettersAndSetters_Sync(t *testing.T) {
 	c := rollbar.NewSync("", "", "", "", "")
+	c.Transport = &TestTransport{}
 	testGettersAndSetters(c, t)
 }
 
@@ -316,8 +320,10 @@ func testGettersAndSetters(client *rollbar.Client, t *testing.T) {
 	codeVersion := "CodeVersion"
 	host := "SomeHost"
 	root := "////"
+	fingerprint := true
 	scrubHeaders := regexp.MustCompile("Foo")
 	scrubFields := regexp.MustCompile("squirrel|doggo")
+	captureIP := rollbar.CaptureIpNone
 
 	errorIfEqual(token, client.Token(), t)
 	errorIfEqual(environment, client.Environment(), t)
@@ -326,6 +332,10 @@ func testGettersAndSetters(client *rollbar.Client, t *testing.T) {
 	errorIfEqual(codeVersion, client.CodeVersion(), t)
 	errorIfEqual(host, client.ServerHost(), t)
 	errorIfEqual(root, client.ServerRoot(), t)
+	errorIfEqual(fingerprint, client.Fingerprint(), t)
+	errorIfEqual(captureIP, client.CaptureIp(), t)
+	errorIfEqual(scrubHeaders, client.ScrubHeaders(), t)
+	errorIfEqual(scrubFields, client.ScrubFields(), t)
 
 	if client.Fingerprint() {
 		t.Error("expected fingerprint to default to false")
@@ -348,10 +358,11 @@ func testGettersAndSetters(client *rollbar.Client, t *testing.T) {
 	client.SetCodeVersion(codeVersion)
 	client.SetServerHost(host)
 	client.SetServerRoot(root)
-	client.SetFingerprint(true)
+	client.SetFingerprint(fingerprint)
 	client.SetLogger(&rollbar.SilentClientLogger{})
 	client.SetScrubHeaders(scrubHeaders)
 	client.SetScrubFields(scrubFields)
+	client.SetCaptureIp(captureIP)
 
 	client.SetEnabled(true)
 
@@ -362,6 +373,10 @@ func testGettersAndSetters(client *rollbar.Client, t *testing.T) {
 	errorIfNotEqual(codeVersion, client.CodeVersion(), t)
 	errorIfNotEqual(host, client.ServerHost(), t)
 	errorIfNotEqual(root, client.ServerRoot(), t)
+	errorIfNotEqual(fingerprint, client.Fingerprint(), t)
+	errorIfNotEqual(captureIP, client.CaptureIp(), t)
+	errorIfNotEqual(scrubHeaders, client.ScrubHeaders(), t)
+	errorIfNotEqual(scrubFields, client.ScrubFields(), t)
 
 	if !client.Fingerprint() {
 		t.Error("expected fingerprint to default to false")
@@ -374,15 +389,39 @@ func testGettersAndSetters(client *rollbar.Client, t *testing.T) {
 	if !client.ScrubFields().MatchString("squirrel") {
 		t.Error("expected matching scrub field")
 	}
+
+	client.ErrorWithLevel(rollbar.ERR, errors.New("Bork"))
+
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+		configuredOptions := configuredOptionsFromData(data)
+
+		errorIfNotEqual(environment, configuredOptions["environment"].(string), t)
+		errorIfNotEqual(endpoint, configuredOptions["endpoint"].(string), t)
+		errorIfNotEqual(platform, configuredOptions["platform"].(string), t)
+		errorIfNotEqual(codeVersion, configuredOptions["codeVersion"].(string), t)
+		errorIfNotEqual(host, configuredOptions["serverHost"].(string), t)
+		errorIfNotEqual(root, configuredOptions["serverRoot"].(string), t)
+		errorIfNotEqual(fingerprint, configuredOptions["fingerprint"].(bool), t)
+		errorIfNotEqual(scrubHeaders, configuredOptions["scrubHeaders"].(*regexp.Regexp), t)
+		errorIfNotEqual(scrubFields, configuredOptions["scrubFields"].(*regexp.Regexp), t)
+
+	} else {
+		t.Fail()
+	}
 }
 
-func errorIfEqual(a, b string, t *testing.T) {
+func errorIfEqual(a, b interface{}, t *testing.T) {
 	if a == b {
 		t.Error("Expected", a, " != ", b)
 	}
 }
 
-func errorIfNotEqual(a, b string, t *testing.T) {
+func errorIfNotEqual(a, b interface{}, t *testing.T) {
 	if a != b {
 		t.Error("Expected", a, " == ", b)
 	}
@@ -408,6 +447,13 @@ func TestSetPerson(t *testing.T) {
 		errorIfNotEqual(id, person["id"], t)
 		errorIfNotEqual(username, person["username"], t)
 		errorIfNotEqual(email, person["email"], t)
+
+		configuredOptions := configuredOptionsFromData(data)
+		configuredPerson := configuredOptions["person"].(map[string]string)
+
+		errorIfNotEqual(id, configuredPerson["Id"], t)
+		errorIfNotEqual(username, configuredPerson["Username"], t)
+		errorIfNotEqual(email, configuredPerson["Email"], t)
 	} else {
 		t.Fail()
 	}
@@ -452,6 +498,52 @@ func TestTransform(t *testing.T) {
 		if data["some_custom_field"] != "hello_world" {
 			t.Error("data should have field set by transform")
 		}
+		configuredOptions := configuredOptionsFromData(data)
+		if !strings.Contains(configuredOptions["transform"].(string), "TestTransform.func1") {
+			t.Error("data should have transform in diagnostic object")
+		}
+	} else {
+		t.Fail()
+	}
+}
+
+func TestSetUnwrapper(t *testing.T) {
+	client := testClient()
+	client.SetUnwrapper(rollbar.DefaultUnwrapper)
+
+	client.ErrorWithLevel(rollbar.ERR, errors.New("Bork"))
+
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+		configuredOptions := configuredOptionsFromData(data)
+		if !strings.Contains(configuredOptions["unwrapper"].(string), "func1") {
+			t.Error("data should have unwrapper in diagnostic object")
+		}
+	} else {
+		t.Fail()
+	}
+}
+
+func TestSetStackTracer(t *testing.T) {
+	client := testClient()
+	client.SetStackTracer(rollbar.DefaultStackTracer)
+
+	client.ErrorWithLevel(rollbar.ERR, errors.New("Bork"))
+
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+		configuredOptions := configuredOptionsFromData(data)
+		if !strings.Contains(configuredOptions["stackTracer"].(string), "func2") {
+			t.Error("data should have stackTracer in diagnostic object")
+		}
 	} else {
 		t.Fail()
 	}
@@ -484,3 +576,10 @@ func TestEnabled(t *testing.T) {
 		t.Fail()
 	}
 }
+
+func configuredOptionsFromData(data map[string]interface{}) map[string]interface{} {
+	notifier := data["notifier"].(map[string]interface{})
+	diagnostic := notifier["diagnostic"].(map[string]interface{})
+	configuredOptions := diagnostic["configuredOptions"].(map[string]interface{})
+	return configuredOptions
+}