Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add breadcrumb support #234

Open
wants to merge 8 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
matrix:
os: [ubuntu-20.04, windows-latest]
go-version: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17', '1.18', '1.19', '1.20', '1.21', '1.22']
env:
GO111MODULE: on

steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.7.3
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* Limit resource usage while sending events asynchronously \
Added MainContext configuration option for providing context from main app
[#231](https://github.com/bugsnag/bugsnag-go/pull/231)
* Add breadcrumb support
[#234](https://github.com/bugsnag/bugsnag-go/pull/234)

## 2.4.0 (2024-04-15)

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
TEST?=./...

export GO111MODULE=auto
export GO111MODULE=on

default: alldeps test

Expand Down
47 changes: 47 additions & 0 deletions features/breadcrumbs.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Feature: Breadcrumbs

Background:
Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa"
Given I configure the bugsnag endpoint
Given I have built the service "app"

Scenario: Disabling breadcrumbs
Given I set environment variable "ENABLED_BREADCRUMB_TYPES" to "[]"
When I run the go service "app" with the test case "disable-breadcrumbs"
When I wait to receive 2 requests after the start up session

Then the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Then the payload field "events.0.breadcrumbs" is null for request 0

Then the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Then the payload field "events.0.breadcrumbs" is null for request 1

Scenario: Automatic breadcrumbs
When I run the go service "app" with the test case "automatic-breadcrumbs"
When I wait to receive 2 requests after the start up session

Then the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Then the payload field "events.0.breadcrumbs" is an array with 1 elements for request 0
Then the payload field "events.0.breadcrumbs.0.name" equals "Bugsnag loaded" for request 0
Then the payload field "events.0.breadcrumbs.0.type" equals "state" for request 0

Then the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Then the payload field "events.0.breadcrumbs" is an array with 2 elements for request 1
Then the payload field "events.0.breadcrumbs.0.name" equals "oops" for request 1
Then the payload field "events.0.breadcrumbs.0.type" equals "error" for request 1
Then the payload field "events.0.breadcrumbs.1.name" equals "Bugsnag loaded" for request 1
Then the payload field "events.0.breadcrumbs.1.type" equals "state" for request 1

Scenario: Setting max breadcrumbs
Given I set environment variable "ENABLED_BREADCRUMB_TYPES" to "[]"
Given I set environment variable "MAXIMUM_BREADCRUMBS" to "5"
When I run the go service "app" with the test case "maximum-breadcrumbs"
When I wait to receive 1 requests after the start up session

Then the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"
Then the payload field "events.0.breadcrumbs" is an array with 5 elements
Then the payload field "events.0.breadcrumbs.0.name" equals "Crumb 9"
Then the payload field "events.0.breadcrumbs.1.name" equals "Crumb 8"
Then the payload field "events.0.breadcrumbs.2.name" equals "Crumb 7"
Then the payload field "events.0.breadcrumbs.3.name" equals "Crumb 6"
Then the payload field "events.0.breadcrumbs.4.name" equals "Crumb 5"
3 changes: 3 additions & 0 deletions features/fixtures/app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2
# Ensure subsequent steps are re-run if the GO_VERSION variable changes
ARG GO_VERSION

# Required for go 1.11 as modules are not enabled by default
ENV GO111MODULE="on"

# Get bugsnag dependencies using a conditional call to run go get or go install based on the go version
RUN if [[ $(echo -e "1.11\n$GO_VERSION\n1.16" | sort -V | head -2 | tail -1) == "$GO_VERSION" ]]; then \
echo "Version is between 1.11 and 1.16, running go get"; \
Expand Down
23 changes: 22 additions & 1 deletion features/fixtures/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
Expand Down Expand Up @@ -45,6 +46,15 @@ func configureBasicBugsnag(testcase string, ctx context.Context) {
config.AutoCaptureSessions = acs
}

if maximumBreadcrumbs, err := strconv.Atoi(os.Getenv("MAXIMUM_BREADCRUMBS")); err == nil {
config.MaximumBreadcrumbs = bugsnag.MaximumBreadcrumbs(maximumBreadcrumbs)
}

var enabledBreadcrumbTypes []bugsnag.BreadcrumbType
if err := json.Unmarshal([]byte(os.Getenv("ENABLED_BREADCRUMB_TYPES")), &enabledBreadcrumbTypes); err == nil {
config.EnabledBreadcrumbTypes = enabledBreadcrumbTypes
}

switch testcase {
case "endpoint-notify":
config.Endpoints = bugsnag.Endpoints{Notify: os.Getenv("BUGSNAG_ENDPOINT")}
Expand Down Expand Up @@ -98,14 +108,16 @@ func main() {
sendAndExit()
case "user":
user()
case "multiple-handled":
case "multiple-handled", "disable-breadcrumbs", "automatic-breadcrumbs":
multipleHandled()
case "multiple-unhandled":
multipleUnhandled()
case "make-unhandled-with-callback":
handledToUnhandled()
case "nested-error":
nestedHandledError()
case "maximum-breadcrumbs":
maximumBreadcrumbs()
default:
log.Println("Not a valid test flag: " + *test)
os.Exit(1)
Expand Down Expand Up @@ -309,6 +321,15 @@ func nestedHandledError() {
}
}

func maximumBreadcrumbs() {
bugsnag.Configure(bugsnag.Configuration{Synchronous: true})
ctx := bugsnag.StartSession(context.Background())
for i := 0; i < 10; i++ {
bugsnag.LeaveBreadcrumb(fmt.Sprintf("Crumb %v", i))
}
bugsnag.Notify(fmt.Errorf("test error"), ctx)
}

func login(token string) error {
val, err := checkValue(len(token) * -1)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions features/fixtures/autoconfigure/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2
# Ensure subsequent steps are re-run if the GO_VERSION variable changes
ARG GO_VERSION

# Required for go 1.11 as modules are not enabled by default
ENV GO111MODULE="on"

# Get bugsnag dependencies using a conditional call to run go get or go install based on the go version
RUN if [[ $(echo -e "1.11\n$GO_VERSION\n1.16" | sort -V | head -2 | tail -1) == "$GO_VERSION" ]]; then \
echo "Version is between 1.11 and 1.16, running go get"; \
Expand Down
2 changes: 2 additions & 0 deletions features/fixtures/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ services:
- APP_VERSION
- APP_TYPE
- AUTO_CAPTURE_SESSIONS
- MAXIMUM_BREADCRUMBS
- ENABLED_BREADCRUMB_TYPES
- HOSTNAME
- NOTIFY_RELEASE_STAGES
- RELEASE_STAGE
Expand Down
3 changes: 3 additions & 0 deletions features/fixtures/net_http/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2
# Ensure subsequent steps are re-run if the GO_VERSION variable changes
ARG GO_VERSION

# Required for go 1.11 as modules are not enabled by default
ENV GO111MODULE="on"

# Get bugsnag dependencies using a conditional call to run go get or go install based on the go version
RUN if [[ $(echo -e "1.11\n$GO_VERSION\n1.16" | sort -V | head -2 | tail -1) == "$GO_VERSION" ]]; then \
echo "Version is between 1.11 and 1.16, running go get"; \
Expand Down
10 changes: 5 additions & 5 deletions features/handled.feature
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Scenario: Sending an event using a callback to modify report contents
And the event "severityReason.type" equals "userCallbackSetSeverity"
And the event "context" equals "nonfatal.go:14"
And the "file" of stack frame 0 equals "main.go"
And stack frame 0 contains a local function spanning 245 to 253
And stack frame 0 contains a local function spanning 257 to 265
And the "file" of stack frame 1 equals ">insertion<"
And the "lineNumber" of stack frame 1 equals 0

Expand All @@ -50,7 +50,7 @@ Scenario: Marking an error as unhandled in a callback
And the event "severityReason.type" equals "userCallbackSetSeverity"
And the event "severityReason.unhandledOverridden" is true
And the "file" of stack frame 0 equals "main.go"
And stack frame 0 contains a local function spanning 257 to 262
And stack frame 0 contains a local function spanning 269 to 274

Scenario: Unwrapping the causes of a handled error
When I run the go service "app" with the test case "nested-error"
Expand All @@ -59,12 +59,12 @@ Scenario: Unwrapping the causes of a handled error
And the event "unhandled" is false
And the event "severity" equals "warning"
And the event "exceptions.0.message" equals "terminate process"
And the "lineNumber" of stack frame 0 equals 295
And the "lineNumber" of stack frame 0 equals 307
And the "file" of stack frame 0 equals "main.go"
And the "method" of stack frame 0 equals "nestedHandledError"
And the event "exceptions.1.message" equals "login failed"
And the event "exceptions.1.stacktrace.0.file" equals "main.go"
And the event "exceptions.1.stacktrace.0.lineNumber" equals 315
And the event "exceptions.1.stacktrace.0.lineNumber" equals 336
And the event "exceptions.2.message" equals "invalid token"
And the event "exceptions.2.stacktrace.0.file" equals "main.go"
And the event "exceptions.2.stacktrace.0.lineNumber" equals 323
And the event "exceptions.2.stacktrace.0.lineNumber" equals 344
167 changes: 167 additions & 0 deletions v2/breadcrumb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package bugsnag

import "time"

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

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) 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{}
}
breadcrumbs.breadcrumbs = append([]Breadcrumb{breadcrumb}, breadcrumbs.breadcrumbs...)
if configuration.MaximumBreadcrumbs != nil {
breadcrumbs.breadcrumbs = configuration.MaximumBreadcrumbs.trimBreadcrumbs(breadcrumbs.breadcrumbs)
}
}
}

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