Skip to content

Commit

Permalink
Add breadcrumb support
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesLindsay0 committed Aug 7, 2024
1 parent c7a0901 commit c6f1ab3
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 7 deletions.
97 changes: 97 additions & 0 deletions v2/breadcrumb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package bugsnag

type BreadcrumbType = string

const (
// Changing screens or content being displayed, with a defined destination and optionally a previous location.
BreadcrumbTypeNavigation BreadcrumbType = "navigation"
// Sending and receiving requests and responses.
BreadcrumbTypeRequest BreadcrumbType = "request"
// Performing an intensive task or query.
BreadcrumbTypeProcess BreadcrumbType = "process"
// Messages and severity sent to a logging platform.
BreadcrumbTypeLog BreadcrumbType = "log"
// Actions performed by the user, like text input, button presses, or confirming/cancelling an alert dialog.
BreadcrumbTypeUser BreadcrumbType = "user"
// Changing the overall state of an app, such as closing, pausing, or being moved to the background, as well as device state changes like memory or battery warnings and network connectivity changes.
BreadcrumbTypeState BreadcrumbType = "state"
// An error which was reported to Bugsnag encountered in the same session.
BreadcrumbTypeError BreadcrumbType = "error"
// User-defined, manually added breadcrumbs.
BreadcrumbTypeManual BreadcrumbType = "manual"
)

// Key value metadata that is displayed with the breadcrumb.
type BreadcrumbMetaData map[string]interface{}

// Remove any values from meta-data that have keys matching the filters,
// and any that are recursive data-structures.
func (meta BreadcrumbMetaData) sanitize(filters []string) interface{} {
return sanitizer{
Filters: filters,
Seen: make([]interface{}, 0),
}.Sanitize(meta)
}

type Breadcrumb struct {
// The time at which the event occurred, in ISO 8601 format.
Timestamp string
// A short summary describing the event, such as the user action taken or a new application state.
Name string
// A category which describes the breadcrumb.
Type BreadcrumbType
// Additional information about the event, as key/value pairs.
MetaData BreadcrumbMetaData
}

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

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

// 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{}
}

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 {
return true
}

// run in reverse order
for i := range breadcrumbs.OnBreadcrumbCallbacks {
callback := breadcrumbs.OnBreadcrumbCallbacks[len(breadcrumbs.OnBreadcrumbCallbacks)-i-1]
if !callback(breadcrumb) {
return false
}
}
return true
}

// 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 {
if breadcrumbs.runBreadcrumbCallbacks(&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]
}
}
return nil
}
239 changes: 239 additions & 0 deletions v2/breadcrumb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package bugsnag_test

import (
"fmt"
"net/http/httptest"
"testing"

"github.com/bitly/go-simplejson"
"github.com/bugsnag/bugsnag-go/v2"
"github.com/bugsnag/bugsnag-go/v2/testutil"
)

func TestDefaultBreadcrumbValues(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
defer testServer.Close()
notifier.LeaveBreadcrumb("test breadcrumb")
notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)

if len(breadcrumbs) != 1 {
t.Fatal("expected 1 breadcrumb")
}
if breadcrumbs[0].Name != "test breadcrumb" {
t.Fatal("expected breadcrumb name")
}
if len(breadcrumbs[0].Timestamp) < 6 {
t.Fatal("expected timestamp")
}
if len(breadcrumbs[0].MetaData) != 0 {
t.Fatal("expected no metadata")
}
if breadcrumbs[0].Type != bugsnag.BreadcrumbTypeManual {
t.Fatal("expected manual type")
}
}

func TestCustomBreadcrumbValues(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
defer testServer.Close()
notifier.LeaveBreadcrumb("test breadcrumb", bugsnag.BreadcrumbMetaData{"hello": "world"}, bugsnag.BreadcrumbTypeProcess)
notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)

