Repository: rollbar/rollbar-go
Pull request #10 (2018-01-29 23:13:47)

Make public API conform to standard
I am not sure about this change, however this is conforming to our
notifier API standard albeit not quite idiomatic Go.

Before this change the public API was the same as the internal API
meaning that depending on the arguments you have to call a different
function:

    Error
    ErrorWithExtras
    RequestErrorWithStackSkipWithExtras
    Message
    MessageWithExtras

et cetera. The first parameter was always the level, and the rest of the
name determined what arguments you pass. The alternative is what is
implemented here:

     Critical
     Error
     Warning
     Info
     Debug

which take `...interface{}` and `Log(level string, xs ...interface{})`
which they all delegate to with the specific level set. We then do
runtime type checking on the input parameters to call into the internal
API described above. This matches what our other notifiers do, but it
uses some gross Go doing the type casting and is arguably less clear to
the typical Go developer.


Patch:
diff --git a/client.go b/client.go
index a31b57b..2ff4819 100644
--- a/client.go
+++ b/client.go
@@ -11,20 +11,25 @@ import (
 	"runtime"
 )
 
-// AsyncClient is the default concrete implementation of the Client interface
-// which sends all data to Rollbar asynchronously.
+// An instance of Client can be used to interact with Rollbar via the configured Transport.
+// The functions at the root of the `rollbar` package are the recommend way of using a Client. One
+// should not need to manage instances of the Client type manually in most normal scenarios.
+// However, if you want to customize the underlying transport layer, or you need to have
+// independent instances of a Client, then you can use the constructors provided for this
+// type.
 type Client struct {
 	io.Closer
 	Transport     Transport
 	configuration configuration
 }
 
-// New returns the default implementation of a Client
+// New returns the default implementation of a Client.
+// This uses the AsyncTransport.
 func New(token, environment, codeVersion, serverHost, serverRoot string) *Client {
 	return NewAsync(token, environment, codeVersion, serverHost, serverRoot)
 }
 
-// NewAsync builds an asynchronous implementation of the Client interface
+// NewAsync builds a Client with the asynchronous implementation of the transport interface.
 func NewAsync(token, environment, codeVersion, serverHost, serverRoot string) *Client {
 	configuration := createConfiguration(token, environment, codeVersion, serverHost, serverRoot)
 	transport := NewTransport(token, configuration.endpoint)
@@ -34,6 +39,7 @@ func NewAsync(token, environment, codeVersion, serverHost, serverRoot string) *C
 	}
 }
 
+// NewSync builds a Client with the synchronous implementation of the transport interface.
 func NewSync(token, environment, codeVersion, serverHost, serverRoot string) *Client {
 	configuration := createConfiguration(token, environment, codeVersion, serverHost, serverRoot)
 	transport := NewSyncTransport(token, configuration.endpoint)
@@ -43,7 +49,9 @@ func NewSync(token, environment, codeVersion, serverHost, serverRoot string) *Cl
 	}
 }
 
-// Rollbar access token.
+// A Rollbar access token with scope "post_server_item"
+// It is required to set this value before any of the other functions herein will be able to work
+// properly. This also configures the underlying Transport.
 func (c *Client) SetToken(token string) {
 	c.configuration.token = token
 	c.Transport.SetToken(token)
@@ -97,7 +105,8 @@ func (c *Client) SetPerson(id, username, email string) {
 	}
 }
 
-// ClearPerson clears any previously set person information.
+// ClearPerson clears any previously set person information. See `SetPerson` for more
+// information.
 func (c *Client) ClearPerson() {
 	c.configuration.person = person{}
 }
@@ -158,18 +167,18 @@ func (c *Client) CodeVersion() string {
 	return c.configuration.codeVersion
 }
 
-// host: The server hostname. Will be indexed.
+// The server hostname. Will be indexed.
 func (c *Client) ServerHost() string {
 	return c.configuration.serverHost
 }
 
