Repository: rollbar/rollbar-go (go)

Issue:
Support a checkIgnore function
Other libraries allow for a function to be called before posting to Rollbar to check if the given error should be ignored and not sent to Rollbar. This is a convenient hook for doing a variety of customization that would be nice to have.

Pull request description:
Add the ability to wrap functions to capture/report panics
Fixes #2 
Also add a checkIgnore function for these

Patch:
diff --git a/.gitignore b/.gitignore
index 215b5b3..dd3f285 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +1,2 @@
 rollbar.test
+.env
diff --git a/client.go b/client.go
index 7ec27d5..850cf35 100644
--- a/client.go
+++ b/client.go
@@ -103,6 +103,16 @@ func (c *Client) SetScrubFields(fields *regexp.Regexp) {
 	c.configuration.scrubFields = fields
 }
 
+// CheckIgnore is called during the recovery process of a panic that
+// occurred inside a function wrapped by Wrap or WrapAndWait
+// Return true if you wish to ignore this panic, false if you wish to
+// report it to Rollbar. If an error is the argument to the panic, then
+// 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.configuration.checkIgnore = checkIgnore
+}
+
 // -- Getters
 
 // Rollbar access token.
@@ -260,6 +270,62 @@ func (c *Client) RequestMessageWithExtras(level string, r *http.Request, msg str
 	c.push(body)
 }
 
+// -- Panics
+
+// Wrap calls f and then recovers and reports a panic to Rollbar if it occurs.
+// If an error is captured it is subsequently returned
+func (c *Client) Wrap(f func()) (err interface{}) {
+	defer func() {
+		err = recover()
+		switch val := err.(type) {
+		case nil:
+			return
+		case error:
+			if c.configuration.checkIgnore(val.Error()) {
+				return
+			}
+			c.ErrorWithStackSkip(CRIT, val, 2)
+		default:
+			str := fmt.Sprint(val)
+			if c.configuration.checkIgnore(str) {
+				return
+			}
+			c.Message(CRIT, str)
+		}
+	}()
+
+	f()
+	return
+}
+
+// WrapAndWait calls f, and recovers and reports a panic to Rollbar if it occurs.
+// This also waits before returning to ensure the message was reported
+// If an error is captured it is subsequently returned.
+func (c *Client) WrapAndWait(f func()) (err interface{}) {
+	defer func() {
+		err = recover()
+		switch val := err.(type) {
+		case nil:
+			return
+		case error:
+			if c.configuration.checkIgnore(val.Error()) {
+				return
+			}
+			c.ErrorWithStackSkip(CRIT, val, 2)
+		default:
+			str := fmt.Sprint(val)
+			if c.configuration.checkIgnore(str) {
+				return
+			}
+			c.Message(CRIT, str)
+		}
+		c.Wait()
+	}()
+
+	f()
+	return
+}
+
 // -- Misc.
 
 // Wait will call the Wait method of the Transport. If using an asyncronous
@@ -309,6 +375,7 @@ type configuration struct {
 	fingerprint  bool
 	scrubHeaders *regexp.Regexp
 	scrubFields  *regexp.Regexp
+	checkIgnore  func(string) bool
 }
 
 func createConfiguration(token, environment, codeVersion, serverHost, serverRoot string) configuration {
@@ -327,6 +394,7 @@ func createConfiguration(token, environment, codeVersion, serverHost, serverRoot
 		serverHost:   hostname,
 		serverRoot:   serverRoot,
 		fingerprint:  false,
+		checkIgnore:  func(_s string) bool { return false },
 	}
 }
 