if len(breadcrumbs) != 1 {
t.Fatal("expected 1 breadcrumb")
}
if breadcrumbs[0].Name != "test breadcrumb" {
t.Fatal("expected breadcrumb name")
}
if len(breadcrumbs[0].Timestamp) < 6 {
t.Fatal("expected timestamp")
}
if len(breadcrumbs[0].MetaData) != 1 || breadcrumbs[0].MetaData["hello"] != "world" {
t.Fatal("expected correct metadata")
}
if breadcrumbs[0].Type != bugsnag.BreadcrumbTypeProcess {
t.Fatal("expected process type")
}
}

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

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

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

if len(breadcrumbs) != defaultMaximum {
t.Fatal("incorrect number of breadcrumbs")
}
for i := 0; i < defaultMaximum; i++ {
if breadcrumbs[i].Name != fmt.Sprintf("breadcrumb%v", defaultMaximum*2-i) {
t.Fatal("invalid breadcrumb at ", i)
}
}
}

func TestCustomMaxBreadcrumbs(t *testing.T) {
customMaximum := 5
testServer, reports, notifier := setupServer(bugsnag.Configuration{MaximumBreadcrumbs: customMaximum})
defer testServer.Close()

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

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)
}
}
}

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

callback1Called := false
callback2Called := false
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
callback2Called = true
if breadcrumb.Name != "breadcrumb" {
t.Fatal("incorrect name")
}
if callback1Called == false {
t.Fatal("callbacks should occur in reverse order")
}
return true
})
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
callback1Called = true
if breadcrumb.Name != "breadcrumb" {
t.Fatal("incorrect name")
}
if callback2Called == true {
t.Fatal("callbacks should occur in reverse order")
}
return true
})

notifier.LeaveBreadcrumb("breadcrumb")

if !callback2Called {
t.Fatal("breadcrumb callback not called")
}

notifier.Notify(fmt.Errorf("test error"))
if len(getBreadcrumbs(reports)) != 1 {
t.Fatal("expected one breadcrumb")
}
}

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

callbackCalled := false
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
t.Fatal("Callback should be canceled")
return true
})
notifier.BreadcrumbState.OnBreadcrumb(func(breadcrumb *bugsnag.Breadcrumb) bool {
callbackCalled = true
return false
})

notifier.LeaveBreadcrumb("breadcrumb")

if !callbackCalled {
t.Fatal("first breadcrumb callback not called")
}

notifier.Notify(fmt.Errorf("test error"))
if len(getBreadcrumbs(reports)) != 0 {
t.Fatal("breadcrumb not canceled")
}
}

func TestSendNoBreadcrumbs(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
defer testServer.Close()
notifier.Notify(fmt.Errorf("test error"))
if len(getBreadcrumbs(reports)) != 0 {
t.Fatal("expected no breadcrumbs")
}
}

func TestSendOrderedBreadcrumbs(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
defer testServer.Close()
notifier.LeaveBreadcrumb("breadcrumb1")
notifier.LeaveBreadcrumb("breadcrumb2")
notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)
if len(breadcrumbs) != 2 {
t.Fatal("expected 2 breadcrumbs", breadcrumbs)
}
if breadcrumbs[0].Name != "breadcrumb2" || breadcrumbs[1].Name != "breadcrumb1" {
t.Fatal("expected ordered breadcrumbs", breadcrumbs)
}
}

func TestSendCleanMetadata(t *testing.T) {
testServer, reports, notifier := setupServer(bugsnag.Configuration{})
defer testServer.Close()
type Recursive struct {
Inner *Recursive
}
recursiveValue := Recursive{}
recursiveValue.Inner = &recursiveValue
notifier.LeaveBreadcrumb("breadcrumb2", bugsnag.BreadcrumbMetaData{"recursive": recursiveValue})
notifier.Notify(fmt.Errorf("test error"))
breadcrumbs := getBreadcrumbs(reports)
if len(breadcrumbs) != 1 {
t.Fatal("expected 1 breadcrumb", breadcrumbs)
}
if breadcrumbs[0].MetaData["recursive"].(map[string]interface{})["Inner"] != "[RECURSION]" {
t.Fatal("remove recursive")
}
}