-// root: Path to the application code root, not including the final slash.
+// Path to the application code root, not including the final slash.
 // Used to collapse non-project code when displaying tracebacks.
 func (c *Client) ServerRoot() string {
 	return c.configuration.serverRoot
 }
 
-// custom: Any arbitrary metadata you want to send.
+// Any arbitrary metadata you want to send with every subsequently sent item.
 func (c *Client) Custom() map[string]interface{} {
 	return c.configuration.custom
 }
@@ -193,8 +202,8 @@ func (c *Client) ScrubFields() *regexp.Regexp {
 
 var noExtras map[string]interface{}
 
-// Error sends an error to Rollbar with the given severity level.
-func (c *Client) Error(level string, err error) {
+// ErrorWithLevel sends an error to Rollbar with the given severity level.
+func (c *Client) ErrorWithLevel(level string, err error) {
 	c.ErrorWithExtras(level, err, noExtras)
 }
 
@@ -342,10 +351,8 @@ func (c *Client) WrapAndWait(f func()) (err interface{}) {
 	return
 }
 
-// -- Misc.
-
 // Wait will call the Wait method of the Transport. If using an asyncronous
-// transport then this will blow until until the queue of
+// transport then this will block until the queue of
 // errors / messages is empty. If using a syncronous transport then there
 // is no queue so this will be a no-op.
 func (c *Client) Wait() {
@@ -359,26 +366,18 @@ func (c *Client) Close() error {
 	return c.Transport.Close()
 }
 
-// Build the main JSON structure that will be sent to Rollbar with the
-// appropriate metadata.
 func (c *Client) buildBody(level, title string, extras map[string]interface{}) map[string]interface{} {
 	return buildBody(c.configuration, level, title, extras)
 }
 
-// Extract error details from a Request to a format that Rollbar accepts.
 func (c *Client) requestDetails(r *http.Request) map[string]interface{} {
 	return requestDetails(c.configuration, r)
 }
 
-// -- POST handling
-
-// Queue the given JSON body to be POSTed to Rollbar.
 func (c *Client) push(body map[string]interface{}) error {
 	return c.Transport.Send(body)
 }
 
-// -- Internal
-
 type person struct {
 	id       string
 	username string
@@ -422,8 +421,6 @@ func createConfiguration(token, environment, codeVersion, serverHost, serverRoot
 	}
 }
 
-// -- POST handling
-
 func clientPost(token, endpoint string, body map[string]interface{}) error {
 	if len(token) == 0 {
 		rollbarError("empty token")
diff --git a/doc.go b/doc.go
index 8f432d5..fbecd5d 100644
--- a/doc.go
+++ b/doc.go
@@ -18,14 +18,57 @@ Basic Usage
 
     result, err := DoSomething()
     if err != nil {
-      rollbar.Error(rollbar.ERR, err)
+      rollbar.Error(err)
     }
 
-    rollbar.Message("info", "Message body goes here")
+    rollbar.Info("Message body goes here")
 
     rollbar.Wait()
   }
 
-The interface exposed via the functions at the root of the `rollbar` package provide a convient way to interact with Rollbar without having to instantiate and manage your own instance of the `Client` type. There are two implementations of the `Client` type, `AsyncClient` and `SyncClient`.
+
+This package is designed to be used via the functions exposed at the root of the `rollbar` package. These work by managing a single instance of the `Client` type that is configurable via the setter functions at the root of the package.
+
+If you wish for more fine grained control over the client or you wish to have multiple independent clients then you can create and manage your own instances of the `Client` type.
+
+We provide two implementations of the `Transport` interface, `AsyncTransport` and `SyncTransport`. These manage the communication with the network layer. The Async version uses a buffered channel to communicate with the Rollbar API in a separate go routine. The Sync version is fully synchronous. It is possible to create your own `Transport` and configure a Client to use your prefered implementation.
+
+Go does not provide a mechanism for handling all panics automatically, therefore we provide two functions `Wrap` and `WrapAndWait` to make working with panics easier. They both take a function and then report to Rollbar if that function panics. They use the recover mechanism to capture the panic, and therefore if you wish your process to have the normal behaviour on panic (i.e. to crash), you will need to re-panic the result of calling `Wrap`. For example,
+
+  package main
+
+  import (
+    "errors"
+    "github.com/rollbar/rollbar-go"
+  )
+
+  func PanickyFunction() {
+    panic(errors.New("AHHH!!!!"))
+  }
+
+  func main() {
+    rollbar.SetToken("MY_TOKEN")
+    err := rollbar.Wrap(PanickyFunction)
+    if err != nil {
+      // This means our function panic'd
+      // Uncomment the next line to get normal
+      // crash on panic behaviour
+      // panic(err)
+    }
+    rollbar.Wait()
+  }
+
+The above pattern of calling `Wrap(...)` and then `Wait(...)` can be combined via `WrapAndWait(...)`. When `WrapAndWait(...)` returns if there was a panic it has already been sent to the Rollbar API. The error is still returned by this function if there is one.
+
+
+Due to the nature of the `error` type in Go, it can be difficult to attribute errors to their original origin without doing some extra work. To account for this, we define the interface `CauseStacker`:
+
+    type CauseStacker interface {
+      error
+      Cause() error
+      Stack() Stack
+    }
+
+One can implement this interface for custom Error types to be able to build up a chain of stack traces. In order to get stack the correct stacks, callers must call BuildStack on their own at the time that the cause is wrapped. This is the least intrusive mechanism for gathering this information due to the decisions made by the Go runtime to not track this information.
 */
 package rollbar
diff --git a/example/main.go b/example/main.go
index bac7b95..c12b72e 100644
--- a/example/main.go
+++ b/example/main.go
@@ -22,7 +22,7 @@ func helloJson(w http.ResponseWriter, r *http.Request) {
 		fmt.Println("key:", k)
 		fmt.Println("val:", v)
 	}
-	rollbar.RequestMessage(rollbar.INFO, r, "Example message json")
+	rollbar.Info(r, "Example message json")
 	fmt.Fprintf(w, "Hello world!")
 }
 
@@ -34,7 +34,7 @@ func helloForm(w http.ResponseWriter, r *http.Request) {
 		fmt.Println("key:", k)
 		fmt.Println("val:", strings.Join(v, " "))
 	}
-	rollbar.RequestMessage(rollbar.INFO, r, "Example message form")
+	rollbar.Info("Example message form")
 	fmt.Fprintf(w, "Hello world!")
 }
 
diff --git a/rollbar.go b/rollbar.go
index c3138fd..5d2155d 100644
--- a/rollbar.go
+++ b/rollbar.go
@@ -27,7 +27,9 @@ var (
 	nilErrTitle = "<nil>"
 )
 
-// Rollbar access token.
+// A Rollbar access token with scope "post_server_item"
+// It is required to set this value before any of the other functions herein will be able to work
+// properly.
 func SetToken(token string) {
 	std.SetToken(token)
 }
@@ -37,23 +39,36 @@ func SetEnvironment(environment string) {
 	std.SetEnvironment(environment)
 }
 
+// The endpoint to post items to.
+// The default value is https://api.rollbar.com/api/1/item/
+func SetEndpoint(endpoint string) {
+	std.SetEndpoint(endpoint)
+}
+
+// Platform is the platform reported for all Rollbar items. The default is
+// the running operating system (darwin, freebsd, linux, etc.) but it can
+// also be application specific (client, heroku, etc.).
+func SetPlatform(platform string) {
+	std.SetPlatform(platform)
+}
+
 // String describing the running code version on the server
 func SetCodeVersion(codeVersion string) {
 	std.SetCodeVersion(codeVersion)
 }
 
-// host: The server hostname. Will be indexed.
+// The server hostname. Will be indexed.
 func SetServerHost(serverHost string) {
 	std.SetServerHost(serverHost)
 }
 
-// root: Path to the application code root, not including the final slash.
+// Path to the application code root, not including the final slash.
 // Used to collapse non-project code when displaying tracebacks.
 func SetServerRoot(serverRoot string) {
 	std.SetServerRoot(serverRoot)
 }
 
-// custom: Any arbitrary metadata you want to send.
+// Any arbitrary metadata you want to send with every subsequently sent item.
 func SetCustom(custom map[string]interface{}) {
 	std.SetCustom(custom)
 }
@@ -87,11 +102,18 @@ func SetPerson(id, username, email string) {
 	std.SetPerson(id, username, email)
 }
 
-// ClearPerson clears any previously set person information.
+// ClearPerson clears any previously set person information. See `SetPerson` for more information.
 func ClearPerson() {
 	std.ClearPerson()
 }
 
+// Whether or not to use custom client-side fingerprint
+// based on a CRC32 checksum. The alternative is to let the server compute a fingerprint for each
+// item. The default is false.
+func SetFingerprint(fingerprint bool) {
+	std.SetFingerprint(fingerprint)
+}
+
 // -- Getters
 
 // Rollbar access token.
@@ -104,32 +126,174 @@ func Environment() string {
 	return std.Environment()
 }
 
-// String describing the running code version on the server
+// Get the currently configured endpoint.
+func Endpoint() string {
+	return std.Endpoint()
+}
+
+// Platform is the platform reported for all Rollbar items. The default is
+// the running operating system (darwin, freebsd, linux, etc.) but it can
+// also be application specific (client, heroku, etc.).
+func Platform() string {
+	return std.Platform()
+}
+
+// String describing the running code version on the server.
 func CodeVersion() string {
 	return std.CodeVersion()
 }
 
-// host: The server hostname. Will be indexed.
+// The server hostname. Will be indexed.
 func ServerHost() string {
 	return std.ServerHost()
 }
 
-// root: Path to the application code root, not including the final slash.
+// Path to the application code root, not including the final slash.
 // Used to collapse non-project code when displaying tracebacks.
 func ServerRoot() string {
 	return std.ServerRoot()
 }
 
-// custom: Any arbitrary metadata you want to send.
+// Any arbitrary metadata you want to send with every subsequently sent item.
 func Custom() map[string]interface{} {
 	return std.Custom()
 }
 
+// Whether or not to use a custom client-side fingerprint.
+func Fingerprint() bool {
+	return std.Fingerprint()
+}
+
+// -- Reporting
+
+// Report an item with level `critical`. This function recognizes arguments with the following types:
+//    *http.Request
+//    error
+//    string
+//    map[string]interface{}
+//    int
+// The string and error types are mutually exclusive.
+// If an error is present then a stack trace is captured. If an int is also present then we skip
+// that number of stack frames. If the map is present it is used as extra custom data in the
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func Critical(interfaces ...interface{}) {
+	Log(CRIT, interfaces...)
+}
+
+// Report an item with level `error`. This function recognizes arguments with the following types:
+//    *http.Request
+//    error
+//    string
+//    map[string]interface{}
+//    int
+// The string and error types are mutually exclusive.
+// If an error is present then a stack trace is captured. If an int is also present then we skip
+// that number of stack frames. If the map is present it is used as extra custom data in the
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func Error(interfaces ...interface{}) {
+	Log(ERR, interfaces...)
+}
+
+// Report an item with level `warning`. This function recognizes arguments with the following types:
+//    *http.Request
+//    error
+//    string
+//    map[string]interface{}
+//    int
+// The string and error types are mutually exclusive.
+// If an error is present then a stack trace is captured. If an int is also present then we skip
+// that number of stack frames. If the map is present it is used as extra custom data in the
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func Warning(interfaces ...interface{}) {
+	Log(WARN, interfaces...)
+}
+
+// Report an item with level `info`. This function recognizes arguments with the following types:
+//    *http.Request
+//    error
+//    string
+//    map[string]interface{}
+//    int
+// The string and error types are mutually exclusive.
+// If an error is present then a stack trace is captured. If an int is also present then we skip
+// that number of stack frames. If the map is present it is used as extra custom data in the
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func Info(interfaces ...interface{}) {
+	Log(INFO, interfaces...)
+}
+
+// Report an item with level `debug`. This function recognizes arguments with the following types:
+//    *http.Request
+//    error
+//    string
+//    map[string]interface{}
+//    int
+// The string and error types are mutually exclusive.
+// If an error is present then a stack trace is captured. If an int is also present then we skip
+// that number of stack frames. If the map is present it is used as extra custom data in the
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func Debug(interfaces ...interface{}) {
+	Log(DEBUG, interfaces...)
+}
+
+// Report an item with the given level. This function recognizes arguments with the following types:
+//    *http.Request
+//    error
+//    string
+//    map[string]interface{}
+//    int
+// The string and error types are mutually exclusive.
+// If an error is present then a stack trace is captured. If an int is also present then we skip
+// that number of stack frames. If the map is present it is used as extra custom data in the
+// item. If a string is present without an error, then we log a message without a stack
+// trace. If a request is present we extract as much relevant information from it as we can.
+func Log(level string, interfaces ...interface{}) {
+	var r *http.Request
+	var err error
+	var skip int
+	var extras map[string]interface{}
+	var msg string
+	for _, ival := range interfaces {
+		switch val := ival.(type) {
+		case *http.Request:
+			r = val
+		case error:
+			err = val
+		case int:
+			skip = val
+		case string:
+			msg = val
+		case map[string]interface{}:
+			extras = val
+		default:
+			rollbarError("Unknown input type: %T", val)
+		}
+	}
+	if err != nil {
+		if r == nil {
+			std.ErrorWithStackSkipWithExtras(level, err, skip, extras)
+		} else {
+			std.RequestErrorWithStackSkipWithExtras(level, r, err, skip, extras)
+		}
+	} else {
+		if r == nil {
+			std.MessageWithExtras(level, msg, extras)
+		} else {
+			std.RequestMessageWithExtras(level, r, msg, extras)
+		}
+	}
+}
+
 // -- Error reporting
 
-// Error asynchronously sends an error to Rollbar with the given severity level.
-func Error(level string, err error) {
-	std.Error(level, err)
+// ErrorWithLevel asynchronously sends an error to Rollbar with the given severity level.
+func ErrorWithLevel(level string, err error) {
+	std.ErrorWithLevel(level, err)
 }
 
 // ErrorWithExtras asynchronously sends an error to Rollbar with the given


Test patch:
diff --git a/client_test.go b/client_test.go
index 61ef6bc..7b56e8b 100644
--- a/client_test.go
+++ b/client_test.go
@@ -165,47 +165,47 @@ func errorIfNotEqual(a, b string, t *testing.T) {
 }
 
 func TestSetPerson(t *testing.T) {
-  client := testClient()
-  id, username, email := "42", "bork", "bork@foobar.com"
+	client := testClient()
+	id, username, email := "42", "bork", "bork@foobar.com"
 
-  client.SetPerson(id, username, email)
-  client.Error(rollbar.ERR, errors.New("Person Bork"))
+	client.SetPerson(id, username, email)
+	client.ErrorWithLevel(rollbar.ERR, errors.New("Person 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{})
-    if data["person"] == nil {
-      t.Error("data should have person")
-    }
-    person := data["person"].(map[string]string)
-    errorIfNotEqual(id, person["id"], t)
-    errorIfNotEqual(username, person["username"], t)
-    errorIfNotEqual(email, person["email"], t)
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+		if data["person"] == nil {
+			t.Error("data should have person")
+		}
+		person := data["person"].(map[string]string)
+		errorIfNotEqual(id, person["id"], t)
+		errorIfNotEqual(username, person["username"], t)
+		errorIfNotEqual(email, person["email"], t)
 	} else {
 		t.Fail()
 	}
 }
 
 func TestClearPerson(t *testing.T) {
-  client := testClient()
-  id, username, email := "42", "bork", "bork@foobar.com"
+	client := testClient()
+	id, username, email := "42", "bork", "bork@foobar.com"
 
-  client.SetPerson(id, username, email)
-  client.ClearPerson()
-  client.Error(rollbar.ERR, errors.New("Person Bork"))
+	client.SetPerson(id, username, email)
+	client.ClearPerson()
+	client.ErrorWithLevel(rollbar.ERR, errors.New("Person 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{})
-    if data["person"] != nil {
-      t.Error("data should not have a person")
-    }
+		body := transport.Body
+		if body["data"] == nil {
+			t.Error("body should have data")
+		}
+		data := body["data"].(map[string]interface{})
+		if data["person"] != nil {
+			t.Error("data should not have a person")
+		}
 	} else {
 		t.Fail()
 	}
diff --git a/rollbar_test.go b/rollbar_test.go
index 7a83526..92a7f76 100644
--- a/rollbar_test.go
+++ b/rollbar_test.go
@@ -21,7 +21,7 @@ func testErrorStack(s string) {
 }
 
 func testErrorStack2(s string) {
-	Error("error", errors.New(s))
+	ErrorWithLevel("error", errors.New(s))
 }
 
 func testErrorStackWithSkip(s string) {
@@ -32,6 +32,14 @@ func testErrorStackWithSkip2(s string) {
 	ErrorWithStackSkip("error", errors.New(s), 2)
 }
 
+func testErrorStackWithSkipGeneric(s string) {
+	testErrorStackWithSkipGeneric2(s)
+}
+
+func testErrorStackWithSkipGeneric2(s string) {
+	Warning(errors.New(s), 2)
+}
+
 func TestErrorClass(t *testing.T) {
 	errors := map[string]error{
 		"{508e076d}":          fmt.Errorf("Something is broken!"),
@@ -55,8 +63,8 @@ func TestEverything(t *testing.T) {
 		t.Error("Token should be as set")
 	}
 
-	Error("critical", errors.New("Normal critical error"))
-	Error("error", &CustomError{"This is a custom error"})
+	ErrorWithLevel("critical", errors.New("Normal critical error"))
+	ErrorWithLevel("error", &CustomError{"This is a custom error"})
 
 	testErrorStack("This error should have a nice stacktrace")
 	testErrorStackWithSkip("This error should have a skipped stacktrace")
@@ -76,6 +84,34 @@ func TestEverything(t *testing.T) {
 	Wait()
 }
 
+func TestEverythingGeneric(t *testing.T) {
+	SetToken(os.Getenv("TOKEN"))
+	SetEnvironment("test")
+	if Token() != os.Getenv("TOKEN") {
+		t.Error("Token should be as set")
+	}
+	if Environment() != "test" {
+		t.Error("Token should be as set")
+	}
+
+	Critical(errors.New("Normal generic critical error"))
+	Error(&CustomError{"This is a generic custom error"})
+
+	testErrorStackWithSkipGeneric("This generic error should have a skipped stacktrace")
+
+	done := make(chan bool)
+	go func() {
+		testErrorStack("I'm in a generic goroutine")
+		done <- true
+	}()
+	<-done
+
+	Error("This is a generic error message")
+	Info("And this is a generic info message")
+
+	Wait()
+}
+
 func TestBuildBody(t *testing.T) {
 	// custom provided at config time
 	baseCustom := map[string]interface{}{