diff --git a/rollbar.go b/rollbar.go
index b7d7600..659a850 100644
--- a/rollbar.go
+++ b/rollbar.go
@@ -4,6 +4,7 @@ import (
 	"log"
 	"net/http"
 	"os"
+	"regexp"
 )
 
 const (
@@ -57,6 +58,28 @@ func SetCustom(custom map[string]interface{}) {
 	std.SetCustom(custom)
 }
 
+// Regular expression used to match headers for scrubbing
+// The default value is regexp.MustCompile("Authorization")
+func SetScrubHeaders(headers *regexp.Regexp) {
+	std.SetScrubHeaders(headers)
+}
+
+// Regular expression to match keys in the item payload for scrubbing
+// The default vlaue is regexp.MustCompile("password|secret|token"),
+func SetScrubFields(fields *regexp.Regexp) {
+	std.SetScrubFields(fields)
+}
+
+// CheckIgnore is called during the recovery process of a panic that
+// occurred inside a function wrapped by Wrap or WrapAndWait
+// Return true if you wish to ignore this panic, false if you wish to
+// report it to Rollbar. If an error is the argument to the panic, then
+// this function is called with the result of calling Error(), otherwise
+// the string representation of the value is passed to this function.
+func SetCheckIgnore(checkIgnore func(string) bool) {
+	std.SetCheckIgnore(checkIgnore)
+}
+
 // -- Getters
 
 // Rollbar access token.
@@ -173,6 +196,19 @@ func Wait() {
 	std.Wait()
 }
 
+// Wrap calls f and then recovers and reports a panic to Rollbar if it occurs.
+// If an error is captured it is subsequently returned.
+func Wrap(f func()) interface{} {
+	return std.Wrap(f)
+}
+
+// WrapAndWait calls f, and recovers and reports a panic to Rollbar if it occurs.
+// This also waits before returning to ensure the message was reported
+// If an error is captured it is subsequently returned.
+func WrapAndWait(f func()) interface{} {
+	return std.WrapAndWait(f)
+}
+
 // Errors can implement this interface to create a trace_chain
 // Callers are required to call BuildStack on their own at the
 // time the cause is wrapped.


Test patch:
diff --git a/client_test.go b/client_test.go
new file mode 100644
index 0000000..d2477ea
--- /dev/null
+++ b/client_test.go
@@ -0,0 +1,165 @@
+package rollbar_test
+
+import (
+	"errors"
+	"github.com/rollbar/rollbar-go"
+	"testing"
+)
+
+type TestTransport struct {
+	Body       map[string]interface{}
+	WaitCalled bool
+}
+
+func (t *TestTransport) Close() error {
+	return nil
+}
+
+func (t *TestTransport) Wait() {
+	t.WaitCalled = true
+}
+
+func (t *TestTransport) SetToken(_t string)    {}
+func (t *TestTransport) SetEndpoint(_e string) {}
+func (t *TestTransport) Send(body map[string]interface{}) error {
+	t.Body = body
+	return nil
+}
+
+func testClient() *rollbar.Client {
+	c := rollbar.New("", "test", "", "", "")
+	c.Transport = &TestTransport{}
+	return c
+}
+
+func TestWrap(t *testing.T) {
+	client := testClient()
+	err := errors.New("bork")
+	result := client.Wrap(func() {
+		panic(err)
+	})
+	if err != result {
+		t.Error("Got:", result, "Expected:", err)
+	}
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.WaitCalled {
+			t.Error("Wait called unexpectedly")
+		}
+	} else {
+		t.Fail()
+	}
+}
+
+func TestWrapNonError(t *testing.T) {
+	client := testClient()
+	err := "hello rollbar"
+	result := client.Wrap(func() {
+		panic(err)
+	})
+	if err != result {
+		t.Error("Got:", result, "Expected:", err)
+	}
+}
+
+func TestWrapIgnore(t *testing.T) {
+	client := testClient()
+	err := errors.New("bork 42")
+	client.SetCheckIgnore(func(msg string) bool {
+		if msg == "bork 42" {
+			return true
+		}
+		return false
+	})
+	result := client.Wrap(func() {
+		panic(err)
+	})
+	if err != result {
+		t.Error("Got:", result, "Expected:", err)
+	}
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if transport.Body != nil {
+			t.Error("Expected Body to be nil, got:", transport.Body)
+		}
+	} else {
+		t.Fail()
+	}
+}
+
+func TestWrapAndWait(t *testing.T) {
+	client := testClient()
+	err := errors.New("bork")
+	result := client.WrapAndWait(func() {
+		panic(err)
+	})
+	if err != result {
+		t.Error("Got:", result, "Expected:", err)
+	}
+	if transport, ok := client.Transport.(*TestTransport); ok {
+		if !transport.WaitCalled {
+			t.Error("Expected wait to be called")
+		}
+	} else {
+		t.Fail()
+	}
+}
+
+func TestGettersAndSetters_Default(t *testing.T) {
+	c := testClient()
+	testGettersAndSetters(c, t)
+}
+
+func TestGettersAndSetters_Async(t *testing.T) {
+	c := rollbar.NewAsync("", "", "", "", "")
+	testGettersAndSetters(c, t)
+}
+
+func TestGettersAndSetters_Sync(t *testing.T) {
+	c := rollbar.NewSync("", "", "", "", "")
+	testGettersAndSetters(c, t)
+}
+
+func testGettersAndSetters(client *rollbar.Client, t *testing.T) {
+	token := "abc123"
+	environment := "TestEnvironment"
+	endpoint := "SomeEndpoint"
+	platform := "ThePlatform"
+	codeVersion := "CodeVersion"
+	host := "SomeHost"
+	root := "////"
+
+	errorIfEqual(token, client.Token(), t)
+	errorIfEqual(environment, client.Environment(), t)
+	errorIfEqual(endpoint, client.Endpoint(), t)
+	errorIfEqual(platform, client.Platform(), t)
+	errorIfEqual(codeVersion, client.CodeVersion(), t)
+	errorIfEqual(host, client.ServerHost(), t)
+	errorIfEqual(root, client.ServerRoot(), t)
+
+	client.SetToken(token)
+	client.SetEnvironment(environment)
+	client.SetEndpoint(endpoint)
+	client.SetPlatform(platform)
+	client.SetCodeVersion(codeVersion)
+	client.SetServerHost(host)
+	client.SetServerRoot(root)
+
+	errorIfNotEqual(token, client.Token(), t)
+	errorIfNotEqual(environment, client.Environment(), t)
+	errorIfNotEqual(endpoint, client.Endpoint(), t)
+	errorIfNotEqual(platform, client.Platform(), t)
+	errorIfNotEqual(codeVersion, client.CodeVersion(), t)
+	errorIfNotEqual(host, client.ServerHost(), t)
+	errorIfNotEqual(root, client.ServerRoot(), t)
+}
+
+func errorIfEqual(a, b string, t *testing.T) {
+	if a == b {
+		t.Error("Expected", a, " != ", b)
+	}
+}
+
+func errorIfNotEqual(a, b string, t *testing.T) {
+	if a != b {
+		t.Error("Expected", a, " == ", b)
+	}
+}