func getBreadcrumbs(reports chan []byte) []bugsnag.Breadcrumb {
event, _ := simplejson.NewJson(<-reports)
fmt.Println(event)
firstEventJson := testutil.GetIndex(event, "events", 0)
breadcrumbsJson := testutil.Get(firstEventJson, "breadcrumbs")

breadcrumbs := []bugsnag.Breadcrumb{}
for index := 0; index < len(breadcrumbsJson.MustArray()); index++ {
breadcrumbJson := breadcrumbsJson.GetIndex(index)
fmt.Println(breadcrumbJson)
breadcrumbs = append(breadcrumbs, bugsnag.Breadcrumb{
Timestamp: breadcrumbJson.Get("timestamp").MustString(),
Name: breadcrumbJson.Get("name").MustString(),
Type: breadcrumbJson.Get("type").MustString(),
MetaData: breadcrumbJson.Get("metaData").MustMap(),
})
}
return breadcrumbs
}

func setupServer(configuration bugsnag.Configuration) (*httptest.Server, chan []byte, *bugsnag.Notifier) {
testServer, reports := testutil.Setup()
configuration.APIKey = testutil.TestAPIKey
configuration.Endpoints = bugsnag.Endpoints{Notify: testServer.URL, Sessions: testServer.URL + "/sessions"}
notifier := bugsnag.New(configuration)
return testServer, reports, notifier
}
15 changes: 14 additions & 1 deletion v2/bugsnag.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var sessionTrackingConfig sessions.SessionTrackingConfiguration
// Bugsnag.
// Deprecated: Exposed for developer sanity in testing. Modify at own risk.
var DefaultSessionPublishInterval = 60 * time.Second
var defaultNotifier = Notifier{&Config, nil}
var defaultNotifier = Notifier{&Config, nil, BreadcrumbState{}}
var sessionTracker sessions.SessionTracker

// Configure Bugsnag. The only required setting is the APIKey, which can be
Expand Down Expand Up @@ -155,6 +155,12 @@ func Recover(rawData ...interface{}) {
}
}

// OnBeforeNotify adds a callback to be run before a breadcrumb is added to the default notifier.
// If false is returned, the breadcrumb will be discarded. These callbacks are run in reverse order.
func OnBreadcrumb(callback func(breadcrumb *Breadcrumb) bool) {
defaultNotifier.BreadcrumbState.OnBreadcrumb(callback)
}

// OnBeforeNotify adds a callback to be run before a notification is sent to
// Bugsnag. It can be used to modify the event or its MetaData. Changes made
// to the configuration are local to notifying about this event. To prevent the
Expand Down Expand Up @@ -212,6 +218,12 @@ func HandlerFunc(h http.HandlerFunc, rawData ...interface{}) http.HandlerFunc {
}
}

// Adds a breadcrumb to the default notifier which is sent with subsequent errors.
// Optionally accepts bugsnag.BreadcrumbMetaData and bugsnag.BreadcrumbType.
func LeaveBreadcrumb(message string, rawData ...interface{}) {
defaultNotifier.LeaveBreadcrumb(message, rawData...)
}

// checkForEmptyError checks if the given error (to be reported to Bugsnag) is
// nil. If it is, then log an error message and return another error wrapping
// this error message.
Expand Down Expand Up @@ -253,6 +265,7 @@ func init() {
Logger: log.New(os.Stdout, log.Prefix(), log.Flags()),
PanicHandler: defaultPanicHandler,
Transport: http.DefaultTransport,
MaximumBreadcrumbs: 25,

flushSessionsOnRepanic: true,
})
Expand Down
Loading

0 comments on commit c6f1ab3

Please sign in to comment.