Skip to content
This repository has been archived by the owner on Aug 30, 2023. It is now read-only.

Commit

Permalink
With extra (#190)
Browse files Browse the repository at this point in the history
* Capture extra info using interface{ExtraInfo() map[string]interface{}}

Extract nested extras

* Fix tests for Go 1.10

A couple type errors in string formatting prevented the tests from
running. Runs 1.10.x on travis.

* Add new tests for error with extra

Builds on #168 by adding test cases and removing unused interface
  • Loading branch information
JackWink authored and mattrobenolt committed May 17, 2018
1 parent 7452746 commit ed7bcb3
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 18 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -9,6 +9,7 @@ go:
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- tip

before_install:
Expand Down
59 changes: 43 additions & 16 deletions client.go
Expand Up @@ -68,6 +68,11 @@ func (timestamp *Timestamp) UnmarshalJSON(data []byte) error {
return nil
}

func (timestamp Timestamp) Format(format string) string {
t := time.Time(timestamp)
return t.Format(format)
}

// An Interface is a Sentry interface that will be serialized as JSON.
// It must implement json.Marshaler or use json struct tags.
type Interface interface {
Expand All @@ -83,6 +88,8 @@ type Transport interface {
Send(url, authHeader string, packet *Packet) error
}

type Extra map[string]interface{}

type outgoingPacket struct {
packet *Packet
ch chan error
Expand Down Expand Up @@ -149,34 +156,52 @@ type Packet struct {
Logger string `json:"logger"`

// Optional
Platform string `json:"platform,omitempty"`
Culprit string `json:"culprit,omitempty"`
ServerName string `json:"server_name,omitempty"`
Release string `json:"release,omitempty"`
Environment string `json:"environment,omitempty"`
Tags Tags `json:"tags,omitempty"`
Modules map[string]string `json:"modules,omitempty"`
Fingerprint []string `json:"fingerprint,omitempty"`
Extra map[string]interface{} `json:"extra,omitempty"`
Platform string `json:"platform,omitempty"`
Culprit string `json:"culprit,omitempty"`
ServerName string `json:"server_name,omitempty"`
Release string `json:"release,omitempty"`
Environment string `json:"environment,omitempty"`
Tags Tags `json:"tags,omitempty"`
Modules map[string]string `json:"modules,omitempty"`
Fingerprint []string `json:"fingerprint,omitempty"`
Extra Extra `json:"extra,omitempty"`

Interfaces []Interface `json:"-"`
}

// NewPacket constructs a packet with the specified message and interfaces.
func NewPacket(message string, interfaces ...Interface) *Packet {
extra := map[string]interface{}{
"runtime.Version": runtime.Version(),
"runtime.NumCPU": runtime.NumCPU(),
"runtime.GOMAXPROCS": runtime.GOMAXPROCS(0), // 0 just returns the current value
"runtime.NumGoroutine": runtime.NumGoroutine(),
extra := Extra{}
setExtraDefaults(extra)
return &Packet{
Message: message,
Interfaces: interfaces,
Extra: extra,
}
}

// NewPacketWithExtra constructs a packet with the specified message, extra information, and interfaces.
func NewPacketWithExtra(message string, extra Extra, interfaces ...Interface) *Packet {
if extra == nil {
extra = Extra{}
}
setExtraDefaults(extra)

return &Packet{
Message: message,
Interfaces: interfaces,
Extra: extra,
}
}

func setExtraDefaults(extra Extra) Extra {
extra["runtime.Version"] = runtime.Version()
extra["runtime.NumCPU"] = runtime.NumCPU()
extra["runtime.GOMAXPROCS"] = runtime.GOMAXPROCS(0) // 0 just returns the current value
extra["runtime.NumGoroutine"] = runtime.NumGoroutine()
return extra
}

// Init initializes required fields in a packet. It is typically called by
// Client.Send/Report automatically.
func (packet *Packet) Init(project string) error {
Expand Down Expand Up @@ -681,9 +706,10 @@ func (client *Client) CaptureError(err error, tags map[string]string, interfaces
return ""
}

extra := extractExtra(err)
cause := pkgErrors.Cause(err)

packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...)
packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...)
eventID, _ := client.Capture(packet, tags)

return eventID
Expand All @@ -705,9 +731,10 @@ func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, int
return ""
}

extra := extractExtra(err)
cause := pkgErrors.Cause(err)

packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...)
packet := NewPacketWithExtra(err.Error(), extra, append(append(interfaces, client.context.interfaces()...), NewException(cause, GetOrNewStacktrace(cause, 1, 3, client.includePaths)))...)
eventID, ch := client.Capture(packet, tags)
if eventID != "" {
<-ch
Expand Down
45 changes: 43 additions & 2 deletions client_test.go
Expand Up @@ -118,7 +118,7 @@ func TestPacketInit(t *testing.T) {
t.Errorf("ServerName should not be empty")
}
if packet.Level != ERROR {
t.Errorf("incorrect Level: got %d, want %d", packet.Level, ERROR)
t.Errorf("incorrect Level: got %s, want %s", packet.Level, ERROR)
}
if packet.Logger != "root" {
t.Errorf("incorrect Logger: got %s, want %s", packet.Logger, "root")
Expand Down Expand Up @@ -239,7 +239,7 @@ func TestUnmarshalTimestamp(t *testing.T) {
}

if actual != expected {
t.Errorf("incorrect string; got %s, want %s", actual, expected)
t.Errorf("incorrect string; got %s, want %s", actual.Format("2006-01-02 15:04:05 -0700"), expected.Format("2006-01-02 15:04:05 -0700"))
}
}

Expand Down Expand Up @@ -276,3 +276,44 @@ func TestCaptureNilError(t *testing.T) {
t.Error("expected empty eventID:", eventID)
}
}

func TestNewPacketWithExtraSetsDefault(t *testing.T) {
testCases := []struct {
Extra Extra
Expected Extra
}{
// Defaults should be set when nil is passed
{
Extra: nil,
Expected: setExtraDefaults(Extra{}),
},
// Defaults should be set when empty is passed
{
Extra: Extra{},
Expected: setExtraDefaults(Extra{}),
},
// Packet should always override default keys
{
Extra: Extra{
"runtime.Version": "notagoversion",
},
Expected: setExtraDefaults(Extra{}),
},
// Packet should include our extra info
{
Extra: Extra{
"extra.extra": "extra",
},
Expected: setExtraDefaults(Extra{
"extra.extra": "extra",
}),
},
}

for i, test := range testCases {
packet := NewPacketWithExtra("packet", test.Extra)
if !reflect.DeepEqual(packet.Extra, test.Expected) {
t.Errorf("Case [%d]: Expected packet: %+v, got: %+v", i, test.Expected, packet.Extra)
}
}
}
60 changes: 60 additions & 0 deletions errors.go
@@ -0,0 +1,60 @@
package raven

type causer interface {
Cause() error
}

type errWrappedWithExtra struct {
err error
extraInfo map[string]interface{}
}

func (ewx *errWrappedWithExtra) Error() string {
return ewx.err.Error()
}

func (ewx *errWrappedWithExtra) Cause() error {
return ewx.err
}

func (ewx *errWrappedWithExtra) ExtraInfo() Extra {
return ewx.extraInfo
}

// Adds extra data to an error before reporting to Sentry
func WrapWithExtra(err error, extraInfo map[string]interface{}) error {
return &errWrappedWithExtra{
err: err,
extraInfo: extraInfo,
}
}

type ErrWithExtra interface {
Error() string
Cause() error
ExtraInfo() Extra
}

// Iteratively fetches all the Extra data added to an error,
// and it's underlying errors. Extra data defined first is
// respected, and is not overridden when extracting.
func extractExtra(err error) Extra {
extra := Extra{}

currentErr := err
for currentErr != nil {
if errWithExtra, ok := currentErr.(ErrWithExtra); ok {
for k, v := range errWithExtra.ExtraInfo() {
extra[k] = v
}
}

if errWithCause, ok := currentErr.(causer); ok {
currentErr = errWithCause.Cause()
} else {
currentErr = nil
}
}

return extra
}
144 changes: 144 additions & 0 deletions errors_test.go
@@ -0,0 +1,144 @@
package raven

import (
"fmt"
"reflect"
"testing"

pkgErrors "github.com/pkg/errors"
)

func TestWrapWithExtraGeneratesProperErrWithExtra(t *testing.T) {
errMsg := "This is bad"
baseErr := fmt.Errorf(errMsg)
extraInfo := map[string]interface{}{
"string": "string",
"int": 1,
"float": 1.001,
"bool": false,
}

testErr := WrapWithExtra(baseErr, extraInfo)
wrapped, ok := testErr.(ErrWithExtra)
if !ok {
t.Errorf("Wrapped error does not conform to expected protocol.")
}

if !reflect.DeepEqual(wrapped.Cause(), baseErr) {
t.Errorf("Failed to unwrap error, got %+v, expected %+v", wrapped.Cause(), baseErr)
}

returnedExtra := wrapped.ExtraInfo()
for expectedKey, expectedVal := range extraInfo {
val, ok := returnedExtra[expectedKey]
if !ok {
t.Errorf("Extra data missing key: %s", expectedKey)
}
if val != expectedVal {
t.Errorf("Extra data [%s]: Got: %+v, expected: %+v", expectedKey, val, expectedVal)
}
}

if wrapped.Error() != errMsg {
t.Errorf("Wrong error message, got: %q, expected: %q", wrapped.Error(), errMsg)
}
}

func TestWrapWithExtraGeneratesCausableError(t *testing.T) {
baseErr := fmt.Errorf("this is bad")
testErr := WrapWithExtra(baseErr, nil)
cause := pkgErrors.Cause(testErr)

if !reflect.DeepEqual(cause, baseErr) {
t.Errorf("Failed to unwrap error, got %+v, expected %+v", cause, baseErr)
}
}

func TestExtractErrorPullsExtraData(t *testing.T) {
extraInfo := map[string]interface{}{
"string": "string",
"int": 1,
"float": 1.001,
"bool": false,
}
emptyInfo := map[string]interface{}{}

testCases := []struct {
Error error
Expected map[string]interface{}
}{
// Unwrapped error shouldn't include anything
{
Error: fmt.Errorf("This is bad"),
Expected: emptyInfo,
},
// Wrapped error with nil map should extract as empty info
{
Error: WrapWithExtra(fmt.Errorf("This is bad"), nil),
Expected: emptyInfo,
},
// Wrapped error with empty map should extract as empty info
{
Error: WrapWithExtra(fmt.Errorf("This is bad"), emptyInfo),
Expected: emptyInfo,
},
// Wrapped error with extra info should extract with all data
{
Error: WrapWithExtra(fmt.Errorf("This is bad"), extraInfo),
Expected: extraInfo,
},
// Nested wrapped error should extract all the info
{
Error: WrapWithExtra(
WrapWithExtra(fmt.Errorf("This is bad"),
map[string]interface{}{
"inner": "123",
}),
map[string]interface{}{
"outer": "456",
},
),
Expected: map[string]interface{}{
"inner": "123",
"outer": "456",
},
},
// Futher wrapping of errors shouldn't allow for value override
{
Error: WrapWithExtra(
WrapWithExtra(fmt.Errorf("This is bad"),
map[string]interface{}{
"dontoverride": "123",
}),
map[string]interface{}{
"dontoverride": "456",
},
),
Expected: map[string]interface{}{
"dontoverride": "123",
},
},
}

for i, test := range testCases {
extracted := extractExtra(test.Error)
if len(test.Expected) != len(extracted) {
t.Errorf(
"Case [%d]: Mismatched amount of data between provided and extracted extra. Got: %+v Expected: %+v",
i,
extracted,
test.Expected,
)
}

for expectedKey, expectedVal := range test.Expected {
val, ok := extracted[expectedKey]
if !ok {
t.Errorf("Case [%d]: Extra data missing key: %s", i, expectedKey)
}
if val != expectedVal {
t.Errorf("Case [%d]: Wrong extra data for %q. Got: %+v, expected: %+v", i, expectedKey, val, expectedVal)
}
}
}
}

0 comments on commit ed7bcb3

Please sign in to comment.