├── .github ├── ISSUE_TEMPLATE │ ├── A.md │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── support.md └── workflows │ ├── license-audit.yml │ └── test-package.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── LOCAL_TESTING.md ├── Makefile ├── README.md ├── UPGRADING.md ├── bugsnag.go ├── bugsnag_example_test.go ├── bugsnag_test.go ├── configuration.go ├── configuration_test.go ├── device ├── hostname.go ├── runtimeversions.go └── runtimeversions_test.go ├── doc.go ├── errors ├── README.md ├── error.go ├── error_test.go ├── parse_panic.go ├── parse_panic_test.go └── stackframe.go ├── event.go ├── event_test.go ├── examples ├── README.md ├── http │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go └── using-goroutines │ ├── README.md │ └── main.go ├── features ├── apptype.feature ├── appversion.feature ├── autonotify.feature ├── configuration.feature ├── fixtures │ ├── app │ │ ├── Dockerfile │ │ ├── autoconfig_scenario.go │ │ ├── command.go │ │ ├── handled_scenario.go │ │ ├── main.go │ │ ├── metadata_scenario.go │ │ ├── nethttp_scenario.go │ │ ├── panic_scenario.go │ │ ├── run.sh │ │ ├── session_scenario.go │ │ ├── unhandled_scenario.go │ │ ├── user_scenario.go │ │ └── utils.go │ └── docker-compose.yml ├── handled.feature ├── hostname.feature ├── metadata.feature ├── multieventsession.feature ├── net-http │ ├── appversion.feature │ ├── autonotify.feature │ ├── handled.feature │ ├── onbeforenotify.feature │ ├── recover.feature │ ├── releasestage.feature │ ├── request.feature │ └── user.feature ├── onbeforenotify.feature ├── paramfilters.feature ├── plain_features │ └── panics.feature ├── recover.feature ├── releasestage.feature ├── sessioncontext.feature ├── steps │ └── go_steps.rb ├── support │ └── env.rb └── user.feature ├── gin ├── bugsnaggin.go └── gin_test.go ├── headers ├── prefixed.go └── prefixed_test.go ├── json_tags.go ├── martini ├── bugsnagmiddleware.go └── martini_test.go ├── metadata.go ├── metadata_test.go ├── middleware.go ├── middleware_test.go ├── negroni ├── bugsnagnegroni.go └── negroni_test.go ├── notifier.go ├── notifier_test.go ├── panicwrap.go ├── panicwrap_test.go ├── payload.go ├── payload_test.go ├── report.go ├── report_publisher.go ├── request_extractor.go ├── request_extractor_test.go ├── revel └── bugsnagrevel.go ├── sessions ├── config.go ├── config_test.go ├── integration_test.go ├── payload.go ├── publisher.go ├── publisher_test.go ├── session.go ├── startup.go ├── tracker.go └── tracker_test.go ├── testutil └── json.go └── v2 ├── LICENSE.txt ├── bugsnag.go ├── bugsnag_example_test.go ├── bugsnag_test.go ├── configuration.go ├── configuration_test.go ├── device ├── hostname.go ├── runtimeversions.go └── runtimeversions_test.go ├── doc.go ├── env_metadata.go ├── env_metadata_test.go ├── environment.go ├── environment_test.go ├── errors ├── README.md ├── error.go ├── error_fmt_wrap_test.go ├── error_test.go ├── error_types_test.go ├── error_unwrap.go ├── error_unwrap_test.go ├── parse_panic.go ├── parse_panic_test.go └── stackframe.go ├── event.go ├── event_test.go ├── go.mod ├── go.sum ├── headers ├── prefixed.go └── prefixed_test.go ├── json_tags.go ├── metadata.go ├── metadata_test.go ├── middleware.go ├── middleware_test.go ├── notifier.go ├── notifier_test.go ├── panicwrap.go ├── panicwrap_test.go ├── payload.go ├── payload_test.go ├── report.go ├── report_publisher.go ├── request_extractor.go ├── request_extractor_test.go ├── sessions ├── config.go ├── config_test.go ├── integration_test.go ├── payload.go ├── publisher.go ├── publisher_test.go ├── session.go ├── startup.go ├── tracker.go └── tracker_test.go └── testutil └── json.go /.github/ISSUE_TEMPLATE/A.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Having trouble getting started? 3 | about: Please contact us at support@bugsnag.com for assistance with integrating Bugsnag 4 | into your application. 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | Please checkout our [documentation](https://docs.bugsnag.com/platforms/go/) for guides, references and tutorials. 11 | 12 | If you have questions about your integration please contact us at [support@bugsnag.com](mailto:support@bugsnag.com). 13 | 14 | Alternatively, view additional options at [support.md](../SUPPORT.md). -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the library 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Describe the bug 13 | A clear and concise description of what the bug is. 14 | 15 | ### Steps to reproduce 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | ### Environment 22 | * Bugsnag Go version: 23 | * Go version: 24 | * Integration framework version: 25 | * Martini: 26 | * Negroni: 27 | * net/http: 28 | * Revel: 29 | * Other: 30 | 31 | 36 | 37 | ### Example Repo 38 | 39 | - [ ] Create a minimal repository that can reproduce the issue 40 | - [ ] Link to it here: 41 | 42 | ### Example code snippet 43 | 44 | ``` 45 | # (Insert code sample to reproduce the problem) 46 | ``` 47 | 48 | 49 |
Error messages: 50 | 51 | ``` 52 | 53 | ``` 54 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Description 13 | 16 | 17 | **Describe the solution you'd like** 18 | 19 | 20 | **Describe alternatives you've considered** 21 | 22 | 23 | **Additional context** 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Goal 2 | 3 | 4 | 5 | ## Design 6 | 7 | 8 | 9 | ## Changeset 10 | 11 | 12 | 13 | ## Testing 14 | 15 | -------------------------------------------------------------------------------- /.github/support.md: -------------------------------------------------------------------------------- 1 | ## Are you having trouble getting started? 2 | If you haven't already, please checkout our [documentation](https://docs.bugsnag.com/platforms/go/) for guides, references and tutorials. 3 | 4 | Or, if you wish you can [contact us directly](mailto:support@bugsnag.com) for assistance on integrating Bugsnag into your application, troubleshooting an issue or a question about our supported features. 5 | 6 | When contacting support, please include as much information as necessary, including: 7 | 8 | - example code snippet 9 | - steps to reproduce 10 | - expected/actual behaviour 11 | 12 | * Bugsnag Go version: 13 | * Go version: 14 | * Integration framework version: 15 | * Martini: 16 | * Negroni: 17 | * net/http: 18 | * Revel: 19 | * Other: 20 | 21 | ## Bug or Feature Requests 22 | If you would like to raise a bug or feature request please do so by creating a [New Issue](https://github.com/bugsnag/bugsnag-go/issues/new/choose) and selecting bug or feature. 23 | Please note: we cannot promise that we will fulfil all requests 24 | 25 | ## Pull Requests 26 | If you have made a fix and would like to raise a pull request, please read our [CONTRIBUTING.md](../CONTRIBUTING.md) file before creating the pull request. -------------------------------------------------------------------------------- /.github/workflows/license-audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit bugsnag-go dependency licenses 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | license-audit: 7 | runs-on: ubuntu-latest 8 | defaults: 9 | run: 10 | working-directory: 'go/src/github.com/bugsnag/bugsnag-go/v2' 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | path: 'go/src/github.com/bugsnag/bugsnag-go' # relative to $GITHUB_WORKSPACE 16 | 17 | - name: set GOPATH 18 | run: | 19 | bash -c 'echo "GOPATH=$GITHUB_WORKSPACE/go" >> $GITHUB_ENV' 20 | 21 | - name: Fetch decisions.yml 22 | run: curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/global.yml -o decisions.yml 23 | 24 | - uses: actions/setup-go@v2 25 | with: 26 | go-version: '1.16' 27 | 28 | - name: install dependencies 29 | run: go get -v -d ./... 30 | 31 | - name: Run License Finder 32 | run: > 33 | docker run -v $PWD:/scan licensefinder/license_finder /bin/bash -lc " 34 | cd /scan && 35 | license_finder --decisions-file decisions.yml --enabled-package-managers=gomodules 36 | " 37 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Test package against Go versions 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }}-latest 8 | defaults: 9 | run: 10 | working-directory: 'go/src/github.com/bugsnag/bugsnag-go/v2' # relative to $GITHUB_WORKSPACE 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu, windows] 15 | 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'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | path: 'go/src/github.com/bugsnag/bugsnag-go' # relative to $GITHUB_WORKSPACE 21 | - name: set GOPATH 22 | if: matrix.os == 'ubuntu' 23 | run: | 24 | bash -c 'echo "GOPATH=$GITHUB_WORKSPACE/go" >> $GITHUB_ENV' 25 | - name: set GOPATH 26 | if: matrix.os == 'windows' 27 | run: | 28 | bash -c 'echo "GOPATH=$GITHUB_WORKSPACE\\\\go" >> $GITHUB_ENV' 29 | - name: set GO111MODULE 30 | run: | 31 | bash -c 'echo "GO111MODULE=on" >> $GITHUB_ENV' 32 | - uses: actions/setup-go@v2 33 | with: 34 | go-version: ${{ matrix.go-version }} 35 | - name: install dependencies 36 | run: go get -v -d ./... 37 | - name: run tests 38 | run: go test $(go list ./... | grep -v /features/) 39 | - name: vet package 40 | # go1.12 vet shows spurious 'unknown identifier' issues 41 | if: matrix.go-version != '1.12' 42 | run: go vet $(go list ./... | grep -v /features/) 43 | 44 | - name: install integration dependencies 45 | if: matrix.os == 'ubuntu' 46 | run: | 47 | sudo apt-get update 48 | sudo apt-get install libcurl4-openssl-dev 49 | - name: install Ruby 50 | if: matrix.os == 'ubuntu' 51 | uses: ruby/setup-ruby@v1 52 | with: 53 | ruby-version: '3.2' 54 | bundler-cache: true 55 | working-directory: go/src/github.com/bugsnag/bugsnag-go # relative to $GITHUB_WORKSPACE 56 | - name: maze tests 57 | working-directory: go/src/github.com/bugsnag/bugsnag-go 58 | if: matrix.os == 'ubuntu' 59 | env: 60 | GO_VERSION: ${{ matrix.go-version }} 61 | run: bundle exec maze-runner --color --format progress -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore maze runner generated files 2 | maze_output 3 | vendor 4 | 5 | # ignore the gemfile to prevent testing against stale versions 6 | Gemfile.lock -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | - [Fork](https://help.github.com/articles/fork-a-repo) the [notifier on github](https://github.com/bugsnag/bugsnag-go) 5 | - Build and test your changes 6 | - Commit and push until you are happy with your contribution 7 | - [Make a pull request](https://help.github.com/articles/using-pull-requests) 8 | - Thanks! 9 | 10 | 11 | Installing the go development environment 12 | ------------------------------------- 13 | 14 | 1. Install homebrew 15 | 16 | ``` 17 | ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" 18 | ``` 19 | 20 | 1. Install go 21 | 22 | ``` 23 | brew install go --cross-compile-all 24 | ``` 25 | 26 | 1. Configure `$GOPATH` in `~/.bashrc` 27 | 28 | ``` 29 | export GOPATH="$HOME/go" 30 | export PATH=$PATH:$GOPATH/bin 31 | ``` 32 | 33 | Downloading the code 34 | -------------------- 35 | 36 | You can download the code and its dependencies using 37 | 38 | ``` 39 | go get -t github.com/bugsnag/bugsnag-go/v2 40 | ``` 41 | 42 | It will be put into "$GOPATH/src/github.com/bugsnag/bugsnag-go" 43 | 44 | Then install depend 45 | 46 | 47 | Running Tests 48 | ------------- 49 | 50 | You can run the tests with 51 | 52 | ```shell 53 | go test ./... 54 | ``` 55 | 56 | Making PRs 57 | ---------- 58 | 59 | All PRs should target the `next` branch as their base. This means that we can land them and stage them for a release without making multiple changes to `master` (which would cause multiple releases due to `go get`'s behaviour). 60 | 61 | The exception to this rule is for an urgent bug fix when `next` is already ahead of `master`. See [hotfixes](#hotfixes) for what to do then. 62 | 63 | Releasing a New Version 64 | ----------------------- 65 | 66 | If you are a project maintainer, you can build and release a new version of 67 | `bugsnag-go` as follows: 68 | 69 | #### Planned releases 70 | 71 | **Prerequisite**: All code changes should already have been reviewed and PR'd into the `next` branch before making a release. 72 | 73 | 1. Decide on a version number and date for this release 74 | 1. Add an entry (or update the `TBD` entry if it exists) for this release in `CHANGELOG.md` so that it includes the version number, release date and granular description of what changed 75 | 1. Update the README if necessary 76 | 1. Update the version number in `v2/bugsnag.go` and verify that tests pass. 77 | 1. Commit these changes `git commit -am "Preparing release"` 78 | 1. Create a PR from `next` -> `master` titled `Release vX.X.X`, adding a description to help the reviewer understand the scope of the release 79 | 1. Await PR approval and CI pass 80 | 1. Merge to master on GitHub, using the UI to set the merge commit message to be `vX.X.X` 81 | 1. Create a release from current `master` on GitHub called `vX.X.X`. Copy and paste the markdown from this release's notes in `CHANGELOG.md` (this will create a git tag for you). 82 | 1. Ensure setup guides for Go (and its frameworks) on docs.bugsnag.com are correct and up to date. 83 | 1. Merge `master` into `next` (since we just did a merge commit the other way, this will be a fastforward update) and push it so that it is ready for future PRs. 84 | 85 | 86 | #### Hotfixes 87 | 88 | If a `next` branch already exists and is ahead of `master` but there is a bug fix which needs to go out urgently, check out the latest `master` and create a new hotfix branch `git checkout -b hotfix`. You can then proceed to follow the above steps, substituting `next` for `hotfix`. 89 | 90 | Once released, ensure `master` is merged into `next` so that the changes made on `hotfix` are included. 91 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "bugsnag-maze-runner", "~> 9.14" -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Bugsnag 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /LOCAL_TESTING.md: -------------------------------------------------------------------------------- 1 | 2 | ## Unit tests 3 | * Install old golang version (do not install just 1.11 - it's not compatible with running newer modules): 4 | 5 | ``` 6 | ASDF_GOLANG_OVERWRITE_ARCH=amd64 asdf install golang 1.11.13 7 | ``` 8 | 9 | * If you see error below use `CGO_ENABLED=0`. 10 | 11 | ``` 12 | # crypto/x509 13 | malformed DWARF TagVariable entry 14 | ``` 15 | 16 | ## Local testing with maze runner 17 | 18 | * Maze runner tests require 19 | * Specyfing `GO_VERSION` env variable to set a golang version for docker container. 20 | * Ruby 2.7. 21 | * Running docker. 22 | 23 | * Commands to run tests 24 | 25 | ``` 26 | bundle install 27 | bundle exec bugsnag-maze-runner 28 | bundle exec bugsnag-maze-runner -c features/ 29 | ``` -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST?=./... 2 | 3 | export GO111MODULE=auto 4 | 5 | default: alldeps test 6 | 7 | deps: 8 | go get -v -d ./... 9 | 10 | alldeps: 11 | go get -v -d -t ./... 12 | 13 | updatedeps: 14 | go get -v -d -u ./... 15 | 16 | test: alldeps 17 | @# skipping Gin if the Go version is lower than 1.9, as the latest version of Gin has dropped support for these versions. 18 | @if [ "$(GO_VERSION)" = "1.7" ] || [ "$(GO_VERSION)" = "1.8" ] || [ "$(GO_VERSION)" = "1.9" ]; then \ 19 | go test . ./errors ./martini ./negroni ./sessions ./headers; \ 20 | else \ 21 | go test . ./errors ./gin ./martini ./negroni ./sessions ./headers; \ 22 | fi 23 | @go vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ 24 | go get golang.org/x/tools/cmd/vet; \ 25 | fi 26 | @go vet $(TEST) ; if [ $$? -eq 1 ]; then \ 27 | echo "go-vet: Issues running go vet ./..."; \ 28 | exit 1; \ 29 | fi 30 | 31 | maze: 32 | bundle install 33 | bundle exec bugsnag-maze-runner 34 | 35 | ci: alldeps test 36 | 37 | bench: 38 | go test --bench=.* 39 | 40 | testsetup: 41 | gem update --system 42 | gem install bundler 43 | bundle install 44 | 45 | testplain: testsetup 46 | bundle exec bugsnag-maze-runner -c features/plain_features 47 | 48 | testnethttp: testsetup 49 | bundle exec bugsnag-maze-runner -c features/net_http_features 50 | 51 | testgin: testsetup 52 | bundle exec bugsnag-maze-runner -c features/gin_features 53 | 54 | testmartini: testsetup 55 | bundle exec bugsnag-maze-runner -c features/martini_features 56 | 57 | testnegroni: testsetup 58 | bundle exec bugsnag-maze-runner -c features/negroni_features 59 | 60 | testrevel: testsetup 61 | bundle exec bugsnag-maze-runner -c features/revel_features 62 | 63 | .PHONY: bin checkversion ci default deps generate releasebin test testacc testrace updatedeps testsetup testplain testnethttp testgin testmartini testrevel 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | SmartBear BugSnag logo 6 | 7 | 8 |

Error monitoring and reporting for Go

9 |
10 | 11 | [![Documentation](https://img.shields.io/badge/documentation-latest-blue.svg)](https://docs.bugsnag.com/performance/go/) 12 | [![Go Reference](https://pkg.go.dev/badge/github.com/bugsnag/bugsnag-go.svg)](https://pkg.go.dev/github.com/bugsnag/bugsnag-go) 13 | [![Build status](https://github.com/bugsnag/bugsnag-go/actions/workflows/test-package.yml/badge.svg?branch=master)](https://buildkite.com/bugsnag/bugsnag-go) 14 | 15 | Automatically detect crashes and report errors in your Go apps. Get alerts about errors and panics in real-time, including detailed error reports with diagnostic information. Understand and resolve issues as fast as possible. 16 | 17 | Learn more about BugSnag's [Go error monitoring and error reporting](https://www.bugsnag.com/platforms/go-lang-error-reporting/) solution. 18 | 19 | ## Features 20 | 21 | * Automatically report unhandled errors and panics 22 | * Report handled errors 23 | * Attach user information to determine how many people are affected by a crash 24 | * Send customized diagnostic data 25 | 26 | ## Getting Started 27 | 28 | 1. [Create a BugSnag account](https://bugsnag.com) 29 | 2. Complete the instructions in the integration guide for your framework: 30 | * [Gin](https://docs.bugsnag.com/platforms/go/gin/) 31 | * [Negroni](https://docs.bugsnag.com/platforms/go/negroni/) 32 | * [net/http](https://docs.bugsnag.com/platforms/go/net-http/) 33 | * [Revel](https://docs.bugsnag.com/platforms/go/revel/) 34 | * [Other Go apps](https://docs.bugsnag.com/platforms/go/other/) 35 | 3. Relax! 36 | 37 | ## Support 38 | 39 | * [Search open and closed issues](https://github.com/bugsnag/bugsnag-go/issues?utf8=✓&q=is%3Aissue) for similar problems 40 | * [Report a bug or request a feature](https://github.com/bugsnag/bugsnag-go/issues/new) 41 | 42 | ## Contributing 43 | 44 | All contributors are welcome! For information on how to build, test and release `bugsnag-go`, see our [contributing guide](CONTRIBUTING.md). 45 | 46 | 47 | ## License 48 | 49 | The BugSnag error reporter for Go is free software released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details. 50 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading guide 2 | 3 | ## v1 to v2 4 | 5 | The v2 release adds support for Go modules, removes web framework 6 | integrations from the main repository, and supports library configuration 7 | through environment variables. The following breaking changes occurred as a part 8 | of this release: 9 | 10 | ### Importing the package 11 | 12 | ```diff+go 13 | - import "github.com/bugsnag/bugsnag-go" 14 | + import "github.com/bugsnag/bugsnag-go/v2" 15 | ``` 16 | 17 | ### Removed `Configuration.Endpoint` 18 | 19 | The `Endpoint` configuration option was deprecated as a part of the v1.4.0 20 | release in November 2018. It was replaced with `Endpoints`, which includes 21 | options for configuring both event and session delivery. 22 | 23 | ```diff+go 24 | - config.Endpoint = "https://notify.myserver.example.com" 25 | + config.Endpoints = { 26 | + Notify: "https://notify.myserver.example.com", 27 | + Sessions: "https://sessions.myserver.example.com" 28 | + } 29 | ``` 30 | 31 | ### Moved web framework integrations into separate repositories 32 | 33 | Integrations with Negroni, Revel, and Gin now live in separate repositories, to 34 | prevent implicit dependencies on every framework and to improve the ease of 35 | updating each integration independently. 36 | 37 | ```diff+go 38 | - import "github.com/bugsnag/bugsnag-go/negroni" 39 | + import "github.com/bugsnag/bugsnag-go-negroni" 40 | ``` 41 | 42 | ```diff+go 43 | - import "github.com/bugsnag/bugsnag-go/revel" 44 | + import "github.com/bugsnag/bugsnag-go-revel" 45 | ``` 46 | 47 | ```diff+go 48 | - import "github.com/bugsnag/bugsnag-go/gin" 49 | + import "github.com/bugsnag/bugsnag-go-gin" 50 | ``` 51 | 52 | ### Renamed constants for platform consistency 53 | 54 | ```diff+go 55 | - bugsnag.VERSION 56 | + bugsnag.Version 57 | ``` 58 | -------------------------------------------------------------------------------- /device/hostname.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import "os" 4 | 5 | var hostname string 6 | 7 | // GetHostname returns the hostname of the current device. Caches the hostname 8 | // between calls to ensure this is performant. Returns a blank string in case 9 | // that the hostname cannot be identified. 10 | func GetHostname() string { 11 | if hostname == "" { 12 | hostname, _ = os.Hostname() 13 | } 14 | return hostname 15 | } 16 | -------------------------------------------------------------------------------- /device/runtimeversions.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | // Cached runtime versions that can be updated globally by framework 8 | // integrations through AddVersion. 9 | var versions *RuntimeVersions 10 | 11 | // RuntimeVersions define the various versions of Go and any framework that may 12 | // be in use. 13 | // As a user of the notifier you're unlikely to need to modify this struct. 14 | // As such, the authors reserve the right to introduce breaking changes to the 15 | // properties in this struct. In particular the framework versions are liable 16 | // to change in new versions of the notifier in minor/patch versions. 17 | type RuntimeVersions struct { 18 | Go string `json:"go"` 19 | 20 | Gin string `json:"gin,omitempty"` 21 | Martini string `json:"martini,omitempty"` 22 | Negroni string `json:"negroni,omitempty"` 23 | Revel string `json:"revel,omitempty"` 24 | } 25 | 26 | // GetRuntimeVersions retrieves the recorded runtime versions in a goroutine-safe manner. 27 | func GetRuntimeVersions() *RuntimeVersions { 28 | if versions == nil { 29 | versions = &RuntimeVersions{Go: runtime.Version()} 30 | } 31 | return versions 32 | } 33 | 34 | // AddVersion permits a framework to register its version, assuming it's one of 35 | // the officially supported frameworks. 36 | func AddVersion(framework, version string) { 37 | if versions == nil { 38 | versions = &RuntimeVersions{Go: runtime.Version()} 39 | } 40 | switch framework { 41 | case "Martini": 42 | versions.Martini = version 43 | case "Gin": 44 | versions.Gin = version 45 | case "Negroni": 46 | versions.Negroni = version 47 | case "Revel": 48 | versions.Revel = version 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /device/runtimeversions_test.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func TestPristineRuntimeVersions(t *testing.T) { 9 | versions = nil // reset global variable 10 | rv := GetRuntimeVersions() 11 | for _, tc := range []struct{ name, got, exp string }{ 12 | {name: "Go", got: rv.Go, exp: runtime.Version()}, 13 | {name: "Gin", got: rv.Gin, exp: ""}, 14 | {name: "Martini", got: rv.Martini, exp: ""}, 15 | {name: "Negroni", got: rv.Negroni, exp: ""}, 16 | {name: "Revel", got: rv.Revel, exp: ""}, 17 | } { 18 | if tc.got != tc.exp { 19 | t.Errorf("expected pristine '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) 20 | } 21 | } 22 | } 23 | 24 | func TestModifiedRuntimeVersions(t *testing.T) { 25 | versions = nil // reset global variable 26 | rv := GetRuntimeVersions() 27 | AddVersion("Gin", "1.2.1") 28 | AddVersion("Martini", "1.0.0") 29 | AddVersion("Negroni", "1.0.2") 30 | AddVersion("Revel", "0.20.1") 31 | for _, tc := range []struct{ name, got, exp string }{ 32 | {name: "Go", got: rv.Go, exp: runtime.Version()}, 33 | {name: "Gin", got: rv.Gin, exp: "1.2.1"}, 34 | {name: "Martini", got: rv.Martini, exp: "1.0.0"}, 35 | {name: "Negroni", got: rv.Negroni, exp: "1.0.2"}, 36 | {name: "Revel", got: rv.Revel, exp: "0.20.1"}, 37 | } { 38 | if tc.got != tc.exp { 39 | t.Errorf("expected modified '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bugsnag captures errors in real-time and reports them to BugSnag (http://bugsnag.com). 3 | 4 | Using bugsnag-go is a three-step process. 5 | 6 | 1. As early as possible in your program configure the notifier with your APIKey. This sets up 7 | handling of panics that would otherwise crash your app. 8 | 9 | func init() { 10 | bugsnag.Configure(bugsnag.Configuration{ 11 | APIKey: "YOUR_API_KEY_HERE", 12 | }) 13 | } 14 | 15 | 2. Add bugsnag to places that already catch panics. For example you should add it to the HTTP server 16 | when you call ListenAndServer: 17 | 18 | http.ListenAndServe(":8080", bugsnag.Handler(nil)) 19 | 20 | If that's not possible, you can also wrap each HTTP handler manually: 21 | 22 | http.HandleFunc("/" bugsnag.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { 23 | ... 24 | }) 25 | 26 | 3. To notify BugSnag of an error that is not a panic, pass it to bugsnag.Notify. This will also 27 | log the error message using the configured Logger. 28 | 29 | if err != nil { 30 | bugsnag.Notify(err) 31 | } 32 | 33 | For detailed integration instructions see https://docs.bugsnag.com/platforms/go. 34 | 35 | # Configuration 36 | 37 | The only required configuration is the BugSnag API key which can be obtained by clicking "Project 38 | Settings" on the top of your BugSnag dashboard after signing up. We also recommend you set the 39 | ReleaseStage, AppType, and AppVersion if these make sense for your deployment workflow. 40 | 41 | # RawData 42 | 43 | If you need to attach extra data to BugSnag events, you can do that using the rawData mechanism. 44 | Most of the functions that send errors to BugSnag allow you to pass in any number of interface{} 45 | values as rawData. The rawData can consist of the Severity, Context, User or MetaData types listed 46 | below, and there is also builtin support for *http.Requests. 47 | 48 | bugsnag.Notify(err, bugsnag.SeverityError) 49 | 50 | If you want to add custom tabs to your bugsnag dashboard you can pass any value in as rawData, 51 | and then process it into the event's metadata using a bugsnag.OnBeforeNotify() hook. 52 | 53 | bugsnag.Notify(err, account) 54 | 55 | bugsnag.OnBeforeNotify(func (e *bugsnag.Event, c *bugsnag.Configuration) { 56 | for datum := range e.RawData { 57 | if account, ok := datum.(Account); ok { 58 | e.MetaData.Add("account", "name", account.Name) 59 | e.MetaData.Add("account", "url", account.URL) 60 | } 61 | } 62 | }) 63 | 64 | If necessary you can pass Configuration in as rawData, or modify the Configuration object passed 65 | into OnBeforeNotify hooks. Configuration passed in this way only affects the current notification. 66 | */ 67 | package bugsnag 68 | -------------------------------------------------------------------------------- /errors/README.md: -------------------------------------------------------------------------------- 1 | Adds stacktraces to errors in golang. 2 | 3 | This was made to help build the Bugsnag notifier but can be used standalone if 4 | you like to have stacktraces on errors. 5 | 6 | See [Godoc](https://godoc.org/github.com/bugsnag/bugsnag-go/errors) for the API docs. 7 | -------------------------------------------------------------------------------- /errors/parse_panic.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type uncaughtPanic struct { 9 | typeName string 10 | message string 11 | } 12 | 13 | func (p uncaughtPanic) Error() string { 14 | return p.message 15 | } 16 | 17 | // ParsePanic allows you to get an error object from the output of a go program 18 | // that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap. 19 | func ParsePanic(text string) (*Error, error) { 20 | lines := strings.Split(text, "\n") 21 | prefixes := []string{"panic:", "fatal error:"} 22 | 23 | state := "start" 24 | 25 | var message string 26 | var typeName string 27 | var stack []StackFrame 28 | 29 | for i := 0; i < len(lines); i++ { 30 | line := lines[i] 31 | 32 | if state == "start" { 33 | for _, prefix := range prefixes { 34 | if strings.HasPrefix(line, prefix) { 35 | message = strings.TrimSpace(strings.TrimPrefix(line, prefix)) 36 | typeName = prefix[:len(prefix) - 1] 37 | state = "seek" 38 | break 39 | } 40 | } 41 | if state == "start" { 42 | return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line) 43 | } 44 | 45 | } else if state == "seek" { 46 | if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") { 47 | state = "parsing" 48 | } 49 | 50 | } else if state == "parsing" { 51 | if line == "" || strings.HasPrefix(line, "...") { 52 | state = "done" 53 | break 54 | } 55 | createdBy := false 56 | if strings.HasPrefix(line, "created by ") { 57 | line = strings.TrimPrefix(line, "created by ") 58 | createdBy = true 59 | } 60 | 61 | i++ 62 | 63 | if i >= len(lines) { 64 | return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line) 65 | } 66 | 67 | frame, err := parsePanicFrame(line, lines[i], createdBy) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | stack = append(stack, *frame) 73 | if createdBy { 74 | state = "done" 75 | break 76 | } 77 | } 78 | } 79 | 80 | if state == "done" || state == "parsing" { 81 | return &Error{Err: uncaughtPanic{typeName, message}, frames: stack}, nil 82 | } 83 | return nil, Errorf("could not parse panic: %v", text) 84 | } 85 | 86 | // The lines we're passing look like this: 87 | // 88 | // main.(*foo).destruct(0xc208067e98) 89 | // /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151 90 | func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) { 91 | idx := strings.LastIndex(name, "(") 92 | if idx == -1 && !createdBy { 93 | return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name) 94 | } 95 | if idx != -1 { 96 | name = name[:idx] 97 | } 98 | pkg := "" 99 | 100 | if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { 101 | pkg += name[:lastslash] + "/" 102 | name = name[lastslash+1:] 103 | } 104 | if period := strings.Index(name, "."); period >= 0 { 105 | pkg += name[:period] 106 | name = name[period+1:] 107 | } 108 | 109 | name = strings.Replace(name, "·", ".", -1) 110 | 111 | if !strings.HasPrefix(line, "\t") { 112 | return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line) 113 | } 114 | 115 | idx = strings.LastIndex(line, ":") 116 | if idx == -1 { 117 | return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line) 118 | } 119 | file := line[1:idx] 120 | 121 | number := line[idx+1:] 122 | if idx = strings.Index(number, " +"); idx > -1 { 123 | number = number[:idx] 124 | } 125 | 126 | lno, err := strconv.ParseInt(number, 10, 32) 127 | if err != nil { 128 | return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line) 129 | } 130 | 131 | return &StackFrame{ 132 | File: file, 133 | LineNumber: int(lno), 134 | Package: pkg, 135 | Name: name, 136 | }, nil 137 | } 138 | -------------------------------------------------------------------------------- /errors/stackframe.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | // A StackFrame contains all necessary information about to generate a line 12 | // in a callstack. 13 | type StackFrame struct { 14 | File string 15 | LineNumber int 16 | Name string 17 | Package string 18 | ProgramCounter uintptr 19 | function *runtime.Func 20 | } 21 | 22 | // NewStackFrame popoulates a stack frame object from the program counter. 23 | func NewStackFrame(pc uintptr) (frame StackFrame) { 24 | 25 | frame = StackFrame{ProgramCounter: pc} 26 | if frame.Func() == nil { 27 | return 28 | } 29 | frame.Package, frame.Name = packageAndName(frame.Func()) 30 | 31 | // pc -1 because the program counters we use are usually return addresses, 32 | // and we want to show the line that corresponds to the function call 33 | frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) 34 | return 35 | 36 | } 37 | 38 | // Func returns the function that this stackframe corresponds to 39 | func (frame *StackFrame) Func() *runtime.Func { 40 | return frame.function 41 | } 42 | 43 | // String returns the stackframe formatted in the same way as go does 44 | // in runtime/debug.Stack() 45 | func (frame *StackFrame) String() string { 46 | str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) 47 | 48 | source, err := frame.SourceLine() 49 | if err != nil { 50 | return str 51 | } 52 | 53 | return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) 54 | } 55 | 56 | // SourceLine gets the line of code (from File and Line) of the original source if possible 57 | func (frame *StackFrame) SourceLine() (string, error) { 58 | data, err := ioutil.ReadFile(frame.File) 59 | 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | lines := bytes.Split(data, []byte{'\n'}) 65 | if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { 66 | return "???", nil 67 | } 68 | // -1 because line-numbers are 1 based, but our array is 0 based 69 | return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil 70 | } 71 | 72 | func packageAndName(fn *runtime.Func) (string, string) { 73 | name := fn.Name() 74 | pkg := "" 75 | 76 | // The name includes the path name to the package, which is unnecessary 77 | // since the file name is already included. Plus, it has center dots. 78 | // That is, we see 79 | // runtime/debug.*T·ptrmethod 80 | // and want 81 | // *T.ptrmethod 82 | // Since the package path might contains dots (e.g. code.google.com/...), 83 | // we first remove the path prefix if there is one. 84 | if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { 85 | pkg += name[:lastslash] + "/" 86 | name = name[lastslash+1:] 87 | } 88 | if period := strings.Index(name, "."); period >= 0 { 89 | pkg += name[:period] 90 | name = name[period+1:] 91 | } 92 | 93 | name = strings.Replace(name, "·", ".", -1) 94 | return pkg, name 95 | } 96 | -------------------------------------------------------------------------------- /event_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestPopulateEvent(t *testing.T) { 13 | event := new(Event) 14 | contexts := make(chan context.Context, 1) 15 | reqs := make(chan *http.Request, 1) 16 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | contexts <- AttachRequestData(r.Context(), r) 18 | reqs <- r 19 | })) 20 | defer ts.Close() 21 | 22 | http.Get(ts.URL + "/serenity?q=abcdef") 23 | 24 | ctx, req := <-contexts, <-reqs 25 | populateEventWithContext(ctx, event) 26 | 27 | for _, tc := range []struct{ e, c interface{} }{ 28 | {e: event.Ctx, c: ctx}, 29 | {e: event.Request, c: extractRequestInfoFromReq(req)}, 30 | {e: event.Context, c: req.URL.Path}, 31 | {e: event.User.Id, c: req.RemoteAddr[:strings.LastIndex(req.RemoteAddr, ":")]}, 32 | } { 33 | if !reflect.DeepEqual(tc.e, tc.c) { 34 | t.Errorf("Expected '%+v' and '%+v' to be equal", tc.e, tc.c) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples of working with bugsnag-go 2 | 3 | In this directory you can find example applications of the frameworks we support, and other examples of common use cases. 4 | 5 | The examples that expose a HTTP port will all listen on 9001. 6 | 7 | ## Use cases and frameworks 8 | 9 | * [Capturing panics within goroutines](using-goroutines). Goroutines require special care to avoid crashing the app entirely or cleaning up before an error report can be sent. 10 | This is an example of a panic within a goroutine which is sent to Bugsnag. 11 | * [Using net/http](http) (web server using the standard library) 12 | * [Using Gin](gin) (web framework) 13 | * [Using Negroni](negroni) (web framework) 14 | * [Using Martini](martini) (web framework) 15 | * [Using Revel](revelapp) (web framework) 16 | -------------------------------------------------------------------------------- /examples/http/README.md: -------------------------------------------------------------------------------- 1 | # Example `net/http` application 2 | 3 | This package contains an example `net/http` application, with Bugsnag configured. 4 | 5 | ## Run the example 6 | 7 | 1. Change the API key in `main.go` to a project you've created in [Bugsnag](https://app.bugsnag.com). 8 | 1. Inside `bugsnag-go/examples/http` do: 9 | ```bash 10 | go mod tidy 11 | go run main.go 12 | ``` 13 | 1. The application is now running. You can now visit 14 | ``` 15 | http://localhost:9001/unhandled - to trigger an unhandled panic 16 | http://localhost:9001/handled - to trigger a handled error 17 | ``` 18 | 1. You should now see events for these exceptions in your [Bugsnag dashboard](https://app.bugsnag.com). 19 | -------------------------------------------------------------------------------- /examples/http/go.mod: -------------------------------------------------------------------------------- 1 | module github/bugsnag/bugsnag-go/example/http 2 | 3 | go 1.19 4 | 5 | require github.com/bugsnag/bugsnag-go/v2 v2.2.0 6 | 7 | require ( 8 | github.com/bugsnag/panicwrap v1.3.4 // indirect 9 | github.com/google/uuid v1.3.0 // indirect 10 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 11 | github.com/pkg/errors v0.9.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /examples/http/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= 2 | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= 3 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 4 | github.com/bugsnag/bugsnag-go/v2 v2.2.0 h1:y4JJ6xNJiK4jbmq/BLXe09MGUNRp/r1Zpye6RKcPJJ8= 5 | github.com/bugsnag/bugsnag-go/v2 v2.2.0/go.mod h1:Aoi1ax1kGbbkArShzXUQjxp6jM8gMh4qOtHLis/jY1E= 6 | github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= 7 | github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= 8 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 9 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 10 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 11 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 12 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | -------------------------------------------------------------------------------- /examples/http/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/bugsnag/bugsnag-go/v2" 9 | ) 10 | 11 | // Insert your API key 12 | const apiKey = "YOUR-API-KEY-HERE" 13 | 14 | func main() { 15 | if len(apiKey) != 32 { 16 | fmt.Println("Please set the API key in main.go before running the example") 17 | return 18 | } 19 | 20 | bugsnag.Configure(bugsnag.Configuration{APIKey: apiKey}) 21 | 22 | http.HandleFunc("/unhandled", unhandledCrash) 23 | http.HandleFunc("/handled", handledError) 24 | 25 | fmt.Println("=============================================================================") 26 | fmt.Println("Visit http://localhost:9001/unhandled - To perform an unhandled crash") 27 | fmt.Println("Visit http://localhost:9001/handled - To create a manual error notification") 28 | fmt.Println("=============================================================================") 29 | fmt.Println("") 30 | 31 | http.ListenAndServe(":9001", bugsnag.Handler(nil)) 32 | } 33 | 34 | func unhandledCrash(w http.ResponseWriter, r *http.Request) { 35 | w.WriteHeader(200) 36 | w.Write([]byte("OK\n")) 37 | 38 | // Invalid type assertion, will panic 39 | func(a interface{}) string { return a.(string) }(struct{}{}) 40 | } 41 | 42 | func handledError(w http.ResponseWriter, r *http.Request) { 43 | _, err := os.Open("nonexistent_file.txt") 44 | if err != nil { 45 | bugsnag.Notify(err, r.Context()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/using-goroutines/README.md: -------------------------------------------------------------------------------- 1 | # Managing panics from goroutines 2 | 3 | This package contains an example for how to manage panics in a separate goroutine with Bugsnag. 4 | 5 | ## Run the example 6 | 7 | 1. Change the API key in `main.go` to a project you've created in [Bugsnag](https://app.bugsnag.com). 8 | 1. Inside `bugsnag-go/examples/using-goroutines` do: 9 | ```bash 10 | go get 11 | go run main.go 12 | ``` 13 | 1. The application will run for a split second, starting a new goroutine, which panics. 14 | 1. You should now see events this panic in your [Bugsnag dashboard](https://app.bugsnag.com). 15 | -------------------------------------------------------------------------------- /examples/using-goroutines/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/bugsnag/bugsnag-go/v2" 9 | ) 10 | 11 | // Insert your API key 12 | const apiKey = "YOUR-API-KEY-HERE" 13 | 14 | // The following example will cause two events in your dashboard: 15 | // One event because AutoNotify intercepted a panic. 16 | // The other because Bugsnag noticed your application was about to be taken 17 | // down by a panic. 18 | // To avoid taking down your application and the last event, replace 19 | // bugsnag.AutoNotify with bugsnag.Recover in the below example. 20 | func main() { 21 | if len(apiKey) != 32 { 22 | fmt.Println("Please set your API key in main.go before running example.") 23 | return 24 | } 25 | 26 | bugsnag.Configure(bugsnag.Configuration{APIKey: apiKey}) 27 | 28 | var wg sync.WaitGroup 29 | wg.Add(1) 30 | 31 | go func() { 32 | fmt.Println("Starting new go routine...") 33 | // Manually create a new Bugsnag session for this goroutine 34 | ctx := bugsnag.StartSession(context.Background()) 35 | defer wg.Done() 36 | // AutoNotify captures any panics, repanicking after error reports are sent 37 | defer bugsnag.AutoNotify(ctx) 38 | 39 | // Invalid type assertion, will panic 40 | func(a interface{}) { _ = a.(string) }(struct{}{}) 41 | }() 42 | 43 | wg.Wait() 44 | } 45 | -------------------------------------------------------------------------------- /features/apptype.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring app type 2 | 3 | Background: 4 | Given I set environment variable "BUGSNAG_APP_TYPE" to "background-queue" 5 | 6 | Scenario: An error report contains the configured app type when running a go app 7 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 8 | When I start the service "app" 9 | And I run "HandledErrorScenario" 10 | And I wait to receive an error 11 | And the event "app.type" equals "background-queue" 12 | 13 | Scenario: An session report contains the configured app type when running a go app 14 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 15 | When I start the service "app" 16 | And I run "SendSessionScenario" 17 | And I wait to receive 2 sessions 18 | And the session payload field "app.type" equals "background-queue" 19 | -------------------------------------------------------------------------------- /features/appversion.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring app version 2 | 3 | Background: 4 | And I set environment variable "BUGSNAG_APP_VERSION" to "3.1.2" 5 | 6 | Scenario: An error report contains the configured app type when running a go app 7 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 8 | When I start the service "app" 9 | And I run "HandledErrorScenario" 10 | And I wait to receive an error 11 | And the event "app.version" equals "3.1.2" 12 | 13 | Scenario: A session report contains the configured app type when running a go app 14 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 15 | When I start the service "app" 16 | And I run "SendSessionScenario" 17 | And I wait to receive 2 sessions 18 | And the session payload field "app.version" equals "3.1.2" -------------------------------------------------------------------------------- /features/autonotify.feature: -------------------------------------------------------------------------------- 1 | Feature: Using auto notify 2 | 3 | Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered 4 | When I start the service "app" 5 | And I run "AutonotifyPanicScenario" 6 | And I wait to receive 2 errors 7 | And the exception "errorClass" equals "*errors.errorString" 8 | And the exception "message" equals "Go routine killed with auto notify" 9 | And I discard the oldest error 10 | And the exception "errorClass" equals "panic" 11 | And the exception "message" equals "Go routine killed with auto notify [recovered]" -------------------------------------------------------------------------------- /features/fixtures/app/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION 2 | FROM golang:${GO_VERSION}-alpine 3 | 4 | RUN apk update && apk upgrade && apk add git bash build-base 5 | 6 | ENV GOPATH /app 7 | ENV GO111MODULE="on" 8 | 9 | COPY features /app/src/features 10 | COPY v2 /app/src/github.com/bugsnag/bugsnag-go/v2 11 | WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2 12 | 13 | # Ensure subsequent steps are re-run if the GO_VERSION variable changes 14 | ARG GO_VERSION 15 | 16 | # Get bugsnag dependencies using a conditional call to run go get or go install based on the go version 17 | RUN if [[ $(echo -e "1.11\n$GO_VERSION\n1.16" | sort -V | head -2 | tail -1) == "$GO_VERSION" ]]; then \ 18 | echo "Version is between 1.11 and 1.16, running go get"; \ 19 | go get ./...; \ 20 | else \ 21 | echo "Version is greater than 1.16, running go install"; \ 22 | go install ./...; \ 23 | fi 24 | 25 | WORKDIR /app/src/features/fixtures/app 26 | 27 | # Create app module - avoid locking bugsnag dep by not checking it in 28 | # Skip on old versions of Go which pre-date modules 29 | RUN go mod init && go mod tidy && \ 30 | echo "replace github.com/bugsnag/bugsnag-go/v2 => /app/src/github.com/bugsnag/bugsnag-go/v2" >> go.mod && \ 31 | go mod tidy 32 | 33 | RUN chmod +x run.sh 34 | CMD ["/app/src/features/fixtures/app/run.sh"] -------------------------------------------------------------------------------- /features/fixtures/app/autoconfig_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | bugsnag "github.com/bugsnag/bugsnag-go/v2" 7 | ) 8 | 9 | func AutoconfigPanicScenario(command Command) func() { 10 | scenarioFunc := func() { 11 | panic("PANIQ!") 12 | } 13 | return scenarioFunc 14 | } 15 | 16 | func AutoconfigHandledScenario(command Command) func() { 17 | scenarioFunc := func() { 18 | bugsnag.Notify(fmt.Errorf("gone awry!")) 19 | } 20 | return scenarioFunc 21 | } 22 | 23 | func AutoconfigMetadataScenario(command Command) func() { 24 | scenarioFunc := func() { 25 | bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { 26 | event.MetaData.Add("fruit", "Tomato", "beefsteak") 27 | event.MetaData.Add("snacks", "Carrot", "4") 28 | return nil 29 | }) 30 | bugsnag.Notify(fmt.Errorf("gone awry!")) 31 | } 32 | return scenarioFunc 33 | } -------------------------------------------------------------------------------- /features/fixtures/app/command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | const DEFAULT_MAZE_ADDRESS = "http://localhost:9339" 11 | 12 | type Command struct { 13 | Action string `json:"action,omitempty"` 14 | ScenarioName string `json:"scenario_name,omitempty"` 15 | APIKey string `json:"api_key,omitempty"` 16 | NotifyEndpoint string `json:"notify_endpoint,omitempty"` 17 | SessionsEndpoint string `json:"sessions_endpoint,omitempty"` 18 | UUID string `json:"uuid,omitempty"` 19 | RunUUID string `json:"run_uuid,omitempty"` 20 | } 21 | 22 | func GetCommand(mazeAddress string) Command { 23 | var command Command 24 | mazeURL := fmt.Sprintf("%+v/command", mazeAddress) 25 | client := http.Client{Timeout: 2 * time.Second} 26 | res, err := client.Get(mazeURL) 27 | if err != nil { 28 | fmt.Printf("[Bugsnag] Error while receiving command: %+v\n", err) 29 | return command 30 | } 31 | 32 | if res != nil { 33 | err = json.NewDecoder(res.Body).Decode(&command) 34 | res.Body.Close() 35 | if err != nil { 36 | fmt.Printf("[Bugsnag] Error while decoding command: %+v\n", err) 37 | return command 38 | } 39 | } 40 | 41 | return command 42 | } 43 | -------------------------------------------------------------------------------- /features/fixtures/app/handled_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/bugsnag/bugsnag-go/v2" 10 | ) 11 | 12 | func HandledErrorScenario(command Command) func() { 13 | scenarioFunc := func() { 14 | if _, err := os.Open("nonexistent_file.txt"); err != nil { 15 | if errClass := os.Getenv("ERROR_CLASS"); errClass != "" { 16 | bugsnag.Notify(err, bugsnag.ErrorClass{Name: errClass}) 17 | } else { 18 | bugsnag.Notify(err) 19 | } 20 | } 21 | } 22 | return scenarioFunc 23 | } 24 | 25 | func MultipleHandledErrorsScenario(command Command) func() { 26 | //Make the order of the below predictable 27 | bugsnag.Configure(bugsnag.Configuration{Synchronous: true}) 28 | 29 | scenarioFunc := func() { 30 | ctx := bugsnag.StartSession(context.Background()) 31 | bugsnag.Notify(fmt.Errorf("oops"), ctx) 32 | bugsnag.Notify(fmt.Errorf("oops"), ctx) 33 | } 34 | return scenarioFunc 35 | } 36 | 37 | func NestedHandledErrorScenario(command Command) func() { 38 | scenarioFunc := func() { 39 | if err := Login("token " + os.Getenv("API_KEY")); err != nil { 40 | bugsnag.Notify(NewCustomErr("terminate process", err)) 41 | } else { 42 | i := len(os.Getenv("API_KEY")) 43 | // Some nonsense to avoid inlining checkValue 44 | if val, err := CheckValue(i); err != nil { 45 | fmt.Printf("err: %v, val: %d\n", err, val) 46 | } 47 | if val, err := CheckValue(i - 46); err != nil { 48 | fmt.Printf("err: %v, val: %d\n", err, val) 49 | } 50 | 51 | log.Fatalf("This test is broken - no error was generated.") 52 | } 53 | } 54 | return scenarioFunc 55 | } 56 | 57 | func HandledCallbackErrorScenario(command Command) func() { 58 | scenarioFunc := func() { 59 | bugsnag.Notify(fmt.Errorf("inadequent Prep Error"), func(event *bugsnag.Event) { 60 | event.Context = "nonfatal.go:14" 61 | event.Severity = bugsnag.SeverityInfo 62 | 63 | event.Stacktrace[1].File = ">insertion<" 64 | event.Stacktrace[1].LineNumber = 0 65 | }) 66 | } 67 | return scenarioFunc 68 | } 69 | 70 | func HandledToUnhandledScenario(command Command) func() { 71 | scenarioFunc := func() { 72 | bugsnag.Notify(fmt.Errorf("unknown event"), func(event *bugsnag.Event) { 73 | event.Unhandled = true 74 | event.Severity = bugsnag.SeverityError 75 | }) 76 | } 77 | return scenarioFunc 78 | } 79 | 80 | func OnBeforeNotifyScenario(command Command) func() { 81 | bugsnag.Configure(bugsnag.Configuration{Synchronous: true}) 82 | 83 | scenarioFunc := func() { 84 | bugsnag.OnBeforeNotify( 85 | func(event *bugsnag.Event, config *bugsnag.Configuration) error { 86 | if event.Message == "ignore this error" { 87 | return fmt.Errorf("not sending errors to ignore") 88 | } 89 | // continue notifying as normal 90 | if event.Message == "change error message" { 91 | event.Message = "error message was changed" 92 | } 93 | return nil 94 | }) 95 | bugsnag.Notify(fmt.Errorf("ignore this error")) 96 | bugsnag.Notify(fmt.Errorf("don't ignore this error")) 97 | bugsnag.Notify(fmt.Errorf("change error message")) 98 | } 99 | return scenarioFunc 100 | } 101 | -------------------------------------------------------------------------------- /features/fixtures/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/bugsnag/bugsnag-go/v2" 9 | ) 10 | 11 | var scenariosMap = map[string]func(Command) func(){ 12 | "UnhandledCrashScenario": UnhandledCrashScenario, 13 | "HandledErrorScenario": HandledErrorScenario, 14 | "MultipleUnhandledErrorsScenario": MultipleUnhandledErrorsScenario, 15 | "MultipleHandledErrorsScenario": MultipleHandledErrorsScenario, 16 | "NestedHandledErrorScenario": NestedHandledErrorScenario, 17 | "MetadataScenario": MetadataScenario, 18 | "FilteredMetadataScenario": FilteredMetadataScenario, 19 | "HandledCallbackErrorScenario": HandledCallbackErrorScenario, 20 | "SendSessionScenario": SendSessionScenario, 21 | "HandledToUnhandledScenario": HandledToUnhandledScenario, 22 | "SetUserScenario": SetUserScenario, 23 | "RecoverAfterPanicScenario": RecoverAfterPanicScenario, 24 | "AutonotifyPanicScenario": AutonotifyPanicScenario, 25 | "SessionAndErrorScenario": SessionAndErrorScenario, 26 | "OnBeforeNotifyScenario": OnBeforeNotifyScenario, 27 | "AutoconfigPanicScenario": AutoconfigPanicScenario, 28 | "AutoconfigHandledScenario": AutoconfigHandledScenario, 29 | "AutoconfigMetadataScenario": AutoconfigMetadataScenario, 30 | "HttpServerScenario": HttpServerScenario, 31 | } 32 | 33 | func main() { 34 | addr := os.Getenv("DEFAULT_MAZE_ADDRESS") 35 | if addr == "" { 36 | addr = DEFAULT_MAZE_ADDRESS 37 | } 38 | 39 | endpoints := bugsnag.Endpoints{ 40 | Notify: fmt.Sprintf("%+v/notify", addr), 41 | Sessions: fmt.Sprintf("%+v/sessions", addr), 42 | } 43 | // HAS TO RUN FIRST BECAUSE OF PANIC WRAP 44 | // https://github.com/bugsnag/panicwrap/blob/master/panicwrap.go#L177-L203 45 | bugsnag.Configure(bugsnag.Configuration{ 46 | APIKey: "166f5ad3590596f9aa8d601ea89af845", 47 | Endpoints: endpoints, 48 | }) 49 | // Increase publish rate for testing 50 | bugsnag.DefaultSessionPublishInterval = time.Millisecond * 50 51 | 52 | // Listening to the OS Signals 53 | ticker := time.NewTicker(1 * time.Second) 54 | for { 55 | select { 56 | case <-ticker.C: 57 | command := GetCommand(addr) 58 | fmt.Printf("[Bugsnag] Received command: %+v\n", command) 59 | if command.Action != "run-scenario" { 60 | continue 61 | } 62 | prepareScenarioFunc, ok := scenariosMap[command.ScenarioName] 63 | if ok { 64 | scenarioFunc := prepareScenarioFunc(command) 65 | scenarioFunc() 66 | time.Sleep(200 * time.Millisecond) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /features/fixtures/app/metadata_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bugsnag/bugsnag-go/v2" 7 | ) 8 | 9 | func MetadataScenario(command Command) func() { 10 | scenarioFunc := func() { 11 | customerData := map[string]string{"Name": "Joe Bloggs", "Age": "21"} 12 | bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ 13 | "Scheme": { 14 | "Customer": customerData, 15 | "Level": "Blue", 16 | }, 17 | }) 18 | } 19 | return scenarioFunc 20 | } 21 | 22 | func FilteredMetadataScenario(command Command) func() { 23 | scenarioFunc := func() { 24 | bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ 25 | "Account": { 26 | "Name": "Company XYZ", 27 | "Price(dollars)": "1 Million", 28 | }, 29 | }) 30 | } 31 | return scenarioFunc 32 | } 33 | -------------------------------------------------------------------------------- /features/fixtures/app/nethttp_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/bugsnag/bugsnag-go/v2" 12 | ) 13 | 14 | func HttpServerScenario(Command) func() { 15 | scenarioFunc := func() { 16 | http.HandleFunc("/handled", handledError) 17 | http.HandleFunc("/autonotify-then-recover", unhandledCrash) 18 | http.HandleFunc("/session", session) 19 | http.HandleFunc("/autonotify", autonotify) 20 | http.HandleFunc("/onbeforenotify", onBeforeNotify) 21 | http.HandleFunc("/recover", dontdie) 22 | http.HandleFunc("/user", user) 23 | 24 | http.ListenAndServe(":4512", recoverWrap(bugsnag.Handler(nil))) 25 | } 26 | 27 | return scenarioFunc 28 | } 29 | 30 | // Simple wrapper to send internal server error on panics 31 | func recoverWrap(h http.Handler) http.Handler { 32 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | defer func() { 34 | r := recover() 35 | if r != nil { 36 | http.Error(w, "", http.StatusInternalServerError) 37 | } 38 | }() 39 | h.ServeHTTP(w, r) 40 | }) 41 | } 42 | 43 | func handledError(w http.ResponseWriter, r *http.Request) { 44 | if _, err := os.Open("nonexistent_file.txt"); err != nil { 45 | if errClass := os.Getenv("ERROR_CLASS"); errClass != "" { 46 | bugsnag.Notify(err, r.Context(), bugsnag.ErrorClass{Name: errClass}) 47 | } else { 48 | bugsnag.Notify(err, r.Context()) 49 | } 50 | } 51 | } 52 | 53 | func unhandledCrash(w http.ResponseWriter, r *http.Request) { 54 | // Invalid type assertion, will panic 55 | func(a interface{}) string { 56 | return a.(string) 57 | }(struct{}{}) 58 | } 59 | 60 | func session(w http.ResponseWriter, r *http.Request) { 61 | log.Println("single session") 62 | } 63 | 64 | func autonotify(w http.ResponseWriter, r *http.Request) { 65 | go func(ctx context.Context) { 66 | defer func() { recover() }() 67 | defer bugsnag.AutoNotify(ctx) 68 | panic("Go routine killed with auto notify") 69 | }(r.Context()) 70 | } 71 | 72 | func onBeforeNotify(w http.ResponseWriter, r *http.Request) { 73 | bugsnag.OnBeforeNotify( 74 | func(event *bugsnag.Event, config *bugsnag.Configuration) error { 75 | if event.Message == "Ignore this error" { 76 | return fmt.Errorf("not sending errors to ignore") 77 | } 78 | // continue notifying as normal 79 | if event.Message == "Change error message" { 80 | event.Message = "Error message was changed" 81 | } 82 | return nil 83 | }) 84 | bugsnag.Notify(fmt.Errorf("Ignore this error")) 85 | time.Sleep(100 * time.Millisecond) 86 | bugsnag.Notify(fmt.Errorf("Don't ignore this error")) 87 | time.Sleep(100 * time.Millisecond) 88 | bugsnag.Notify(fmt.Errorf("Change error message")) 89 | time.Sleep(100 * time.Millisecond) 90 | } 91 | 92 | func dontdie(w http.ResponseWriter, r *http.Request) { 93 | defer bugsnag.Recover(r.Context()) 94 | panic("Request killed but recovered") 95 | } 96 | 97 | func user(w http.ResponseWriter, r *http.Request) { 98 | bugsnag.Notify(fmt.Errorf("oops"), r.Context(), bugsnag.User{ 99 | Id: "test-user-id", 100 | Name: "test-user-name", 101 | Email: "test-user-email", 102 | }) 103 | } -------------------------------------------------------------------------------- /features/fixtures/app/panic_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bugsnag/bugsnag-go/v2" 5 | ) 6 | 7 | func AutonotifyPanicScenario(command Command) func() { 8 | scenarioFunc := func() { 9 | defer bugsnag.AutoNotify() 10 | panic("Go routine killed with auto notify") 11 | } 12 | 13 | return scenarioFunc 14 | } 15 | 16 | func RecoverAfterPanicScenario(command Command) func() { 17 | scenarioFunc := func() { 18 | defer bugsnag.Recover() 19 | panic("Go routine killed but recovered") 20 | } 21 | return scenarioFunc 22 | } -------------------------------------------------------------------------------- /features/fixtures/app/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # SIGTERM or SIGINT trapped (likely SIGTERM from docker), pass it onto app 4 | # process 5 | function _term_or_init { 6 | kill -TERM "$APP_PID" 2>/dev/null 7 | wait $APP_PID 8 | } 9 | 10 | # The bugsnag notifier monitor process needs at least 300ms, in order to ensure 11 | # that it can send its notify 12 | function _exit { 13 | sleep 1 14 | } 15 | 16 | trap _term_or_init SIGTERM SIGINT 17 | trap _exit EXIT 18 | 19 | PROC="${@:1}" 20 | $PROC & 21 | 22 | # Wait on the app process to ensure that this script is able to trap the SIGTERM 23 | # signal 24 | APP_PID=$! 25 | wait $APP_PID 26 | 27 | go run . -------------------------------------------------------------------------------- /features/fixtures/app/session_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/bugsnag/bugsnag-go/v2" 8 | ) 9 | 10 | func SendSessionScenario(command Command) func() { 11 | scenarioFunc := func() { 12 | bugsnag.StartSession(context.Background()) 13 | } 14 | return scenarioFunc 15 | } 16 | 17 | func SessionAndErrorScenario(command Command) func() { 18 | scenarioFunc := func() { 19 | ctx := bugsnag.StartSession(context.Background()) 20 | bugsnag.Notify(fmt.Errorf("oops"), ctx) 21 | } 22 | return scenarioFunc 23 | } -------------------------------------------------------------------------------- /features/fixtures/app/unhandled_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/bugsnag/bugsnag-go/v2" 8 | ) 9 | 10 | //go:noinline 11 | func UnhandledCrashScenario(command Command) func() { 12 | scenarioFunc := func() { 13 | fmt.Printf("Calling panic\n") 14 | // Invalid type assertion, will panic 15 | func(a interface{}) string { 16 | return a.(string) 17 | }(struct{}{}) 18 | } 19 | return scenarioFunc 20 | } 21 | 22 | func MultipleUnhandledErrorsScenario(command Command) func() { 23 | scenarioFunc := func() { 24 | //Make the order of the below predictable 25 | notifier := bugsnag.New(bugsnag.Configuration{Synchronous: true}) 26 | notifier.FlushSessionsOnRepanic(false) 27 | 28 | ctx := bugsnag.StartSession(context.Background()) 29 | defer func() { recover() }() 30 | defer notifier.AutoNotify(ctx) 31 | defer notifier.AutoNotify(ctx) 32 | panic("oops") 33 | } 34 | return scenarioFunc 35 | } -------------------------------------------------------------------------------- /features/fixtures/app/user_scenario.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bugsnag/bugsnag-go/v2" 7 | ) 8 | 9 | func SetUserScenario(command Command) func() { 10 | scenarioFunc := func() { 11 | bugsnag.Notify(fmt.Errorf("oops"), bugsnag.User{ 12 | Id: "test-user-id", 13 | Name: "test-user-name", 14 | Email: "test-user-email", 15 | }) 16 | } 17 | 18 | return scenarioFunc 19 | } -------------------------------------------------------------------------------- /features/fixtures/app/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | type CustomErr struct { 9 | msg string 10 | cause error 11 | callers []uintptr 12 | } 13 | 14 | func NewCustomErr(msg string, cause error) error { 15 | callers := make([]uintptr, 8) 16 | runtime.Callers(2, callers) 17 | return CustomErr{ 18 | msg: msg, 19 | cause: cause, 20 | callers: callers, 21 | } 22 | } 23 | 24 | func (err CustomErr) Error() string { 25 | return err.msg 26 | } 27 | 28 | func (err CustomErr) Unwrap() error { 29 | return err.cause 30 | } 31 | 32 | func (err CustomErr) Callers() []uintptr { 33 | return err.callers 34 | } 35 | 36 | func Login(token string) error { 37 | val, err := CheckValue(len(token) * -1) 38 | if err != nil { 39 | return NewCustomErr("login failed", err) 40 | } 41 | fmt.Printf("val: %d\n", val) 42 | return nil 43 | } 44 | 45 | func CheckValue(i int) (int, error) { 46 | if i < 0 { 47 | return 0, NewCustomErr("invalid token", nil) 48 | } else if i%2 == 0 { 49 | return i / 2, nil 50 | } else if i < 9 { 51 | return i * 3, nil 52 | } 53 | 54 | return i * 4, nil 55 | } -------------------------------------------------------------------------------- /features/fixtures/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: ../../ 5 | dockerfile: ./features/fixtures/app/Dockerfile 6 | args: 7 | - GO_VERSION 8 | ports: 9 | - "4512:4512" 10 | environment: 11 | - DEFAULT_MAZE_ADDRESS 12 | - ERROR_CLASS 13 | - BUGSNAG_API_KEY 14 | - BUGSNAG_APP_TYPE 15 | - BUGSNAG_APP_VERSION 16 | - BUGSNAG_AUTO_CAPTURE_SESSIONS 17 | - BUGSNAG_DISABLE_PANIC_HANDLER 18 | - BUGSNAG_HOSTNAME 19 | - BUGSNAG_NOTIFY_ENDPOINT 20 | - BUGSNAG_NOTIFY_RELEASE_STAGES 21 | - BUGSNAG_PARAMS_FILTERS 22 | - BUGSNAG_PROJECT_PACKAGES 23 | - BUGSNAG_RELEASE_STAGE 24 | - BUGSNAG_SESSIONS_ENDPOINT 25 | - BUGSNAG_SOURCE_ROOT 26 | - BUGSNAG_SYNCHRONOUS 27 | - BUGSNAG_METADATA_Carrot 28 | - BUGSNAG_METADATA_device_instance 29 | - BUGSNAG_METADATA_device_runtime_level 30 | - BUGSNAG_METADATA_framework_version 31 | - BUGSNAG_METADATA_fruit_Tomato 32 | - BUGSNAG_METADATA_snacks_Carrot 33 | restart: "no" 34 | 35 | gin: 36 | build: 37 | context: . 38 | dockerfile: gin/Dockerfile 39 | args: 40 | - GO_VERSION 41 | - GIN_VERSION 42 | ports: 43 | - "4511:4511" 44 | environment: 45 | - API_KEY 46 | - ERROR_CLASS 47 | - BUGSNAG_ENDPOINT 48 | - APP_VERSION 49 | - APP_TYPE 50 | - HOSTNAME 51 | - NOTIFY_RELEASE_STAGES 52 | - RELEASE_STAGE 53 | - PARAMS_FILTERS 54 | - AUTO_CAPTURE_SESSIONS 55 | - SYNCHRONOUS 56 | - SERVER_PORT 57 | - BUGSNAG_SOURCE_ROOT 58 | - BUGSNAG_PROJECT_PACKAGES 59 | restart: "no" 60 | command: go run main.go 61 | 62 | martini: 63 | build: 64 | context: . 65 | dockerfile: martini/Dockerfile 66 | args: 67 | - GO_VERSION 68 | ports: 69 | - "4513:4513" 70 | environment: 71 | - API_KEY 72 | - ERROR_CLASS 73 | - BUGSNAG_ENDPOINT 74 | - APP_VERSION 75 | - APP_TYPE 76 | - HOSTNAME 77 | - NOTIFY_RELEASE_STAGES 78 | - RELEASE_STAGE 79 | - PARAMS_FILTERS 80 | - AUTO_CAPTURE_SESSIONS 81 | - SYNCHRONOUS 82 | - SERVER_PORT 83 | - BUGSNAG_SOURCE_ROOT 84 | - BUGSNAG_PROJECT_PACKAGES 85 | restart: "no" 86 | command: go run main.go 87 | 88 | negroni: 89 | build: 90 | context: . 91 | dockerfile: negroni/Dockerfile 92 | args: 93 | - GO_VERSION 94 | - NEGRONI_VERSION 95 | ports: 96 | - "4514:4514" 97 | environment: 98 | - API_KEY 99 | - ERROR_CLASS 100 | - BUGSNAG_ENDPOINT 101 | - APP_VERSION 102 | - APP_TYPE 103 | - HOSTNAME 104 | - NOTIFY_RELEASE_STAGES 105 | - RELEASE_STAGE 106 | - PARAMS_FILTERS 107 | - AUTO_CAPTURE_SESSIONS 108 | - SYNCHRONOUS 109 | - SERVER_PORT 110 | - BUGSNAG_SOURCE_ROOT 111 | - BUGSNAG_PROJECT_PACKAGES 112 | restart: "no" 113 | command: go run main.go 114 | 115 | revel: 116 | build: 117 | context: . 118 | dockerfile: revel/Dockerfile 119 | args: 120 | - GO_VERSION 121 | - REVEL_VERSION 122 | - REVEL_CMD_VERSION 123 | ports: 124 | - "4515:4515" 125 | environment: 126 | - API_KEY 127 | - ERROR_CLASS 128 | - BUGSNAG_ENDPOINT 129 | - APP_VERSION 130 | - APP_TYPE 131 | - HOSTNAME 132 | - NOTIFY_RELEASE_STAGES 133 | - RELEASE_STAGE 134 | - PARAMS_FILTERS 135 | - AUTO_CAPTURE_SESSIONS 136 | - SYNCHRONOUS 137 | - SERVER_PORT 138 | - USE_PROPERTIES_FILE_CONFIG 139 | - BUGSNAG_SOURCE_ROOT 140 | - BUGSNAG_PROJECT_PACKAGES 141 | restart: "no" 142 | command: ./test/run.sh 143 | -------------------------------------------------------------------------------- /features/handled.feature: -------------------------------------------------------------------------------- 1 | Feature: Plain handled errors 2 | 3 | Background: 4 | Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" 5 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 6 | 7 | Scenario: A handled error sends a report 8 | When I start the service "app" 9 | And I run "HandledErrorScenario" 10 | And I wait to receive an error 11 | And the event "unhandled" is false 12 | And the event "severity" equals "warning" 13 | And the event "severityReason.type" equals "handledError" 14 | And the exception "errorClass" matches "\*os.PathError|\*fs.PathError" 15 | And the "file" of stack frame 0 equals "handled_scenario.go" 16 | 17 | Scenario: A handled error sends a report with a custom name 18 | Given I set environment variable "ERROR_CLASS" to "MyCustomErrorClass" 19 | When I start the service "app" 20 | And I run "HandledErrorScenario" 21 | And I wait to receive an error 22 | And the event "unhandled" is false 23 | And the event "severity" equals "warning" 24 | And the event "severityReason.type" equals "handledError" 25 | And the exception "errorClass" equals "MyCustomErrorClass" 26 | And the "file" of stack frame 0 equals "handled_scenario.go" 27 | 28 | Scenario: Sending an event using a callback to modify report contents 29 | When I start the service "app" 30 | And I run "HandledCallbackErrorScenario" 31 | And I wait to receive an error 32 | And the event "unhandled" is false 33 | And the event "severity" equals "info" 34 | And the event "severityReason.type" equals "userCallbackSetSeverity" 35 | And the event "context" equals "nonfatal.go:14" 36 | And the "file" of stack frame 0 equals "handled_scenario.go" 37 | And the "lineNumber" of stack frame 0 equals 59 38 | And the "file" of stack frame 1 equals ">insertion<" 39 | And the "lineNumber" of stack frame 1 equals 0 40 | 41 | Scenario: Marking an error as unhandled in a callback 42 | When I start the service "app" 43 | And I run "HandledToUnhandledScenario" 44 | And I wait to receive an error 45 | And the event "unhandled" is true 46 | And the event "severity" equals "error" 47 | And the event "severityReason.type" equals "userCallbackSetSeverity" 48 | And the event "severityReason.unhandledOverridden" is true 49 | And the "file" of stack frame 0 equals "handled_scenario.go" 50 | And the "lineNumber" of stack frame 0 equals 72 51 | 52 | Scenario: Unwrapping the causes of a handled error 53 | When I start the service "app" 54 | And I run "NestedHandledErrorScenario" 55 | And I wait to receive an error 56 | And the event "unhandled" is false 57 | And the event "severity" equals "warning" 58 | And the event "exceptions.0.message" equals "terminate process" 59 | And the "lineNumber" of stack frame 0 equals 40 60 | And the "file" of stack frame 0 equals "handled_scenario.go" 61 | And the "method" of stack frame 0 equals "NestedHandledErrorScenario.func1" 62 | And the event "exceptions.1.message" equals "login failed" 63 | And the event "exceptions.1.stacktrace.0.file" equals "utils.go" 64 | And the event "exceptions.1.stacktrace.0.lineNumber" equals 39 65 | And the event "exceptions.2.message" equals "invalid token" 66 | And the event "exceptions.2.stacktrace.0.file" equals "utils.go" 67 | And the event "exceptions.2.stacktrace.0.lineNumber" equals 47 -------------------------------------------------------------------------------- /features/hostname.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring hostname 2 | 3 | Scenario: An error report contains the configured hostname 4 | Given I set environment variable "BUGSNAG_HOSTNAME" to "server-1a" 5 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 6 | When I start the service "app" 7 | And I run "HandledErrorScenario" 8 | And I wait to receive an error 9 | And the event "device.hostname" equals "server-1a" 10 | 11 | Scenario: A session report contains the configured hostname 12 | Given I set environment variable "BUGSNAG_HOSTNAME" to "server-1a" 13 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 14 | When I start the service "app" 15 | And I run "SendSessionScenario" 16 | And I wait to receive 2 sessions 17 | And the session payload field "device.hostname" equals "server-1a" -------------------------------------------------------------------------------- /features/metadata.feature: -------------------------------------------------------------------------------- 1 | Feature: Sending meta data 2 | 3 | Scenario: An error report contains custom meta data 4 | When I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 5 | And I start the service "app" 6 | And I run "MetadataScenario" 7 | And I wait to receive an error 8 | And the event "metaData.Scheme.Customer.Name" equals "Joe Bloggs" 9 | And the event "metaData.Scheme.Customer.Age" equals "21" 10 | And the event "metaData.Scheme.Level" equals "Blue" -------------------------------------------------------------------------------- /features/multieventsession.feature: -------------------------------------------------------------------------------- 1 | Feature: Reporting multiple handled and unhandled errors in the same session 2 | 3 | Background: 4 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 5 | 6 | Scenario: Handled errors know about previous reported handled errors 7 | When I start the service "app" 8 | And I run "MultipleHandledErrorsScenario" 9 | And I wait to receive 2 errors 10 | And the event handled sessions count equals 1 11 | And I discard the oldest error 12 | And the event handled sessions count equals 2 13 | 14 | Scenario: Unhandled errors know about previous reported handled errors 15 | When I start the service "app" 16 | And I run "MultipleUnhandledErrorsScenario" 17 | And I wait to receive 2 errors 18 | And the event unhandled sessions count equals 1 19 | And I discard the oldest error 20 | And the event unhandled sessions count equals 2 -------------------------------------------------------------------------------- /features/net-http/appversion.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring app version 2 | 3 | Background: 4 | And I set environment variable "BUGSNAG_APP_VERSION" to "3.1.2" 5 | 6 | Scenario: A error report contains the configured app type when using a net http app 7 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 8 | When I start the service "app" 9 | And I run "HttpServerScenario" 10 | And I wait for the host "localhost" to open port "4512" 11 | And I open the URL "http://localhost:4512/handled" 12 | And I wait to receive an error 13 | And I should receive no sessions 14 | And the event "app.version" equals "3.1.2" 15 | 16 | Scenario: A session report contains the configured app type when using a net http app 17 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 18 | When I start the service "app" 19 | And I run "HttpServerScenario" 20 | And I wait for the host "localhost" to open port "4512" 21 | And I open the URL "http://localhost:4512/session" 22 | And I wait to receive a session 23 | And the session payload field "app.version" equals "3.1.2" 24 | -------------------------------------------------------------------------------- /features/net-http/autonotify.feature: -------------------------------------------------------------------------------- 1 | Feature: Using auto notify 2 | 3 | Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered 4 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 5 | When I start the service "app" 6 | And I run "HttpServerScenario" 7 | And I wait for the host "localhost" to open port "4512" 8 | And I open the URL "http://localhost:4512/autonotify-then-recover" 9 | Then I wait to receive an error 10 | And the event "unhandled" is true 11 | And the exception "errorClass" equals "*runtime.TypeAssertionError" 12 | And the exception "message" matches "interface conversion: interface ({} )?is struct {}, not string" 13 | 14 | Scenario: An error report is sent when a go routine crashes which is reported through auto notify 15 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 16 | When I start the service "app" 17 | And I run "HttpServerScenario" 18 | And I wait for the host "localhost" to open port "4512" 19 | And I open the URL "http://localhost:4512/autonotify" 20 | Then I wait to receive an error 21 | And the event "unhandled" is true 22 | And the exception "errorClass" equals "*errors.errorString" 23 | And the exception "message" equals "Go routine killed with auto notify" -------------------------------------------------------------------------------- /features/net-http/handled.feature: -------------------------------------------------------------------------------- 1 | Feature: Handled errors 2 | 3 | Background: 4 | Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" 5 | 6 | Scenario: A handled error sends a report 7 | When I start the service "app" 8 | And I run "HttpServerScenario" 9 | And I wait for the host "localhost" to open port "4512" 10 | And I open the URL "http://localhost:4512/handled" 11 | Then I wait to receive an error 12 | And the event "unhandled" is false 13 | And the event "severity" equals "warning" 14 | And the event "severityReason.type" equals "handledError" 15 | And the exception "errorClass" matches "\*os.PathError|\*fs.PathError" 16 | And the "file" of stack frame 0 equals "nethttp_scenario.go" 17 | 18 | Scenario: A handled error sends a report with a custom name 19 | Given I set environment variable "ERROR_CLASS" to "MyCustomErrorClass" 20 | When I start the service "app" 21 | And I run "HttpServerScenario" 22 | And I wait for the host "localhost" to open port "4512" 23 | And I open the URL "http://localhost:4512/handled" 24 | Then I wait to receive an error 25 | And the event "unhandled" is false 26 | And the event "severity" equals "warning" 27 | And the event "severityReason.type" equals "handledError" 28 | And the exception "errorClass" equals "MyCustomErrorClass" 29 | And the "file" of stack frame 0 equals "nethttp_scenario.go" -------------------------------------------------------------------------------- /features/net-http/onbeforenotify.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring on before notify 2 | 3 | Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another 4 | When I start the service "app" 5 | And I run "HttpServerScenario" 6 | And I wait for the host "localhost" to open port "4512" 7 | And I open the URL "http://localhost:4512/onbeforenotify" 8 | Then I wait to receive 2 errors 9 | And the exception "message" equals "Don't ignore this error" 10 | And I discard the oldest error 11 | And the exception "message" equals "Error message was changed" -------------------------------------------------------------------------------- /features/net-http/recover.feature: -------------------------------------------------------------------------------- 1 | Feature: Using recover 2 | 3 | Scenario: An error report is sent when request crashes but is recovered 4 | When I start the service "app" 5 | And I run "HttpServerScenario" 6 | And I wait for the host "localhost" to open port "4512" 7 | And I open the URL "http://localhost:4512/recover" 8 | Then I wait to receive an error 9 | And the exception "errorClass" equals "*errors.errorString" 10 | And the exception "message" equals "Request killed but recovered" -------------------------------------------------------------------------------- /features/net-http/releasestage.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring release stage 2 | 3 | Background: 4 | Given I set environment variable "BUGSNAG_RELEASE_STAGE" to "my-stage" 5 | 6 | Scenario: An error report is sent with configured release stage 7 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 8 | When I start the service "app" 9 | And I run "HttpServerScenario" 10 | And I wait for the host "localhost" to open port "4512" 11 | And I open the URL "http://localhost:4512/handled" 12 | Then I wait to receive an error 13 | And the event "app.releaseStage" equals "my-stage" 14 | 15 | Scenario: A session report contains the configured app type 16 | Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 17 | When I start the service "app" 18 | And I run "HttpServerScenario" 19 | And I wait for the host "localhost" to open port "4512" 20 | And I open the URL "http://localhost:4512/session" 21 | Then I wait to receive a session 22 | And the session payload field "app.releaseStage" equals "my-stage" -------------------------------------------------------------------------------- /features/net-http/request.feature: -------------------------------------------------------------------------------- 1 | Feature: Capturing request information automatically 2 | 3 | Scenario: An error report will automatically contain request information 4 | When I start the service "app" 5 | And I run "HttpServerScenario" 6 | And I wait for the host "localhost" to open port "4512" 7 | And I open the URL "http://localhost:4512/handled" 8 | Then I wait to receive an error 9 | And the event "request.clientIp" is not null 10 | And the event "request.headers.User-Agent" equals "Ruby" 11 | And the event "request.httpMethod" equals "GET" 12 | And the event "request.url" ends with "/handled" 13 | And the event "request.url" starts with "http://" -------------------------------------------------------------------------------- /features/net-http/user.feature: -------------------------------------------------------------------------------- 1 | Feature: Sending user data 2 | 3 | Scenario: An error report contains custom user data 4 | When I start the service "app" 5 | And I run "HttpServerScenario" 6 | And I wait for the host "localhost" to open port "4512" 7 | And I open the URL "http://localhost:4512/user" 8 | Then I wait to receive an error 9 | And the event "user.id" equals "test-user-id" 10 | And the event "user.name" equals "test-user-name" 11 | And the event "user.email" equals "test-user-email" -------------------------------------------------------------------------------- /features/onbeforenotify.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring on before notify 2 | 3 | Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another 4 | When I start the service "app" 5 | And I run "OnBeforeNotifyScenario" 6 | And I wait to receive 2 errors 7 | And the exception "message" equals "don't ignore this error" 8 | And I discard the oldest error 9 | And the exception "message" equals "error message was changed" -------------------------------------------------------------------------------- /features/paramfilters.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring param filters 2 | 3 | Scenario: An error report containing meta data is not filtered when the param filters are set but do not match 4 | Given I set environment variable "BUGSNAG_PARAMS_FILTERS" to "Name" 5 | When I start the service "app" 6 | And I run "FilteredMetadataScenario" 7 | And I wait to receive an error 8 | And the event "metaData.Account.Price(dollars)" equals "1 Million" 9 | 10 | Scenario: An error report containing meta data is filtered when the param filters are set and completely match 11 | Given I set environment variable "BUGSNAG_PARAMS_FILTERS" to "Price(dollars)" 12 | When I start the service "app" 13 | And I run "FilteredMetadataScenario" 14 | And I wait to receive an error 15 | And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" 16 | 17 | Scenario: An error report containing meta data is filtered when the param filters are set and partially match 18 | Given I set environment variable "BUGSNAG_PARAMS_FILTERS" to "Price" 19 | When I start the service "app" 20 | And I run "FilteredMetadataScenario" 21 | And I wait to receive an error 22 | And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" 23 | -------------------------------------------------------------------------------- /features/plain_features/panics.feature: -------------------------------------------------------------------------------- 1 | Feature: Panic handling 2 | 3 | Background: 4 | Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" 5 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 6 | 7 | Scenario: Capturing a panic 8 | When I start the service "app" 9 | And I run "UnhandledCrashScenario" 10 | And I wait to receive an error 11 | And the event "unhandled" is true 12 | And the event "severity" equals "error" 13 | And the event "severityReason.type" equals "unhandledPanic" 14 | And the exception "errorClass" equals "panic" 15 | And the exception "message" matches "^interface conversion: interface.*?is struct {}, not string" 16 | And the "file" of stack frame 0 equals "unhandled_scenario.go" 17 | And the "method" of stack frame 0 equals "UnhandledCrashScenario.func1.1" 18 | And the "file" of stack frame 1 equals "unhandled_scenario.go" 19 | And the "method" of stack frame 1 equals "UnhandledCrashScenario.func1" -------------------------------------------------------------------------------- /features/recover.feature: -------------------------------------------------------------------------------- 1 | Feature: Using recover 2 | 3 | Scenario: An error report is sent when a go routine crashes but recovers 4 | When I start the service "app" 5 | And I run "RecoverAfterPanicScenario" 6 | And I wait to receive an error 7 | And the exception "errorClass" equals "*errors.errorString" 8 | And the exception "message" equals "Go routine killed but recovered" -------------------------------------------------------------------------------- /features/releasestage.feature: -------------------------------------------------------------------------------- 1 | Feature: Configuring release stages and notify release stages 2 | 3 | Scenario: An error report is sent when release stage matches notify release stages 4 | Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" 5 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 6 | And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" 7 | When I start the service "app" 8 | And I run "HandledErrorScenario" 9 | And I wait to receive an error 10 | And the event "app.releaseStage" equals "stage2" 11 | 12 | Scenario: An error report is sent when no notify release stages are specified 13 | Given I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" 14 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 15 | When I start the service "app" 16 | And I run "HandledErrorScenario" 17 | And I wait to receive an error 18 | And the event "app.releaseStage" equals "stage2" 19 | 20 | Scenario: An error report is sent regardless of notify release stages if release stage is not set 21 | Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" 22 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 23 | When I start the service "app" 24 | And I run "HandledErrorScenario" 25 | And I wait to receive an error 26 | 27 | Scenario: An error report is not sent if the release stage does not match the notify release stages 28 | Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" 29 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" 30 | And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage4" 31 | When I start the service "app" 32 | And I run "HandledErrorScenario" 33 | And I should receive no errors 34 | 35 | Scenario: A session report is sent when release stage matches notify release stages 36 | Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" 37 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 38 | And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" 39 | When I start the service "app" 40 | And I run "SendSessionScenario" 41 | And I wait to receive 2 sessions 42 | And the session payload field "app.releaseStage" equals "stage2" 43 | 44 | Scenario: A session report is sent when no notify release stages are specified 45 | Given I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" 46 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 47 | When I start the service "app" 48 | And I run "SendSessionScenario" 49 | And I wait to receive 2 sessions 50 | And the session payload field "app.releaseStage" equals "stage2" 51 | 52 | Scenario: A session report is sent regardless of notify release stages if release stage is not set 53 | Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" 54 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 55 | When I start the service "app" 56 | And I run "SendSessionScenario" 57 | And I wait to receive 2 sessions 58 | 59 | Scenario: A session report is not sent if the release stage does not match the notify release stages 60 | Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" 61 | And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" 62 | And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage4" 63 | When I start the service "app" 64 | And I run "SendSessionScenario" 65 | And I should receive no sessions 66 | -------------------------------------------------------------------------------- /features/sessioncontext.feature: -------------------------------------------------------------------------------- 1 | Feature: Session data inside an error report using a session context 2 | 3 | Scenario: An error report contains a session count when part of a session 4 | When I start the service "app" 5 | And I run "SessionAndErrorScenario" 6 | Then I wait to receive 1 error 7 | # one session is created on start 8 | And I wait to receive 2 session 9 | And I discard the oldest session 10 | And the session payload has a valid sessions array -------------------------------------------------------------------------------- /features/steps/go_steps.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'os' 3 | 4 | When('I run {string}') do |scenario_name| 5 | execute_command 'run-scenario', scenario_name 6 | end 7 | 8 | When('I configure the base endpoint') do 9 | steps %( 10 | When I set environment variable "DEFAULT_MAZE_ADDRESS" to "http://#{local_ip}:9339" 11 | ) 12 | end 13 | 14 | Then('the event unhandled sessions count equals {int}') do |count| 15 | step "the error payload field \"events.0.session.events.unhandled\" equals #{count}" 16 | end 17 | 18 | Then('the event handled sessions count equals {int}') do |count| 19 | step "the error payload field \"events.0.session.events.handled\" equals #{count}" 20 | end 21 | 22 | 23 | def execute_command(action, scenario_name = '') 24 | address = $address ? $address : "#{local_ip}:9339" 25 | 26 | command = { 27 | action: action, 28 | scenario_name: scenario_name, 29 | notify_endpoint: "http://#{address}/notify", 30 | sessions_endpoint: "http://#{address}/sessions", 31 | api_key: $api_key, 32 | } 33 | 34 | $logger.debug("Queuing command: #{command}") 35 | Maze::Server.commands.add command 36 | 37 | # Ensure fixture has read the command 38 | count = 900 39 | sleep 0.1 until Maze::Server.commands.remaining.empty? || (count -= 1) < 1 40 | raise 'Test fixture did not GET /command' unless Maze::Server.commands.remaining.empty? 41 | end 42 | 43 | def local_ip 44 | if OS.mac? 45 | 'host.docker.internal' 46 | else 47 | ip_addr = `ifconfig | grep -Eo 'inet (addr:)?([0-9]*\\\.){3}[0-9]*' | grep -v '127.0.0.1'` 48 | ip_list = /((?:[0-9]*\.){3}[0-9]*)/.match(ip_addr) 49 | ip_list.captures.first 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | Maze.config.enforce_bugsnag_integrity = false 3 | $address = nil 4 | $api_key = "166f5ad3590596f9aa8d601ea89af845" 5 | steps %( 6 | When I configure the base endpoint 7 | ) 8 | end 9 | 10 | Maze.config.add_validator('error') do |validator| 11 | validator.validate_header('bugsnag-api-key') { |value| value.eql?($api_key) } 12 | validator.validate_header('content-type') { |value| value.eql?('application/json') } 13 | validator.validate_header('bugsnag-payload-version') { |value| value.eql?('4') } 14 | validator.validate_header('bugsnag-sent-at') { |value| Date.iso8601(value) } 15 | 16 | validator.element_has_value('notifier.name', 'Bugsnag Go') 17 | validator.each_element_exists(['notifier.url', 'notifier.version', 'events']) 18 | validator.each_event_contains_each(['severity', 'severityReason.type', 'unhandled', 'exceptions']) 19 | end 20 | 21 | Maze.config.add_validator('session') do |validator| 22 | validator.validate_header('bugsnag-api-key') { |value| value.eql?($api_key) } 23 | validator.validate_header('content-type') { |value| value.eql?('application/json') } 24 | validator.validate_header('bugsnag-payload-version') { |value| value.eql?('1.0') } 25 | validator.validate_header('bugsnag-sent-at') { |value| Date.iso8601(value) } 26 | 27 | validator.element_has_value('notifier.name', 'Bugsnag Go') 28 | validator.each_element_exists(['notifier.url', 'notifier.version', 'app', 'device']) 29 | end 30 | -------------------------------------------------------------------------------- /features/user.feature: -------------------------------------------------------------------------------- 1 | Feature: Sending user data 2 | 3 | Scenario: An error report contains custom user data 4 | When I start the service "app" 5 | And I run "SetUserScenario" 6 | And I wait to receive an error 7 | And the event "user.id" equals "test-user-id" 8 | And the event "user.name" equals "test-user-name" 9 | And the event "user.email" equals "test-user-email" -------------------------------------------------------------------------------- /gin/bugsnaggin.go: -------------------------------------------------------------------------------- 1 | package bugsnaggin 2 | 3 | import ( 4 | "github.com/bugsnag/bugsnag-go" 5 | "github.com/bugsnag/bugsnag-go/device" 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | const FrameworkName string = "Gin" 10 | 11 | // AutoNotify sends any panics to bugsnag, and then re-raises them. 12 | // You should use this after another middleware that 13 | // returns an error page to the client, for example gin.Recovery(). 14 | // The arguments can be any RawData to pass to Bugsnag, most usually 15 | // you'll pass a bugsnag.Configuration object. 16 | func AutoNotify(rawData ...interface{}) gin.HandlerFunc { 17 | // Configure bugsnag with the passed in configuration (for manual notifications) 18 | for _, datum := range rawData { 19 | if c, ok := datum.(bugsnag.Configuration); ok { 20 | bugsnag.Configure(c) 21 | } 22 | } 23 | 24 | device.AddVersion(FrameworkName, gin.Version) 25 | state := bugsnag.HandledState{ 26 | SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, 27 | OriginalSeverity: bugsnag.SeverityError, 28 | Unhandled: true, 29 | Framework: FrameworkName, 30 | } 31 | rawData = append(rawData, state) 32 | return func(c *gin.Context) { 33 | r := c.Copy().Request 34 | notifier := bugsnag.New(append(rawData, r)...) 35 | ctx := bugsnag.AttachRequestData(r.Context(), r) 36 | if notifier.Config.IsAutoCaptureSessions() { 37 | ctx = bugsnag.StartSession(ctx) 38 | } 39 | c.Request = r.WithContext(ctx) 40 | 41 | notifier.FlushSessionsOnRepanic(false) 42 | defer notifier.AutoNotify(ctx) 43 | c.Next() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /gin/gin_test.go: -------------------------------------------------------------------------------- 1 | package bugsnaggin_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/bitly/go-simplejson" 11 | "github.com/bugsnag/bugsnag-go" 12 | "github.com/bugsnag/bugsnag-go/gin" 13 | . "github.com/bugsnag/bugsnag-go/testutil" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func TestGin(t *testing.T) { 18 | ts, reports := Setup() 19 | defer ts.Close() 20 | 21 | g := gin.Default() 22 | 23 | userID := "1234abcd" 24 | g.Use(bugsnaggin.AutoNotify(bugsnag.Configuration{ 25 | APIKey: TestAPIKey, 26 | Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, 27 | }, bugsnag.User{Id: userID})) 28 | 29 | g.GET("/unhandled", performUnhandledCrash) 30 | g.GET("/handled", performHandledError) 31 | go g.Run(":9079") //This call blocks 32 | 33 | t.Run("AutoNotify", func(st *testing.T) { 34 | time.Sleep(1 * time.Second) 35 | _, err := http.Get("http://localhost:9079/unhandled") 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | report := <-reports 40 | r, _ := simplejson.NewJson(report) 41 | hostname, _ := os.Hostname() 42 | AssertPayload(st, r, fmt.Sprintf(` 43 | { 44 | "apiKey":"166f5ad3590596f9aa8d601ea89af845", 45 | "events":[ 46 | { 47 | "app":{ "releaseStage":"" }, 48 | "context":"/unhandled", 49 | "device":{ "hostname": "%s" }, 50 | "exceptions":[ 51 | { 52 | "errorClass":"*errors.errorString", 53 | "message":"you shouldn't have done that", 54 | "stacktrace":[] 55 | } 56 | ], 57 | "payloadVersion":"4", 58 | "severity":"error", 59 | "severityReason":{ "type":"unhandledErrorMiddleware" }, 60 | "unhandled":true, 61 | "request": { 62 | "url": "http://localhost:9079/unhandled", 63 | "httpMethod": "GET", 64 | "referer": "", 65 | "headers": { 66 | "Accept-Encoding": "gzip" 67 | } 68 | }, 69 | "user":{ "id": "%s" } 70 | } 71 | ], 72 | "notifier":{ 73 | "name":"Bugsnag Go", 74 | "url":"https://github.com/bugsnag/bugsnag-go", 75 | "version": "%s" 76 | } 77 | } 78 | `, hostname, userID, bugsnag.VERSION)) 79 | }) 80 | 81 | t.Run("Manual notify", func(st *testing.T) { 82 | _, err := http.Get("http://localhost:9079/handled") 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | report := <-reports 87 | r, _ := simplejson.NewJson(report) 88 | hostname, _ := os.Hostname() 89 | AssertPayload(st, r, fmt.Sprintf(` 90 | { 91 | "apiKey":"166f5ad3590596f9aa8d601ea89af845", 92 | "events":[ 93 | { 94 | "app":{ "releaseStage":"" }, 95 | "context":"/handled", 96 | "device":{ "hostname": "%s" }, 97 | "exceptions":[ 98 | { 99 | "errorClass":"*errors.errorString", 100 | "message":"Ooopsie", 101 | "stacktrace":[] 102 | } 103 | ], 104 | "payloadVersion":"4", 105 | "severity":"warning", 106 | "severityReason":{ "type":"handledError" }, 107 | "unhandled":false, 108 | "request": { 109 | "url": "http://localhost:9079/handled", 110 | "httpMethod": "GET", 111 | "referer": "", 112 | "headers": { 113 | "Accept-Encoding": "gzip" 114 | } 115 | }, 116 | "user":{ "id": "%s" } 117 | } 118 | ], 119 | "notifier":{ 120 | "name":"Bugsnag Go", 121 | "url":"https://github.com/bugsnag/bugsnag-go", 122 | "version": "%s" 123 | } 124 | } 125 | `, hostname, "987zyx", bugsnag.VERSION)) 126 | }) 127 | } 128 | 129 | func performHandledError(c *gin.Context) { 130 | ctx := c.Request.Context() 131 | bugsnag.Notify(fmt.Errorf("Ooopsie"), ctx, bugsnag.User{Id: "987zyx"}) 132 | } 133 | 134 | func performUnhandledCrash(c *gin.Context) { 135 | panic("you shouldn't have done that") 136 | } 137 | 138 | func crash(a interface{}) string { 139 | return a.(string) 140 | } 141 | -------------------------------------------------------------------------------- /headers/prefixed.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import "time" 4 | 5 | //PrefixedHeaders returns a map of Content-Type and the 'Bugsnag-' headers for 6 | //API key, payload version, and the time at which the request is being sent. 7 | func PrefixedHeaders(apiKey, payloadVersion string) map[string]string { 8 | return map[string]string{ 9 | "Content-Type": "application/json", 10 | "Bugsnag-Api-Key": apiKey, 11 | "Bugsnag-Payload-Version": payloadVersion, 12 | "Bugsnag-Sent-At": time.Now().UTC().Format(time.RFC3339), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /headers/prefixed_test.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const APIKey = "abcd1234abcd1234" 10 | const testPayloadVersion = "3" 11 | 12 | func TestConstantBugsnagPrefixedHeaders(t *testing.T) { 13 | headers := PrefixedHeaders(APIKey, testPayloadVersion) 14 | testCases := []struct { 15 | header string 16 | expected string 17 | }{ 18 | {header: "Content-Type", expected: "application/json"}, 19 | {header: "Bugsnag-Api-Key", expected: APIKey}, 20 | {header: "Bugsnag-Payload-Version", expected: testPayloadVersion}, 21 | } 22 | for _, tc := range testCases { 23 | t.Run(tc.header, func(st *testing.T) { 24 | if got := headers[tc.header]; got != tc.expected { 25 | t.Errorf("Expected headers to contain %s header %s but was %s", tc.header, tc.expected, got) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestTimeDependentBugsnagPrefixedHeaders(t *testing.T) { 32 | headers := PrefixedHeaders(APIKey, testPayloadVersion) 33 | sentAtString := headers["Bugsnag-Sent-At"] 34 | if !strings.HasSuffix(sentAtString, "Z") { 35 | t.Errorf("Error when setting Bugsnag-Sent-At header: %s, doesn't end with a Z", sentAtString) 36 | } 37 | sentAt, err := time.Parse(time.RFC3339, sentAtString) 38 | 39 | if err != nil { 40 | t.Errorf("Error when attempting to parse Bugsnag-Sent-At header: %s", sentAtString) 41 | } 42 | 43 | if now := time.Now(); now.Sub(sentAt) > time.Second || now.Sub(sentAt) < -time.Second { 44 | t.Errorf("Expected Bugsnag-Sent-At header approx. %s but was %s", now.UTC().Format(time.RFC3339), sentAtString) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /json_tags.go: -------------------------------------------------------------------------------- 1 | // The code is stripped from: 2 | // http://golang.org/src/pkg/encoding/json/tags.go?m=text 3 | 4 | package bugsnag 5 | 6 | import ( 7 | "strings" 8 | ) 9 | 10 | // tagOptions is the string following a comma in a struct field's "json" 11 | // tag, or the empty string. It does not include the leading comma. 12 | type tagOptions string 13 | 14 | // parseTag splits a struct field's json tag into its name and 15 | // comma-separated options. 16 | func parseTag(tag string) (string, tagOptions) { 17 | if idx := strings.Index(tag, ","); idx != -1 { 18 | return tag[:idx], tagOptions(tag[idx+1:]) 19 | } 20 | return tag, tagOptions("") 21 | } 22 | 23 | // Contains reports whether a comma-separated list of options 24 | // contains a particular substr flag. substr must be surrounded by a 25 | // string boundary or commas. 26 | func (o tagOptions) Contains(optionName string) bool { 27 | if len(o) == 0 { 28 | return false 29 | } 30 | s := string(o) 31 | for s != "" { 32 | var next string 33 | i := strings.Index(s, ",") 34 | if i >= 0 { 35 | s, next = s[:i], s[i+1:] 36 | } 37 | if s == optionName { 38 | return true 39 | } 40 | s = next 41 | } 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /martini/bugsnagmiddleware.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bugsnagmartini provides a martini middleware that sends 3 | panics to Bugsnag. You should use this middleware in combination 4 | with martini.Recover() if you want to send error messages to your 5 | clients: 6 | 7 | func main() { 8 | m := martini.New() 9 | // used to stop panics bubbling and return a 500 error. 10 | m.Use(martini.Recovery()) 11 | 12 | // used to send panics to Bugsnag. 13 | m.Use(bugsnagmartini.AutoNotify(bugsnag.Configuration{ 14 | APIKey: "YOUR_API_KEY_HERE", 15 | }) 16 | 17 | // ... 18 | } 19 | 20 | This middleware also makes bugsnag available to martini handlers via 21 | the context. 22 | 23 | func myHandler(w http.ResponseWriter, r *http.Request, bugsnag *bugsnag.Notifier) { 24 | // ... 25 | bugsnag.Notify(err) 26 | // ... 27 | } 28 | 29 | */ 30 | package bugsnagmartini 31 | 32 | import ( 33 | "net/http" 34 | 35 | "github.com/bugsnag/bugsnag-go" 36 | "github.com/bugsnag/bugsnag-go/device" 37 | "github.com/go-martini/martini" 38 | ) 39 | 40 | // FrameworkName is the name of the framework this middleware applies to 41 | const FrameworkName string = "Martini" 42 | 43 | // AutoNotify sends any panics to bugsnag, and then re-raises them. 44 | // You should use this after another middleware that 45 | // returns an error page to the client, for example martini.Recover(). 46 | // The arguments can be any RawData to pass to Bugsnag, most usually 47 | // you'll pass a bugsnag.Configuration object. 48 | func AutoNotify(rawData ...interface{}) martini.Handler { 49 | updateGlobalConfig(rawData...) 50 | 51 | device.AddVersion(FrameworkName, "v1.0") // The latest martini release from 2014 52 | state := bugsnag.HandledState{ 53 | SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, 54 | OriginalSeverity: bugsnag.SeverityError, 55 | Unhandled: true, 56 | Framework: FrameworkName, 57 | } 58 | 59 | return func(r *http.Request, c martini.Context) { 60 | // Martini's request-based context for dependency injection means that we can 61 | // attach request data to the notifier (one notifier <=> one request) itself. 62 | // This means that request data will show up when doing just notifier.Notify(err) 63 | notifier := bugsnag.New(append(rawData, r, state)...) 64 | 65 | // In case users use bugsnag.Notify instead of the mapped notifier. 66 | ctx := bugsnag.AttachRequestData(r.Context(), r) 67 | 68 | if notifier.Config.IsAutoCaptureSessions() { 69 | ctx = bugsnag.StartSession(ctx) 70 | } 71 | notifier.FlushSessionsOnRepanic(false) 72 | c.Map(r.WithContext(ctx)) 73 | defer notifier.AutoNotify(ctx) 74 | c.Map(notifier) 75 | c.Next() 76 | } 77 | } 78 | 79 | func updateGlobalConfig(rawData ...interface{}) { 80 | for i, datum := range rawData { 81 | if c, ok := datum.(bugsnag.Configuration); ok { 82 | if c.ReleaseStage == "" { 83 | c.ReleaseStage = martini.Env 84 | } 85 | bugsnag.Configure(c) 86 | rawData[i] = nil 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /martini/martini_test.go: -------------------------------------------------------------------------------- 1 | package bugsnagmartini_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | simplejson "github.com/bitly/go-simplejson" 11 | "github.com/bugsnag/bugsnag-go" 12 | "github.com/bugsnag/bugsnag-go/martini" 13 | . "github.com/bugsnag/bugsnag-go/testutil" 14 | "github.com/go-martini/martini" 15 | ) 16 | 17 | func performHandledError(notifier *bugsnag.Notifier, r *http.Request) { 18 | ctx := r.Context() 19 | notifier.Notify(fmt.Errorf("Ooopsie"), ctx, bugsnag.User{Id: "987zyx"}) 20 | } 21 | 22 | func performUnhandledCrash() { 23 | panic("something bad just happened") 24 | } 25 | 26 | func TestMartini(t *testing.T) { 27 | ts, reports := Setup() 28 | defer ts.Close() 29 | 30 | config := bugsnag.Configuration{ 31 | APIKey: TestAPIKey, 32 | Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, 33 | } 34 | bugsnag.Configure(config) 35 | 36 | m := martini.Classic() 37 | 38 | userID := "1234abcd" 39 | 40 | m.Use(martini.Recovery()) 41 | m.Use(bugsnagmartini.AutoNotify(bugsnag.User{Id: userID})) 42 | 43 | m.Get("/unhandled", performUnhandledCrash) 44 | m.Get("/handled", performHandledError) 45 | go m.RunOnAddr(":9077") 46 | 47 | t.Run("AutoNotify", func(st *testing.T) { 48 | time.Sleep(1 * time.Second) 49 | _, err := http.Get("http://localhost:9077/unhandled") 50 | if err != nil { 51 | t.Error(err) 52 | } 53 | report := <-reports 54 | r, _ := simplejson.NewJson(report) 55 | hostname, _ := os.Hostname() 56 | AssertPayload(st, r, fmt.Sprintf(` 57 | { 58 | "apiKey": "%s", 59 | "events":[ 60 | { 61 | "app":{ "releaseStage":"" }, 62 | "context":"/unhandled", 63 | "device":{ "hostname": "%s" }, 64 | "exceptions":[ 65 | { 66 | "errorClass":"*errors.errorString", 67 | "message":"something bad just happened", 68 | "stacktrace":[] 69 | } 70 | ], 71 | "payloadVersion":"4", 72 | "severity":"error", 73 | "severityReason":{ "type":"unhandledErrorMiddleware" }, 74 | "unhandled":true, 75 | "request": { 76 | "httpMethod": "GET", 77 | "url": "http://localhost:9077/unhandled", 78 | "headers": { 79 | "Accept-Encoding": "gzip" 80 | } 81 | }, 82 | "user":{ "id": "%s" } 83 | } 84 | ], 85 | "notifier":{ 86 | "name":"Bugsnag Go", 87 | "url":"https://github.com/bugsnag/bugsnag-go", 88 | "version": "%s" 89 | } 90 | } 91 | `, TestAPIKey, hostname, userID, bugsnag.VERSION)) 92 | }) 93 | 94 | t.Run("Notify", func(st *testing.T) { 95 | time.Sleep(1 * time.Second) 96 | _, err := http.Get("http://localhost:9077/handled") 97 | if err != nil { 98 | t.Error(err) 99 | } 100 | report := <-reports 101 | r, _ := simplejson.NewJson(report) 102 | hostname, _ := os.Hostname() 103 | AssertPayload(st, r, fmt.Sprintf(` 104 | { 105 | "apiKey": "%s", 106 | "events":[ 107 | { 108 | "app":{ "releaseStage":"" }, 109 | "device":{ "hostname": "%s" }, 110 | "exceptions":[ 111 | { 112 | "errorClass":"*errors.errorString", 113 | "message":"Ooopsie", 114 | "stacktrace":[] 115 | } 116 | ], 117 | "payloadVersion":"4", 118 | "severity":"error", 119 | "severityReason":{ "type":"unhandledErrorMiddleware" }, 120 | "request": { 121 | "url": "http://localhost:9077/handled", 122 | "httpMethod": "GET", 123 | "headers": { 124 | "Accept-Encoding": "gzip" 125 | } 126 | }, 127 | "unhandled":true, 128 | "user":{ "id": "%s" } 129 | } 130 | ], 131 | "notifier":{ 132 | "name":"Bugsnag Go", 133 | "url":"https://github.com/bugsnag/bugsnag-go", 134 | "version": "%s" 135 | } 136 | } 137 | `, TestAPIKey, hostname, "987zyx", bugsnag.VERSION)) 138 | }) 139 | 140 | } 141 | 142 | func crash(a interface{}) string { 143 | return a.(string) 144 | } 145 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type ( 8 | beforeFunc func(*Event, *Configuration) error 9 | 10 | // MiddlewareStacks keep middleware in the correct order. They are 11 | // called in reverse order, so if you add a new middleware it will 12 | // be called before all existing middleware. 13 | middlewareStack struct { 14 | before []beforeFunc 15 | } 16 | ) 17 | 18 | // AddMiddleware adds a new middleware to the outside of the existing ones, 19 | // when the middlewareStack is Run it will be run before all middleware that 20 | // have been added before. 21 | func (stack *middlewareStack) OnBeforeNotify(middleware beforeFunc) { 22 | stack.before = append(stack.before, middleware) 23 | } 24 | 25 | // Run causes all the middleware to be run. If they all permit it the next callback 26 | // will be called with all the middleware on the stack. 27 | func (stack *middlewareStack) Run(event *Event, config *Configuration, next func() error) error { 28 | // run all the before filters in reverse order 29 | for i := range stack.before { 30 | before := stack.before[len(stack.before)-i-1] 31 | 32 | severity := event.Severity 33 | err := stack.runBeforeFilter(before, event, config) 34 | if err != nil { 35 | return err 36 | } 37 | if event.Severity != severity { 38 | event.handledState.SeverityReason = SeverityReasonCallbackSpecified 39 | } 40 | } 41 | 42 | return next() 43 | } 44 | 45 | func (stack *middlewareStack) runBeforeFilter(f beforeFunc, event *Event, config *Configuration) error { 46 | defer func() { 47 | if err := recover(); err != nil { 48 | config.logf("bugsnag/middleware: unexpected panic: %v", err) 49 | } 50 | }() 51 | 52 | return f(event, config) 53 | } 54 | 55 | // catchMiddlewarePanic is used to log any panics that happen inside Middleware, 56 | // we wouldn't want to not notify Bugsnag in this case. 57 | func catchMiddlewarePanic(event *Event, config *Configuration, next func() error) { 58 | } 59 | 60 | // httpRequestMiddleware is added OnBeforeNotify by default. It takes information 61 | // from an http.Request passed in as rawData, and adds it to the Event. You can 62 | // use this as a template for writing your own Middleware. 63 | func httpRequestMiddleware(event *Event, config *Configuration) error { 64 | for _, datum := range event.RawData { 65 | if request, ok := datum.(*http.Request); ok && request != nil { 66 | event.MetaData.Update(MetaData{ 67 | "request": { 68 | "params": request.URL.Query(), 69 | }, 70 | }) 71 | } 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/bugsnag/bugsnag-go/errors" 7 | "log" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestMiddlewareOrder(t *testing.T) { 13 | 14 | err := fmt.Errorf("test") 15 | data := []interface{}{errors.New(err, 1)} 16 | event, config := newEvent(data, &defaultNotifier) 17 | 18 | result := make([]int, 0, 7) 19 | stack := middlewareStack{} 20 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 21 | result = append(result, 2) 22 | return nil 23 | }) 24 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 25 | result = append(result, 1) 26 | return nil 27 | }) 28 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 29 | result = append(result, 0) 30 | return nil 31 | }) 32 | 33 | stack.Run(event, config, func() error { 34 | result = append(result, 3) 35 | return nil 36 | }) 37 | 38 | if !reflect.DeepEqual(result, []int{0, 1, 2, 3}) { 39 | t.Errorf("unexpected middleware order %v", result) 40 | } 41 | } 42 | 43 | func TestBeforeNotifyReturnErr(t *testing.T) { 44 | 45 | stack := middlewareStack{} 46 | err := fmt.Errorf("test") 47 | data := []interface{}{errors.New(err, 1)} 48 | event, config := newEvent(data, &defaultNotifier) 49 | 50 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 51 | return err 52 | }) 53 | 54 | called := false 55 | 56 | e := stack.Run(event, config, func() error { 57 | called = true 58 | return nil 59 | }) 60 | 61 | if e != err { 62 | t.Errorf("Middleware didn't return the error") 63 | } 64 | 65 | if called == true { 66 | t.Errorf("Notify was called when BeforeNotify returned False") 67 | } 68 | } 69 | 70 | func TestBeforeNotifyPanic(t *testing.T) { 71 | 72 | stack := middlewareStack{} 73 | err := fmt.Errorf("test") 74 | event, _ := newEvent([]interface{}{errors.New(err, 1)}, &defaultNotifier) 75 | 76 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 77 | panic("oops") 78 | }) 79 | 80 | called := false 81 | b := &bytes.Buffer{} 82 | 83 | stack.Run(event, &Configuration{Logger: log.New(b, log.Prefix(), 0)}, func() error { 84 | called = true 85 | return nil 86 | }) 87 | 88 | logged := b.String() 89 | 90 | if logged != "bugsnag/middleware: unexpected panic: oops\n" { 91 | t.Errorf("Logged: %s", logged) 92 | } 93 | 94 | if called == false { 95 | t.Errorf("Notify was not called when BeforeNotify panicked") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /negroni/bugsnagnegroni.go: -------------------------------------------------------------------------------- 1 | package bugsnagnegroni 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/bugsnag/bugsnag-go" 7 | "github.com/bugsnag/bugsnag-go/device" 8 | "github.com/urfave/negroni" 9 | ) 10 | 11 | // FrameworkName is the name of the framework this middleware applies to 12 | const FrameworkName string = "Negroni" 13 | 14 | type handler struct { 15 | rawData []interface{} 16 | } 17 | 18 | // AutoNotify sends any panics to bugsnag, and then re-raises them. 19 | func AutoNotify(rawData ...interface{}) negroni.Handler { 20 | updateGlobalConfig(rawData...) 21 | device.AddVersion(FrameworkName, "unknown") // Negroni exposes no version prop. 22 | state := bugsnag.HandledState{ 23 | SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, 24 | OriginalSeverity: bugsnag.SeverityError, 25 | Unhandled: true, 26 | Framework: FrameworkName, 27 | } 28 | rawData = append(rawData, state) 29 | return &handler{rawData: rawData} 30 | } 31 | 32 | func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 33 | // Record a session if auto capture sessions is enabled 34 | ctx := bugsnag.AttachRequestData(r.Context(), r) 35 | if bugsnag.Config.IsAutoCaptureSessions() { 36 | ctx = bugsnag.StartSession(ctx) 37 | } 38 | request := r.WithContext(ctx) 39 | notifier := bugsnag.New(h.rawData...) 40 | notifier.FlushSessionsOnRepanic(false) 41 | defer notifier.AutoNotify(ctx) 42 | next(rw, request) 43 | 44 | } 45 | 46 | func updateGlobalConfig(rawData ...interface{}) { 47 | for i, datum := range rawData { 48 | if c, ok := datum.(bugsnag.Configuration); ok { 49 | bugsnag.Configure(c) 50 | rawData[i] = nil 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /negroni/negroni_test.go: -------------------------------------------------------------------------------- 1 | package bugsnagnegroni_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | simplejson "github.com/bitly/go-simplejson" 11 | "github.com/bugsnag/bugsnag-go" 12 | "github.com/bugsnag/bugsnag-go/negroni" 13 | . "github.com/bugsnag/bugsnag-go/testutil" 14 | "github.com/urfave/negroni" 15 | ) 16 | 17 | const userID = "1234abcd" 18 | 19 | func TestNegroni(t *testing.T) { 20 | ts, reports := Setup() 21 | config := bugsnag.Configuration{ 22 | APIKey: TestAPIKey, 23 | Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, 24 | } 25 | mux := http.NewServeMux() 26 | mux.HandleFunc("/unhandled", unhandledCrashHandler) 27 | mux.HandleFunc("/handled", handledCrashHandler) 28 | 29 | hostname, _ := os.Hostname() 30 | 31 | n := negroni.New() 32 | n.Use(negroni.NewRecovery()) 33 | n.Use(bugsnagnegroni.AutoNotify(config, bugsnag.User{Id: userID})) 34 | n.UseHandler(mux) 35 | 36 | go http.ListenAndServe(":9078", n) 37 | 38 | t.Run("AutoNotify", func(st *testing.T) { 39 | time.Sleep(500 * time.Millisecond) 40 | http.Get("http://localhost:9078/unhandled") 41 | report := <-reports 42 | r, _ := simplejson.NewJson(report) 43 | AssertPayload(st, r, fmt.Sprintf(` 44 | { 45 | "apiKey":"166f5ad3590596f9aa8d601ea89af845", 46 | "events":[ 47 | { 48 | "app":{ "releaseStage":"" }, 49 | "context":"/unhandled", 50 | "device":{ "hostname": "%s" }, 51 | "exceptions":[ 52 | { 53 | "errorClass":"*errors.errorString", 54 | "message":"something went terribly wrong", 55 | "stacktrace":[] 56 | } 57 | ], 58 | "payloadVersion":"4", 59 | "severity":"error", 60 | "severityReason":{ "type":"unhandledErrorMiddleware" }, 61 | "unhandled":true, 62 | "request": { 63 | "url": "http://localhost:9078/unhandled", 64 | "httpMethod": "GET", 65 | "referer": "", 66 | "headers": { 67 | "Accept-Encoding": "gzip" 68 | } 69 | }, 70 | "user":{ "id": "%s" } 71 | } 72 | ], 73 | "notifier":{ 74 | "name":"Bugsnag Go", 75 | "url":"https://github.com/bugsnag/bugsnag-go", 76 | "version": "%s" 77 | } 78 | } 79 | `, hostname, userID, bugsnag.VERSION)) 80 | }) 81 | 82 | t.Run("Notify", func(st *testing.T) { 83 | time.Sleep(500 * time.Millisecond) 84 | http.Get("http://localhost:9078/handled") 85 | report := <-reports 86 | r, _ := simplejson.NewJson(report) 87 | AssertPayload(st, r, fmt.Sprintf(` 88 | { 89 | "apiKey":"166f5ad3590596f9aa8d601ea89af845", 90 | "events":[ 91 | { 92 | "app":{ "releaseStage":"" }, 93 | "context":"/handled", 94 | "device":{ "hostname": "%s" }, 95 | "exceptions":[ 96 | { 97 | "errorClass":"*errors.errorString", 98 | "message":"Ooopsie", 99 | "stacktrace":[] 100 | } 101 | ], 102 | "payloadVersion":"4", 103 | "severity":"warning", 104 | "severityReason":{ "type":"handledError" }, 105 | "unhandled": false, 106 | "request": { 107 | "url": "http://localhost:9078/handled", 108 | "httpMethod": "GET", 109 | "referer": "", 110 | "headers": { 111 | "Accept-Encoding": "gzip" 112 | } 113 | }, 114 | "user":{ "id": "%s" } 115 | } 116 | ], 117 | "notifier":{ 118 | "name":"Bugsnag Go", 119 | "url":"https://github.com/bugsnag/bugsnag-go", 120 | "version": "%s" 121 | } 122 | } 123 | `, hostname, userID, bugsnag.VERSION)) 124 | }) 125 | } 126 | 127 | func unhandledCrashHandler(w http.ResponseWriter, req *http.Request) { 128 | panic("something went terribly wrong") 129 | } 130 | 131 | func handledCrashHandler(w http.ResponseWriter, req *http.Request) { 132 | bugsnag.Notify(fmt.Errorf("Ooopsie"), bugsnag.User{Id: userID}, req.Context()) 133 | } 134 | 135 | func crash(a interface{}) string { 136 | return a.(string) 137 | } 138 | -------------------------------------------------------------------------------- /panicwrap.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "github.com/bugsnag/bugsnag-go/errors" 5 | "github.com/bugsnag/bugsnag-go/sessions" 6 | "github.com/bugsnag/panicwrap" 7 | ) 8 | 9 | // Forks and re-runs your program to add panic monitoring. This function does 10 | // not return on one process, instead listening on stderr of the other process, 11 | // which returns nil. 12 | // 13 | // Related: https://godoc.org/github.com/bugsnag/panicwrap#BasicMonitor 14 | func defaultPanicHandler() { 15 | defer defaultNotifier.dontPanic() 16 | ctx := sessions.SendStartupSession(&sessionTrackingConfig) 17 | 18 | err := panicwrap.BasicMonitor(func(output string) { 19 | toNotify, err := errors.ParsePanic(output) 20 | 21 | if err != nil { 22 | defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) 23 | } 24 | state := HandledState{SeverityReasonUnhandledPanic, SeverityError, true, ""} 25 | defaultNotifier.NotifySync(toNotify, true, state, ctx) 26 | 27 | }) 28 | 29 | if err != nil { 30 | defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /payload.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "runtime" 9 | "sync" 10 | "time" 11 | 12 | "github.com/bugsnag/bugsnag-go/device" 13 | "github.com/bugsnag/bugsnag-go/headers" 14 | "github.com/bugsnag/bugsnag-go/sessions" 15 | ) 16 | 17 | const notifyPayloadVersion = "4" 18 | 19 | var sessionMutex sync.Mutex 20 | 21 | type payload struct { 22 | *Event 23 | *Configuration 24 | } 25 | 26 | type hash map[string]interface{} 27 | 28 | func (p *payload) deliver() error { 29 | 30 | if len(p.APIKey) != 32 { 31 | return fmt.Errorf("bugsnag/payload.deliver: invalid api key: '%s'", p.APIKey) 32 | } 33 | 34 | buf, err := p.MarshalJSON() 35 | 36 | if err != nil { 37 | return fmt.Errorf("bugsnag/payload.deliver: %v", err) 38 | } 39 | 40 | client := http.Client{ 41 | Transport: p.Transport, 42 | } 43 | req, err := http.NewRequest("POST", p.Endpoints.Notify, bytes.NewBuffer(buf)) 44 | if err != nil { 45 | return fmt.Errorf("bugsnag/payload.deliver unable to create request: %v", err) 46 | } 47 | for k, v := range headers.PrefixedHeaders(p.APIKey, notifyPayloadVersion) { 48 | req.Header.Add(k, v) 49 | } 50 | resp, err := client.Do(req) 51 | if err != nil { 52 | return fmt.Errorf("bugsnag/payload.deliver: %v", err) 53 | } 54 | defer resp.Body.Close() 55 | 56 | if resp.StatusCode != 200 { 57 | return fmt.Errorf("bugsnag/payload.deliver: Got HTTP %s", resp.Status) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (p *payload) MarshalJSON() ([]byte, error) { 64 | return json.Marshal(reportJSON{ 65 | APIKey: p.APIKey, 66 | Events: []eventJSON{ 67 | eventJSON{ 68 | App: &appJSON{ 69 | ReleaseStage: p.ReleaseStage, 70 | Type: p.AppType, 71 | Version: p.AppVersion, 72 | }, 73 | Context: p.Context, 74 | Device: &deviceJSON{ 75 | Hostname: p.Hostname, 76 | OsName: runtime.GOOS, 77 | RuntimeVersions: device.GetRuntimeVersions(), 78 | }, 79 | Request: p.Request, 80 | Exceptions: p.exceptions(), 81 | GroupingHash: p.GroupingHash, 82 | Metadata: p.MetaData.sanitize(p.ParamsFilters), 83 | PayloadVersion: notifyPayloadVersion, 84 | Session: p.makeSession(), 85 | Severity: p.Severity.String, 86 | SeverityReason: p.severityReasonPayload(), 87 | Unhandled: p.Unhandled, 88 | User: p.User, 89 | }, 90 | }, 91 | Notifier: notifierJSON{ 92 | Name: "Bugsnag Go", 93 | URL: "https://github.com/bugsnag/bugsnag-go", 94 | Version: VERSION, 95 | }, 96 | }) 97 | } 98 | 99 | func (p *payload) makeSession() *sessionJSON { 100 | // If a context has not been applied to the payload then assume that no 101 | // session has started either 102 | if p.Ctx == nil { 103 | return nil 104 | } 105 | 106 | sessionMutex.Lock() 107 | defer sessionMutex.Unlock() 108 | session := sessions.IncrementEventCountAndGetSession(p.Ctx, p.Unhandled) 109 | if session != nil { 110 | s := *session 111 | return &sessionJSON{ 112 | ID: s.ID, 113 | StartedAt: s.StartedAt.UTC().Format(time.RFC3339), 114 | Events: sessions.EventCounts{ 115 | Handled: s.EventCounts.Handled, 116 | Unhandled: s.EventCounts.Unhandled, 117 | }, 118 | } 119 | } 120 | return nil 121 | } 122 | 123 | func (p *payload) severityReasonPayload() *severityReasonJSON { 124 | if reason := p.handledState.SeverityReason; reason != "" { 125 | json := &severityReasonJSON{ 126 | Type: reason, 127 | UnhandledOverridden: p.handledState.Unhandled != p.Unhandled, 128 | } 129 | if p.handledState.Framework != "" { 130 | json.Attributes = make(map[string]string, 1) 131 | json.Attributes["framework"] = p.handledState.Framework 132 | } 133 | return json 134 | } 135 | return nil 136 | } 137 | 138 | func (p *payload) exceptions() []exceptionJSON { 139 | exceptions := []exceptionJSON{ 140 | exceptionJSON{ 141 | ErrorClass: p.ErrorClass, 142 | Message: p.Message, 143 | Stacktrace: p.Stacktrace, 144 | }, 145 | } 146 | 147 | if p.Error == nil { 148 | return exceptions 149 | } 150 | 151 | cause := p.Error.Cause 152 | for cause != nil { 153 | exceptions = append(exceptions, exceptionJSON{ 154 | ErrorClass: cause.TypeName(), 155 | Message: cause.Error(), 156 | Stacktrace: generateStacktrace(cause, p.Configuration), 157 | }) 158 | cause = cause.Cause 159 | } 160 | 161 | return exceptions 162 | } 163 | -------------------------------------------------------------------------------- /payload_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/bugsnag/bugsnag-go/errors" 11 | "github.com/bugsnag/bugsnag-go/sessions" 12 | ) 13 | 14 | const expSmall = `{"apiKey":"","events":[{"app":{"releaseStage":""},"device":{"osName":"%s","runtimeVersions":{"go":"%s"}},"exceptions":[{"errorClass":"","message":"","stacktrace":null}],"metaData":{},"payloadVersion":"4","severity":"","unhandled":false}],"notifier":{"name":"Bugsnag Go","url":"https://github.com/bugsnag/bugsnag-go","version":"1.9.1"}}` 15 | 16 | // The large payload has a timestamp in it which makes it awkward to assert against. 17 | // Instead, assert that the timestamp property exist, along with the rest of the expected payload 18 | const expLargePre = `{"apiKey":"166f5ad3590596f9aa8d601ea89af845","events":[{"app":{"releaseStage":"mega-production","type":"gin","version":"1.5.3"},"context":"/api/v2/albums","device":{"hostname":"super.duper.site","osName":"%s","runtimeVersions":{"go":"%s"}},"exceptions":[{"errorClass":"error class","message":"error message goes here","stacktrace":[{"method":"doA","file":"a.go","lineNumber":65},{"method":"fetchB","file":"b.go","lineNumber":99,"inProject":true},{"method":"incrementI","file":"i.go","lineNumber":651}]}],"groupingHash":"custom grouping hash","metaData":{"custom tab":{"my key":"my value"}},"payloadVersion":"4","session":{"startedAt":"` 19 | const expLargePost = `,"severity":"info","severityReason":{"type":"unhandledError","attributes":{"framework":"gin"}},"unhandled":true,"user":{"id":"1234baerg134","name":"Kool Kidz on da bus","email":"typo@busgang.com"}}],"notifier":{"name":"Bugsnag Go","url":"https://github.com/bugsnag/bugsnag-go","version":"1.9.1"}}` 20 | 21 | func TestMarshalEmptyPayload(t *testing.T) { 22 | sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig) 23 | p := payload{&Event{Ctx: context.Background()}, &Configuration{}} 24 | bytes, _ := p.MarshalJSON() 25 | exp := fmt.Sprintf(expSmall, runtime.GOOS, runtime.Version()) 26 | if got := string(bytes[:]); got != exp { 27 | t.Errorf("Payload different to what was expected. \nGot: %s\nExp: %s", got, exp) 28 | } 29 | } 30 | 31 | func TestMarshalLargePayload(t *testing.T) { 32 | payload := makeLargePayload() 33 | bytes, _ := payload.MarshalJSON() 34 | got := string(bytes[:]) 35 | expPre := fmt.Sprintf(expLargePre, runtime.GOOS, runtime.Version()) 36 | if !strings.Contains(got, expPre) { 37 | t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expPre, got) 38 | 39 | } 40 | if !strings.Contains(got, expLargePost) { 41 | t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expLargePost, got) 42 | } 43 | } 44 | 45 | func makeLargePayload() *payload { 46 | stackframes := []StackFrame{ 47 | {Method: "doA", File: "a.go", LineNumber: 65, InProject: false}, 48 | {Method: "fetchB", File: "b.go", LineNumber: 99, InProject: true}, 49 | {Method: "incrementI", File: "i.go", LineNumber: 651, InProject: false}, 50 | } 51 | user := User{ 52 | Id: "1234baerg134", 53 | Name: "Kool Kidz on da bus", 54 | Email: "typo@busgang.com", 55 | } 56 | handledState := HandledState{ 57 | SeverityReason: SeverityReasonUnhandledError, 58 | OriginalSeverity: severity{String: "error"}, 59 | Unhandled: true, 60 | Framework: "gin", 61 | } 62 | 63 | ctx := context.Background() 64 | ctx = StartSession(ctx) 65 | 66 | event := Event{ 67 | Error: &errors.Error{}, 68 | RawData: nil, 69 | ErrorClass: "error class", 70 | Message: "error message goes here", 71 | Stacktrace: stackframes, 72 | Context: "/api/v2/albums", 73 | Severity: SeverityInfo, 74 | GroupingHash: "custom grouping hash", 75 | User: &user, 76 | Ctx: ctx, 77 | MetaData: map[string]map[string]interface{}{ 78 | "custom tab": map[string]interface{}{ 79 | "my key": "my value", 80 | }, 81 | }, 82 | Unhandled: true, 83 | handledState: handledState, 84 | } 85 | config := Configuration{ 86 | APIKey: testAPIKey, 87 | ReleaseStage: "mega-production", 88 | AppType: "gin", 89 | AppVersion: "1.5.3", 90 | Hostname: "super.duper.site", 91 | } 92 | return &payload{&event, &config} 93 | } 94 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "github.com/bugsnag/bugsnag-go/device" 5 | "github.com/bugsnag/bugsnag-go/sessions" 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | type reportJSON struct { 10 | APIKey string `json:"apiKey"` 11 | Events []eventJSON `json:"events"` 12 | Notifier notifierJSON `json:"notifier"` 13 | } 14 | 15 | type notifierJSON struct { 16 | Name string `json:"name"` 17 | URL string `json:"url"` 18 | Version string `json:"version"` 19 | } 20 | 21 | type eventJSON struct { 22 | App *appJSON `json:"app"` 23 | Context string `json:"context,omitempty"` 24 | Device *deviceJSON `json:"device,omitempty"` 25 | Request *RequestJSON `json:"request,omitempty"` 26 | Exceptions []exceptionJSON `json:"exceptions"` 27 | GroupingHash string `json:"groupingHash,omitempty"` 28 | Metadata interface{} `json:"metaData"` 29 | PayloadVersion string `json:"payloadVersion"` 30 | Session *sessionJSON `json:"session,omitempty"` 31 | Severity string `json:"severity"` 32 | SeverityReason *severityReasonJSON `json:"severityReason,omitempty"` 33 | Unhandled bool `json:"unhandled"` 34 | User *User `json:"user,omitempty"` 35 | } 36 | 37 | type sessionJSON struct { 38 | StartedAt string `json:"startedAt"` 39 | ID uuid.UUID `json:"id"` 40 | Events sessions.EventCounts `json:"events"` 41 | } 42 | 43 | type appJSON struct { 44 | ReleaseStage string `json:"releaseStage"` 45 | Type string `json:"type,omitempty"` 46 | Version string `json:"version,omitempty"` 47 | } 48 | 49 | type exceptionJSON struct { 50 | ErrorClass string `json:"errorClass"` 51 | Message string `json:"message"` 52 | Stacktrace []StackFrame `json:"stacktrace"` 53 | } 54 | 55 | type severityReasonJSON struct { 56 | Type SeverityReason `json:"type,omitempty"` 57 | Attributes map[string]string `json:"attributes,omitempty"` 58 | UnhandledOverridden bool `json:"unhandledOverridden,omitempty"` 59 | } 60 | 61 | type deviceJSON struct { 62 | Hostname string `json:"hostname,omitempty"` 63 | OsName string `json:"osName,omitempty"` 64 | 65 | RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions,omitempty"` 66 | } 67 | 68 | // RequestJSON is the request information that populates the Request tab in the dashboard. 69 | type RequestJSON struct { 70 | ClientIP string `json:"clientIp,omitempty"` 71 | Headers map[string]string `json:"headers,omitempty"` 72 | HTTPMethod string `json:"httpMethod,omitempty"` 73 | URL string `json:"url,omitempty"` 74 | Referer string `json:"referer,omitempty"` 75 | } 76 | -------------------------------------------------------------------------------- /report_publisher.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import "fmt" 4 | 5 | type reportPublisher interface { 6 | publishReport(*payload) error 7 | } 8 | 9 | type defaultReportPublisher struct{} 10 | 11 | func (*defaultReportPublisher) publishReport(p *payload) error { 12 | p.logf("notifying bugsnag: %s", p.Message) 13 | if !p.notifyInReleaseStage() { 14 | return fmt.Errorf("not notifying in %s", p.ReleaseStage) 15 | } 16 | if p.Synchronous { 17 | return p.deliver() 18 | } 19 | 20 | go func(p *payload) { 21 | if err := p.deliver(); err != nil { 22 | // Ensure that any errors are logged if they occur in a goroutine. 23 | p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err) 24 | } 25 | }(p) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /request_extractor.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | const requestContextKey requestKey = iota 11 | 12 | type requestKey int 13 | 14 | // AttachRequestData returns a child of the given context with the request 15 | // object attached for later extraction by the notifier in order to 16 | // automatically record request data 17 | func AttachRequestData(ctx context.Context, r *http.Request) context.Context { 18 | return context.WithValue(ctx, requestContextKey, r) 19 | } 20 | 21 | // extractRequestInfo looks for the request object that the notifier 22 | // automatically attaches to the context when using any of the supported 23 | // frameworks or bugsnag.HandlerFunc or bugsnag.Handler, and returns sub-object 24 | // supported by the notify API. 25 | func extractRequestInfo(ctx context.Context) (*RequestJSON, *http.Request) { 26 | if req := getRequestIfPresent(ctx); req != nil { 27 | return extractRequestInfoFromReq(req), req 28 | } 29 | return nil, nil 30 | } 31 | 32 | // extractRequestInfoFromReq extracts the request information the notify API 33 | // understands from the given HTTP request. Returns the sub-object supported by 34 | // the notify API. 35 | func extractRequestInfoFromReq(req *http.Request) *RequestJSON { 36 | return &RequestJSON{ 37 | ClientIP: req.RemoteAddr, 38 | HTTPMethod: req.Method, 39 | URL: sanitizeURL(req), 40 | Referer: req.Referer(), 41 | Headers: parseRequestHeaders(req.Header), 42 | } 43 | } 44 | 45 | // sanitizeURL will build up the URL matching the request. It will filter query parameters to remove sensitive fields. 46 | // The query part of the URL might appear differently (different order of parameters) if any filtering was done. 47 | func sanitizeURL(req *http.Request) string { 48 | scheme := "http" 49 | if req.TLS != nil { 50 | scheme = "https" 51 | } 52 | 53 | rawQuery := req.URL.RawQuery 54 | parsedQuery, err := url.ParseQuery(req.URL.RawQuery) 55 | 56 | if err != nil { 57 | return scheme + "://" + req.Host + req.RequestURI 58 | } 59 | 60 | changed := false 61 | for key, values := range parsedQuery { 62 | if contains(Config.ParamsFilters, key) { 63 | for i := range values { 64 | values[i] = "BUGSNAG_URL_FILTERED" 65 | changed = true 66 | } 67 | } 68 | } 69 | 70 | if changed { 71 | rawQuery = parsedQuery.Encode() 72 | rawQuery = strings.Replace(rawQuery, "BUGSNAG_URL_FILTERED", "[FILTERED]", -1) 73 | } 74 | 75 | u := url.URL{ 76 | Scheme: scheme, 77 | Host: req.Host, 78 | Path: req.URL.Path, 79 | RawQuery: rawQuery, 80 | } 81 | return u.String() 82 | } 83 | 84 | func parseRequestHeaders(header map[string][]string) map[string]string { 85 | headers := make(map[string]string) 86 | for k, v := range header { 87 | // Headers can have multiple values, in which case we report them as csv 88 | if contains(Config.ParamsFilters, k) { 89 | headers[k] = "[FILTERED]" 90 | } else { 91 | headers[k] = strings.Join(v, ",") 92 | } 93 | } 94 | return headers 95 | } 96 | 97 | func contains(slice []string, e string) bool { 98 | for _, s := range slice { 99 | if strings.Contains(strings.ToLower(e), strings.ToLower(s)) { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | 106 | func getRequestIfPresent(ctx context.Context) *http.Request { 107 | if ctx == nil { 108 | return nil 109 | } 110 | val := ctx.Value(requestContextKey) 111 | if val == nil { 112 | return nil 113 | } 114 | return val.(*http.Request) 115 | } 116 | -------------------------------------------------------------------------------- /request_extractor_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRequestInformationGetsExtracted(t *testing.T) { 13 | contexts := make(chan context.Context, 1) 14 | hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | ctx := r.Context() 16 | ctx = AttachRequestData(ctx, r) 17 | contexts <- ctx 18 | }) 19 | ts := httptest.NewServer(hf) 20 | defer ts.Close() 21 | http.Get(ts.URL + "/1234abcd?fish=bird") 22 | 23 | reqJSON, req := extractRequestInfo(<-contexts) 24 | if reqJSON.ClientIP == "" { 25 | t.Errorf("expected to find an IP address for the request but was blank") 26 | } 27 | if got, exp := reqJSON.HTTPMethod, "GET"; got != exp { 28 | t.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) 29 | } 30 | if got, exp := req.URL.Path, "/1234abcd"; got != exp { 31 | t.Errorf("expected request URL to be '%s' but was '%s'", exp, got) 32 | } 33 | if got, exp := reqJSON.URL, "/1234abcd?fish=bird"; !strings.Contains(got, exp) { 34 | t.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) 35 | } 36 | if got, exp := reqJSON.Referer, ""; got != exp { 37 | t.Errorf("expected request referer to be '%s' but was '%s'", exp, got) 38 | } 39 | if got, exp := reqJSON.Headers["Accept-Encoding"], "gzip"; got != exp { 40 | t.Errorf("expected Accept-Encoding to be '%s' but was '%s'", exp, got) 41 | } 42 | if got, exp := reqJSON.Headers["User-Agent"], "Go-http-client"; !strings.Contains(got, exp) { 43 | t.Errorf("expected user agent to contain '%s' but was '%s'", exp, got) 44 | } 45 | } 46 | 47 | func TestRequestExtractorCanHandleAbsentContext(t *testing.T) { 48 | if got, _ := extractRequestInfo(nil); got != nil { 49 | //really just testing that nothing panics here 50 | t.Errorf("expected nil contexts to give nil sub-objects, but was '%s'", got) 51 | } 52 | if got, _ := extractRequestInfo(context.Background()); got != nil { 53 | //really just testing that nothing panics here 54 | t.Errorf("expected contexts without requst info to give nil sub-objects, but was '%s'", got) 55 | } 56 | } 57 | 58 | func TestExtractRequestInfoFromReq_RedactURL(t *testing.T) { 59 | testCases := []struct { 60 | in url.URL 61 | exp string 62 | }{ 63 | {in: url.URL{}, exp: "http://example.com"}, 64 | {in: url.URL{Path: "/"}, exp: "http://example.com/"}, 65 | {in: url.URL{Path: "/foo.html"}, exp: "http://example.com/foo.html"}, 66 | {in: url.URL{Path: "/foo.html", RawQuery: "q=something&bar=123"}, exp: "http://example.com/foo.html?q=something&bar=123"}, 67 | {in: url.URL{Path: "/foo.html", RawQuery: "foo=1&foo=2&foo=3"}, exp: "http://example.com/foo.html?foo=1&foo=2&foo=3"}, 68 | 69 | // Invalid query string. 70 | {in: url.URL{Path: "/foo", RawQuery: "%"}, exp: "http://example.com/foo?%"}, 71 | 72 | // Query params contain secrets 73 | {in: url.URL{Path: "/foo.html", RawQuery: "access_token=something"}, exp: "http://example.com/foo.html?access_token=[FILTERED]"}, 74 | {in: url.URL{Path: "/foo.html", RawQuery: "access_token=something&access_token=&foo=bar"}, exp: "http://example.com/foo.html?access_token=[FILTERED]&access_token=[FILTERED]&foo=bar"}, 75 | } 76 | 77 | for _, tc := range testCases { 78 | requestURI := tc.in.Path 79 | if tc.in.RawQuery != "" { 80 | requestURI += "?" + tc.in.RawQuery 81 | } 82 | req := &http.Request{ 83 | Host: "example.com", 84 | URL: &tc.in, 85 | RequestURI: requestURI, 86 | } 87 | result := extractRequestInfoFromReq(req) 88 | if result.URL != tc.exp { 89 | t.Errorf("expected URL to be '%s' but was '%s'", tc.exp, result.URL) 90 | } 91 | } 92 | } 93 | 94 | func TestParseHeadersWillSanitiseIllegalParams(t *testing.T) { 95 | headers := make(map[string][]string) 96 | headers["password"] = []string{"correct horse battery staple"} 97 | headers["secret"] = []string{"I am Banksy"} 98 | headers["authorization"] = []string{"licence to kill -9"} 99 | headers["custom-made-secret"] = []string{"I'm the insider at Sotheby's"} 100 | for k, v := range parseRequestHeaders(headers) { 101 | if v != "[FILTERED]" { 102 | t.Errorf("expected '%s' to be [FILTERED], but was '%s'", k, v) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /sessions/config_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestConfigDoesNotChangeGivenBlankValues(t *testing.T) { 11 | c := testConfig() 12 | exp := testConfig() 13 | c.Update(&SessionTrackingConfiguration{}) 14 | tt := []struct { 15 | name string 16 | expected interface{} 17 | got interface{} 18 | }{ 19 | {"PublishInterval", exp.PublishInterval, c.PublishInterval}, 20 | {"APIKey", exp.APIKey, c.APIKey}, 21 | {"Endpoint", exp.Endpoint, c.Endpoint}, 22 | {"Version", exp.Version, c.Version}, 23 | {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, 24 | {"Hostname", exp.Hostname, c.Hostname}, 25 | {"AppType", exp.AppType, c.AppType}, 26 | {"AppVersion", exp.AppVersion, c.AppVersion}, 27 | {"Transport", exp.Transport, c.Transport}, 28 | {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, 29 | } 30 | for _, tc := range tt { 31 | if !reflect.DeepEqual(tc.got, tc.expected) { 32 | t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) 33 | } 34 | } 35 | } 36 | 37 | func TestConfigUpdatesGivenNonDefaultValues(t *testing.T) { 38 | c := testConfig() 39 | exp := SessionTrackingConfiguration{ 40 | PublishInterval: 40 * time.Second, 41 | APIKey: "api234", 42 | Endpoint: "https://docs.bugsnag.com/platforms/go/", 43 | Version: "2.7.3", 44 | ReleaseStage: "Production", 45 | Hostname: "Brian's Surface", 46 | AppType: "Revel API", 47 | AppVersion: "6.3.9", 48 | NotifyReleaseStages: []string{"staging", "production"}, 49 | } 50 | c.Update(&exp) 51 | tt := []struct { 52 | name string 53 | expected interface{} 54 | got interface{} 55 | }{ 56 | {"PublishInterval", exp.PublishInterval, c.PublishInterval}, 57 | {"APIKey", exp.APIKey, c.APIKey}, 58 | {"Endpoint", exp.Endpoint, c.Endpoint}, 59 | {"Version", exp.Version, c.Version}, 60 | {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, 61 | {"Hostname", exp.Hostname, c.Hostname}, 62 | {"AppType", exp.AppType, c.AppType}, 63 | {"AppVersion", exp.AppVersion, c.AppVersion}, 64 | {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, 65 | } 66 | for _, tc := range tt { 67 | if !reflect.DeepEqual(tc.got, tc.expected) { 68 | t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) 69 | } 70 | } 71 | } 72 | 73 | func testConfig() SessionTrackingConfiguration { 74 | return SessionTrackingConfiguration{ 75 | PublishInterval: 20 * time.Second, 76 | APIKey: "api123", 77 | Endpoint: "https://bugsnag.com/jobs", //If you like what you see... ;) 78 | Version: "1.6.2", 79 | ReleaseStage: "Staging", 80 | Hostname: "Russ's MacbookPro", 81 | AppType: "Gin API", 82 | AppVersion: "5.2.8", 83 | NotifyReleaseStages: []string{"staging", "production"}, 84 | Transport: http.DefaultTransport, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sessions/payload.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | 7 | "github.com/bugsnag/bugsnag-go/device" 8 | ) 9 | 10 | // notifierPayload defines the .notifier subobject of the payload 11 | type notifierPayload struct { 12 | Name string `json:"name"` 13 | URL string `json:"url"` 14 | Version string `json:"version"` 15 | } 16 | 17 | // appPayload defines the .app subobject of the payload 18 | type appPayload struct { 19 | Type string `json:"type,omitempty"` 20 | ReleaseStage string `json:"releaseStage,omitempty"` 21 | Version string `json:"version,omitempty"` 22 | } 23 | 24 | // devicePayload defines the .device subobject of the payload 25 | type devicePayload struct { 26 | OsName string `json:"osName,omitempty"` 27 | Hostname string `json:"hostname,omitempty"` 28 | 29 | RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions"` 30 | } 31 | 32 | // sessionCountsPayload defines the .sessionCounts subobject of the payload 33 | type sessionCountsPayload struct { 34 | StartedAt string `json:"startedAt"` 35 | SessionsStarted int `json:"sessionsStarted"` 36 | } 37 | 38 | // sessionPayload defines the top level payload object 39 | type sessionPayload struct { 40 | Notifier *notifierPayload `json:"notifier"` 41 | App *appPayload `json:"app"` 42 | Device *devicePayload `json:"device"` 43 | SessionCounts []sessionCountsPayload `json:"sessionCounts"` 44 | } 45 | 46 | // makeSessionPayload creates a sessionPayload based off of the given sessions and config 47 | func makeSessionPayload(sessions []*Session, config *SessionTrackingConfiguration) *sessionPayload { 48 | releaseStage := config.ReleaseStage 49 | if releaseStage == "" { 50 | releaseStage = "production" 51 | } 52 | hostname := config.Hostname 53 | if hostname == "" { 54 | hostname = device.GetHostname() 55 | } 56 | 57 | return &sessionPayload{ 58 | Notifier: ¬ifierPayload{ 59 | Name: "Bugsnag Go", 60 | URL: "https://github.com/bugsnag/bugsnag-go", 61 | Version: config.Version, 62 | }, 63 | App: &appPayload{ 64 | Type: config.AppType, 65 | Version: config.AppVersion, 66 | ReleaseStage: releaseStage, 67 | }, 68 | Device: &devicePayload{ 69 | OsName: runtime.GOOS, 70 | Hostname: hostname, 71 | RuntimeVersions: device.GetRuntimeVersions(), 72 | }, 73 | SessionCounts: []sessionCountsPayload{ 74 | { 75 | //This timestamp assumes that we're sending these off once a minute 76 | StartedAt: sessions[0].StartedAt.UTC().Format(time.RFC3339), 77 | SessionsStarted: len(sessions), 78 | }, 79 | }, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /sessions/publisher.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/bugsnag/bugsnag-go/headers" 10 | ) 11 | 12 | // sessionPayloadVersion defines the current version of the payload that's 13 | // being sent to the session server. 14 | const sessionPayloadVersion = "1.0" 15 | 16 | type sessionPublisher interface { 17 | publish(sessions []*Session) error 18 | } 19 | 20 | type httpClient interface { 21 | Do(*http.Request) (*http.Response, error) 22 | } 23 | 24 | type publisher struct { 25 | config *SessionTrackingConfiguration 26 | client httpClient 27 | } 28 | 29 | // publish builds a payload from the given sessions and publishes them to the 30 | // session server. Returns any errors that happened as part of publishing. 31 | func (p *publisher) publish(sessions []*Session) error { 32 | if p.config.Endpoint == "" { 33 | // Session tracking is disabled, likely because the notify endpoint was 34 | // changed without changing the sessions endpoint 35 | // We've already logged a warning in this case, so no need to spam the 36 | // log every minute 37 | return nil 38 | } 39 | if apiKey := p.config.APIKey; len(apiKey) != 32 { 40 | return fmt.Errorf("bugsnag/sessions/publisher.publish invalid API key: '%s'", apiKey) 41 | } 42 | nrs, rs := p.config.NotifyReleaseStages, p.config.ReleaseStage 43 | if rs != "" && (nrs != nil && !contains(nrs, rs)) { 44 | // Always send sessions if the release stage is not set, but don't send any 45 | // sessions when notify release stages don't match the current release stage 46 | return nil 47 | } 48 | if len(sessions) == 0 { 49 | return fmt.Errorf("bugsnag/sessions/publisher.publish requested publication of 0") 50 | } 51 | p.config.mutex.Lock() 52 | defer p.config.mutex.Unlock() 53 | payload := makeSessionPayload(sessions, p.config) 54 | buf, err := json.Marshal(payload) 55 | if err != nil { 56 | return fmt.Errorf("bugsnag/sessions/publisher.publish unable to marshal json: %v", err) 57 | } 58 | req, err := http.NewRequest("POST", p.config.Endpoint, bytes.NewBuffer(buf)) 59 | if err != nil { 60 | return fmt.Errorf("bugsnag/sessions/publisher.publish unable to create request: %v", err) 61 | } 62 | for k, v := range headers.PrefixedHeaders(p.config.APIKey, sessionPayloadVersion) { 63 | req.Header.Add(k, v) 64 | } 65 | res, err := p.client.Do(req) 66 | if err != nil { 67 | return fmt.Errorf("bugsnag/sessions/publisher.publish unable to deliver session: %v", err) 68 | } 69 | defer func(res *http.Response) { 70 | if err := res.Body.Close(); err != nil { 71 | p.config.logf("%v", err) 72 | } 73 | }(res) 74 | if res.StatusCode != 202 { 75 | return fmt.Errorf("bugsnag/session.publish expected 202 response status, got HTTP %s", res.Status) 76 | } 77 | return nil 78 | } 79 | 80 | func contains(coll []string, e string) bool { 81 | for _, s := range coll { 82 | if s == e { 83 | return true 84 | } 85 | } 86 | return false 87 | } 88 | -------------------------------------------------------------------------------- /sessions/session.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | // EventCounts register how many handled/unhandled events have happened for 10 | // this session 11 | type EventCounts struct { 12 | Handled int `json:"handled"` 13 | Unhandled int `json:"unhandled"` 14 | } 15 | 16 | // Session represents a start time and a unique ID that identifies the session. 17 | type Session struct { 18 | StartedAt time.Time 19 | ID uuid.UUID 20 | EventCounts *EventCounts 21 | } 22 | 23 | func newSession() *Session { 24 | return &Session{ 25 | StartedAt: time.Now(), 26 | ID: uuid.New(), 27 | EventCounts: &EventCounts{}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sessions/startup.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/bugsnag/panicwrap" 9 | ) 10 | 11 | // SendStartupSession is called by Bugsnag on startup, which will send a 12 | // session to Bugsnag and return a context to represent the session of the main 13 | // goroutine. This is the session associated with any fatal panics that are 14 | // caught by panicwrap. 15 | func SendStartupSession(config *SessionTrackingConfiguration) context.Context { 16 | ctx := context.Background() 17 | session := newSession() 18 | if !config.IsAutoCaptureSessions() || isApplicationProcess() { 19 | return ctx 20 | } 21 | publisher := &publisher{ 22 | config: config, 23 | client: &http.Client{Transport: config.Transport}, 24 | } 25 | go publisher.publish([]*Session{session}) 26 | return context.WithValue(ctx, contextSessionKey, session) 27 | } 28 | 29 | // Checks to see if this is the application process, as opposed to the process 30 | // that monitors for panics 31 | func isApplicationProcess() bool { 32 | // Application process is run first, and this will only have been set when 33 | // the monitoring process runs 34 | return "" == os.Getenv(panicwrap.DEFAULT_COOKIE_KEY) 35 | } 36 | -------------------------------------------------------------------------------- /sessions/tracker_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type testPublisher struct { 11 | mutex sync.Mutex 12 | sessionsReceived [][]*Session 13 | } 14 | 15 | var pub = testPublisher{ 16 | mutex: sync.Mutex{}, 17 | sessionsReceived: [][]*Session{}, 18 | } 19 | 20 | func (pub *testPublisher) publish(sessions []*Session) error { 21 | pub.mutex.Lock() 22 | defer pub.mutex.Unlock() 23 | pub.sessionsReceived = append(pub.sessionsReceived, sessions) 24 | return nil 25 | } 26 | 27 | func TestStartSessionModifiesContext(t *testing.T) { 28 | type ctxKey string 29 | var k ctxKey 30 | k, v := "key", "val" 31 | st, c := makeSessionTracker() 32 | defer close(c) 33 | 34 | ctx := st.StartSession(context.WithValue(context.Background(), k, v)) 35 | if got, exp := ctx.Value(k), v; got != exp { 36 | t.Errorf("Changed pre-existing key '%s' with value '%s' into %s", k, v, got) 37 | } 38 | if got := ctx.Value(contextSessionKey); got == nil { 39 | t.Fatalf("No session information applied to context %v", ctx) 40 | } 41 | 42 | verifyValidSession(t, IncrementEventCountAndGetSession(ctx, true)) 43 | } 44 | 45 | func TestShouldOnlyWriteWhenReceivingSessions(t *testing.T) { 46 | st, c := makeSessionTracker() 47 | defer close(c) 48 | go st.processSessions() 49 | time.Sleep(10 * st.config.PublishInterval) // Would publish many times in this time period if there were sessions 50 | 51 | if got := pub.sessionsReceived; len(got) != 0 { 52 | t.Errorf("pub was invoked unexpectedly %d times with arguments: %v", len(got), got) 53 | } 54 | 55 | for i := 0; i < 50000; i++ { 56 | st.StartSession(context.Background()) 57 | } 58 | time.Sleep(st.config.PublishInterval * 2) 59 | 60 | var sessions []*Session 61 | pub.mutex.Lock() 62 | defer pub.mutex.Unlock() 63 | for _, s := range pub.sessionsReceived { 64 | for _, session := range s { 65 | verifyValidSession(t, session) 66 | sessions = append(sessions, session) 67 | } 68 | } 69 | if exp, got := 50000, len(sessions); exp != got { 70 | t.Errorf("Expected %d sessions but got %d", exp, got) 71 | } 72 | 73 | } 74 | 75 | func makeSessionTracker() (*sessionTracker, chan *Session) { 76 | c := make(chan *Session, 1) 77 | return &sessionTracker{ 78 | config: &SessionTrackingConfiguration{ 79 | PublishInterval: time.Millisecond * 10, //Publish very fast 80 | }, 81 | sessionChannel: c, 82 | sessions: []*Session{}, 83 | publisher: &pub, 84 | }, c 85 | } 86 | 87 | func verifyValidSession(t *testing.T, s *Session) { 88 | if (s.StartedAt == time.Time{}) { 89 | t.Errorf("Expected start time to be set but was nil") 90 | } 91 | if len(s.ID) != 16 { 92 | t.Errorf("Expected UUID to be a valid V4 UUID but was %s", s.ID) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /v2/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Bugsnag 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /v2/device/hostname.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import "os" 4 | 5 | var hostname string 6 | 7 | // GetHostname returns the hostname of the current device. Caches the hostname 8 | // between calls to ensure this is performant. Returns a blank string in case 9 | // that the hostname cannot be identified. 10 | func GetHostname() string { 11 | if hostname == "" { 12 | hostname, _ = os.Hostname() 13 | } 14 | return hostname 15 | } 16 | -------------------------------------------------------------------------------- /v2/device/runtimeversions.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "runtime" 5 | ) 6 | 7 | // Cached runtime versions that can be updated globally by framework 8 | // integrations through AddVersion. 9 | var versions *RuntimeVersions 10 | 11 | // RuntimeVersions define the various versions of Go and any framework that may 12 | // be in use. 13 | // As a user of the notifier you're unlikely to need to modify this struct. 14 | // As such, the authors reserve the right to introduce breaking changes to the 15 | // properties in this struct. In particular the framework versions are liable 16 | // to change in new versions of the notifier in minor/patch versions. 17 | type RuntimeVersions struct { 18 | Go string `json:"go"` 19 | 20 | Gin string `json:"gin,omitempty"` 21 | Martini string `json:"martini,omitempty"` 22 | Negroni string `json:"negroni,omitempty"` 23 | Revel string `json:"revel,omitempty"` 24 | } 25 | 26 | // GetRuntimeVersions retrieves the recorded runtime versions in a goroutine-safe manner. 27 | func GetRuntimeVersions() *RuntimeVersions { 28 | if versions == nil { 29 | versions = &RuntimeVersions{Go: runtime.Version()} 30 | } 31 | return versions 32 | } 33 | 34 | // AddVersion permits a framework to register its version, assuming it's one of 35 | // the officially supported frameworks. 36 | func AddVersion(framework, version string) { 37 | if versions == nil { 38 | versions = &RuntimeVersions{Go: runtime.Version()} 39 | } 40 | switch framework { 41 | case "Martini": 42 | versions.Martini = version 43 | case "Gin": 44 | versions.Gin = version 45 | case "Negroni": 46 | versions.Negroni = version 47 | case "Revel": 48 | versions.Revel = version 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /v2/device/runtimeversions_test.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | ) 7 | 8 | func TestPristineRuntimeVersions(t *testing.T) { 9 | versions = nil // reset global variable 10 | rv := GetRuntimeVersions() 11 | for _, tc := range []struct{ name, got, exp string }{ 12 | {name: "Go", got: rv.Go, exp: runtime.Version()}, 13 | {name: "Gin", got: rv.Gin, exp: ""}, 14 | {name: "Martini", got: rv.Martini, exp: ""}, 15 | {name: "Negroni", got: rv.Negroni, exp: ""}, 16 | {name: "Revel", got: rv.Revel, exp: ""}, 17 | } { 18 | if tc.got != tc.exp { 19 | t.Errorf("expected pristine '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) 20 | } 21 | } 22 | } 23 | 24 | func TestModifiedRuntimeVersions(t *testing.T) { 25 | versions = nil // reset global variable 26 | rv := GetRuntimeVersions() 27 | AddVersion("Gin", "1.2.1") 28 | AddVersion("Martini", "1.0.0") 29 | AddVersion("Negroni", "1.0.2") 30 | AddVersion("Revel", "0.20.1") 31 | for _, tc := range []struct{ name, got, exp string }{ 32 | {name: "Go", got: rv.Go, exp: runtime.Version()}, 33 | {name: "Gin", got: rv.Gin, exp: "1.2.1"}, 34 | {name: "Martini", got: rv.Martini, exp: "1.0.0"}, 35 | {name: "Negroni", got: rv.Negroni, exp: "1.0.2"}, 36 | {name: "Revel", got: rv.Revel, exp: "0.20.1"}, 37 | } { 38 | if tc.got != tc.exp { 39 | t.Errorf("expected modified '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /v2/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bugsnag captures errors in real-time and reports them to BugSnag (http://bugsnag.com). 3 | 4 | Using bugsnag-go is a three-step process. 5 | 6 | 1. As early as possible in your program configure the notifier with your APIKey. This sets up 7 | handling of panics that would otherwise crash your app. 8 | 9 | func init() { 10 | bugsnag.Configure(bugsnag.Configuration{ 11 | APIKey: "YOUR_API_KEY_HERE", 12 | }) 13 | } 14 | 15 | 2. Add bugsnag to places that already catch panics. For example you should add it to the HTTP server 16 | when you call ListenAndServer: 17 | 18 | http.ListenAndServe(":8080", bugsnag.Handler(nil)) 19 | 20 | If that's not possible, you can also wrap each HTTP handler manually: 21 | 22 | http.HandleFunc("/" bugsnag.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { 23 | ... 24 | }) 25 | 26 | 3. To notify BugSnag of an error that is not a panic, pass it to bugsnag.Notify. This will also 27 | log the error message using the configured Logger. 28 | 29 | if err != nil { 30 | bugsnag.Notify(err) 31 | } 32 | 33 | For detailed integration instructions see https://docs.bugsnag.com/platforms/go. 34 | 35 | # Configuration 36 | 37 | The only required configuration is the BugSnag API key which can be obtained by clicking "Project 38 | Settings" on the top of your BugSnag dashboard after signing up. We also recommend you set the 39 | ReleaseStage, AppType, and AppVersion if these make sense for your deployment workflow. 40 | 41 | # RawData 42 | 43 | If you need to attach extra data to BugSnag events, you can do that using the rawData mechanism. 44 | Most of the functions that send errors to BugSnag allow you to pass in any number of interface{} 45 | values as rawData. The rawData can consist of the Severity, Context, User or MetaData types listed 46 | below, and there is also builtin support for *http.Requests. 47 | 48 | bugsnag.Notify(err, bugsnag.SeverityError) 49 | 50 | If you want to add custom tabs to your bugsnag dashboard you can pass any value in as rawData, 51 | and then process it into the event's metadata using a bugsnag.OnBeforeNotify() hook. 52 | 53 | bugsnag.Notify(err, account) 54 | 55 | bugsnag.OnBeforeNotify(func (e *bugsnag.Event, c *bugsnag.Configuration) { 56 | for datum := range e.RawData { 57 | if account, ok := datum.(Account); ok { 58 | e.MetaData.Add("account", "name", account.Name) 59 | e.MetaData.Add("account", "url", account.URL) 60 | } 61 | } 62 | }) 63 | 64 | If necessary you can pass Configuration in as rawData, or modify the Configuration object passed 65 | into OnBeforeNotify hooks. Configuration passed in this way only affects the current notification. 66 | */ 67 | package bugsnag 68 | -------------------------------------------------------------------------------- /v2/env_metadata.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const metadataPrefix string = "BUGSNAG_METADATA_" 9 | const metadataPrefixLen int = len(metadataPrefix) 10 | const metadataDefaultTab string = "custom" 11 | 12 | type envMetadata struct { 13 | tab string 14 | key string 15 | value string 16 | } 17 | 18 | func loadEnvMetadata(environ []string) []envMetadata { 19 | metadata := make([]envMetadata, 0) 20 | for _, value := range environ { 21 | key, value, err := parseEnvironmentPair(value) 22 | if err != nil { 23 | continue 24 | } 25 | if keypath, err := parseMetadataKeypath(key); err == nil { 26 | tab, key := splitTabKeyValues(keypath) 27 | metadata = append(metadata, envMetadata{tab, key, value}) 28 | } 29 | } 30 | return metadata 31 | } 32 | 33 | func splitTabKeyValues(keypath string) (string, string) { 34 | key_components := strings.SplitN(keypath, "_", 2) 35 | if len(key_components) > 1 { 36 | return key_components[0], key_components[1] 37 | } 38 | return metadataDefaultTab, keypath 39 | } 40 | 41 | func parseMetadataKeypath(key string) (string, error) { 42 | if strings.HasPrefix(key, metadataPrefix) && len(key) > metadataPrefixLen { 43 | return strings.TrimPrefix(key, metadataPrefix), nil 44 | } 45 | return "", fmt.Errorf("No metadata prefix found") 46 | } 47 | -------------------------------------------------------------------------------- /v2/env_metadata_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import "testing" 4 | 5 | func TestParseMetadataKeypath(t *testing.T) { 6 | type output struct { 7 | keypath string 8 | err string 9 | } 10 | cases := map[string]output{ 11 | "": {"", "No metadata prefix found"}, 12 | "BUGSNAG_METADATA_": {"", "No metadata prefix found"}, 13 | "BUGSNAG_METADATA_key": {"key", ""}, 14 | "BUGSNAG_METADATA_device_foo": {"device_foo", ""}, 15 | "BUGSNAG_METADATA_device_foo_two": {"device_foo_two", ""}, 16 | } 17 | 18 | for input, expected := range cases { 19 | keypath, err := parseMetadataKeypath(input) 20 | if len(expected.err) > 0 && (err == nil || err.Error() != expected.err) { 21 | t.Errorf("expected error with message '%s', got '%v'", expected.err, err) 22 | } 23 | if expected.keypath != keypath { 24 | t.Errorf("expected keypath '%s', got '%s'", expected.keypath, keypath) 25 | } 26 | } 27 | } 28 | 29 | func TestLoadEnvMetadata(t *testing.T) { 30 | cases := map[string]envMetadata{ 31 | "": {"", "", ""}, 32 | "BUGSNAG_METADATA_Orange=tomato_paste": {"custom", "Orange", "tomato_paste"}, 33 | "BUGSNAG_METADATA_true_orange=tomato_paste": {"true", "orange", "tomato_paste"}, 34 | "BUGSNAG_METADATA_color_Orange=tomato_paste": {"color", "Orange", "tomato_paste"}, 35 | "BUGSNAG_METADATA_color_Orange_hue=tomato_paste": {"color", "Orange_hue", "tomato_paste"}, 36 | "BUGSNAG_METADATA_crayonColor_Magenta=tomato_paste": {"crayonColor", "Magenta", "tomato_paste"}, 37 | "BUGSNAG_METADATA_crayonColor_Magenta_hue=tomato_paste": {"crayonColor", "Magenta_hue", "tomato_paste"}, 38 | } 39 | 40 | for input, expected := range cases { 41 | metadata := loadEnvMetadata([]string{input}) 42 | 43 | if len(expected.tab) == 0 { 44 | for _, m := range metadata { 45 | t.Errorf("erroneously added a value for '%s' to tab '%s':'%s'", input, m.tab, m.key) 46 | } 47 | } else { 48 | if len(metadata) != 1 { 49 | t.Fatalf("wrong number of metadata elements: %d %v", len(metadata), metadata) 50 | } 51 | m := metadata[0] 52 | if m.tab != expected.tab { 53 | t.Errorf("wrong tab '%s'", expected.tab) 54 | continue 55 | } 56 | if m.key != expected.key { 57 | t.Errorf("wrong key '%s'", expected.key) 58 | continue 59 | } 60 | if m.value != expected.value { 61 | t.Errorf("incorrect value added to keypath: '%s'", m.value) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /v2/environment.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func parseEnvironmentPair(pair string) (string, string, error) { 9 | components := strings.SplitN(pair, "=", 2) 10 | if len(components) < 2 { 11 | return "", "", fmt.Errorf("Not a '='-delimited key pair") 12 | } 13 | return components[0], components[1], nil 14 | } 15 | -------------------------------------------------------------------------------- /v2/environment_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestParsePairs(t *testing.T) { 9 | type output struct { 10 | key, value string 11 | err error 12 | } 13 | 14 | cases := map[string]output{ 15 | "":{"", "", fmt.Errorf("Not a '='-delimited key pair")}, 16 | "key=value":{"key", "value", nil}, 17 | "key=value=bar":{"key", "value=bar", nil}, 18 | "something":{"", "", fmt.Errorf("Not a '='-delimited key pair")}, 19 | } 20 | for input, expected := range cases { 21 | key, value, err := parseEnvironmentPair(input) 22 | if expected.err != nil && (err == nil || err.Error() != expected.err.Error()) { 23 | t.Errorf("expected error '%v', got '%v'", expected.err, err) 24 | } 25 | if key != expected.key || value != expected.value { 26 | t.Errorf("expected pair '%s'='%s', got '%s'='%s'", expected.key, expected.value, key, value) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /v2/errors/README.md: -------------------------------------------------------------------------------- 1 | Adds stacktraces to errors in golang. 2 | 3 | This was made to help build the Bugsnag notifier but can be used standalone if 4 | you like to have stacktraces on errors. 5 | 6 | See [Godoc](https://godoc.org/github.com/bugsnag/bugsnag-go/v2/errors) for the API docs. 7 | -------------------------------------------------------------------------------- /v2/errors/error_fmt_wrap_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package errors 4 | 5 | import ( 6 | "fmt" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | func TestUnwrapErrorsCause(t *testing.T) { 12 | _, _, line, ok := runtime.Caller(0) // grab line immediately before error generators 13 | err1 := fmt.Errorf("invalid token") 14 | err2 := fmt.Errorf("login failed: %w", err1) 15 | err3 := fmt.Errorf("terminate process: %w", err2) 16 | unwrapped := New(err3, 0) 17 | if !ok { 18 | t.Fatalf("Something has gone wrong with loading the current stack") 19 | } 20 | if unwrapped.Error() != "terminate process: login failed: invalid token" { 21 | t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) 22 | } 23 | assertStacksMatch(t, []StackFrame{ 24 | StackFrame{Name: "TestUnwrapErrorsCause", File: "errors/error_fmt_wrap_test.go", LineNumber: line + 4}, 25 | }, unwrapped.StackFrames()) 26 | if unwrapped.Cause == nil { 27 | t.Fatalf("Failed to capture cause error") 28 | } 29 | if unwrapped.Cause.Error() != "login failed: invalid token" { 30 | t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error()) 31 | } 32 | if len(unwrapped.Cause.StackFrames()) > 0 { 33 | t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.StackFrames()) 34 | } 35 | if unwrapped.Cause.Cause == nil { 36 | t.Fatalf("Failed to capture nested cause error") 37 | } 38 | if len(unwrapped.Cause.Cause.StackFrames()) > 0 { 39 | t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames()) 40 | } 41 | if unwrapped.Cause.Cause.Cause != nil { 42 | t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /v2/errors/error_unwrap.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package errors 4 | 5 | import ( 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // Unwrap returns the result of calling errors.Unwrap on the underlying error 10 | func (err *Error) Unwrap() error { 11 | return errors.Unwrap(err.Err) 12 | } 13 | -------------------------------------------------------------------------------- /v2/errors/error_unwrap_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package errors 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func TestFindingErrorInChain(t *testing.T) { 13 | baseErr := errors.New("base error") 14 | wrappedErr := errors.Wrap(baseErr, "failed") 15 | err := New(wrappedErr, 0) 16 | 17 | if !errors.Is(err, baseErr) { 18 | t.Errorf("Failed to find base error: %s", err.Error()) 19 | } 20 | } 21 | 22 | func TestErrorUnwrapping(t *testing.T) { 23 | baseErr := errors.New("base error") 24 | wrappedErr := fmt.Errorf("failed: %w", baseErr) 25 | err := New(wrappedErr, 0) 26 | 27 | unwrapped := errors.Unwrap(err) 28 | 29 | if unwrapped != baseErr { 30 | t.Errorf("Failed to find base error: %s", unwrapped.Error()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /v2/errors/parse_panic.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type uncaughtPanic struct { 9 | typeName string 10 | message string 11 | } 12 | 13 | func (p uncaughtPanic) Error() string { 14 | return p.message 15 | } 16 | 17 | // ParsePanic allows you to get an error object from the output of a go program 18 | // that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap. 19 | func ParsePanic(text string) (*Error, error) { 20 | lines := strings.Split(text, "\n") 21 | prefixes := []string{"panic:", "fatal error:"} 22 | 23 | state := "start" 24 | 25 | var message string 26 | var typeName string 27 | var stack []StackFrame 28 | 29 | for i := 0; i < len(lines); i++ { 30 | line := lines[i] 31 | 32 | if state == "start" { 33 | for _, prefix := range prefixes { 34 | if strings.HasPrefix(line, prefix) { 35 | message = strings.TrimSpace(strings.TrimPrefix(line, prefix)) 36 | typeName = prefix[:len(prefix) - 1] 37 | state = "seek" 38 | break 39 | } 40 | } 41 | if state == "start" { 42 | return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line) 43 | } 44 | 45 | } else if state == "seek" { 46 | if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") { 47 | state = "parsing" 48 | } 49 | 50 | } else if state == "parsing" { 51 | if line == "" { 52 | state = "done" 53 | break 54 | } 55 | createdBy := false 56 | if strings.HasPrefix(line, "created by ") { 57 | line = strings.TrimPrefix(line, "created by ") 58 | createdBy = true 59 | } 60 | 61 | i++ 62 | 63 | if i >= len(lines) { 64 | return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line) 65 | } 66 | 67 | frame, err := parsePanicFrame(line, lines[i], createdBy) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | stack = append(stack, *frame) 73 | if createdBy { 74 | state = "done" 75 | break 76 | } 77 | } 78 | } 79 | 80 | if state == "done" || state == "parsing" { 81 | return &Error{Err: uncaughtPanic{typeName, message}, frames: stack}, nil 82 | } 83 | return nil, Errorf("could not parse panic: %v", text) 84 | } 85 | 86 | // The lines we're passing look like this: 87 | // 88 | // main.(*foo).destruct(0xc208067e98) 89 | // /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151 90 | func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) { 91 | idx := strings.LastIndex(name, "(") 92 | if idx == -1 && !createdBy { 93 | return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name) 94 | } 95 | if idx != -1 { 96 | name = name[:idx] 97 | } 98 | pkg := "" 99 | 100 | if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { 101 | pkg += name[:lastslash] + "/" 102 | name = name[lastslash+1:] 103 | } 104 | if period := strings.Index(name, "."); period >= 0 { 105 | pkg += name[:period] 106 | name = name[period+1:] 107 | } 108 | 109 | name = strings.Replace(name, "·", ".", -1) 110 | 111 | if !strings.HasPrefix(line, "\t") { 112 | return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line) 113 | } 114 | 115 | idx = strings.LastIndex(line, ":") 116 | if idx == -1 { 117 | return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line) 118 | } 119 | file := line[1:idx] 120 | 121 | number := line[idx+1:] 122 | if idx = strings.Index(number, " +"); idx > -1 { 123 | number = number[:idx] 124 | } 125 | 126 | lno, err := strconv.ParseInt(number, 10, 32) 127 | if err != nil { 128 | return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line) 129 | } 130 | 131 | return &StackFrame{ 132 | File: file, 133 | LineNumber: int(lno), 134 | Package: pkg, 135 | Name: name, 136 | }, nil 137 | } 138 | -------------------------------------------------------------------------------- /v2/errors/stackframe.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "runtime" 8 | "strings" 9 | ) 10 | 11 | // A StackFrame contains all necessary information about to generate a line 12 | // in a callstack. 13 | type StackFrame struct { 14 | File string 15 | LineNumber int 16 | Name string 17 | Package string 18 | ProgramCounter uintptr 19 | function *runtime.Func 20 | } 21 | 22 | // NewStackFrame popoulates a stack frame object from the program counter. 23 | func NewStackFrame(pc uintptr) (frame StackFrame) { 24 | 25 | frame = StackFrame{ProgramCounter: pc} 26 | if frame.Func() == nil { 27 | return 28 | } 29 | frame.Package, frame.Name = packageAndName(frame.Func()) 30 | 31 | // pc -1 because the program counters we use are usually return addresses, 32 | // and we want to show the line that corresponds to the function call 33 | frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) 34 | return 35 | 36 | } 37 | 38 | // Func returns the function that this stackframe corresponds to 39 | func (frame *StackFrame) Func() *runtime.Func { 40 | return frame.function 41 | } 42 | 43 | // String returns the stackframe formatted in the same way as go does 44 | // in runtime/debug.Stack() 45 | func (frame *StackFrame) String() string { 46 | str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) 47 | 48 | source, err := frame.SourceLine() 49 | if err != nil { 50 | return str 51 | } 52 | 53 | return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) 54 | } 55 | 56 | // SourceLine gets the line of code (from File and Line) of the original source if possible 57 | func (frame *StackFrame) SourceLine() (string, error) { 58 | data, err := ioutil.ReadFile(frame.File) 59 | 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | lines := bytes.Split(data, []byte{'\n'}) 65 | if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { 66 | return "???", nil 67 | } 68 | // -1 because line-numbers are 1 based, but our array is 0 based 69 | return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil 70 | } 71 | 72 | func packageAndName(fn *runtime.Func) (string, string) { 73 | if fn == nil { 74 | return "", "" 75 | } 76 | 77 | name := fn.Name() 78 | pkg := "" 79 | 80 | // The name includes the path name to the package, which is unnecessary 81 | // since the file name is already included. Plus, it has center dots. 82 | // That is, we see 83 | // runtime/debug.*T·ptrmethod 84 | // and want 85 | // *T.ptrmethod 86 | // Since the package path might contains dots (e.g. code.google.com/...), 87 | // we first remove the path prefix if there is one. 88 | if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { 89 | pkg += name[:lastslash] + "/" 90 | name = name[lastslash+1:] 91 | } 92 | if period := strings.Index(name, "."); period >= 0 { 93 | pkg += name[:period] 94 | name = name[period+1:] 95 | } 96 | 97 | name = strings.Replace(name, "·", ".", -1) 98 | return pkg, name 99 | } 100 | -------------------------------------------------------------------------------- /v2/event_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestPopulateEvent(t *testing.T) { 13 | event := new(Event) 14 | contexts := make(chan context.Context, 1) 15 | reqs := make(chan *http.Request, 1) 16 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | contexts <- AttachRequestData(r.Context(), r) 18 | reqs <- r 19 | })) 20 | defer ts.Close() 21 | 22 | http.Get(ts.URL + "/serenity?q=abcdef") 23 | 24 | ctx, req := <-contexts, <-reqs 25 | populateEventWithContext(ctx, event) 26 | 27 | for _, tc := range []struct{ e, c interface{} }{ 28 | {e: event.Ctx, c: ctx}, 29 | {e: event.Request, c: extractRequestInfoFromReq(req)}, 30 | {e: event.Context, c: req.URL.Path}, 31 | {e: event.User.Id, c: req.RemoteAddr[:strings.LastIndex(req.RemoteAddr, ":")]}, 32 | } { 33 | if !reflect.DeepEqual(tc.e, tc.c) { 34 | t.Errorf("Expected '%+v' and '%+v' to be equal", tc.e, tc.c) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bugsnag/bugsnag-go/v2 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/bitly/go-simplejson v0.5.1 7 | github.com/bugsnag/panicwrap v1.3.4 8 | github.com/google/uuid v1.6.0 9 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 10 | github.com/pkg/errors v0.9.1 11 | ) 12 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= 2 | github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= 3 | github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= 4 | github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= 5 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 6 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 8 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | -------------------------------------------------------------------------------- /v2/headers/prefixed.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // PrefixedHeaders returns a map of Content-Type and the 'Bugsnag-' headers for 8 | // API key, payload version, and the time at which the request is being sent. 9 | func PrefixedHeaders(apiKey, payloadVersion string) map[string]string { 10 | return map[string]string{ 11 | "Content-Type": "application/json", 12 | "Bugsnag-Api-Key": apiKey, 13 | "Bugsnag-Payload-Version": payloadVersion, 14 | "Bugsnag-Sent-At": time.Now().UTC().Format(time.RFC3339), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /v2/headers/prefixed_test.go: -------------------------------------------------------------------------------- 1 | package headers 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const APIKey = "abcd1234abcd1234" 10 | const testPayloadVersion = "3" 11 | 12 | func TestConstantBugsnagPrefixedHeaders(t *testing.T) { 13 | headers := PrefixedHeaders(APIKey, testPayloadVersion) 14 | testCases := []struct { 15 | header string 16 | expected string 17 | }{ 18 | {header: "Content-Type", expected: "application/json"}, 19 | {header: "Bugsnag-Api-Key", expected: APIKey}, 20 | {header: "Bugsnag-Payload-Version", expected: testPayloadVersion}, 21 | } 22 | for _, tc := range testCases { 23 | t.Run(tc.header, func(st *testing.T) { 24 | if got := headers[tc.header]; got != tc.expected { 25 | t.Errorf("Expected headers to contain %s header %s but was %s", tc.header, tc.expected, got) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func TestTimeDependentBugsnagPrefixedHeaders(t *testing.T) { 32 | headers := PrefixedHeaders(APIKey, testPayloadVersion) 33 | sentAtString := headers["Bugsnag-Sent-At"] 34 | if !strings.HasSuffix(sentAtString, "Z") { 35 | t.Errorf("Error when setting Bugsnag-Sent-At header: %s, doesn't end with a Z", sentAtString) 36 | } 37 | sentAt, err := time.Parse(time.RFC3339, sentAtString) 38 | 39 | if err != nil { 40 | t.Errorf("Error when attempting to parse Bugsnag-Sent-At header: %s", sentAtString) 41 | } 42 | 43 | if now := time.Now(); now.Sub(sentAt) > time.Second || now.Sub(sentAt) < -time.Second { 44 | t.Errorf("Expected Bugsnag-Sent-At header approx. %s but was %s", now.UTC().Format(time.RFC3339), sentAtString) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /v2/json_tags.go: -------------------------------------------------------------------------------- 1 | // The code is stripped from: 2 | // http://golang.org/src/pkg/encoding/json/tags.go?m=text 3 | 4 | package bugsnag 5 | 6 | import ( 7 | "strings" 8 | ) 9 | 10 | // tagOptions is the string following a comma in a struct field's "json" 11 | // tag, or the empty string. It does not include the leading comma. 12 | type tagOptions string 13 | 14 | // parseTag splits a struct field's json tag into its name and 15 | // comma-separated options. 16 | func parseTag(tag string) (string, tagOptions) { 17 | if idx := strings.Index(tag, ","); idx != -1 { 18 | return tag[:idx], tagOptions(tag[idx+1:]) 19 | } 20 | return tag, tagOptions("") 21 | } 22 | 23 | // Contains reports whether a comma-separated list of options 24 | // contains a particular substr flag. substr must be surrounded by a 25 | // string boundary or commas. 26 | func (o tagOptions) Contains(optionName string) bool { 27 | if len(o) == 0 { 28 | return false 29 | } 30 | s := string(o) 31 | for s != "" { 32 | var next string 33 | i := strings.Index(s, ",") 34 | if i >= 0 { 35 | s, next = s[:i], s[i+1:] 36 | } 37 | if s == optionName { 38 | return true 39 | } 40 | s = next 41 | } 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /v2/middleware.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type ( 8 | beforeFunc func(*Event, *Configuration) error 9 | 10 | // MiddlewareStacks keep middleware in the correct order. They are 11 | // called in reverse order, so if you add a new middleware it will 12 | // be called before all existing middleware. 13 | middlewareStack struct { 14 | before []beforeFunc 15 | } 16 | ) 17 | 18 | // AddMiddleware adds a new middleware to the outside of the existing ones, 19 | // when the middlewareStack is Run it will be run before all middleware that 20 | // have been added before. 21 | func (stack *middlewareStack) OnBeforeNotify(middleware beforeFunc) { 22 | stack.before = append(stack.before, middleware) 23 | } 24 | 25 | // Run causes all the middleware to be run. If they all permit it the next callback 26 | // will be called with all the middleware on the stack. 27 | func (stack *middlewareStack) Run(event *Event, config *Configuration, next func() error) error { 28 | // run all the before filters in reverse order 29 | for i := range stack.before { 30 | before := stack.before[len(stack.before)-i-1] 31 | 32 | severity := event.Severity 33 | err := stack.runBeforeFilter(before, event, config) 34 | if err != nil { 35 | return err 36 | } 37 | if event.Severity != severity { 38 | event.handledState.SeverityReason = SeverityReasonCallbackSpecified 39 | } 40 | } 41 | 42 | return next() 43 | } 44 | 45 | func (stack *middlewareStack) runBeforeFilter(f beforeFunc, event *Event, config *Configuration) error { 46 | defer func() { 47 | if err := recover(); err != nil { 48 | config.logf("bugsnag/middleware: unexpected panic: %v", err) 49 | } 50 | }() 51 | 52 | return f(event, config) 53 | } 54 | 55 | // httpRequestMiddleware is added OnBeforeNotify by default. It takes information 56 | // from an http.Request passed in as rawData, and adds it to the Event. You can 57 | // use this as a template for writing your own Middleware. 58 | func httpRequestMiddleware(event *Event, config *Configuration) error { 59 | for _, datum := range event.RawData { 60 | if request, ok := datum.(*http.Request); ok && request != nil { 61 | event.MetaData.Update(MetaData{ 62 | "request": { 63 | "params": request.URL.Query(), 64 | }, 65 | }) 66 | } 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /v2/middleware_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/bugsnag/bugsnag-go/v2/errors" 12 | ) 13 | 14 | func TestMiddlewareOrder(t *testing.T) { 15 | 16 | err := fmt.Errorf("test") 17 | data := []interface{}{errors.New(err, 1)} 18 | event, config := newEvent(data, &defaultNotifier) 19 | 20 | result := make([]int, 0, 7) 21 | stack := middlewareStack{} 22 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 23 | result = append(result, 2) 24 | return nil 25 | }) 26 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 27 | result = append(result, 1) 28 | return nil 29 | }) 30 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 31 | result = append(result, 0) 32 | return nil 33 | }) 34 | 35 | stack.Run(event, config, func() error { 36 | result = append(result, 3) 37 | return nil 38 | }) 39 | 40 | if !reflect.DeepEqual(result, []int{0, 1, 2, 3}) { 41 | t.Errorf("unexpected middleware order %v", result) 42 | } 43 | } 44 | 45 | func TestBeforeNotifyReturnErr(t *testing.T) { 46 | 47 | stack := middlewareStack{} 48 | err := fmt.Errorf("test") 49 | data := []interface{}{errors.New(err, 1)} 50 | event, config := newEvent(data, &defaultNotifier) 51 | 52 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 53 | return err 54 | }) 55 | 56 | called := false 57 | 58 | e := stack.Run(event, config, func() error { 59 | called = true 60 | return nil 61 | }) 62 | 63 | if e != err { 64 | t.Errorf("Middleware didn't return the error") 65 | } 66 | 67 | if called == true { 68 | t.Errorf("Notify was called when BeforeNotify returned False") 69 | } 70 | } 71 | 72 | func TestBeforeNotifyPanic(t *testing.T) { 73 | 74 | stack := middlewareStack{} 75 | err := fmt.Errorf("test") 76 | event, _ := newEvent([]interface{}{errors.New(err, 1)}, &defaultNotifier) 77 | 78 | stack.OnBeforeNotify(func(e *Event, c *Configuration) error { 79 | panic("oops") 80 | }) 81 | 82 | called := false 83 | b := &bytes.Buffer{} 84 | 85 | stack.Run(event, &Configuration{Logger: log.New(b, log.Prefix(), 0)}, func() error { 86 | called = true 87 | return nil 88 | }) 89 | 90 | logged := b.String() 91 | 92 | if logged != "bugsnag/middleware: unexpected panic: oops\n" { 93 | t.Errorf("Logged: %s", logged) 94 | } 95 | 96 | if called == false { 97 | t.Errorf("Notify was not called when BeforeNotify panicked") 98 | } 99 | } 100 | 101 | func TestHttpRequestMiddleware(t *testing.T) { 102 | var req *http.Request 103 | rawData := []interface{}{req} 104 | 105 | event := &Event{RawData: rawData} 106 | config := &Configuration{} 107 | err := httpRequestMiddleware(event, config) 108 | if err != nil { 109 | t.Errorf("Should not happen") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /v2/panicwrap.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "github.com/bugsnag/bugsnag-go/v2/errors" 5 | "github.com/bugsnag/bugsnag-go/v2/sessions" 6 | "github.com/bugsnag/panicwrap" 7 | ) 8 | 9 | // Forks and re-runs your program to add panic monitoring. This function does 10 | // not return on one process, instead listening on stderr of the other process, 11 | // which returns nil. 12 | // 13 | // Related: https://godoc.org/github.com/bugsnag/panicwrap#BasicMonitor 14 | func defaultPanicHandler() { 15 | defer defaultNotifier.dontPanic() 16 | ctx := sessions.SendStartupSession(&sessionTrackingConfig) 17 | 18 | err := panicwrap.BasicMonitor(func(output string) { 19 | toNotify, err := errors.ParsePanic(output) 20 | 21 | if err != nil { 22 | defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) 23 | } 24 | state := HandledState{SeverityReasonUnhandledPanic, SeverityError, true, ""} 25 | defaultNotifier.NotifySync(toNotify, true, state, ctx) 26 | 27 | }) 28 | 29 | if err != nil { 30 | defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /v2/payload.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "runtime" 9 | "sync" 10 | "time" 11 | 12 | "github.com/bugsnag/bugsnag-go/v2/device" 13 | "github.com/bugsnag/bugsnag-go/v2/headers" 14 | "github.com/bugsnag/bugsnag-go/v2/sessions" 15 | ) 16 | 17 | const notifyPayloadVersion = "4" 18 | 19 | var sessionMutex sync.Mutex 20 | 21 | type payload struct { 22 | *Event 23 | *Configuration 24 | } 25 | 26 | type hash map[string]interface{} 27 | 28 | func (p *payload) deliver() error { 29 | 30 | if len(p.APIKey) != 32 { 31 | return fmt.Errorf("bugsnag/payload.deliver: invalid api key: '%s'", p.APIKey) 32 | } 33 | 34 | buf, err := p.MarshalJSON() 35 | if err != nil { 36 | return fmt.Errorf("bugsnag/payload.deliver: %v", err) 37 | } 38 | 39 | client := http.Client{ 40 | Transport: p.Transport, 41 | } 42 | req, err := http.NewRequest("POST", p.Endpoints.Notify, bytes.NewBuffer(buf)) 43 | if err != nil { 44 | return fmt.Errorf("bugsnag/payload.deliver unable to create request: %v", err) 45 | } 46 | for k, v := range headers.PrefixedHeaders(p.APIKey, notifyPayloadVersion) { 47 | req.Header.Add(k, v) 48 | } 49 | resp, err := client.Do(req) 50 | if err != nil { 51 | return fmt.Errorf("bugsnag/payload.deliver: %v", err) 52 | } 53 | defer resp.Body.Close() 54 | 55 | if resp.StatusCode != 200 { 56 | return fmt.Errorf("bugsnag/payload.deliver: Got HTTP %s", resp.Status) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func (p *payload) MarshalJSON() ([]byte, error) { 63 | return json.Marshal(reportJSON{ 64 | APIKey: p.APIKey, 65 | Events: []eventJSON{ 66 | eventJSON{ 67 | App: &appJSON{ 68 | ReleaseStage: p.ReleaseStage, 69 | Type: p.AppType, 70 | Version: p.AppVersion, 71 | }, 72 | Context: p.Context, 73 | Device: &deviceJSON{ 74 | Hostname: p.Hostname, 75 | OsName: runtime.GOOS, 76 | RuntimeVersions: device.GetRuntimeVersions(), 77 | }, 78 | Request: p.Request, 79 | Exceptions: p.exceptions(), 80 | GroupingHash: p.GroupingHash, 81 | Metadata: p.MetaData.sanitize(p.ParamsFilters), 82 | PayloadVersion: notifyPayloadVersion, 83 | Session: p.makeSession(), 84 | Severity: p.Severity.String, 85 | SeverityReason: p.severityReasonPayload(), 86 | Unhandled: p.Unhandled, 87 | User: p.User, 88 | }, 89 | }, 90 | Notifier: notifierJSON{ 91 | Name: "Bugsnag Go", 92 | URL: "https://github.com/bugsnag/bugsnag-go", 93 | Version: Version, 94 | }, 95 | }) 96 | } 97 | 98 | func (p *payload) makeSession() *sessionJSON { 99 | // If a context has not been applied to the payload then assume that no 100 | // session has started either 101 | if p.Ctx == nil { 102 | return nil 103 | } 104 | 105 | sessionMutex.Lock() 106 | defer sessionMutex.Unlock() 107 | session := sessions.IncrementEventCountAndGetSession(p.Ctx, p.Unhandled) 108 | if session != nil { 109 | s := *session 110 | return &sessionJSON{ 111 | ID: s.ID, 112 | StartedAt: s.StartedAt.UTC().Format(time.RFC3339), 113 | Events: sessions.EventCounts{ 114 | Handled: s.EventCounts.Handled, 115 | Unhandled: s.EventCounts.Unhandled, 116 | }, 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | func (p *payload) severityReasonPayload() *severityReasonJSON { 123 | if reason := p.handledState.SeverityReason; reason != "" { 124 | json := &severityReasonJSON{ 125 | Type: reason, 126 | UnhandledOverridden: p.handledState.Unhandled != p.Unhandled, 127 | } 128 | if p.handledState.Framework != "" { 129 | json.Attributes = make(map[string]string, 1) 130 | json.Attributes["framework"] = p.handledState.Framework 131 | } 132 | return json 133 | } 134 | return nil 135 | } 136 | 137 | func (p *payload) exceptions() []exceptionJSON { 138 | exceptions := []exceptionJSON{ 139 | exceptionJSON{ 140 | ErrorClass: p.ErrorClass, 141 | Message: p.Message, 142 | Stacktrace: p.Stacktrace, 143 | }, 144 | } 145 | 146 | if p.Error == nil { 147 | return exceptions 148 | } 149 | 150 | cause := p.Error.Cause 151 | for cause != nil { 152 | exceptions = append(exceptions, exceptionJSON{ 153 | ErrorClass: cause.TypeName(), 154 | Message: cause.Error(), 155 | Stacktrace: generateStacktrace(cause, p.Configuration), 156 | }) 157 | cause = cause.Cause 158 | } 159 | 160 | return exceptions 161 | } 162 | -------------------------------------------------------------------------------- /v2/payload_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/bugsnag/bugsnag-go/v2/errors" 11 | "github.com/bugsnag/bugsnag-go/v2/sessions" 12 | ) 13 | 14 | const expSmall = `{"apiKey":"","events":[{"app":{"releaseStage":""},"device":{"osName":"%s","runtimeVersions":{"go":"%s"}},"exceptions":[{"errorClass":"","message":"","stacktrace":null}],"metaData":{},"payloadVersion":"4","severity":"","unhandled":false}],"notifier":{"name":"Bugsnag Go","url":"https://github.com/bugsnag/bugsnag-go","version":"` + Version + `"}}` 15 | 16 | // The large payload has a timestamp in it which makes it awkward to assert against. 17 | // Instead, assert that the timestamp property exist, along with the rest of the expected payload 18 | const expLargePre = `{"apiKey":"166f5ad3590596f9aa8d601ea89af845","events":[{"app":{"releaseStage":"mega-production","type":"gin","version":"1.5.3"},"context":"/api/v2/albums","device":{"hostname":"super.duper.site","osName":"%s","runtimeVersions":{"go":"%s"}},"exceptions":[{"errorClass":"error class","message":"error message goes here","stacktrace":[{"method":"doA","file":"a.go","lineNumber":65},{"method":"fetchB","file":"b.go","lineNumber":99,"inProject":true},{"method":"incrementI","file":"i.go","lineNumber":651}]}],"groupingHash":"custom grouping hash","metaData":{"custom tab":{"my key":"my value"}},"payloadVersion":"4","session":{"startedAt":"` 19 | const expLargePost = `,"severity":"info","severityReason":{"type":"unhandledError","attributes":{"framework":"gin"}},"unhandled":true,"user":{"id":"1234baerg134","name":"Kool Kidz on da bus","email":"typo@busgang.com"}}],"notifier":{"name":"Bugsnag Go","url":"https://github.com/bugsnag/bugsnag-go","version":"` + Version + `"}}` 20 | 21 | func TestMarshalEmptyPayload(t *testing.T) { 22 | sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig) 23 | p := payload{&Event{Ctx: context.Background()}, &Configuration{}} 24 | bytes, _ := p.MarshalJSON() 25 | exp := fmt.Sprintf(expSmall, runtime.GOOS, runtime.Version()) 26 | if got := string(bytes[:]); got != exp { 27 | t.Errorf("Payload different to what was expected. \nGot: %s\nExp: %s", got, exp) 28 | } 29 | } 30 | 31 | func TestMarshalLargePayload(t *testing.T) { 32 | payload := makeLargePayload() 33 | bytes, _ := payload.MarshalJSON() 34 | got := string(bytes[:]) 35 | expPre := fmt.Sprintf(expLargePre, runtime.GOOS, runtime.Version()) 36 | if !strings.Contains(got, expPre) { 37 | t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expPre, got) 38 | 39 | } 40 | if !strings.Contains(got, expLargePost) { 41 | t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expLargePost, got) 42 | } 43 | } 44 | 45 | func makeLargePayload() *payload { 46 | stackframes := []StackFrame{ 47 | {Method: "doA", File: "a.go", LineNumber: 65, InProject: false}, 48 | {Method: "fetchB", File: "b.go", LineNumber: 99, InProject: true}, 49 | {Method: "incrementI", File: "i.go", LineNumber: 651, InProject: false}, 50 | } 51 | user := User{ 52 | Id: "1234baerg134", 53 | Name: "Kool Kidz on da bus", 54 | Email: "typo@busgang.com", 55 | } 56 | handledState := HandledState{ 57 | SeverityReason: SeverityReasonUnhandledError, 58 | OriginalSeverity: severity{String: "error"}, 59 | Unhandled: true, 60 | Framework: "gin", 61 | } 62 | 63 | ctx := context.Background() 64 | ctx = StartSession(ctx) 65 | 66 | event := Event{ 67 | Error: &errors.Error{}, 68 | RawData: nil, 69 | ErrorClass: "error class", 70 | Message: "error message goes here", 71 | Stacktrace: stackframes, 72 | Context: "/api/v2/albums", 73 | Severity: SeverityInfo, 74 | GroupingHash: "custom grouping hash", 75 | User: &user, 76 | Ctx: ctx, 77 | MetaData: map[string]map[string]interface{}{ 78 | "custom tab": map[string]interface{}{ 79 | "my key": "my value", 80 | }, 81 | }, 82 | Unhandled: true, 83 | handledState: handledState, 84 | } 85 | config := Configuration{ 86 | APIKey: testAPIKey, 87 | ReleaseStage: "mega-production", 88 | AppType: "gin", 89 | AppVersion: "1.5.3", 90 | Hostname: "super.duper.site", 91 | } 92 | return &payload{&event, &config} 93 | } 94 | -------------------------------------------------------------------------------- /v2/report.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "github.com/bugsnag/bugsnag-go/v2/device" 5 | "github.com/bugsnag/bugsnag-go/v2/sessions" 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | type reportJSON struct { 10 | APIKey string `json:"apiKey"` 11 | Events []eventJSON `json:"events"` 12 | Notifier notifierJSON `json:"notifier"` 13 | } 14 | 15 | type notifierJSON struct { 16 | Name string `json:"name"` 17 | URL string `json:"url"` 18 | Version string `json:"version"` 19 | } 20 | 21 | type eventJSON struct { 22 | App *appJSON `json:"app"` 23 | Context string `json:"context,omitempty"` 24 | Device *deviceJSON `json:"device,omitempty"` 25 | Request *RequestJSON `json:"request,omitempty"` 26 | Exceptions []exceptionJSON `json:"exceptions"` 27 | GroupingHash string `json:"groupingHash,omitempty"` 28 | Metadata interface{} `json:"metaData"` 29 | PayloadVersion string `json:"payloadVersion"` 30 | Session *sessionJSON `json:"session,omitempty"` 31 | Severity string `json:"severity"` 32 | SeverityReason *severityReasonJSON `json:"severityReason,omitempty"` 33 | Unhandled bool `json:"unhandled"` 34 | User *User `json:"user,omitempty"` 35 | } 36 | 37 | type sessionJSON struct { 38 | StartedAt string `json:"startedAt"` 39 | ID uuid.UUID `json:"id"` 40 | Events sessions.EventCounts `json:"events"` 41 | } 42 | 43 | type appJSON struct { 44 | ReleaseStage string `json:"releaseStage"` 45 | Type string `json:"type,omitempty"` 46 | Version string `json:"version,omitempty"` 47 | } 48 | 49 | type exceptionJSON struct { 50 | ErrorClass string `json:"errorClass"` 51 | Message string `json:"message"` 52 | Stacktrace []StackFrame `json:"stacktrace"` 53 | } 54 | 55 | type severityReasonJSON struct { 56 | Type SeverityReason `json:"type,omitempty"` 57 | Attributes map[string]string `json:"attributes,omitempty"` 58 | UnhandledOverridden bool `json:"unhandledOverridden,omitempty"` 59 | } 60 | 61 | type deviceJSON struct { 62 | Hostname string `json:"hostname,omitempty"` 63 | OsName string `json:"osName,omitempty"` 64 | 65 | RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions,omitempty"` 66 | } 67 | 68 | // RequestJSON is the request information that populates the Request tab in the dashboard. 69 | type RequestJSON struct { 70 | ClientIP string `json:"clientIp,omitempty"` 71 | Headers map[string]string `json:"headers,omitempty"` 72 | HTTPMethod string `json:"httpMethod,omitempty"` 73 | URL string `json:"url,omitempty"` 74 | Referer string `json:"referer,omitempty"` 75 | } 76 | -------------------------------------------------------------------------------- /v2/report_publisher.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type reportPublisher interface { 9 | publishReport(*payload) error 10 | setMainProgramContext(context.Context) 11 | delivery() 12 | } 13 | 14 | func (defPub *defaultReportPublisher) delivery() { 15 | waitForEnd: 16 | for { 17 | select { 18 | case <-defPub.mainProgramCtx.Done(): 19 | defPub.isClosing = true 20 | break waitForEnd 21 | case p, ok := <-defPub.eventsChan: 22 | if ok { 23 | if err := p.deliver(); err != nil { 24 | // Ensure that any errors are logged if they occur in a goroutine. 25 | p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err) 26 | } 27 | } else { 28 | p.logf("Event channel closed") 29 | return 30 | } 31 | } 32 | } 33 | 34 | // Send remaining elements from the queue 35 | close(defPub.eventsChan) 36 | for p := range defPub.eventsChan { 37 | if err := p.deliver(); err != nil { 38 | // Ensure that any errors are logged if they occur in a goroutine. 39 | p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err) 40 | } 41 | } 42 | } 43 | 44 | type defaultReportPublisher struct { 45 | eventsChan chan *payload 46 | mainProgramCtx context.Context 47 | isClosing bool 48 | } 49 | 50 | func newPublisher() reportPublisher { 51 | defPub := defaultReportPublisher{isClosing: false, mainProgramCtx: context.TODO()} 52 | defPub.eventsChan = make(chan *payload, 100) 53 | 54 | return &defPub 55 | } 56 | 57 | func (defPub *defaultReportPublisher) setMainProgramContext(ctx context.Context) { 58 | defPub.mainProgramCtx = ctx 59 | } 60 | 61 | func (defPub *defaultReportPublisher) publishReport(p *payload) error { 62 | p.logf("notifying bugsnag: %s", p.Message) 63 | if !p.notifyInReleaseStage() { 64 | return fmt.Errorf("not notifying in %s", p.ReleaseStage) 65 | } 66 | if p.Synchronous { 67 | return p.deliver() 68 | } 69 | 70 | if defPub.isClosing { 71 | return fmt.Errorf("main program is stopping, new events won't be sent") 72 | } 73 | 74 | select { 75 | case defPub.eventsChan <- p: 76 | default: 77 | p.logf("Events channel full. Discarding value") 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /v2/request_extractor.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | const requestContextKey requestKey = iota 11 | 12 | type requestKey int 13 | 14 | // AttachRequestData returns a child of the given context with the request 15 | // object attached for later extraction by the notifier in order to 16 | // automatically record request data 17 | func AttachRequestData(ctx context.Context, r *http.Request) context.Context { 18 | return context.WithValue(ctx, requestContextKey, r) 19 | } 20 | 21 | // extractRequestInfo looks for the request object that the notifier 22 | // automatically attaches to the context when using any of the supported 23 | // frameworks or bugsnag.HandlerFunc or bugsnag.Handler, and returns sub-object 24 | // supported by the notify API. 25 | func extractRequestInfo(ctx context.Context) (*RequestJSON, *http.Request) { 26 | if req := getRequestIfPresent(ctx); req != nil { 27 | return extractRequestInfoFromReq(req), req 28 | } 29 | return nil, nil 30 | } 31 | 32 | // extractRequestInfoFromReq extracts the request information the notify API 33 | // understands from the given HTTP request. Returns the sub-object supported by 34 | // the notify API. 35 | func extractRequestInfoFromReq(req *http.Request) *RequestJSON { 36 | return &RequestJSON{ 37 | ClientIP: req.RemoteAddr, 38 | HTTPMethod: req.Method, 39 | URL: sanitizeURL(req), 40 | Referer: req.Referer(), 41 | Headers: parseRequestHeaders(req.Header), 42 | } 43 | } 44 | 45 | // sanitizeURL will build up the URL matching the request. It will filter query parameters to remove sensitive fields. 46 | // The query part of the URL might appear differently (different order of parameters) if any filtering was done. 47 | func sanitizeURL(req *http.Request) string { 48 | scheme := "http" 49 | if req.TLS != nil { 50 | scheme = "https" 51 | } 52 | 53 | rawQuery := req.URL.RawQuery 54 | parsedQuery, err := url.ParseQuery(req.URL.RawQuery) 55 | 56 | if err != nil { 57 | return scheme + "://" + req.Host + req.RequestURI 58 | } 59 | 60 | changed := false 61 | for key, values := range parsedQuery { 62 | if contains(Config.ParamsFilters, key) { 63 | for i := range values { 64 | values[i] = "BUGSNAG_URL_FILTERED" 65 | changed = true 66 | } 67 | } 68 | } 69 | 70 | if changed { 71 | rawQuery = parsedQuery.Encode() 72 | rawQuery = strings.Replace(rawQuery, "BUGSNAG_URL_FILTERED", "[FILTERED]", -1) 73 | } 74 | 75 | u := url.URL{ 76 | Scheme: scheme, 77 | Host: req.Host, 78 | Path: req.URL.Path, 79 | RawQuery: rawQuery, 80 | } 81 | return u.String() 82 | } 83 | 84 | func parseRequestHeaders(header map[string][]string) map[string]string { 85 | headers := make(map[string]string) 86 | for k, v := range header { 87 | // Headers can have multiple values, in which case we report them as csv 88 | if contains(Config.ParamsFilters, k) { 89 | headers[k] = "[FILTERED]" 90 | } else { 91 | headers[k] = strings.Join(v, ",") 92 | } 93 | } 94 | return headers 95 | } 96 | 97 | func contains(slice []string, e string) bool { 98 | for _, s := range slice { 99 | if strings.Contains(strings.ToLower(e), strings.ToLower(s)) { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | 106 | func getRequestIfPresent(ctx context.Context) *http.Request { 107 | if ctx == nil { 108 | return nil 109 | } 110 | val := ctx.Value(requestContextKey) 111 | if val == nil { 112 | return nil 113 | } 114 | return val.(*http.Request) 115 | } 116 | -------------------------------------------------------------------------------- /v2/request_extractor_test.go: -------------------------------------------------------------------------------- 1 | package bugsnag 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRequestInformationGetsExtracted(t *testing.T) { 13 | contexts := make(chan context.Context, 1) 14 | hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | ctx := r.Context() 16 | ctx = AttachRequestData(ctx, r) 17 | contexts <- ctx 18 | }) 19 | ts := httptest.NewServer(hf) 20 | defer ts.Close() 21 | http.Get(ts.URL + "/1234abcd?fish=bird") 22 | 23 | reqJSON, req := extractRequestInfo(<-contexts) 24 | if reqJSON.ClientIP == "" { 25 | t.Errorf("expected to find an IP address for the request but was blank") 26 | } 27 | if got, exp := reqJSON.HTTPMethod, "GET"; got != exp { 28 | t.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) 29 | } 30 | if got, exp := req.URL.Path, "/1234abcd"; got != exp { 31 | t.Errorf("expected request URL to be '%s' but was '%s'", exp, got) 32 | } 33 | if got, exp := reqJSON.URL, "/1234abcd?fish=bird"; !strings.Contains(got, exp) { 34 | t.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) 35 | } 36 | if got, exp := reqJSON.Referer, ""; got != exp { 37 | t.Errorf("expected request referer to be '%s' but was '%s'", exp, got) 38 | } 39 | if got, exp := reqJSON.Headers["Accept-Encoding"], "gzip"; got != exp { 40 | t.Errorf("expected Accept-Encoding to be '%s' but was '%s'", exp, got) 41 | } 42 | if got, exp := reqJSON.Headers["User-Agent"], "Go-http-client"; !strings.Contains(got, exp) { 43 | t.Errorf("expected user agent to contain '%s' but was '%s'", exp, got) 44 | } 45 | } 46 | 47 | func TestRequestExtractorCanHandleAbsentContext(t *testing.T) { 48 | if got, _ := extractRequestInfo(nil); got != nil { 49 | //really just testing that nothing panics here 50 | t.Errorf("expected nil contexts to give nil sub-objects, but was '%s'", got) 51 | } 52 | if got, _ := extractRequestInfo(context.Background()); got != nil { 53 | //really just testing that nothing panics here 54 | t.Errorf("expected contexts without requst info to give nil sub-objects, but was '%s'", got) 55 | } 56 | } 57 | 58 | func TestExtractRequestInfoFromReq_RedactURL(t *testing.T) { 59 | testCases := []struct { 60 | in url.URL 61 | exp string 62 | }{ 63 | {in: url.URL{}, exp: "http://example.com"}, 64 | {in: url.URL{Path: "/"}, exp: "http://example.com/"}, 65 | {in: url.URL{Path: "/foo.html"}, exp: "http://example.com/foo.html"}, 66 | {in: url.URL{Path: "/foo.html", RawQuery: "q=something&bar=123"}, exp: "http://example.com/foo.html?q=something&bar=123"}, 67 | {in: url.URL{Path: "/foo.html", RawQuery: "foo=1&foo=2&foo=3"}, exp: "http://example.com/foo.html?foo=1&foo=2&foo=3"}, 68 | 69 | // Invalid query string. 70 | {in: url.URL{Path: "/foo", RawQuery: "%"}, exp: "http://example.com/foo?%"}, 71 | 72 | // Query params contain secrets 73 | {in: url.URL{Path: "/foo.html", RawQuery: "access_token=something"}, exp: "http://example.com/foo.html?access_token=[FILTERED]"}, 74 | {in: url.URL{Path: "/foo.html", RawQuery: "access_token=something&access_token=&foo=bar"}, exp: "http://example.com/foo.html?access_token=[FILTERED]&access_token=[FILTERED]&foo=bar"}, 75 | } 76 | 77 | for _, tc := range testCases { 78 | requestURI := tc.in.Path 79 | if tc.in.RawQuery != "" { 80 | requestURI += "?" + tc.in.RawQuery 81 | } 82 | req := &http.Request{ 83 | Host: "example.com", 84 | URL: &tc.in, 85 | RequestURI: requestURI, 86 | } 87 | result := extractRequestInfoFromReq(req) 88 | if result.URL != tc.exp { 89 | t.Errorf("expected URL to be '%s' but was '%s'", tc.exp, result.URL) 90 | } 91 | } 92 | } 93 | 94 | func TestParseHeadersWillSanitiseIllegalParams(t *testing.T) { 95 | headers := make(map[string][]string) 96 | headers["password"] = []string{"correct horse battery staple"} 97 | headers["secret"] = []string{"I am Banksy"} 98 | headers["authorization"] = []string{"licence to kill -9"} 99 | headers["custom-made-secret"] = []string{"I'm the insider at Sotheby's"} 100 | for k, v := range parseRequestHeaders(headers) { 101 | if v != "[FILTERED]" { 102 | t.Errorf("expected '%s' to be [FILTERED], but was '%s'", k, v) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /v2/sessions/config_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestConfigDoesNotChangeGivenBlankValues(t *testing.T) { 11 | c := testConfig() 12 | exp := testConfig() 13 | c.Update(&SessionTrackingConfiguration{}) 14 | tt := []struct { 15 | name string 16 | expected interface{} 17 | got interface{} 18 | }{ 19 | {"PublishInterval", exp.PublishInterval, c.PublishInterval}, 20 | {"APIKey", exp.APIKey, c.APIKey}, 21 | {"Endpoint", exp.Endpoint, c.Endpoint}, 22 | {"Version", exp.Version, c.Version}, 23 | {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, 24 | {"Hostname", exp.Hostname, c.Hostname}, 25 | {"AppType", exp.AppType, c.AppType}, 26 | {"AppVersion", exp.AppVersion, c.AppVersion}, 27 | {"Transport", exp.Transport, c.Transport}, 28 | {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, 29 | } 30 | for _, tc := range tt { 31 | if !reflect.DeepEqual(tc.got, tc.expected) { 32 | t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) 33 | } 34 | } 35 | } 36 | 37 | func TestConfigUpdatesGivenNonDefaultValues(t *testing.T) { 38 | c := testConfig() 39 | exp := SessionTrackingConfiguration{ 40 | PublishInterval: 40 * time.Second, 41 | APIKey: "api234", 42 | Endpoint: "https://docs.bugsnag.com/platforms/go/", 43 | Version: "2.7.3", 44 | ReleaseStage: "Production", 45 | Hostname: "Brian's Surface", 46 | AppType: "Revel API", 47 | AppVersion: "6.3.9", 48 | NotifyReleaseStages: []string{"staging", "production"}, 49 | } 50 | c.Update(&exp) 51 | tt := []struct { 52 | name string 53 | expected interface{} 54 | got interface{} 55 | }{ 56 | {"PublishInterval", exp.PublishInterval, c.PublishInterval}, 57 | {"APIKey", exp.APIKey, c.APIKey}, 58 | {"Endpoint", exp.Endpoint, c.Endpoint}, 59 | {"Version", exp.Version, c.Version}, 60 | {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, 61 | {"Hostname", exp.Hostname, c.Hostname}, 62 | {"AppType", exp.AppType, c.AppType}, 63 | {"AppVersion", exp.AppVersion, c.AppVersion}, 64 | {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, 65 | } 66 | for _, tc := range tt { 67 | if !reflect.DeepEqual(tc.got, tc.expected) { 68 | t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) 69 | } 70 | } 71 | } 72 | 73 | func testConfig() SessionTrackingConfiguration { 74 | return SessionTrackingConfiguration{ 75 | PublishInterval: 20 * time.Second, 76 | APIKey: "api123", 77 | Endpoint: "https://bugsnag.com/jobs", //If you like what you see... ;) 78 | Version: "1.6.2", 79 | ReleaseStage: "Staging", 80 | Hostname: "Russ's MacbookPro", 81 | AppType: "Gin API", 82 | AppVersion: "5.2.8", 83 | NotifyReleaseStages: []string{"staging", "production"}, 84 | Transport: http.DefaultTransport, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /v2/sessions/payload.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | 7 | "github.com/bugsnag/bugsnag-go/v2/device" 8 | ) 9 | 10 | // notifierPayload defines the .notifier subobject of the payload 11 | type notifierPayload struct { 12 | Name string `json:"name"` 13 | URL string `json:"url"` 14 | Version string `json:"version"` 15 | } 16 | 17 | // appPayload defines the .app subobject of the payload 18 | type appPayload struct { 19 | Type string `json:"type,omitempty"` 20 | ReleaseStage string `json:"releaseStage,omitempty"` 21 | Version string `json:"version,omitempty"` 22 | } 23 | 24 | // devicePayload defines the .device subobject of the payload 25 | type devicePayload struct { 26 | OsName string `json:"osName,omitempty"` 27 | Hostname string `json:"hostname,omitempty"` 28 | 29 | RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions"` 30 | } 31 | 32 | // sessionCountsPayload defines the .sessionCounts subobject of the payload 33 | type sessionCountsPayload struct { 34 | StartedAt string `json:"startedAt"` 35 | SessionsStarted int `json:"sessionsStarted"` 36 | } 37 | 38 | // sessionPayload defines the top level payload object 39 | type sessionPayload struct { 40 | Notifier *notifierPayload `json:"notifier"` 41 | App *appPayload `json:"app"` 42 | Device *devicePayload `json:"device"` 43 | SessionCounts []sessionCountsPayload `json:"sessionCounts"` 44 | } 45 | 46 | // makeSessionPayload creates a sessionPayload based off of the given sessions and config 47 | func makeSessionPayload(sessions []*Session, config *SessionTrackingConfiguration) *sessionPayload { 48 | releaseStage := config.ReleaseStage 49 | if releaseStage == "" { 50 | releaseStage = "production" 51 | } 52 | hostname := config.Hostname 53 | if hostname == "" { 54 | hostname = device.GetHostname() 55 | } 56 | 57 | return &sessionPayload{ 58 | Notifier: ¬ifierPayload{ 59 | Name: "Bugsnag Go", 60 | URL: "https://github.com/bugsnag/bugsnag-go", 61 | Version: config.Version, 62 | }, 63 | App: &appPayload{ 64 | Type: config.AppType, 65 | Version: config.AppVersion, 66 | ReleaseStage: releaseStage, 67 | }, 68 | Device: &devicePayload{ 69 | OsName: runtime.GOOS, 70 | Hostname: hostname, 71 | RuntimeVersions: device.GetRuntimeVersions(), 72 | }, 73 | SessionCounts: []sessionCountsPayload{ 74 | { 75 | //This timestamp assumes that we're sending these off once a minute 76 | StartedAt: sessions[0].StartedAt.UTC().Format(time.RFC3339), 77 | SessionsStarted: len(sessions), 78 | }, 79 | }, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /v2/sessions/publisher.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/bugsnag/bugsnag-go/v2/headers" 10 | ) 11 | 12 | // sessionPayloadVersion defines the current version of the payload that's 13 | // being sent to the session server. 14 | const sessionPayloadVersion = "1.0" 15 | 16 | type sessionPublisher interface { 17 | publish(sessions []*Session) error 18 | } 19 | 20 | type httpClient interface { 21 | Do(*http.Request) (*http.Response, error) 22 | } 23 | 24 | type publisher struct { 25 | config *SessionTrackingConfiguration 26 | client httpClient 27 | } 28 | 29 | // publish builds a payload from the given sessions and publishes them to the 30 | // session server. Returns any errors that happened as part of publishing. 31 | func (p *publisher) publish(sessions []*Session) error { 32 | if p.config.Endpoint == "" { 33 | // Session tracking is disabled, likely because the notify endpoint was 34 | // changed without changing the sessions endpoint 35 | // We've already logged a warning in this case, so no need to spam the 36 | // log every minute 37 | return nil 38 | } 39 | 40 | if apiKey := p.config.APIKey; len(apiKey) != 32 { 41 | return fmt.Errorf("bugsnag/sessions/publisher.publish invalid API key: '%s'", apiKey) 42 | } 43 | 44 | nrs, rs := p.config.NotifyReleaseStages, p.config.ReleaseStage 45 | if rs != "" && (nrs != nil && !contains(nrs, rs)) { 46 | // Always send sessions if the release stage is not set, but don't send any 47 | // sessions when notify release stages don't match the current release stage 48 | return nil 49 | } 50 | 51 | if len(sessions) == 0 { 52 | return fmt.Errorf("bugsnag/sessions/publisher.publish requested publication of 0") 53 | } 54 | 55 | p.config.mutex.Lock() 56 | defer p.config.mutex.Unlock() 57 | 58 | payload := makeSessionPayload(sessions, p.config) 59 | buf, err := json.Marshal(payload) 60 | if err != nil { 61 | return fmt.Errorf("bugsnag/sessions/publisher.publish unable to marshal json: %v", err) 62 | } 63 | 64 | req, err := http.NewRequest("POST", p.config.Endpoint, bytes.NewBuffer(buf)) 65 | if err != nil { 66 | return fmt.Errorf("bugsnag/sessions/publisher.publish unable to create request: %v", err) 67 | } 68 | 69 | for k, v := range headers.PrefixedHeaders(p.config.APIKey, sessionPayloadVersion) { 70 | req.Header.Add(k, v) 71 | } 72 | 73 | res, err := p.client.Do(req) 74 | if err != nil { 75 | return fmt.Errorf("bugsnag/sessions/publisher.publish unable to deliver session: %v", err) 76 | } 77 | 78 | defer func(res *http.Response) { 79 | if err := res.Body.Close(); err != nil { 80 | p.config.logf("%v", err) 81 | } 82 | }(res) 83 | 84 | if res.StatusCode != 202 { 85 | return fmt.Errorf("bugsnag/session.publish expected 202 response status, got HTTP %s", res.Status) 86 | } 87 | return nil 88 | } 89 | 90 | func contains(coll []string, e string) bool { 91 | for _, s := range coll { 92 | if s == e { 93 | return true 94 | } 95 | } 96 | return false 97 | } 98 | -------------------------------------------------------------------------------- /v2/sessions/session.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "time" 5 | 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | // EventCounts register how many handled/unhandled events have happened for 10 | // this session 11 | type EventCounts struct { 12 | Handled int `json:"handled"` 13 | Unhandled int `json:"unhandled"` 14 | } 15 | 16 | // Session represents a start time and a unique ID that identifies the session. 17 | type Session struct { 18 | StartedAt time.Time 19 | ID uuid.UUID 20 | EventCounts *EventCounts 21 | } 22 | 23 | func newSession() *Session { 24 | return &Session{ 25 | StartedAt: time.Now(), 26 | ID: uuid.New(), 27 | EventCounts: &EventCounts{}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /v2/sessions/startup.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/bugsnag/panicwrap" 9 | ) 10 | 11 | // SendStartupSession is called by Bugsnag on startup, which will send a 12 | // session to Bugsnag and return a context to represent the session of the main 13 | // goroutine. This is the session associated with any fatal panics that are 14 | // caught by panicwrap. 15 | func SendStartupSession(config *SessionTrackingConfiguration) context.Context { 16 | ctx := context.Background() 17 | session := newSession() 18 | if !config.IsAutoCaptureSessions() || isApplicationProcess() { 19 | return ctx 20 | } 21 | publisher := &publisher{ 22 | config: config, 23 | client: &http.Client{Transport: config.Transport}, 24 | } 25 | go publisher.publish([]*Session{session}) 26 | return context.WithValue(ctx, contextSessionKey, session) 27 | } 28 | 29 | // Checks to see if this is the application process, as opposed to the process 30 | // that monitors for panics 31 | func isApplicationProcess() bool { 32 | // Application process is run first, and this will only have been set when 33 | // the monitoring process runs 34 | return "" == os.Getenv(panicwrap.DEFAULT_COOKIE_KEY) 35 | } 36 | -------------------------------------------------------------------------------- /v2/sessions/tracker_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | type testPublisher struct { 11 | mutex sync.Mutex 12 | sessionsReceived [][]*Session 13 | } 14 | 15 | var pub = testPublisher{ 16 | mutex: sync.Mutex{}, 17 | sessionsReceived: [][]*Session{}, 18 | } 19 | 20 | func (pub *testPublisher) publish(sessions []*Session) error { 21 | pub.mutex.Lock() 22 | defer pub.mutex.Unlock() 23 | pub.sessionsReceived = append(pub.sessionsReceived, sessions) 24 | return nil 25 | } 26 | 27 | func TestStartSessionModifiesContext(t *testing.T) { 28 | type ctxKey string 29 | var k ctxKey 30 | k, v := "key", "val" 31 | st, c := makeSessionTracker() 32 | defer close(c) 33 | 34 | ctx := st.StartSession(context.WithValue(context.Background(), k, v)) 35 | if got, exp := ctx.Value(k), v; got != exp { 36 | t.Errorf("Changed pre-existing key '%s' with value '%s' into %s", k, v, got) 37 | } 38 | if got := ctx.Value(contextSessionKey); got == nil { 39 | t.Fatalf("No session information applied to context %v", ctx) 40 | } 41 | 42 | verifyValidSession(t, IncrementEventCountAndGetSession(ctx, true)) 43 | } 44 | 45 | func TestShouldOnlyWriteWhenReceivingSessions(t *testing.T) { 46 | st, c := makeSessionTracker() 47 | defer close(c) 48 | go st.processSessions() 49 | time.Sleep(10 * st.config.PublishInterval) // Would publish many times in this time period if there were sessions 50 | 51 | if got := pub.sessionsReceived; len(got) != 0 { 52 | t.Errorf("pub was invoked unexpectedly %d times with arguments: %v", len(got), got) 53 | } 54 | 55 | for i := 0; i < 50000; i++ { 56 | st.StartSession(context.Background()) 57 | } 58 | time.Sleep(time.Millisecond * 500) // wait for sessions to get consumed 59 | 60 | var sessions []*Session 61 | pub.mutex.Lock() 62 | defer pub.mutex.Unlock() 63 | for _, s := range pub.sessionsReceived { 64 | for _, session := range s { 65 | verifyValidSession(t, session) 66 | sessions = append(sessions, session) 67 | } 68 | } 69 | if exp, got := 50000, len(sessions); exp != got { 70 | t.Errorf("Expected %d sessions but got %d", exp, got) 71 | } 72 | 73 | } 74 | 75 | func makeSessionTracker() (*sessionTracker, chan *Session) { 76 | c := make(chan *Session, 1) 77 | return &sessionTracker{ 78 | config: &SessionTrackingConfiguration{ 79 | PublishInterval: time.Millisecond * 10, //Publish very fast 80 | }, 81 | sessionChannel: c, 82 | sessions: []*Session{}, 83 | publisher: &pub, 84 | }, c 85 | } 86 | 87 | func verifyValidSession(t *testing.T, s *Session) { 88 | if (s.StartedAt == time.Time{}) { 89 | t.Errorf("Expected start time to be set but was nil") 90 | } 91 | if len(s.ID) != 16 { 92 | t.Errorf("Expected UUID to be a valid V4 UUID but was %s", s.ID) 93 | } 94 | } 95 | --------------------------------------------------------------------------------