Skip to content

Commit

Permalink
Default breadcrumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesLindsay0 committed Aug 8, 2024
1 parent b32a2ff commit ea66ad1
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 94 deletions.
110 changes: 90 additions & 20 deletions v2/breadcrumb.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package bugsnag

import "time"

type BreadcrumbType = string

const (
Expand Down Expand Up @@ -44,37 +46,55 @@ type Breadcrumb struct {
MetaData BreadcrumbMetaData
}

type maximumBreadcrumbsValue interface {
isValid() bool
trimBreadcrumbs(breadcrumbs []Breadcrumb) []Breadcrumb
}

type MaximumBreadcrumbs int

func (length MaximumBreadcrumbs) isValid() bool {
return length >= 0 && length <= 100
}

func (length MaximumBreadcrumbs) trimBreadcrumbs(breadcrumbs []Breadcrumb) []Breadcrumb {
if int(length) >= 0 && len(breadcrumbs) > int(length) {
return breadcrumbs[:int(length)]
}
return breadcrumbs
}

type (
// A breadcrumb callback that returns if the breadcrumb should be added.
OnBreadcrumbCallback func(*Breadcrumb) bool
onBreadcrumbCallback func(*Breadcrumb) bool

BreadcrumbState struct {
breadcrumbState struct {
// These callbacks are run in reverse order and determine if the breadcrumb should be added.
OnBreadcrumbCallbacks []OnBreadcrumbCallback
onBreadcrumbCallbacks []onBreadcrumbCallback
// Currently added breadcrumbs in order from newest to oldest
Breadcrumbs []Breadcrumb
breadcrumbs []Breadcrumb
}
)

// OnBreadcrumb adds a callback to be run before a breadcrumb is added.
// onBreadcrumb adds a callback to be run before a breadcrumb is added.
// If false is returned, the breadcrumb will be discarded.
func (breadcrumbs *BreadcrumbState) OnBreadcrumb(callback OnBreadcrumbCallback) {
if breadcrumbs.OnBreadcrumbCallbacks == nil {
breadcrumbs.OnBreadcrumbCallbacks = []OnBreadcrumbCallback{}
func (breadcrumbs *breadcrumbState) onBreadcrumb(callback onBreadcrumbCallback) {
if breadcrumbs.onBreadcrumbCallbacks == nil {
breadcrumbs.onBreadcrumbCallbacks = []onBreadcrumbCallback{}
}

breadcrumbs.OnBreadcrumbCallbacks = append(breadcrumbs.OnBreadcrumbCallbacks, callback)
breadcrumbs.onBreadcrumbCallbacks = append(breadcrumbs.onBreadcrumbCallbacks, callback)
}

// Runs all the OnBreadcrumb callbacks, returning true if the breadcrumb should be added.
func (breadcrumbs *BreadcrumbState) runBreadcrumbCallbacks(breadcrumb *Breadcrumb) bool {
if breadcrumbs.OnBreadcrumbCallbacks == nil {
func (breadcrumbs *breadcrumbState) runBreadcrumbCallbacks(breadcrumb *Breadcrumb) bool {
if breadcrumbs.onBreadcrumbCallbacks == nil {
return true
}

// run in reverse order
for i := range breadcrumbs.OnBreadcrumbCallbacks {
callback := breadcrumbs.OnBreadcrumbCallbacks[len(breadcrumbs.OnBreadcrumbCallbacks)-i-1]
for i := range breadcrumbs.onBreadcrumbCallbacks {
callback := breadcrumbs.onBreadcrumbCallbacks[len(breadcrumbs.onBreadcrumbCallbacks)-i-1]
if !callback(breadcrumb) {
return false
}
Expand All @@ -83,15 +103,65 @@ func (breadcrumbs *BreadcrumbState) runBreadcrumbCallbacks(breadcrumb *Breadcrum
}

// Add the breadcrumb onto the list of breadcrumbs, ensuring that the number of breadcrumbs remains below maximumBreadcrumbs.
func (breadcrumbs *BreadcrumbState) appendBreadcrumb(breadcrumb Breadcrumb, maximumBreadcrumbs int) error {
func (breadcrumbs *breadcrumbState) leaveBreadcrumb(message string, configuration *Configuration, rawData ...interface{}) {
breadcrumb := Breadcrumb{
Timestamp: time.Now().Format(time.RFC3339),
Name: message,
Type: BreadcrumbTypeManual,
MetaData: BreadcrumbMetaData{},
}
for _, datum := range rawData {
switch datum := datum.(type) {
case BreadcrumbMetaData:
breadcrumb.MetaData = datum
case BreadcrumbType:
breadcrumb.Type = datum
default:
panic("Unexpected type")
}
}

if breadcrumbs.runBreadcrumbCallbacks(&breadcrumb) {
if breadcrumbs.Breadcrumbs == nil {
breadcrumbs.Breadcrumbs = []Breadcrumb{}
if breadcrumbs.breadcrumbs == nil {
breadcrumbs.breadcrumbs = []Breadcrumb{}
}
breadcrumbs.Breadcrumbs = append([]Breadcrumb{breadcrumb}, breadcrumbs.Breadcrumbs...)
if len(breadcrumbs.Breadcrumbs) > 0 && len(breadcrumbs.Breadcrumbs) > maximumBreadcrumbs {
breadcrumbs.Breadcrumbs = breadcrumbs.Breadcrumbs[:len(breadcrumbs.Breadcrumbs)-1]
breadcrumbs.breadcrumbs = append([]Breadcrumb{breadcrumb}, breadcrumbs.breadcrumbs...)
if configuration.MaximumBreadcrumbs != nil {
breadcrumbs.breadcrumbs = configuration.MaximumBreadcrumbs.trimBreadcrumbs(breadcrumbs.breadcrumbs)
}
}
return nil
}

func (configuration *Configuration) breadcrumbEnabled(breadcrumbType BreadcrumbType) bool {
if configuration.EnabledBreadcrumbTypes == nil {
return true
}
for _, enabled := range configuration.EnabledBreadcrumbTypes {
if enabled == breadcrumbType {
return true
}
}
return false
}

func (breadcrumbs *breadcrumbState) leaveBugsnagStartBreadcrumb(configuration *Configuration) {
if configuration.breadcrumbEnabled(BreadcrumbTypeState) {
breadcrumbs.leaveBreadcrumb("Bugsnag loaded", configuration, BreadcrumbTypeState)
}
}

func (breadcrumbs *breadcrumbState) leaveEventBreadcrumb(event *Event, configuration *Configuration) {
if event == nil {
return
}
if !configuration.breadcrumbEnabled(BreadcrumbTypeError) {
return
}
metadata := BreadcrumbMetaData{
"errorClass": event.ErrorClass,
"message": event.Message,
"unhandled": event.Unhandled,
"severity": event.Severity.String,
}
breadcrumbs.leaveBreadcrumb(event.Error.Error(), configuration, BreadcrumbTypeError, metadata)
}
140 changes: 113 additions & 27 deletions v2/breadcrumb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func TestDefaultBreadcrumbValues(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()
notifier.LeaveBreadcrumb("test breadcrumb")
notifier.Notify(fmt.Errorf("test error"))
Expand All @@ -35,7 +35,7 @@ func TestDefaultBreadcrumbValues(t *testing.T) {
}

func TestCustomBreadcrumbValues(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()
notifier.LeaveBreadcrumb("test breadcrumb", bugsnag.BreadcrumbMetaData{"hello": "world"}, bugsnag.BreadcrumbTypeProcess)
notifier.Notify(fmt.Errorf("test error"))
Expand All @@ -59,9 +59,9 @@ func TestCustomBreadcrumbValues(t *testing.T) {
}

func TestDefaultMaxBreadcrumbs(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()
defaultMaximum := 25
defaultMaximum := 50

for i := 1; i <= defaultMaximum*2; i++ {
notifier.LeaveBreadcrumb(fmt.Sprintf("breadcrumb%v", i))
Expand All @@ -81,34 +81,44 @@ func TestDefaultMaxBreadcrumbs(t *testing.T) {
}

func TestCustomMaxBreadcrumbs(t *testing.T) {
customMaximum := 5
testServer, reports, notifier := setupServer(bugsnag.Configuration{MaximumBreadcrumbs: customMaximum})
defer testServer.Close()
for _, customMaximum := range []int{-1, 0, 1, 99, 100, 101} {
testServer, reports, notifier := setupServer(bugsnag.Configuration{
MaximumBreadcrumbs: bugsnag.MaximumBreadcrumbs(customMaximum),
EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{},
})
defer testServer.Close()

for i := 1; i <= customMaximum*2; i++ {
notifier.LeaveBreadcrumb(fmt.Sprintf("breadcrumb%v", i))
}
breadcrumbsToAdd := 200
for i := 1; i <= breadcrumbsToAdd; i++ {
notifier.LeaveBreadcrumb(fmt.Sprintf("breadcrumb%v", i))
}

notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)
notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)

if len(breadcrumbs) != customMaximum {
t.Fatal("incorrect number of breadcrumbs")
}
for i := 0; i < customMaximum; i++ {
if breadcrumbs[i].Name != fmt.Sprintf("breadcrumb%v", customMaximum*2-i) {
t.Fatal("invalid breadcrumb at ", i)
expectedBreadcrumbs := customMaximum
// The default value should be kept when the custom value is invalid
if customMaximum < 0 || customMaximum > 100 {
expectedBreadcrumbs = 50
}
if len(breadcrumbs) != expectedBreadcrumbs {
t.Fatal("incorrect number of breadcrumbs, expected", expectedBreadcrumbs, "but found", len(breadcrumbs))
}
for i := 0; i < expectedBreadcrumbs; i++ {
if breadcrumbs[i].Name != fmt.Sprintf("breadcrumb%v", breadcrumbsToAdd-i) {
t.Fatal("invalid breadcrumb at", i, "with custom maximum of", customMaximum)
}
}
}
}

func TestBreadcrumbCallbacksAreReversed(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()

callback1Called := false
callback2Called := false
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
notifier.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
callback2Called = true
if breadcrumb.Name != "breadcrumb" {
t.Fatal("incorrect name")
Expand All @@ -118,7 +128,7 @@ func TestBreadcrumbCallbacksAreReversed(t *testing.T) {
}
return true
})
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
notifier.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
callback1Called = true
if breadcrumb.Name != "breadcrumb" {
t.Fatal("incorrect name")
Expand All @@ -142,15 +152,15 @@ func TestBreadcrumbCallbacksAreReversed(t *testing.T) {
}

func TestBreadcrumbCallbacksCanCancel(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()

callbackCalled := false
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
notifier.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
t.Fatal("Callback should be canceled")
return true
})
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
notifier.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
callbackCalled = true
return false
})
Expand All @@ -168,7 +178,7 @@ func TestBreadcrumbCallbacksCanCancel(t *testing.T) {
}

func TestSendNoBreadcrumbs(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()
notifier.Notify(fmt.Errorf("test error"))
if len(getBreadcrumbs(reports)) != 0 {
Expand All @@ -177,7 +187,7 @@ func TestSendNoBreadcrumbs(t *testing.T) {
}

func TestSendOrderedBreadcrumbs(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()
notifier.LeaveBreadcrumb("breadcrumb1")
notifier.LeaveBreadcrumb("breadcrumb2")
Expand All @@ -191,9 +201,85 @@ func TestSendOrderedBreadcrumbs(t *testing.T) {
}
}

func TestSendCleanMetadata(t *testing.T) {
func TestBugsnagStart(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{bugsnag.BreadcrumbTypeState}})
defer testServer.Close()
notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)
if len(breadcrumbs) != 1 {
t.Fatal("expected 1 breadcrumb", breadcrumbs)
}
if breadcrumbs[0].Name != "Bugsnag loaded" {
t.Fatal("expected the name to be 'Bugsnag loaded' but got", breadcrumbs[0].Name)
}
if breadcrumbs[0].Type != bugsnag.BreadcrumbTypeState {
t.Fatal("expected the type to be 'state' but got", breadcrumbs[0].Type)
}
if len(breadcrumbs[0].MetaData) != 0 {
t.Fatal("expected no metadata but got", breadcrumbs[0].MetaData)
}
}

func TestBugsnagErrorBreadcrumb(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{bugsnag.BreadcrumbTypeError}})
defer testServer.Close()
notifier.Notify(fmt.Errorf("test error 1"))
breadcrumbs := getBreadcrumbs(reports)
if len(breadcrumbs) != 0 {
t.Fatal("expected 0 breadcrumbs", breadcrumbs)
}
notifier.Notify(fmt.Errorf("test error 2"))
breadcrumbs = getBreadcrumbs(reports)
if len(breadcrumbs) != 1 {
t.Fatal("expected 1 breadcrumb", breadcrumbs)
}
if breadcrumbs[0].Name != "test error 1" {
t.Fatal("expected the name to be 'test error 1' but got", breadcrumbs[0].Name)
}
if breadcrumbs[0].Type != bugsnag.BreadcrumbTypeError {
t.Fatal("expected the type to be 'error' but got", breadcrumbs[0].Type)
}
if len(breadcrumbs[0].MetaData) != 4 {
t.Fatal("expected 4 pieces of metadata metadata but got", breadcrumbs[0].MetaData)
}
if breadcrumbs[0].MetaData["errorClass"] != "*errors.errorString" {
t.Fatal("expected the errorClass to be '*errors.errorString' but got", breadcrumbs[0].MetaData["errorClass"])
}
if breadcrumbs[0].MetaData["message"] != "test error 1" {
t.Fatal("expected the message to be 'test error 1' but got", breadcrumbs[0].MetaData["message"])
}
if breadcrumbs[0].MetaData["unhandled"] != false {
t.Fatal("expected unhandled to be false")
}
if breadcrumbs[0].MetaData["severity"] != "info" {
t.Fatal("expected the severity to be 'info' bug got", breadcrumbs[0].MetaData["severity"])
}
}

func TestBreadcrumbsEnabledByDefault(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
defer testServer.Close()
notifier.Notify(fmt.Errorf("test error 1"))
breadcrumbs := getBreadcrumbs(reports)
if len(breadcrumbs) != 1 {
t.Fatal("expected 1 breadcrumb", breadcrumbs)
}
notifier.Notify(fmt.Errorf("test error 2"))
breadcrumbs = getBreadcrumbs(reports)
if len(breadcrumbs) != 2 {
t.Fatal("expected 2 breadcrumb", breadcrumbs)
}
if breadcrumbs[0].Name != "test error 1" {
t.Fatal("expected the name to be 'test error 1' but got", breadcrumbs[0].Name)
}
if breadcrumbs[1].Name != "Bugsnag loaded" {
t.Fatal("expected the name to be 'Bugsnag loaded' but got", breadcrumbs[1].Name)
}
}

func TestSendCleanMetadata(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{EnabledBreadcrumbTypes: []bugsnag.BreadcrumbType{}})
defer testServer.Close()
type Recursive struct {
Inner *Recursive
}
Expand Down
Loading

0 comments on commit ea66ad1

Please sign in to comment.