pax_global_header00006660000000000000000000000064143215471140014514gustar00rootroot0000000000000052 comment=694009f298a5cda9e44c63bbafb99f4fc4516518 bugsnag-go-2.2.0/000077500000000000000000000000001432154711400135465ustar00rootroot00000000000000bugsnag-go-2.2.0/.github/000077500000000000000000000000001432154711400151065ustar00rootroot00000000000000bugsnag-go-2.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001432154711400172715ustar00rootroot00000000000000bugsnag-go-2.2.0/.github/ISSUE_TEMPLATE/A.md000066400000000000000000000007621432154711400200000ustar00rootroot00000000000000--- name: Having trouble getting started? about: Please contact us at support@bugsnag.com for assistance with integrating Bugsnag into your application. title: '' labels: '' assignees: '' --- Please checkout our [documentation](https://docs.bugsnag.com/platforms/go/) for guides, references and tutorials. If you have questions about your integration please contact us at [support@bugsnag.com](mailto:support@bugsnag.com). Alternatively, view additional options at [support.md](../SUPPORT.md).bugsnag-go-2.2.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000020261432154711400217630ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve the library title: '' labels: '' assignees: '' --- ### Describe the bug A clear and concise description of what the bug is. ### Steps to reproduce 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error ### Environment * Bugsnag Go version: * Go version: * Integration framework version: * Martini: * Negroni: * net/http: * Revel: * Other: ### Example Repo - [ ] Create a minimal repository that can reproduce the issue - [ ] Link to it here: ### Example code snippet ``` # (Insert code sample to reproduce the problem) ```
Error messages: ``` ```
bugsnag-go-2.2.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000013251432154711400230170ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- ### Description **Describe the solution you'd like** **Describe alternatives you've considered** **Additional context** bugsnag-go-2.2.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000003441432154711400207100ustar00rootroot00000000000000## Goal ## Design ## Changeset ## Testing bugsnag-go-2.2.0/.github/support.md000066400000000000000000000021531432154711400171450ustar00rootroot00000000000000## Are you having trouble getting started? If you haven't already, please checkout our [documentation](https://docs.bugsnag.com/platforms/go/) for guides, references and tutorials. 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. When contacting support, please include as much information as necessary, including: - example code snippet - steps to reproduce - expected/actual behaviour * Bugsnag Go version: * Go version: * Integration framework version: * Martini: * Negroni: * net/http: * Revel: * Other: ## Bug or Feature Requests 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. Please note: we cannot promise that we will fulfil all requests ## Pull Requests 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.bugsnag-go-2.2.0/.github/workflows/000077500000000000000000000000001432154711400171435ustar00rootroot00000000000000bugsnag-go-2.2.0/.github/workflows/license-audit.yml000066400000000000000000000017561432154711400224250ustar00rootroot00000000000000name: Audit bugsnag-go dependency licenses on: [push, pull_request] jobs: license-audit: runs-on: ubuntu-latest defaults: run: working-directory: 'go/src/github.com/bugsnag/bugsnag-go/v2' steps: - uses: actions/checkout@v2 with: path: 'go/src/github.com/bugsnag/bugsnag-go' # relative to $GITHUB_WORKSPACE - name: set GOPATH run: | bash -c 'echo "GOPATH=$GITHUB_WORKSPACE/go" >> $GITHUB_ENV' - name: Fetch decisions.yml run: curl https://raw.githubusercontent.com/bugsnag/license-audit/master/config/decision_files/global.yml -o decisions.yml - uses: actions/setup-go@v2 with: go-version: '1.16' - name: install dependencies run: go get -v -d ./... - name: Run License Finder run: > docker run -v $PWD:/scan licensefinder/license_finder /bin/bash -lc " cd /scan && license_finder --decisions-file decisions.yml --enabled-package-managers=gomodules " bugsnag-go-2.2.0/.github/workflows/test-package.yml000066400000000000000000000031311432154711400222340ustar00rootroot00000000000000name: Test package against Go versions on: [ push, pull_request ] jobs: test: runs-on: ${{ matrix.os }}-latest defaults: run: working-directory: 'go/src/github.com/bugsnag/bugsnag-go/v2' # relative to $GITHUB_WORKSPACE strategy: fail-fast: false matrix: os: [ubuntu, windows] go-version: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16'] steps: - uses: actions/checkout@v2 with: path: 'go/src/github.com/bugsnag/bugsnag-go' # relative to $GITHUB_WORKSPACE - name: set GOPATH if: matrix.os == 'ubuntu' run: | bash -c 'echo "GOPATH=$GITHUB_WORKSPACE/go" >> $GITHUB_ENV' - name: set GOPATH if: matrix.os == 'windows' run: | bash -c 'echo "GOPATH=$GITHUB_WORKSPACE\\\\go" >> $GITHUB_ENV' - uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - name: install dependencies run: go get -v -d ./... - name: run tests run: go test ./... - name: vet package # go1.12 vet shows spurious 'unknown identifier' issues if: matrix.go-version != '1.12' run: go vet ./... - name: install integration dependencies if: matrix.os == 'ubuntu' run: | sudo apt-get install docker-compose sudo gem install bundler bundle install - name: maze tests working-directory: go/src/github.com/bugsnag/bugsnag-go # relative to $GITHUB_WORKSPACE if: matrix.os == 'ubuntu' env: GO_VERSION: ${{ matrix.go-version }} run: bundle exec bugsnag-maze-runner --color --format progress bugsnag-go-2.2.0/.gitignore000066400000000000000000000001241432154711400155330ustar00rootroot00000000000000# Ignore maze runner generated files maze_output vendor features/fixtures/testbuildbugsnag-go-2.2.0/CHANGELOG.md000066400000000000000000000266541432154711400153740ustar00rootroot00000000000000# Changelog ## 2.2.0 (2022-10-12) ### Enhancements * Support pkg/errors `Unwrap()` on `errors.Error` objects [#194](https://github.com/bugsnag/bugsnag-go/pull/194) [Jayce Pulsipher](https://github.com/jaycetde) * Document double star glob patterns are available for `ProjectPackages` subpackage names. [#184](https://github.com/bugsnag/bugsnag-go/pull/184) [Genta Kamitani](https://github.com/genkami) ### Bug fixes * Replace the gofrs/uuid dependency to maintain support for older versions of Go [#196](https://github.com/bugsnag/bugsnag-go/pull/196) ## 1.9.1 (2022-10-12) ### Bug fixes * Replace the gofrs/uuid dependency to maintain support for older versions of Go [#196](https://github.com/bugsnag/bugsnag-go/pull/196) ## 2.1.2 (2021-08-24) ### Enhancements * Update panicwrap dependency to v1.3.4 which fixes build support for linux & darwin arm64. ## 2.1.1 (2021-04-19) ### Enhancements * Update panicwrap dependency to 1.3.2, adding support for darwin arm64 ## 2.1.0 (2021-01-27) ### Enhancements * Support appending metadata through environment variables prefixed with `BUGSNAG_METADATA_` ### Bug fixes * Fix `GOPATH`, `SourceRoot` and project package path stripping from stack traces on Windows by using the correct path separators. ## 2.0.0 (2021-01-18) The v2 release adds support for Go modules, removes web framework integrations from the main repository, and supports library configuration through environment variables. The new module is available via: ```go import "github.com/bugsnag/bugsnag-go/v2" ``` ### Breaking Changes * Removed `Configuration.Endpoint`. Use `Configuration.Endpoints` instead. For more info and an example, see the [Upgrading guide](./UPGRADING.md) * Web framework integrations have been moved to separate repositories: * [bugsnag-go-gin](https://github.com/bugsnag/bugsnag-go-gin) * [bugsnag-go-negroni](https://github.com/bugsnag/bugsnag-go-negroni) * [bugsnag-go-revel](https://github.com/bugsnag/bugsnag-go-revel) * The `martini` framework integration has been retired * `bugsnag.VERSION` has been renamed `bugsnag.Version` ### Enhancements * Support configuring Bugsnag through environment variables * Support reporting panics caused by overflowing the stack ## 1.9.0 (2021-01-05) ### Enhancements * Support capturing "fatal error"-style panics from go, such as from concurrent map read/writes, out of memory errors, and nil goroutines. ## 1.8.0 (2020-12-03) ### Enhancements * Support unwrapping the underlying causes from an error, including attached stack trace contents if available. Any reported error which implements the following interface: ```go type errorWithCause interface { Unwrap() error } ``` will have the cause included as a previous error in the resulting event. The cause information will be available on the Bugsnag dashboard and is available for inspection in callbacks on the `errors.Error` object. ```go bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { if event.Error.Cause != nil { fmt.Printf("This error was caused by %v", event.Error.Cause.Error()) } return nil }) ``` ## 1.7.0 (2020-11-18) ### Enhancements * Support for changing the handled-ness of an event prior to delivery. This allows for otherwise handled events to affect a project's stability score. ```go bugsnag.Notify(err, func(event *bugsnag.Event) { event.Unhandled = true }) ``` ## 1.6.0 (2020-11-12) ### Enhancements * Extract stacktrace contents on errors wrapped by [`pkg/errors`](https://github.com/pkg/errors). [#144](https://github.com/bugsnag/bugsnag-go/pull/144) * Support modifying an individual event using a callback function argument. ```go bugsnag.Notify(err, func(event *bugsnag.Event) { event.ErrorClass = "Unexpected Termination" event.MetaData.Update(loadJobData()) if event.Stacktrace[0].File = "mylogger.go" { event.Stacktrace = event.Stacktrace[1:] } }) ``` The stack trace of an event is now mutable so frames can be removed or modified. [#146](https://github.com/bugsnag/bugsnag-go/pull/146) ### Bug fixes * Send web framework name with severity reason if set. Previously this value was ignored, obscuring the severity reason for failed web requests captured by bugsnag middleware. [#143](https://github.com/bugsnag/bugsnag-go/pull/143) ## 1.5.4 (2020-10-28) ### Bug fixes * Account for inlined frames when unwinding stack traces by using `runtime.CallersFrames`. [#114](https://github.com/bugsnag/bugsnag-go/pull/114) [#140](https://github.com/bugsnag/bugsnag-go/pull/140) ## 1.5.3 (2019-07-11) This release adds runtime version data to the report and session payloads, which will show up under the Device tab in the Bugsnag dashboard. ### Enhancements * Ignore Gin unit tests when running against the latest version of Gin on Go versions below 1.10 as Gin has dropped support for these versions. [#121](https://github.com/bugsnag/bugsnag-go/pull/121) * Introduce runtime version data to the report and session payloads. Additionally adds the OS name to reports. [#122](https://github.com/bugsnag/bugsnag-go/pull/122) ## 1.5.2 (2019-05-20) This release adds `"access_token"` to the default list of keys to filter and introduces filtering of URL query parameters under the request tab. ### Enhancements * Adds filtering of URL parameters in the request tab of an event. Additionally adds `access_token` to the `ParamsFilters` by default. [#117](https://github.com/bugsnag/bugsnag-go/pull/117) [Adam Renberg Tamm](https://github.com/tgwizard) * Ignore Gin unit tests when running against the latest version of Gin on Go 1.7 as Gin has dropped support for Go 1.6 and Go 1.7. [#118](https://github.com/bugsnag/bugsnag-go/pull/118) ## 1.5.1 (2019-04-15) This release re-introduces prioritizing user specified error classes over the inferred error class. ### Bug fixes * Fixes a bug introduced in `v1.4.0` where `bugsnag.Notify(err, bugsnag.ErrorClass{Name: "MyCustomErrorClass"})` is not respected. [#115](https://github.com/bugsnag/bugsnag-go/pull/115) ## 1.5.0 (2019-03-26) ### Enhancements * Testing improvements [#105](https://github.com/bugsnag/bugsnag-go/pull/105) * Only run full test suite on PRs targeting master * Test against the latest release of go (currently 1.12) rather than go's unstable master branch * App engine has not been supported for a while. This release removes the app engine-specific code and tests from the codebase [#109](https://github.com/bugsnag/bugsnag-go/pull/109). ## 1.4.1 (2019-03-18) This release fixes a compilation error on Windows. Due to a missing implementation in the Go library, Windows users may have to send two interrupt signals to interrupt the application. Other signals are unaffected. Additionally, ensure data sanitisation behaves the same for both request data and metadata. ### Bug fixes * Use the `os` package instead of `syscall` to re-send signals, as `syscall` varies per platform, which caused a compilation error. * Make sure that all data sanitization using `Config.ParamsFilters` behaves the same. [#104](https://github.com/bugsnag/bugsnag-go/pull/104) [Adam Renberg Tamm](https://github.com/tgwizard) ## 1.4.0 (2018-11-19) This release is a big non-breaking revamp of the notifier. Most importantly, this release introduces session tracking to Go applications. As of this release we require that you use Go 1.7 or higher. ### Features * Session tracking to be able to show a stability score in the dashboard. Automatic recording of sessions for net/http, gin, revel, negroni and martini. Automatic capturing of sessions can be disabled using the `AutoCaptureSessions` configuration parameter. * Automatic recording of HTTP request information such as HTTP method, headers, URL and query parameters. ### Enhancements * Migrate report payload version from 3 to 4. * Improve test coverage and introduce maze runner tests. Simplify integration tests for Negroni, Gin and Martini. * Deprecate the use of the old `Endpoint` configuration parameter, and allow users of on-premise to configure both the notify endpoint and the sessions endpoint. * `bugsnag.Notify()` now accepts a `context.Context` object, generally from `*http.Request`'s `r.Context()`, which Bugsnag can extract session and request information from. * Improve and augment examples (`bugsnag_example_test.go`) for documentation. * Improve example applications (`examples/` directory) to get up and running faster. * Clarify and improve GoDocs. * Improved serialization performance and safety of the report payload. * Filter HTTP headers based on the `FiltersParams`. * Revel enhancements: * Ensure all non-code configuration options are configurable from config file. * Stop using deprecated logger. * Attempt to configure a what we can from the revel configuration options. * Make NotifyReleaseStages work consistently with other notifiers, both for sessions and for reports. * Also filter out 'authorization' and 'cookie' by default, to match other notifiers. ### Bug fixes * Address compile errors test failures that failed the build. * Don't crash when calling `bugsnag.Notify(nil)` * Other minor bug fixes that came to light after improving test coverage. ## 1.3.2 (2018-10-05) ### Bug fixes * Ensure error reports for fatal crashes gets sent [#77](https://github.com/bugsnag/bugsnag-go/pull/77) ## 1.3.1 (2018-03-14) ### Bug fixes * Add support for Revel v0.18 [#63](https://github.com/bugsnag/bugsnag-go/pull/63) [Cameron Halter](https://github.com/EightB1ts) ## 1.3.0 (2017-10-02) ### Enhancements * Track whether an error report was captured automatically * Add SourceRoot as a configuration option, defaulting to `$GOPATH` ## 1.2.2 (2017-08-25) ### Bug fixes * Point osext dependency at upstream, update with fixes ## 1.2.1 (2017-07-31) ### Bug fixes * Improve goroutine panic reporting by sending reports synchronously in the case that a goroutine is about to be cleaned up [#52](https://github.com/bugsnag/bugsnag-go/pull/52) ## 1.2.0 (2017-07-03) ### Enhancements * Support custom stack frame implementations [alexanderwilling](https://github.com/alexanderwilling) [#43](https://github.com/bugsnag/bugsnag-go/issues/43) * Support app.type in error reports [Jascha Ephraim](https://github.com/jaschaephraim) [#51](https://github.com/bugsnag/bugsnag-go/pull/51) ### Bug fixes * Mend nil pointer panic in metadata [Johan Sageryd](https://github.com/jsageryd) [#46](https://github.com/bugsnag/bugsnag-go/pull/46) ## 1.1.1 (2016-12-16) ### Bug fixes * Replace empty error class property in reports with "error" ## 1.1.0 (2016-11-07) ### Enhancements * Add middleware for Gin [Mike Bull](https://github.com/bullmo) [#40](https://github.com/bugsnag/bugsnag-go/pull/40) * Add middleware for Negroni [am-manideep](https://github.com/am-manideep) [#28](https://github.com/bugsnag/bugsnag-go/pull/28) * Support stripping subpackage names [Facundo Ferrer](https://github.com/fjferrer) [#25](https://github.com/bugsnag/bugsnag-go/pull/25) * Support using `ErrorWithCallers` to create a stacktrace for errors [Conrad Irwin](https://github.com/ConradIrwin) [#35](https://github.com/bugsnag/bugsnag-go/pull/35) ## 1.0.5 ### Bug fixes * Avoid swallowing errors which occur upon delivery 1.0.4 ----- - Fix appengine integration broken by 1.0.3 1.0.3 ----- - Allow any Logger with a Printf method. 1.0.2 ----- - Use bugsnag copies of dependencies to avoid potential link rot 1.0.1 ----- - gofmt/golint/govet docs improvements. 1.0.0 ----- bugsnag-go-2.2.0/CONTRIBUTING.md000066400000000000000000000063031432154711400160010ustar00rootroot00000000000000Contributing ============ - [Fork](https://help.github.com/articles/fork-a-repo) the [notifier on github](https://github.com/bugsnag/bugsnag-go) - Build and test your changes - Commit and push until you are happy with your contribution - [Make a pull request](https://help.github.com/articles/using-pull-requests) - Thanks! Installing the go development environment ------------------------------------- 1. Install homebrew ``` ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" ``` 1. Install go ``` brew install go --cross-compile-all ``` 1. Configure `$GOPATH` in `~/.bashrc` ``` export GOPATH="$HOME/go" export PATH=$PATH:$GOPATH/bin ``` Downloading the code -------------------- You can download the code and its dependencies using ``` go get -t github.com/bugsnag/bugsnag-go/v2 ``` It will be put into "$GOPATH/src/github.com/bugsnag/bugsnag-go" Then install depend Running Tests ------------- You can run the tests with ```shell go test ./... ``` Making PRs ---------- 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). 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. Releasing a New Version ----------------------- If you are a project maintainer, you can build and release a new version of `bugsnag-go` as follows: #### Planned releases **Prerequisite**: All code changes should already have been reviewed and PR'd into the `next` branch before making a release. 1. Decide on a version number and date for this release 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 1. Update the README if necessary 1. Update the version number in `v2/bugsnag.go` and verify that tests pass. 1. Commit these changes `git commit -am "Preparing release"` 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 1. Await PR approval and CI pass 1. Merge to master on GitHub, using the UI to set the merge commit message to be `vX.X.X` 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). 1. Ensure setup guides for Go (and its frameworks) on docs.bugsnag.com are correct and up to date. 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. #### Hotfixes 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`. Once released, ensure `master` is merged into `next` so that the changes made on `hotfix` are included. bugsnag-go-2.2.0/Gemfile000066400000000000000000000001661432154711400150440ustar00rootroot00000000000000source 'https://rubygems.org' gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'v1' bugsnag-go-2.2.0/Gemfile.lock000066400000000000000000000022661432154711400157760ustar00rootroot00000000000000GIT remote: https://github.com/bugsnag/maze-runner revision: 7377529a77eb7585afc66cd2080fcdc4eea3306a branch: v1 specs: bugsnag-maze-runner (1.1.0) cucumber (~> 3.1.0) cucumber-expressions (= 5.0.15) minitest (~> 5.0) os (~> 1.0.0) rack (~> 2.0.0) rake (~> 12.3.3) test-unit (~> 3.2.0) GEM remote: https://rubygems.org/ specs: backports (3.21.0) builder (3.2.4) cucumber (3.1.0) builder (>= 2.1.2) cucumber-core (~> 3.1.0) cucumber-expressions (~> 5.0.4) cucumber-wire (~> 0.0.1) diff-lcs (~> 1.3) gherkin (~> 5.0) multi_json (>= 1.7.5, < 2.0) multi_test (>= 0.1.2) cucumber-core (3.1.0) backports (>= 3.8.0) cucumber-tag_expressions (~> 1.1.0) gherkin (>= 5.0.0) cucumber-expressions (5.0.15) cucumber-tag_expressions (1.1.1) cucumber-wire (0.0.1) diff-lcs (1.4.4) gherkin (5.1.0) minitest (5.14.4) multi_json (1.15.0) multi_test (0.1.2) os (1.0.1) power_assert (2.0.0) rack (2.0.9) rake (12.3.3) test-unit (3.2.9) power_assert PLATFORMS ruby DEPENDENCIES bugsnag-maze-runner! BUNDLED WITH 2.1.4 bugsnag-go-2.2.0/LICENSE.txt000066400000000000000000000020331432154711400153670ustar00rootroot00000000000000Copyright (c) 2014 Bugsnag Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bugsnag-go-2.2.0/Makefile000066400000000000000000000030461432154711400152110ustar00rootroot00000000000000TEST?=./... export GO111MODULE=auto default: alldeps test deps: go get -v -d ./... alldeps: go get -v -d -t ./... updatedeps: go get -v -d -u ./... test: alldeps @# skipping Gin if the Go version is lower than 1.9, as the latest version of Gin has dropped support for these versions. @if [ "$(GO_VERSION)" = "1.7" ] || [ "$(GO_VERSION)" = "1.8" ] || [ "$(GO_VERSION)" = "1.9" ]; then \ go test . ./errors ./martini ./negroni ./sessions ./headers; \ else \ go test . ./errors ./gin ./martini ./negroni ./sessions ./headers; \ fi @go vet 2>/dev/null ; if [ $$? -eq 3 ]; then \ go get golang.org/x/tools/cmd/vet; \ fi @go vet $(TEST) ; if [ $$? -eq 1 ]; then \ echo "go-vet: Issues running go vet ./..."; \ exit 1; \ fi maze: bundle install bundle exec bugsnag-maze-runner ci: alldeps test bench: go test --bench=.* testsetup: gem update --system gem install bundler bundle install testplain: testsetup bundle exec bugsnag-maze-runner -c features/plain_features testnethttp: testsetup bundle exec bugsnag-maze-runner -c features/net_http_features testgin: testsetup bundle exec bugsnag-maze-runner -c features/gin_features testmartini: testsetup bundle exec bugsnag-maze-runner -c features/martini_features testnegroni: testsetup bundle exec bugsnag-maze-runner -c features/negroni_features testrevel: testsetup bundle exec bugsnag-maze-runner -c features/revel_features .PHONY: bin checkversion ci default deps generate releasebin test testacc testrace updatedeps testsetup testplain testnethttp testgin testmartini testrevel bugsnag-go-2.2.0/README.md000066400000000000000000000047011432154711400150270ustar00rootroot00000000000000# Bugsnag error reporter for Go [![Latest Version](http://img.shields.io/github/release/bugsnag/bugsnag-go.svg?style=flat-square)](https://github.com/bugsnag/bugsnag-go/releases) [![Build Status](https://travis-ci.com/bugsnag/bugsnag-go.svg?branch=master)](https://travis-ci.com/bugsnag/bugsnag-go?branch=master) [![Go Documentation](http://img.shields.io/badge/godoc-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/bugsnag/bugsnag-go) 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. Learn more about Bugsnag's [Go error monitoring and error reporting](https://www.bugsnag.com/platforms/go-lang-error-reporting/) solution. ## Features * Automatically report unhandled errors and panics * Report handled errors * Attach user information to determine how many people are affected by a crash * Send customized diagnostic data ## Getting Started 1. [Create a Bugsnag account](https://bugsnag.com) 2. Complete the instructions in the integration guide for your framework: * [Martini](https://docs.bugsnag.com/platforms/go/martini) * [Negroni](https://docs.bugsnag.com/platforms/go/negroni) * [net/http](https://docs.bugsnag.com/platforms/go/net-http) * [Revel](https://docs.bugsnag.com/platforms/go/revel) * [Other Go apps](https://docs.bugsnag.com/platforms/go/other) 3. Relax! ## Support * Read the configuration reference: * [Martini](https://docs.bugsnag.com/platforms/go/martini/configuration-options/) * [Negroni](https://docs.bugsnag.com/platforms/go/negroni/configuration-options/) * [net/http](https://docs.bugsnag.com/platforms/go/net-http/configuration-options/) * [Revel](https://docs.bugsnag.com/platforms/go/revel/configuration-options/) * [Other Go apps](https://docs.bugsnag.com/platforms/go/other/configuration-options/) * [Search open and closed issues](https://github.com/bugsnag/bugsnag-go/issues?utf8=✓&q=is%3Aissue) for similar problems * [Report a bug or request a feature](https://github.com/bugsnag/bugsnag-go/issues/new) ## Contributing All contributors are welcome! For information on how to build, test and release `bugsnag-go`, see our [contributing guide](CONTRIBUTING.md). ## License The Bugsnag exception reporter for Go is free software released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details. bugsnag-go-2.2.0/UPGRADING.md000066400000000000000000000030051432154711400154060ustar00rootroot00000000000000# Upgrading guide ## v1 to v2 The v2 release adds support for Go modules, removes web framework integrations from the main repository, and supports library configuration through environment variables. The following breaking changes occurred as a part of this release: ### Importing the package ```diff+go - import "github.com/bugsnag/bugsnag-go" + import "github.com/bugsnag/bugsnag-go/v2" ``` ### Removed `Configuration.Endpoint` The `Endpoint` configuration option was deprecated as a part of the v1.4.0 release in November 2018. It was replaced with `Endpoints`, which includes options for configuring both event and session delivery. ```diff+go - config.Endpoint = "https://notify.myserver.example.com" + config.Endpoints = { + Notify: "https://notify.myserver.example.com", + Sessions: "https://sessions.myserver.example.com" + } ``` ### Moved web framework integrations into separate repositories Integrations with Negroni, Revel, and Gin now live in separate repositories, to prevent implicit dependencies on every framework and to improve the ease of updating each integration independently. ```diff+go - import "github.com/bugsnag/bugsnag-go/negroni" + import "github.com/bugsnag/bugsnag-go-negroni" ``` ```diff+go - import "github.com/bugsnag/bugsnag-go/revel" + import "github.com/bugsnag/bugsnag-go-revel" ``` ```diff+go - import "github.com/bugsnag/bugsnag-go/gin" + import "github.com/bugsnag/bugsnag-go-gin" ``` ### Renamed constants for platform consistency ```diff+go - bugsnag.VERSION + bugsnag.Version ``` bugsnag-go-2.2.0/bugsnag.go000066400000000000000000000237431432154711400155340ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" "log" "net/http" "os" "path/filepath" "runtime" "sync" "time" "github.com/bugsnag/bugsnag-go/device" "github.com/bugsnag/bugsnag-go/errors" "github.com/bugsnag/bugsnag-go/sessions" // Fixes a bug with SHA-384 intermediate certs on some platforms. // - https://github.com/bugsnag/bugsnag-go/issues/9 _ "crypto/sha512" ) // VERSION defines the version of this Bugsnag notifier const VERSION = "1.9.1" var panicHandlerOnce sync.Once var sessionTrackerOnce sync.Once var middleware middlewareStack // Config is the configuration for the default bugsnag notifier. var Config Configuration var sessionTrackingConfig sessions.SessionTrackingConfiguration // DefaultSessionPublishInterval defines how often sessions should be sent to // Bugsnag. // Deprecated: Exposed for developer sanity in testing. Modify at own risk. var DefaultSessionPublishInterval = 60 * time.Second var defaultNotifier = Notifier{&Config, nil} var sessionTracker sessions.SessionTracker // Configure Bugsnag. The only required setting is the APIKey, which can be // obtained by clicking on "Settings" in your Bugsnag dashboard. This function // is also responsible for installing the global panic handler, so it should be // called as early as possible in your initialization process. func Configure(config Configuration) { Config.update(&config) updateSessionConfig() // Only do once in case the user overrides the default panichandler, and // configures multiple times. panicHandlerOnce.Do(Config.PanicHandler) } // StartSession creates new context from the context.Context instance with // Bugsnag session data attached. Will start the session tracker if not already // started func StartSession(ctx context.Context) context.Context { sessionTrackerOnce.Do(startSessionTracking) return sessionTracker.StartSession(ctx) } // Notify sends an error.Error to Bugsnag along with the current stack trace. // If at all possible, it is recommended to pass in a context.Context, e.g. // from a http.Request or bugsnag.StartSession() as Bugsnag will be able to // extract additional information in some cases. The rawData is used to send // extra information along with the error. For example you can pass the current // http.Request to Bugsnag to see information about it in the dashboard, or set // the severity of the notification. For a detailed list of the information // that can be extracted, see // https://docs.bugsnag.com/platforms/go/reporting-handled-errors/ func Notify(err error, rawData ...interface{}) error { if e := checkForEmptyError(err); e != nil { return e } // Stripping one stackframe to not include this function in the stacktrace // for a manual notification. skipFrames := 1 return defaultNotifier.Notify(errors.New(err, skipFrames), rawData...) } // AutoNotify logs a panic on a goroutine and then repanics. // It should only be used in places that have existing panic handlers further // up the stack. // Although it's not strictly enforced, it's highly recommended to pass a // context.Context object that has at one-point been returned from // bugsnag.StartSession. Doing so ensures your stability score remains accurate, // and future versions of Bugsnag may extract more useful information from this // context. // The rawData is used to send extra information along with any // panics that are handled this way. // Usage: // go func() { // ctx := bugsnag.StartSession(context.Background()) // defer bugsnag.AutoNotify(ctx) // // (possibly crashy code) // }() // See also: bugsnag.Recover() func AutoNotify(rawData ...interface{}) { if err := recover(); err != nil { severity := defaultNotifier.getDefaultSeverity(rawData, SeverityError) state := HandledState{SeverityReasonHandledPanic, severity, true, ""} rawData = append([]interface{}{state}, rawData...) // We strip the following stackframes as they don't add much info // - runtime/$arch - e.g. runtime/asm_amd64.s#call32 // - runtime/panic.go#gopanic // Panics have their own stacktrace, so no stripping of the current stack skipFrames := 2 defaultNotifier.NotifySync(errors.New(err, skipFrames), true, rawData...) sessionTracker.FlushSessions() panic(err) } } // Recover logs a panic on a goroutine and then recovers. // Although it's not strictly enforced, it's highly recommended to pass a // context.Context object that has at one-point been returned from // bugsnag.StartSession. Doing so ensures your stability score remains accurate, // and future versions of Bugsnag may extract more useful information from this // context. // The rawData is used to send extra information along with // any panics that are handled this way // Usage: // go func() { // ctx := bugsnag.StartSession(context.Background()) // defer bugsnag.Recover(ctx) // // (possibly crashy code) // }() // If you wish that any panics caught by the call to Recover shall affect your // stability score (it does not by default): // go func() { // ctx := bugsnag.StartSession(context.Background()) // defer bugsnag.Recover(ctx, bugsnag.HandledState{Unhandled: true}) // // (possibly crashy code) // }() // See also: bugsnag.AutoNotify() func Recover(rawData ...interface{}) { if err := recover(); err != nil { severity := defaultNotifier.getDefaultSeverity(rawData, SeverityWarning) state := HandledState{SeverityReasonHandledPanic, severity, false, ""} rawData = append([]interface{}{state}, rawData...) // We strip the following stackframes as they don't add much info // - runtime/$arch - e.g. runtime/asm_amd64.s#call32 // - runtime/panic.go#gopanic // Panics have their own stacktrace, so no stripping of the current stack skipFrames := 2 defaultNotifier.Notify(errors.New(err, skipFrames), rawData...) } } // OnBeforeNotify adds a callback to be run before a notification is sent to // Bugsnag. It can be used to modify the event or its MetaData. Changes made // to the configuration are local to notifying about this event. To prevent the // event from being sent to Bugsnag return an error, this error will be // returned from bugsnag.Notify() and the event will not be sent. func OnBeforeNotify(callback func(event *Event, config *Configuration) error) { middleware.OnBeforeNotify(callback) } // Handler creates an http Handler that notifies Bugsnag any panics that // happen. It then repanics so that the default http Server panic handler can // handle the panic too. The rawData is used to send extra information along // with any panics that are handled this way. func Handler(h http.Handler, rawData ...interface{}) http.Handler { notifier := New(rawData...) if h == nil { h = http.DefaultServeMux } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { request := r // Record a session if auto notify session is enabled ctx := r.Context() if Config.IsAutoCaptureSessions() { ctx = StartSession(ctx) } ctx = AttachRequestData(ctx, request) request = r.WithContext(ctx) defer notifier.AutoNotify(ctx, request) h.ServeHTTP(w, request) }) } // HandlerFunc creates an http HandlerFunc that notifies Bugsnag about any // panics that happen. It then repanics so that the default http Server panic // handler can handle the panic too. The rawData is used to send extra // information along with any panics that are handled this way. If you have // already wrapped your http server using bugsnag.Handler() you don't also need // to wrap each HandlerFunc. func HandlerFunc(h http.HandlerFunc, rawData ...interface{}) http.HandlerFunc { notifier := New(rawData...) return func(w http.ResponseWriter, r *http.Request) { request := r // Record a session if auto notify session is enabled ctx := request.Context() if notifier.Config.IsAutoCaptureSessions() { ctx = StartSession(ctx) } ctx = AttachRequestData(ctx, request) request = request.WithContext(ctx) defer notifier.AutoNotify(ctx) h(w, request) } } // checkForEmptyError checks if the given error (to be reported to Bugsnag) is // nil. If it is, then log an error message and return another error wrapping // this error message. func checkForEmptyError(err error) error { if err != nil { return nil } msg := "attempted to notify Bugsnag without supplying an error. Bugsnag not notified" Config.Logger.Printf("ERROR: " + msg) return fmt.Errorf(msg) } func init() { // Set up builtin middlewarez OnBeforeNotify(httpRequestMiddleware) // Default configuration sourceRoot := "" if gopath := os.Getenv("GOPATH"); len(gopath) > 0 { sourceRoot = filepath.Join(gopath, "src") + "/" } else { sourceRoot = filepath.Join(runtime.GOROOT(), "src") + "/" } Config.update(&Configuration{ APIKey: "", Endpoints: Endpoints{ Notify: "https://notify.bugsnag.com", Sessions: "https://sessions.bugsnag.com", }, Hostname: device.GetHostname(), AppType: "", AppVersion: "", AutoCaptureSessions: true, ReleaseStage: "", ParamsFilters: []string{"password", "secret", "authorization", "cookie", "access_token"}, SourceRoot: sourceRoot, ProjectPackages: []string{"main*"}, NotifyReleaseStages: nil, Logger: log.New(os.Stdout, log.Prefix(), log.Flags()), PanicHandler: defaultPanicHandler, Transport: http.DefaultTransport, flushSessionsOnRepanic: true, }) updateSessionConfig() } func startSessionTracking() { if sessionTracker == nil { updateSessionConfig() sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig) } } func updateSessionConfig() { sessionTrackingConfig.Update(&sessions.SessionTrackingConfiguration{ APIKey: Config.APIKey, AutoCaptureSessions: Config.AutoCaptureSessions, Endpoint: Config.Endpoints.Sessions, Version: VERSION, PublishInterval: DefaultSessionPublishInterval, Transport: Config.Transport, ReleaseStage: Config.ReleaseStage, Hostname: Config.Hostname, AppType: Config.AppType, AppVersion: Config.AppVersion, NotifyReleaseStages: Config.NotifyReleaseStages, Logger: Config.Logger, }) } bugsnag-go-2.2.0/bugsnag_example_test.go000066400000000000000000000075101432154711400203000ustar00rootroot00000000000000package bugsnag_test import ( "context" "fmt" "net" "net/http" "time" "github.com/bugsnag/bugsnag-go" ) var exampleAPIKey = "166f5ad3590596f9aa8d601ea89af845" func ExampleAutoNotify() { bugsnag.Configure(bugsnag.Configuration{APIKey: exampleAPIKey}) createAccount := func(ctx context.Context) { fmt.Println("Creating account and passing context around...") } ctx := bugsnag.StartSession(context.Background()) defer bugsnag.AutoNotify(ctx) createAccount(ctx) // Output: // Creating account and passing context around... } func ExampleRecover() { bugsnag.Configure(bugsnag.Configuration{APIKey: exampleAPIKey}) panicFunc := func() { fmt.Println("About to panic") panic("Oh noes") } // Will recover when panicFunc panics func() { ctx := bugsnag.StartSession(context.Background()) defer bugsnag.Recover(ctx) panicFunc() }() fmt.Println("Panic recovered") // Output: About to panic // Panic recovered } func ExampleConfigure() { bugsnag.Configure(bugsnag.Configuration{ APIKey: "YOUR_API_KEY_HERE", ReleaseStage: "production", // See bugsnag.Configuration for other fields }) } func ExampleHandler() { handleReq := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Handling HTTP request") } // Set up your http handlers as usual http.HandleFunc("/", handleReq) // use bugsnag.Handler(nil) to wrap the default http handlers // so that Bugsnag is automatically notified about panics. http.ListenAndServe(":1234", bugsnag.Handler(nil)) } func ExampleHandler_customServer() { handleReq := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Handling GET") } // If you're using a custom server, set the handlers explicitly. http.HandleFunc("/", handleReq) srv := http.Server{ Addr: ":1234", ReadTimeout: 10 * time.Second, // use bugsnag.Handler(nil) to wrap the default http handlers // so that Bugsnag is automatically notified about panics. Handler: bugsnag.Handler(nil), } srv.ListenAndServe() } func ExampleHandler_customHandlers() { handleReq := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Handling GET") } // If you're using custom handlers, wrap the handlers explicitly. handler := http.NewServeMux() http.HandleFunc("/", handleReq) // use bugsnag.Handler(handler) to wrap the handlers so that Bugsnag is // automatically notified about panics http.ListenAndServe(":1234", bugsnag.Handler(handler)) } func ExampleNotify() { ctx := context.Background() ctx = bugsnag.StartSession(ctx) _, err := net.Listen("tcp", ":80") if err != nil { bugsnag.Notify(err, ctx) } } func ExampleNotify_details() { ctx := context.Background() ctx = bugsnag.StartSession(ctx) _, err := net.Listen("tcp", ":80") if err != nil { bugsnag.Notify(err, ctx, // show as low-severity bugsnag.SeverityInfo, // set the context bugsnag.Context{String: "createlistener"}, // pass the user id in to count users affected. bugsnag.User{Id: "123456789"}, // custom meta-data tab bugsnag.MetaData{ "Listen": { "Protocol": "tcp", "Port": "80", }, }, ) } } func ExampleOnBeforeNotify() { type Job struct { Retry bool UserID string UserEmail string } bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { // Search all the RawData for any *Job pointers that we're passed in // to bugsnag.Notify() and friends. for _, datum := range event.RawData { if job, ok := datum.(*Job); ok { // don't notify bugsnag about errors in retries if job.Retry { return fmt.Errorf("bugsnag middleware: not notifying about job retry") } // add the job as a tab on Bugsnag.com event.MetaData.AddStruct("Job", job) // set the user correctly event.User = &bugsnag.User{Id: job.UserID, Email: job.UserEmail} } } // continue notifying as normal return nil }) } bugsnag-go-2.2.0/bugsnag_test.go000066400000000000000000000504041432154711400165650ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" "io/ioutil" "log" "net" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go/sessions" ) // The line numbers of this method are used in tests. // If you move this function you'll have to change tests func crashyHandler(w http.ResponseWriter, r *http.Request) { c := make(chan int) close(c) c <- 1 } type _recurse struct { Recurse *_recurse } const ( unhandled = true handled = false ) var testAPIKey = "166f5ad3590596f9aa8d601ea89af845" type logger struct{ msg string } func (l *logger) Printf(format string, v ...interface{}) { l.msg = format } // setup sets up a simple sessionTracker and returns a test event server for receiving the event payloads. // report payloads published to the returned server's URL will be put on the returned channel func setup() (*httptest.Server, chan []byte) { reports := make(chan []byte, 10) sessionTracker = &testSessionTracker{} return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) reports <- body })), reports } type testSessionTracker struct{} func (t *testSessionTracker) StartSession(context.Context) context.Context { return context.Background() } func (t *testSessionTracker) IncrementEventCountAndGetSession(context.Context, bool) *sessions.Session { return &sessions.Session{} } func (t *testSessionTracker) FlushSessions() {} func TestConfigure(t *testing.T) { Configure(Configuration{ APIKey: testAPIKey, }) if Config.APIKey != testAPIKey { t.Errorf("Setting APIKey didn't work") } if New().Config.APIKey != testAPIKey { t.Errorf("Setting APIKey didn't work for new notifiers") } } func TestNotify(t *testing.T) { ts, reports := setup() defer ts.Close() sessionTracker = nil startSessionTracking() recurse := _recurse{} recurse.Recurse = &recurse OnBeforeNotify(func(event *Event, config *Configuration) error { if event.Context == "testing" { event.GroupingHash = "lol" } return nil }) md := MetaData{"test": {"password": "sneaky", "value": "able", "broken": complex(1, 2), "recurse": recurse}} user := User{Id: "123", Name: "Conrad", Email: "me@cirw.in"} config := generateSampleConfig(ts.URL) Notify(fmt.Errorf("hello world"), StartSession(context.Background()), config, user, ErrorClass{Name: "ExpectedErrorClass"}, Context{"testing"}, md) json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } event := getIndex(json, "events", 0) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "testing", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "lol", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "warning", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledError}, Unhandled: false, Request: &RequestJSON{}, User: &User{Id: "123", Name: "Conrad", Email: "me@cirw.in"}, Exceptions: []exceptionJSON{{ErrorClass: "ExpectedErrorClass", Message: "hello world"}}, }) assertValidSession(t, event, handled) for k, exp := range map[string]string{ "metaData.test.password": "[FILTERED]", "metaData.test.value": "able", "metaData.test.broken": "[complex128]", "metaData.test.recurse.Recurse": "[RECURSION]", } { if got := getString(event, k); got != exp { t.Errorf("Expected %s to be '%s' but was '%s'", k, exp, got) } } exception := getIndex(event, "exceptions", 0) verifyExistsInStackTrace(t, exception, &StackFrame{File: "bugsnag_test.go", Method: "TestNotify", LineNumber: 98, InProject: true}) } type testPublisher struct { sync bool } func (tp *testPublisher) publishReport(p *payload) error { tp.sync = p.Synchronous return nil } func TestNotifySyncThenAsync(t *testing.T) { ts, _ := setup() defer ts.Close() Configure(generateSampleConfig(ts.URL)) //async by default pub := new(testPublisher) publisher = pub defer func() { publisher = new(defaultReportPublisher) }() Notify(fmt.Errorf("oopsie")) if pub.sync { t.Errorf("Expected notify to be async by default") } defaultNotifier.NotifySync(fmt.Errorf("oopsie"), true) if !pub.sync { t.Errorf("Expected notify to be sent synchronously when calling NotifySync with true") } Notify(fmt.Errorf("oopsie")) if pub.sync { t.Errorf("Expected notify to be sent asynchronously when calling Notify regardless of previous NotifySync call") } } func TestHandlerFunc(t *testing.T) { eventserver, reports := setup() defer eventserver.Close() Configure(generateSampleConfig(eventserver.URL)) t.Run("unhandled", func(st *testing.T) { sessionTracker = nil startSessionTracking() ts := httptest.NewServer(HandlerFunc(crashyHandler)) defer ts.Close() http.Get(ts.URL + "/unhandled") json, _ := simplejson.NewJson(<-reports) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "/unhandled", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, Request: &RequestJSON{ Headers: map[string]string{"Accept-Encoding": "gzip"}, HTTPMethod: "GET", URL: ts.URL + "/unhandled", }, User: &User{Id: "127.0.0.1", Name: "", Email: ""}, Exceptions: []exceptionJSON{{ErrorClass: "runtime.plainError", Message: "send on closed channel"}}, }) event := getIndex(json, "events", 0) if got, exp := getString(event, "request.headers.Accept-Encoding"), "gzip"; got != exp { st.Errorf("expected Accept-Encoding header to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.httpMethod"), "GET"; got != exp { st.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.url"), "/unhandled"; !strings.Contains(got, exp) { st.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) } assertValidSession(st, event, unhandled) }) t.Run("handled", func(st *testing.T) { ts := httptest.NewServer(HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Notify(fmt.Errorf("oopsie"), r.Context()) })) defer ts.Close() http.Get(ts.URL + "/handled") json, _ := simplejson.NewJson(<-reports) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "/handled", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 1, Unhandled: 0}}, Severity: "warning", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledError}, Unhandled: false, Request: &RequestJSON{ Headers: map[string]string{"Accept-Encoding": "gzip"}, HTTPMethod: "GET", URL: ts.URL + "/handled", }, User: &User{Id: "127.0.0.1", Name: "", Email: ""}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "oopsie"}}, }) event := getIndex(json, "events", 0) if got, exp := getString(event, "request.headers.Accept-Encoding"), "gzip"; got != exp { st.Errorf("expected Accept-Encoding header to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.httpMethod"), "GET"; got != exp { st.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.url"), "/handled"; !strings.Contains(got, exp) { st.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) } assertValidSession(st, event, handled) }) } func TestHandler(t *testing.T) { ts, reports := setup() defer ts.Close() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() mux := http.NewServeMux() mux.HandleFunc("/", crashyHandler) go (&http.Server{ Addr: l.Addr().String(), Handler: Handler(mux, generateSampleConfig(ts.URL), SeverityInfo), ErrorLog: log.New(ioutil.Discard, log.Prefix(), 0), }).Serve(l) sessionTracker = nil startSessionTracking() http.Get("http://" + l.Addr().String() + "/ok?foo=bar") json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "/ok", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "info", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, User: &User{Id: "127.0.0.1", Name: "", Email: ""}, Request: &RequestJSON{ Headers: map[string]string{"Accept-Encoding": "gzip"}, HTTPMethod: "GET", URL: "http://" + l.Addr().String() + "/ok?foo=bar", }, Exceptions: []exceptionJSON{{ErrorClass: "runtime.plainError", Message: "send on closed channel"}}, }) event := getIndex(json, "events", 0) if got, exp := getString(event, "request.headers.Accept-Encoding"), "gzip"; got != exp { t.Errorf("expected Accept-Encoding header to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.httpMethod"), "GET"; got != exp { t.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.url"), "/ok?foo=bar"; !strings.Contains(got, exp) { t.Errorf("expected request URL to be '%s' but was '%s'", exp, got) } assertValidSession(t, event, unhandled) if got, exp := getFirstString(event, "metaData.request.params.foo"), "bar"; got != exp { t.Errorf("Expected metadata params 'foo' to be '%s' but was '%s'", exp, got) } exception := getIndex(event, "exceptions", 0) verifyExistsInStackTrace(t, exception, &StackFrame{File: "bugsnag_test.go", Method: "crashyHandler", InProject: true, LineNumber: 24}) } func TestAutoNotify(t *testing.T) { ts, reports := setup() defer ts.Close() var panicked error func() { defer func() { p := recover() switch p.(type) { case error: panicked = p.(error) default: t.Fatalf("Unexpected panic happened. Expected 'eggs' Error but was a(n) <%T> with value <%+v>", p, p) } }() defer AutoNotify(StartSession(context.Background()), generateSampleConfig(ts.URL)) panic(fmt.Errorf("eggs")) }() if panicked.Error() != "eggs" { t.Errorf("didn't re-panic") } json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, User: &User{}, Request: &RequestJSON{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "eggs"}}, }) } func TestRecover(t *testing.T) { ts, reports := setup() defer ts.Close() var panicked interface{} func() { defer func() { panicked = recover() }() defer Recover(StartSession(context.Background()), generateSampleConfig(ts.URL)) panic("ham") }() if panicked != nil { t.Errorf("Did not expect a panic but repanicked") } json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "warning", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: false, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "ham"}}, }) } func TestRecoverCustomHandledState(t *testing.T) { ts, reports := setup() defer ts.Close() var panicked interface{} func() { defer func() { panicked = recover() }() handledState := HandledState{ SeverityReason: SeverityReasonHandledPanic, OriginalSeverity: SeverityError, Unhandled: true, } defer Recover(handledState, StartSession(context.Background()), generateSampleConfig(ts.URL)) panic("at the disco?") }() if panicked != nil { t.Errorf("Did not expect a panic but repanicked") } json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "at the disco?"}}, }) } func TestSeverityReasonNotifyCallback(t *testing.T) { ts, reports := setup() defer ts.Close() OnBeforeNotify(func(event *Event, config *Configuration) error { event.Severity = SeverityInfo return nil }) Notify(fmt.Errorf("hello world"), generateSampleConfig(ts.URL), StartSession(context.Background())) json, _ := simplejson.NewJson(<-reports) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "info", SeverityReason: &severityReasonJSON{Type: SeverityReasonCallbackSpecified}, Unhandled: false, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "hello world"}}, }) } func TestNotifyWithoutError(t *testing.T) { ts, reports := setup() defer ts.Close() config := generateSampleConfig(ts.URL) config.Synchronous = true l := logger{} config.Logger = &l Configure(config) Notify(nil, StartSession(context.Background())) select { case r := <-reports: t.Fatalf("Unexpected request made to bugsnag: %+v", string(r)) default: for _, exp := range []string{"ERROR", "error", "Bugsnag", "not notified"} { if got := l.msg; !strings.Contains(got, exp) { t.Errorf("Expected to see '%s' in logged message but logged message was '%s'", exp, got) } } } } func TestConfigureTwice(t *testing.T) { Configure(Configuration{}) if !Config.IsAutoCaptureSessions() { t.Errorf("Expected auto capture sessions to be enabled by default") } Configure(Configuration{AutoCaptureSessions: false}) if Config.IsAutoCaptureSessions() { t.Errorf("Expected auto capture sessions to be disabled when configured") } Configure(Configuration{AutoCaptureSessions: true}) if !Config.IsAutoCaptureSessions() { t.Errorf("Expected auto capture sessions to be enabled when configured") } } func generateSampleConfig(endpoint string) Configuration { return Configuration{ APIKey: testAPIKey, Endpoints: Endpoints{Notify: endpoint}, ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"}, Logger: log.New(ioutil.Discard, log.Prefix(), log.Flags()), ReleaseStage: "test", AppType: "foo", AppVersion: "1.2.3", Hostname: "web1", } } func get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } func getBool(j *simplejson.Json, path string) bool { return get(j, path).MustBool() } func getInt(j *simplejson.Json, path string) int { return get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return get(j, path).MustString() } func getIndex(j *simplejson.Json, path string, index int) *simplejson.Json { return get(j, path).GetIndex(index) } func getFirstString(j *simplejson.Json, path string) string { return getIndex(j, path, 0).MustString() } // assertPayload compares the payload that was received by the event-server to // the expected report JSON payload func assertPayload(t *testing.T, report *simplejson.Json, exp eventJSON) { expException := exp.Exceptions[0] event := getIndex(report, "events", 0) exception := getIndex(event, "exceptions", 0) for _, tc := range []struct { prop string exp, got interface{} }{ {prop: "API Key", exp: testAPIKey, got: getString(report, "apiKey")}, {prop: "notifier name", exp: "Bugsnag Go", got: getString(report, "notifier.name")}, {prop: "notifier version", exp: VERSION, got: getString(report, "notifier.version")}, {prop: "notifier url", exp: "https://github.com/bugsnag/bugsnag-go", got: getString(report, "notifier.url")}, {prop: "exception message", exp: expException.Message, got: getString(exception, "message")}, {prop: "exception error class", exp: expException.ErrorClass, got: getString(exception, "errorClass")}, {prop: "unhandled", exp: exp.Unhandled, got: getBool(event, "unhandled")}, {prop: "app version", exp: exp.App.Version, got: getString(event, "app.version")}, {prop: "app release stage", exp: exp.App.ReleaseStage, got: getString(event, "app.releaseStage")}, {prop: "app type", exp: exp.App.Type, got: getString(event, "app.type")}, {prop: "user id", exp: exp.User.Id, got: getString(event, "user.id")}, {prop: "user name", exp: exp.User.Name, got: getString(event, "user.name")}, {prop: "user email", exp: exp.User.Email, got: getString(event, "user.email")}, {prop: "context", exp: exp.Context, got: getString(event, "context")}, {prop: "device hostname", exp: exp.Device.Hostname, got: getString(event, "device.hostname")}, {prop: "grouping hash", exp: exp.GroupingHash, got: getString(event, "groupingHash")}, {prop: "payload version", exp: "4", got: getString(event, "payloadVersion")}, {prop: "severity", exp: exp.Severity, got: getString(event, "severity")}, {prop: "severity reason type", exp: string(exp.SeverityReason.Type), got: getString(event, "severityReason.type")}, {prop: "request header 'Accept-Encoding'", exp: string(exp.Request.Headers["Accept-Encoding"]), got: getString(event, "request.headers.Accept-Encoding")}, {prop: "request HTTP method", exp: string(exp.Request.HTTPMethod), got: getString(event, "request.httpMethod")}, {prop: "request URL", exp: string(exp.Request.URL), got: getString(event, "request.url")}, } { if tc.got != tc.exp { t.Errorf("Wrong %s: expected '%v' but got '%v'", tc.prop, tc.exp, tc.got) } } } func assertValidSession(t *testing.T, event *simplejson.Json, unhandled bool) { if sessionID := getString(event, "session.id"); len(sessionID) != 36 { t.Errorf("Expected a valid session ID to be set but was '%s'", sessionID) } if _, e := time.Parse(time.RFC3339, getString(event, "session.startedAt")); e != nil { t.Error(e) } expHandled, expUnhandled := 1, 0 if unhandled { expHandled, expUnhandled = expUnhandled, expHandled } if got := getInt(event, "session.events.unhandled"); got != expUnhandled { t.Errorf("Expected %d unhandled events in session but was %d", expUnhandled, got) } if got := getInt(event, "session.events.handled"); got != expHandled { t.Errorf("Expected %d handled events in session but was %d", expHandled, got) } } func verifyExistsInStackTrace(t *testing.T, exception *simplejson.Json, exp *StackFrame) { isFile := func(frame *simplejson.Json) bool { return strings.HasSuffix(getString(frame, "file"), exp.File) } isMethod := func(frame *simplejson.Json) bool { return getString(frame, "method") == exp.Method } isLineNumber := func(frame *simplejson.Json) bool { return getInt(frame, "lineNumber") == exp.LineNumber } arr, _ := exception.Get("stacktrace").Array() for i := 0; i < len(arr); i++ { frame := getIndex(exception, "stacktrace", i) if isFile(frame) && isMethod(frame) && isLineNumber(frame) { return } } t.Errorf("Could not find expected stackframe %v in exception '%v'", exp, exception) } bugsnag-go-2.2.0/configuration.go000066400000000000000000000221151432154711400167450ustar00rootroot00000000000000package bugsnag import ( "log" "net/http" "path/filepath" "strings" ) // Endpoints hold the HTTP endpoints of the notifier. type Endpoints struct { Sessions string Notify string } // Configuration sets up and customizes communication with the Bugsnag API. type Configuration struct { // Your Bugsnag API key, e.g. "c9d60ae4c7e70c4b6c4ebd3e8056d2b8". You can // find this by clicking Settings on https://bugsnag.com/. APIKey string // Deprecated: Use Endpoints (with an 's') instead. // The Endpoint to notify about crashes. This defaults to // "https://notify.bugsnag.com/", if you're using Bugsnag Enterprise then // set it to your internal Bugsnag endpoint. Endpoint string // Endpoints define the HTTP endpoints that the notifier should notify // about crashes and sessions. These default to notify.bugsnag.com for // error reports and sessions.bugsnag.com for sessions. // If you are using bugsnag on-premise you will have to set these to your // Event Server and Session Server endpoints. If the notify endpoint is set // but the sessions endpoint is not, session tracking will be disabled // automatically to avoid leaking session information outside of your // server configuration, and a warning will be logged. Endpoints Endpoints // The current release stage. This defaults to "production" and is used to // filter errors in the Bugsnag dashboard. ReleaseStage string // A specialized type of the application, such as the worker queue or web // framework used, like "rails", "mailman", or "celery" AppType string // The currently running version of the app. This is used to filter errors // in the Bugsnag dasboard. If you set this then Bugsnag will only re-open // resolved errors if they happen in different app versions. AppVersion string // AutoCaptureSessions can be set to false to disable automatic session // tracking. If you want control over what is deemed a session, you can // switch off automatic session tracking with this configuration, and call // bugsnag.StartSession() when appropriate for your application. See the // official docs for instructions and examples of associating handled // errors with sessions and ensuring error rate accuracy on the Bugsnag // dashboard. This will default to true, but is stored as an interface to enable // us to detect when this option has not been set. AutoCaptureSessions interface{} // The hostname of the current server. This defaults to the return value of // os.Hostname() and is graphed in the Bugsnag dashboard. Hostname string // The Release stages to notify in. If you set this then bugsnag-go will // only send notifications to Bugsnag if the ReleaseStage is listed here. NotifyReleaseStages []string // packages that are part of your app. Bugsnag uses this to determine how // to group errors and how to display them on your dashboard. You should // include any packages that are part of your app, and exclude libraries // and helpers. You can list wildcards here, and they'll be expanded using // filepath.Glob. The default value is []string{"main*"} ProjectPackages []string // The SourceRoot is the directory where the application is built, and the // assumed prefix of lines on the stacktrace originating in the parent // application. When set, the prefix is trimmed from callstack file names // before ProjectPackages for better readability and to better group errors // on the Bugsnag dashboard. The default value is $GOPATH/src or $GOROOT/src // if $GOPATH is unset. At runtime, $GOROOT is the root used during the Go // build. SourceRoot string // Any meta-data that matches these filters will be marked as [FILTERED] // before sending a Notification to Bugsnag. It defaults to // []string{"password", "secret"} so that request parameters like password, // password_confirmation and auth_secret will not be sent to Bugsnag. ParamsFilters []string // The PanicHandler is used by Bugsnag to catch unhandled panics in your // application. The default panicHandler uses mitchellh's panicwrap library, // and you can disable this feature by passing an empty: func() {} PanicHandler func() // The logger that Bugsnag should log to. Uses the same defaults as go's // builtin logging package. bugsnag-go logs whenever it notifies Bugsnag // of an error, and when any error occurs inside the library itself. Logger interface { Printf(format string, v ...interface{}) // limited to the functions used } // The http Transport to use, defaults to the default http Transport. This // can be configured if you are in an environment // that has stringent conditions on making http requests. Transport http.RoundTripper // Whether bugsnag should notify synchronously. This defaults to false which // causes bugsnag-go to spawn a new goroutine for each notification. Synchronous bool // Whether the notifier should send all sessions recorded so far to Bugsnag // when repanicking to ensure that no session information is lost in a // fatal crash. flushSessionsOnRepanic bool // TODO: remember to update the update() function when modifying this struct } func (config *Configuration) update(other *Configuration) *Configuration { if other.APIKey != "" { config.APIKey = other.APIKey } if other.Hostname != "" { config.Hostname = other.Hostname } if other.AppType != "" { config.AppType = other.AppType } if other.AppVersion != "" { config.AppVersion = other.AppVersion } if other.SourceRoot != "" { config.SourceRoot = other.SourceRoot } if other.ReleaseStage != "" { config.ReleaseStage = other.ReleaseStage } if other.ParamsFilters != nil { config.ParamsFilters = other.ParamsFilters } if other.ProjectPackages != nil { config.ProjectPackages = other.ProjectPackages } if other.Logger != nil { config.Logger = other.Logger } if other.NotifyReleaseStages != nil { config.NotifyReleaseStages = other.NotifyReleaseStages } if other.PanicHandler != nil { config.PanicHandler = other.PanicHandler } if other.Transport != nil { config.Transport = other.Transport } if other.Synchronous { config.Synchronous = true } if other.AutoCaptureSessions != nil { config.AutoCaptureSessions = other.AutoCaptureSessions } config.updateEndpoints(other.Endpoint, &other.Endpoints) return config } // IsAutoCaptureSessions identifies whether or not the notifier should // automatically capture sessions as requests come in. It's a convenience // wrapper that allows automatic session capturing to be enabled by default. func (config *Configuration) IsAutoCaptureSessions() bool { if config.AutoCaptureSessions == nil { return true // enabled by default } if val, ok := config.AutoCaptureSessions.(bool); ok { return val } // It has been configured to *something* (although not a valid value) // assume the user wanted to disable this option. return false } func (config *Configuration) updateEndpoints(endpoint string, endpoints *Endpoints) { if endpoint != "" { config.Logger.Printf("WARNING: the 'Endpoint' Bugsnag configuration parameter is deprecated in favor of 'Endpoints'") config.Endpoints.Notify = endpoint config.Endpoints.Sessions = "" } if endpoints.Notify != "" { config.Endpoints.Notify = endpoints.Notify if endpoints.Sessions == "" { config.Logger.Printf("WARNING: Bugsnag notify endpoint configured without also configuring the sessions endpoint. No sessions will be recorded") config.Endpoints.Sessions = "" } } if endpoints.Sessions != "" { if endpoints.Notify == "" { panic("FATAL: Bugsnag sessions endpoint configured without also changing the notify endpoint. Bugsnag cannot identify where to report errors") } config.Endpoints.Sessions = endpoints.Sessions } } func (config *Configuration) merge(other *Configuration) *Configuration { return config.clone().update(other) } func (config *Configuration) clone() *Configuration { clone := *config return &clone } func (config *Configuration) isProjectPackage(pkg string) bool { for _, p := range config.ProjectPackages { if d, f := filepath.Split(p); f == "**" { if strings.HasPrefix(pkg, d) { return true } } if match, _ := filepath.Match(p, pkg); match { return true } } return false } func (config *Configuration) stripProjectPackages(file string) string { trimmedFile := file if strings.HasPrefix(trimmedFile, config.SourceRoot) { trimmedFile = strings.TrimPrefix(trimmedFile, config.SourceRoot) } for _, p := range config.ProjectPackages { if len(p) > 2 && p[len(p)-2] == '/' && p[len(p)-1] == '*' { p = p[:len(p)-1] } else if p[len(p)-1] == '*' && p[len(p)-2] == '*' { p = p[:len(p)-2] } else { p = p + "/" } if strings.HasPrefix(trimmedFile, p) { return strings.TrimPrefix(trimmedFile, p) } } return trimmedFile } func (config *Configuration) logf(fmt string, args ...interface{}) { if config != nil && config.Logger != nil { config.Logger.Printf(fmt, args...) } else { log.Printf(fmt, args...) } } func (config *Configuration) notifyInReleaseStage() bool { if config.NotifyReleaseStages == nil { return true } if config.ReleaseStage == "" { return true } for _, r := range config.NotifyReleaseStages { if r == config.ReleaseStage { return true } } return false } bugsnag-go-2.2.0/configuration_test.go000066400000000000000000000233461432154711400200130ustar00rootroot00000000000000package bugsnag import ( "log" "os" "strings" "testing" ) func TestNotifyReleaseStages(t *testing.T) { notify := " " var tt = []struct { releaseStage string notifyReleaseStages []string expected bool }{ { releaseStage: "production", expected: true, }, { releaseStage: "production", notifyReleaseStages: []string{"development", "production"}, expected: true, }, { releaseStage: "staging", notifyReleaseStages: []string{"development", "production"}, expected: false, }, { notifyReleaseStages: []string{"development", "production"}, expected: true, }, } for _, tc := range tt { rs, nrs, exp := tc.releaseStage, tc.notifyReleaseStages, tc.expected config := &Configuration{ReleaseStage: rs, NotifyReleaseStages: nrs} if config.notifyInReleaseStage() != exp { if !exp { notify = " not " } t.Errorf("expected%sto notify when release stage is '%s' and notify release stages are '%+v'", notify, rs, nrs) } } } func TestIsProjectPackage(t *testing.T) { Configure(Configuration{ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com/b/*", "example.com/c/*/*", "example.com/d/**", "example.com/e", }}) var testCases = []struct { Path string Included bool }{ {"", false}, {"main", true}, {"runtime", false}, {"star", true}, {"sta", false}, {"starred", true}, {"star/foo", false}, {"example.com/a", true}, {"example.com/b", false}, {"example.com/b/", true}, {"example.com/b/foo", true}, {"example.com/b/foo/bar", false}, {"example.com/c/foo/bar", true}, {"example.com/c/foo/bar/baz", false}, {"example.com/d/foo/bar", true}, {"example.com/d/foo/bar/baz", true}, {"example.com/e", true}, } for _, s := range testCases { if Config.isProjectPackage(s.Path) != s.Included { t.Error("literal project package doesn't work:", s.Path, s.Included) } } } func TestStripProjectPackage(t *testing.T) { gopath := os.Getenv("GOPATH") Configure(Configuration{ ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com/b/*", "example.com/c/**", }, SourceRoot: gopath + "/src/", }) var testCases = []struct { File string Stripped string }{ {"main.go", "main.go"}, {"runtime.go", "runtime.go"}, {"star.go", "star.go"}, {"example.com/a/foo.go", "foo.go"}, {"example.com/b/foo/bar.go", "foo/bar.go"}, {"example.com/b/foo.go", "foo.go"}, {"example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"example.com/c/a/b/foo.go", "a/b/foo.go"}, {gopath + "/src/runtime.go", "runtime.go"}, {gopath + "/src/example.com/a/foo.go", "foo.go"}, {gopath + "/src/example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {gopath + "/src/example.com/c/a/b/foo.go", "a/b/foo.go"}, } for _, tc := range testCases { if s := Config.stripProjectPackages(tc.File); s != tc.Stripped { t.Error("stripProjectPackage did not remove expected path:", tc.File, tc.Stripped, "was:", s) } } } func TestStripCustomSourceRoot(t *testing.T) { Configure(Configuration{ ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com/b/*", "example.com/c/**", }, SourceRoot: "/Users/bob/code/go/src/", }) var testCases = []struct { File string Stripped string }{ {"main.go", "main.go"}, {"runtime.go", "runtime.go"}, {"star.go", "star.go"}, {"example.com/a/foo.go", "foo.go"}, {"example.com/b/foo/bar.go", "foo/bar.go"}, {"example.com/b/foo.go", "foo.go"}, {"example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"example.com/c/a/b/foo.go", "a/b/foo.go"}, {"/Users/bob/code/go/src/runtime.go", "runtime.go"}, {"/Users/bob/code/go/src/example.com/a/foo.go", "foo.go"}, {"/Users/bob/code/go/src/example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"/Users/bob/code/go/src/example.com/c/a/b/foo.go", "a/b/foo.go"}, } for _, tc := range testCases { if s := Config.stripProjectPackages(tc.File); s != tc.Stripped { t.Error("stripProjectPackage did not remove expected path:", tc.File, tc.Stripped, "was:", s) } } } type CustomTestLogger struct { loggedMessages []string } func (logger *CustomTestLogger) Printf(format string, v ...interface{}) { logger.loggedMessages = append(logger.loggedMessages, format) } func TestConfiguringCustomLogger(t *testing.T) { l1 := log.New(os.Stdout, "", log.Lshortfile) l2 := &CustomTestLogger{} var testCases = []struct { config Configuration notify bool msg string }{ { config: Configuration{ReleaseStage: "production", NotifyReleaseStages: []string{"development", "production"}, Logger: l1}, }, { config: Configuration{ReleaseStage: "production", NotifyReleaseStages: []string{"development", "production"}, Logger: l2}, }, } for _, testCase := range testCases { Configure(testCase.config) // call printf just to illustrate it is present as the compiler does most of the hard work testCase.config.Logger.Printf("hello %s", "bugsnag") } } func TestEndpointDeprecationWarning(t *testing.T) { defaultNotify := "https://notify.bugsnag.com/" defaultSessions := "https://sessions.bugsnag.com/" setUp := func() (*Configuration, *CustomTestLogger) { logger := &CustomTestLogger{} return &Configuration{ Endpoints: Endpoints{ Notify: defaultNotify, Sessions: defaultSessions, }, Logger: logger, }, logger } t.Run("Setting Endpoint gives deprecation warning", func(st *testing.T) { c, logger := setUp() config := Configuration{Endpoint: "https://endpoint.whatever.com/"} c.update(&config) if got := logger.loggedMessages; len(got) != 1 { st.Errorf("Expected exactly one logged message but got %d: %v", len(got), got) } got := logger.loggedMessages[0] for _, exp := range []string{"WARNING", "Bugsnag", "Endpoint", "Endpoints", "deprecated"} { if !strings.Contains(got, exp) { st.Errorf("Expected logger message containing '%s' when configuring but got %s.", exp, got) } } if got, exp := c.Endpoints.Notify, config.Endpoint; got != exp { st.Errorf("Expected notify endpoint '%s' but got '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, ""; got != exp { st.Errorf("Expected sessions endpoint '%s' but got '%s'", exp, got) } }) t.Run("Setting Endpoints.Notify without setting Endpoints.Sessions gives session disabled warning", func(st *testing.T) { c, logger := setUp() config := Configuration{ Endpoints: Endpoints{ Notify: "https://notify.whatever.com/", }, } keywords := []string{"WARNING", "Bugsnag", "notify", "No sessions"} c.update(&config) if got := len(logger.loggedMessages); got != 1 { st.Errorf("Expected exactly one logged message but got %d", got) } got := logger.loggedMessages[0] for _, exp := range keywords { if !strings.Contains(got, exp) { st.Errorf("Expected logger message containing '%s' when configuring but got %s.", exp, got) } } if got, exp := c.Endpoints.Notify, config.Endpoints.Notify; got != exp { st.Errorf("Expected notify endpoint to be '%s' but was '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, ""; got != exp { st.Errorf("Expected sessions endpoint to be '%s' but was '%s'", exp, got) } }) t.Run("Setting Endpoints.Sessions without setting Endpoints.Notify should panic", func(st *testing.T) { c, _ := setUp() defer func() { if err := recover(); err != nil { got := err.(string) for _, exp := range []string{"FATAL", "Bugsnag", "notify", "sessions"} { if !strings.Contains(got, exp) { st.Errorf("Expected panic error containing '%s' when configuring but got %s.", exp, got) } } } else { st.Errorf("Expected a panic to happen but didn't") } }() c.update(&Configuration{ Endpoints: Endpoints{ Sessions: "https://sessions.whatever.com/", }, }) }) t.Run("Should not complain if both Endpoints.Notify and Endpoints.Sessions are configured", func(st *testing.T) { notifyEndpoint, sessionsEndpoint := "https://notify.whatever.com", "https://sessions.whatever.com" config := Configuration{ Endpoints: Endpoints{ Notify: notifyEndpoint, Sessions: sessionsEndpoint, }, } c, logger := setUp() c.update(&config) if len(logger.loggedMessages) != 0 { st.Errorf("Did not expect any messages to be logged but logged: %v", logger.loggedMessages) } if got, exp := c.Endpoints.Notify, notifyEndpoint; got != exp { st.Errorf("Expected Notify endpoint: '%s', but was: '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, sessionsEndpoint; got != exp { st.Errorf("Expected Sessions endpoint: '%s', but was: '%s'", exp, got) } }) t.Run("Should not complain if Endpoints are not configured", func(st *testing.T) { c, logger := setUp() c.update(&Configuration{}) if len(logger.loggedMessages) != 0 { st.Errorf("Did not expect any messages to be logged but logged: %v", logger.loggedMessages) } if got, exp := c.Endpoints.Notify, defaultNotify; got != exp { st.Errorf("Expected Notify endpoint: '%s', but was: '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, defaultSessions; got != exp { st.Errorf("Expected Sessions endpoint: '%s', but was: '%s'", exp, got) } }) } func TestIsAutoCaptureSessions(t *testing.T) { defaultConfig := Configuration{} if !defaultConfig.IsAutoCaptureSessions() { t.Errorf("Expected automatic session tracking to be enabled by default, but was disabled") } enabledConfig := Configuration{AutoCaptureSessions: true} if !enabledConfig.IsAutoCaptureSessions() { t.Errorf("Expected automatic session tracking to be enabled when so configured, but was disabled") } disabledConfig := Configuration{AutoCaptureSessions: false} if disabledConfig.IsAutoCaptureSessions() { t.Errorf("Expected automatic session tracking to be disabled when so configured, but enabled") } } bugsnag-go-2.2.0/device/000077500000000000000000000000001432154711400150055ustar00rootroot00000000000000bugsnag-go-2.2.0/device/hostname.go000066400000000000000000000005371432154711400171570ustar00rootroot00000000000000package device import "os" var hostname string // GetHostname returns the hostname of the current device. Caches the hostname // between calls to ensure this is performant. Returns a blank string in case // that the hostname cannot be identified. func GetHostname() string { if hostname == "" { hostname, _ = os.Hostname() } return hostname } bugsnag-go-2.2.0/device/runtimeversions.go000066400000000000000000000026741432154711400206210ustar00rootroot00000000000000package device import ( "runtime" ) // Cached runtime versions that can be updated globally by framework // integrations through AddVersion. var versions *RuntimeVersions // RuntimeVersions define the various versions of Go and any framework that may // be in use. // As a user of the notifier you're unlikely to need to modify this struct. // As such, the authors reserve the right to introduce breaking changes to the // properties in this struct. In particular the framework versions are liable // to change in new versions of the notifier in minor/patch versions. type RuntimeVersions struct { Go string `json:"go"` Gin string `json:"gin,omitempty"` Martini string `json:"martini,omitempty"` Negroni string `json:"negroni,omitempty"` Revel string `json:"revel,omitempty"` } // GetRuntimeVersions retrieves the recorded runtime versions in a goroutine-safe manner. func GetRuntimeVersions() *RuntimeVersions { if versions == nil { versions = &RuntimeVersions{Go: runtime.Version()} } return versions } // AddVersion permits a framework to register its version, assuming it's one of // the officially supported frameworks. func AddVersion(framework, version string) { if versions == nil { versions = &RuntimeVersions{Go: runtime.Version()} } switch framework { case "Martini": versions.Martini = version case "Gin": versions.Gin = version case "Negroni": versions.Negroni = version case "Revel": versions.Revel = version } } bugsnag-go-2.2.0/device/runtimeversions_test.go000066400000000000000000000024001432154711400216430ustar00rootroot00000000000000package device import ( "runtime" "testing" ) func TestPristineRuntimeVersions(t *testing.T) { versions = nil // reset global variable rv := GetRuntimeVersions() for _, tc := range []struct{ name, got, exp string }{ {name: "Go", got: rv.Go, exp: runtime.Version()}, {name: "Gin", got: rv.Gin, exp: ""}, {name: "Martini", got: rv.Martini, exp: ""}, {name: "Negroni", got: rv.Negroni, exp: ""}, {name: "Revel", got: rv.Revel, exp: ""}, } { if tc.got != tc.exp { t.Errorf("expected pristine '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) } } } func TestModifiedRuntimeVersions(t *testing.T) { versions = nil // reset global variable rv := GetRuntimeVersions() AddVersion("Gin", "1.2.1") AddVersion("Martini", "1.0.0") AddVersion("Negroni", "1.0.2") AddVersion("Revel", "0.20.1") for _, tc := range []struct{ name, got, exp string }{ {name: "Go", got: rv.Go, exp: runtime.Version()}, {name: "Gin", got: rv.Gin, exp: "1.2.1"}, {name: "Martini", got: rv.Martini, exp: "1.0.0"}, {name: "Negroni", got: rv.Negroni, exp: "1.0.2"}, {name: "Revel", got: rv.Revel, exp: "0.20.1"}, } { if tc.got != tc.exp { t.Errorf("expected modified '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) } } } bugsnag-go-2.2.0/doc.go000066400000000000000000000045641432154711400146530ustar00rootroot00000000000000/* Package bugsnag captures errors in real-time and reports them to Bugsnag (http://bugsnag.com). Using bugsnag-go is a three-step process. 1. As early as possible in your program configure the notifier with your APIKey. This sets up handling of panics that would otherwise crash your app. func init() { bugsnag.Configure(bugsnag.Configuration{ APIKey: "YOUR_API_KEY_HERE", }) } 2. Add bugsnag to places that already catch panics. For example you should add it to the HTTP server when you call ListenAndServer: http.ListenAndServe(":8080", bugsnag.Handler(nil)) If that's not possible, you can also wrap each HTTP handler manually: http.HandleFunc("/" bugsnag.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { ... }) 3. To notify Bugsnag of an error that is not a panic, pass it to bugsnag.Notify. This will also log the error message using the configured Logger. if err != nil { bugsnag.Notify(err) } For detailed integration instructions see https://bugsnag.com/docs/notifiers/go. Configuration The only required configuration is the Bugsnag API key which can be obtained by clicking "Settings" on the top of https://bugsnag.com/ after signing up. We also recommend you set the ReleaseStage, AppType, and AppVersion if these make sense for your deployment workflow. RawData If you need to attach extra data to Bugsnag notifications you can do that using the rawData mechanism. Most of the functions that send errors to Bugsnag allow you to pass in any number of interface{} values as rawData. The rawData can consist of the Severity, Context, User or MetaData types listed below, and there is also builtin support for *http.Requests. bugsnag.Notify(err, bugsnag.SeverityError) If you want to add custom tabs to your bugsnag dashboard you can pass any value in as rawData, and then process it into the event's metadata using a bugsnag.OnBeforeNotify() hook. bugsnag.Notify(err, account) bugsnag.OnBeforeNotify(func (e *bugsnag.Event, c *bugsnag.Configuration) { for datum := range e.RawData { if account, ok := datum.(Account); ok { e.MetaData.Add("account", "name", account.Name) e.MetaData.Add("account", "url", account.URL) } } }) If necessary you can pass Configuration in as rawData, or modify the Configuration object passed into OnBeforeNotify hooks. Configuration passed in this way only affects the current notification. */ package bugsnag bugsnag-go-2.2.0/errors/000077500000000000000000000000001432154711400150625ustar00rootroot00000000000000bugsnag-go-2.2.0/errors/README.md000066400000000000000000000003651432154711400163450ustar00rootroot00000000000000Adds stacktraces to errors in golang. This was made to help build the Bugsnag notifier but can be used standalone if you like to have stacktraces on errors. See [Godoc](https://godoc.org/github.com/bugsnag/bugsnag-go/errors) for the API docs. bugsnag-go-2.2.0/errors/error.go000066400000000000000000000107701432154711400165470ustar00rootroot00000000000000// Package errors provides errors that have stack-traces. package errors import ( "bytes" "fmt" "github.com/pkg/errors" "reflect" "runtime" ) // The maximum number of stackframes on any error. var MaxStackDepth = 50 // Error is an error with an attached stacktrace. It can be used // wherever the builtin error interface is expected. type Error struct { Err error Cause *Error stack []uintptr frames []StackFrame } // ErrorWithCallers allows passing in error objects that // also have caller information attached. type ErrorWithCallers interface { Error() string Callers() []uintptr } // ErrorWithStackFrames allows the stack to be rebuilt from the stack frames, thus // allowing to use the Error type when the program counter is not available. type ErrorWithStackFrames interface { Error() string StackFrames() []StackFrame } type errorWithStack interface { StackTrace() errors.StackTrace Error() string } type errorWithCause interface { Unwrap() error } // New makes an Error from the given value. If that value is already an // error then it will be used directly, if not, it will be passed to // fmt.Errorf("%v"). The skip parameter indicates how far up the stack // to start the stacktrace. 0 is from the current call, 1 from its caller, etc. func New(e interface{}, skip int) *Error { var err error switch e := e.(type) { case *Error: return e case ErrorWithCallers: return &Error{ Err: e, stack: e.Callers(), Cause: unwrapCause(e), } case errorWithStack: trace := e.StackTrace() stack := make([]uintptr, len(trace)) for i, ptr := range trace { stack[i] = uintptr(ptr) - 1 } return &Error{ Err: e, Cause: unwrapCause(e), stack: stack, } case ErrorWithStackFrames: stack := make([]uintptr, len(e.StackFrames())) for i, frame := range e.StackFrames() { stack[i] = frame.ProgramCounter } return &Error{ Err: e, Cause: unwrapCause(e), stack: stack, frames: e.StackFrames(), } case error: err = e default: err = fmt.Errorf("%v", e) } stack := make([]uintptr, MaxStackDepth) length := runtime.Callers(2+skip, stack[:]) return &Error{ Err: err, Cause: unwrapCause(err), stack: stack[:length], } } // Errorf creates a new error with the given message. You can use it // as a drop-in replacement for fmt.Errorf() to provide descriptive // errors in return values. func Errorf(format string, a ...interface{}) *Error { return New(fmt.Errorf(format, a...), 1) } // Error returns the underlying error's message. func (err *Error) Error() string { return err.Err.Error() } // Callers returns the raw stack frames as returned by runtime.Callers() func (err *Error) Callers() []uintptr { return err.stack[:] } // Stack returns the callstack formatted the same way that go does // in runtime/debug.Stack() func (err *Error) Stack() []byte { buf := bytes.Buffer{} for _, frame := range err.StackFrames() { buf.WriteString(frame.String()) } return buf.Bytes() } // StackFrames returns an array of frames containing information about the // stack. func (err *Error) StackFrames() []StackFrame { if err.frames == nil { callers := runtime.CallersFrames(err.stack) err.frames = make([]StackFrame, 0, len(err.stack)) for frame, more := callers.Next(); more; frame, more = callers.Next() { if frame.Func == nil { // Ignore fully inlined functions continue } pkg, name := packageAndName(frame.Func) err.frames = append(err.frames, StackFrame{ function: frame.Func, File: frame.File, LineNumber: frame.Line, Name: name, Package: pkg, ProgramCounter: frame.PC, }) } } return err.frames } // TypeName returns the type this error. e.g. *errors.stringError. func (err *Error) TypeName() string { if p, ok := err.Err.(uncaughtPanic); ok { return p.typeName } if name := reflect.TypeOf(err.Err).String(); len(name) > 0 { return name } return "error" } func unwrapCause(err interface{}) *Error { if causer, ok := err.(errorWithCause); ok { cause := causer.Unwrap() if cause == nil { return nil } else if hasStack(cause) { // avoid generating a (duplicate) stack from the current frame return New(cause, 0) } else { return &Error{ Err: cause, Cause: unwrapCause(cause), stack: []uintptr{}, } } } return nil } func hasStack(err error) bool { if _, ok := err.(errorWithStack); ok { return true } if _, ok := err.(ErrorWithStackFrames); ok { return true } if _, ok := err.(ErrorWithCallers); ok { return true } return false } bugsnag-go-2.2.0/errors/error_test.go000066400000000000000000000221471432154711400176070ustar00rootroot00000000000000package errors import ( "bytes" "fmt" "io" "runtime" "strings" "testing" "github.com/pkg/errors" ) // fixture functions doing work to avoid inlining func a(i int) error { if b(i + 5) && b(i + 6) { return nil } return fmt.Errorf("not gonna happen") } func b(i int) bool { return c(i+2) > 12 } // panicking function! func c(i int) int { if i > 3 { panic('a') } return i * i } func TestParsePanicStack(t *testing.T) { defer func() { err := New(recover(), 0) if err.Error() != "97" { t.Errorf("Received incorrect error, expected 'a' got '%s'", err.Error()) } if err.TypeName() != "*errors.errorString" { t.Errorf("Error type was '%s'", err.TypeName()) } for index, frame := range err.StackFrames() { if frame.Func() == nil { t.Errorf("Failed to remove nil frame %d", index) } } expected := []StackFrame{ StackFrame{Name: "TestParsePanicStack.func1", File: "errors/error_test.go"}, StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, } assertStacksMatch(t, expected, err.StackFrames()) }() a(1) } func TestParseGeneratedStack(t *testing.T) { err := New(fmt.Errorf("e_too_many_colander"), 0) expected := []StackFrame{ StackFrame{Name: "TestParseGeneratedStack", File: "errors/error_test.go"}, } if err.Error() != "e_too_many_colander" { t.Errorf("Error name was '%s'", err.Error()) } if err.TypeName() != "*errors.errorString" { t.Errorf("Error type was '%s'", err.TypeName()) } for index, frame := range err.StackFrames() { if frame.Func() == nil { t.Errorf("Failed to remove nil frame %d", index) } } assertStacksMatch(t, expected, err.StackFrames()) } func TestSkipWorks(t *testing.T) { defer func() { err := New(recover(), 1) if err.Error() != "97" { t.Errorf("Received incorrect error, expected 'a' got '%s'", err.Error()) } for index, frame := range err.StackFrames() { if frame.Name == "TestSkipWorks.func1" { t.Errorf("Failed to skip frame") } if frame.Func() == nil { t.Errorf("Failed to remove inlined frame %d", index) } } expected := []StackFrame{ StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, } assertStacksMatch(t, expected, err.StackFrames()) }() a(4) } func checkFramesMatch(expected StackFrame, actual StackFrame) bool { if actual.Name != expected.Name { return false } // Not using exact match as it would change depending on whether // the package is being tested within or outside of the $GOPATH if expected.File != "" && !strings.HasSuffix(actual.File, expected.File) { return false } if expected.Package != "" && actual.Package != expected.Package { return false } if expected.LineNumber != 0 && actual.LineNumber != expected.LineNumber { return false } return true } func assertStacksMatch(t *testing.T, expected []StackFrame, actual []StackFrame) { var lastmatch int = 0 var matched int = 0 // loop over the actual stacktrace, checking off expected frames as they // are found. Each one might be in the middle of the stack, but the order // should remain the same. for _, actualFrame := range actual { for index, expectedFrame := range expected { if index < lastmatch { continue } if checkFramesMatch(expectedFrame, actualFrame) { lastmatch = index matched += 1 break } } } if matched != len(expected) { t.Fatalf("failed to find matches for %d frames: '%v'\ngot: '%v'", len(expected)-matched, expected[matched:], actual) } } type testErrorWithStackFrames struct { Err *Error } func (tews *testErrorWithStackFrames) StackFrames() []StackFrame { return tews.Err.StackFrames() } func (tews *testErrorWithStackFrames) Error() string { return tews.Err.Error() } func TestNewError(t *testing.T) { e := func() error { return New("hi", 1) }() if e.Error() != "hi" { t.Errorf("Constructor with a string failed") } if New(fmt.Errorf("yo"), 0).Error() != "yo" { t.Errorf("Constructor with an error failed") } if New(e, 0) != e { t.Errorf("Constructor with an Error failed") } if New(nil, 0).Error() != "" { t.Errorf("Constructor with nil failed") } err := New("foo", 0) tews := &testErrorWithStackFrames{ Err: err, } if bytes.Compare(New(tews, 0).Stack(), err.Stack()) != 0 { t.Errorf("Constructor with ErrorWithStackFrames failed") } } func TestUnwrapPkgError(t *testing.T) { _, _, line, ok := runtime.Caller(0) // grab line immediately before error generator top := func() error { err := fmt.Errorf("OH NO") return errors.Wrap(err, "failed") // the correct line for the top of the stack } unwrapped := New(top(), 0) // if errors.StackTrace detection fails, this line will be top of stack if !ok { t.Fatalf("Something has gone wrong with loading the current stack") } if unwrapped.Error() != "failed: OH NO" { t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) } expected := []StackFrame{ StackFrame{Name: "TestUnwrapPkgError.func1", File: "errors/error_test.go", LineNumber: line + 3}, StackFrame{Name: "TestUnwrapPkgError", File: "errors/error_test.go", LineNumber: line + 5}, } assertStacksMatch(t, expected, unwrapped.StackFrames()) } type customErr struct { msg string cause error callers []uintptr } func newCustomErr(msg string, cause error) error { callers := make([]uintptr, 8) runtime.Callers(2, callers) return customErr{ msg: msg, cause: cause, callers: callers, } } func (err customErr) Error() string { return err.msg } func (err customErr) Unwrap() error { return err.cause } func (err customErr) Callers() []uintptr { return err.callers } func TestUnwrapCustomCause(t *testing.T) { _, _, line, ok := runtime.Caller(0) // grab line immediately before error generators err1 := fmt.Errorf("invalid token") err2 := newCustomErr("login failed", err1) err3 := newCustomErr("terminate process", err2) unwrapped := New(err3, 0) if !ok { t.Fatalf("Something has gone wrong with loading the current stack") } if unwrapped.Error() != "terminate process" { t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) } if unwrapped.Cause == nil { t.Fatalf("Failed to capture cause error") } assertStacksMatch(t, []StackFrame{ StackFrame{Name: "TestUnwrapCustomCause", File: "errors/error_test.go", LineNumber: line + 3}, }, unwrapped.StackFrames()) if unwrapped.Cause.Error() != "login failed" { t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error()) } if unwrapped.Cause.Cause == nil { t.Fatalf("Failed to capture nested cause error") } assertStacksMatch(t, []StackFrame{ StackFrame{Name: "TestUnwrapCustomCause", File: "errors/error_test.go", LineNumber: line + 2}, }, unwrapped.Cause.StackFrames()) if unwrapped.Cause.Cause.Error() != "invalid token" { t.Errorf("Failed to unwrap nested cause error: %s", unwrapped.Cause.Cause.Error()) } if len(unwrapped.Cause.Cause.StackFrames()) > 0 { t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames()) } if unwrapped.Cause.Cause.Cause != nil { t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause) } } func TestUnwrapErrorsCause(t *testing.T) { if !goVersionSupportsErrorWrapping() { t.Skip("%w formatter is supported by go1.13+") } _, _, line, ok := runtime.Caller(0) // grab line immediately before error generators err1 := fmt.Errorf("invalid token") err2 := fmt.Errorf("login failed: %w", err1) err3 := fmt.Errorf("terminate process: %w", err2) unwrapped := New(err3, 0) if !ok { t.Fatalf("Something has gone wrong with loading the current stack") } if unwrapped.Error() != "terminate process: login failed: invalid token" { t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) } assertStacksMatch(t, []StackFrame{ StackFrame{Name: "TestUnwrapErrorsCause", File: "errors/error_test.go", LineNumber: line + 4}, }, unwrapped.StackFrames()) if unwrapped.Cause == nil { t.Fatalf("Failed to capture cause error") } if unwrapped.Cause.Error() != "login failed: invalid token" { t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error()) } if len(unwrapped.Cause.StackFrames()) > 0 { t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.StackFrames()) } if unwrapped.Cause.Cause == nil { t.Fatalf("Failed to capture nested cause error") } if len(unwrapped.Cause.Cause.StackFrames()) > 0 { t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames()) } if unwrapped.Cause.Cause.Cause != nil { t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause) } } func goVersionSupportsErrorWrapping() bool { err1 := fmt.Errorf("inner error") err2 := fmt.Errorf("outer error: %w", err1) return err2.Error() == "outer error: inner error" } func ExampleErrorf() { for i := 1; i <= 2; i++ { if i%2 == 1 { e := Errorf("can only halve even numbers, got %d", i) fmt.Printf("Error: %+v", e) } } // Output: // Error: can only halve even numbers, got 1 } func ExampleNew() { // Wrap io.EOF with the current stack-trace and return it e := New(io.EOF, 0) fmt.Printf("%+v", e) // Output: // EOF } func ExampleNew_skip() { defer func() { if err := recover(); err != nil { // skip 1 frame (the deferred function) and then return the wrapped err err = New(err, 1) } }() } bugsnag-go-2.2.0/errors/parse_panic.go000066400000000000000000000063071432154711400177030ustar00rootroot00000000000000package errors import ( "strconv" "strings" ) type uncaughtPanic struct { typeName string message string } func (p uncaughtPanic) Error() string { return p.message } // ParsePanic allows you to get an error object from the output of a go program // that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap. func ParsePanic(text string) (*Error, error) { lines := strings.Split(text, "\n") prefixes := []string{"panic:", "fatal error:"} state := "start" var message string var typeName string var stack []StackFrame for i := 0; i < len(lines); i++ { line := lines[i] if state == "start" { for _, prefix := range prefixes { if strings.HasPrefix(line, prefix) { message = strings.TrimSpace(strings.TrimPrefix(line, prefix)) typeName = prefix[:len(prefix) - 1] state = "seek" break } } if state == "start" { return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line) } } else if state == "seek" { if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") { state = "parsing" } } else if state == "parsing" { if line == "" || strings.HasPrefix(line, "...") { state = "done" break } createdBy := false if strings.HasPrefix(line, "created by ") { line = strings.TrimPrefix(line, "created by ") createdBy = true } i++ if i >= len(lines) { return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line) } frame, err := parsePanicFrame(line, lines[i], createdBy) if err != nil { return nil, err } stack = append(stack, *frame) if createdBy { state = "done" break } } } if state == "done" || state == "parsing" { return &Error{Err: uncaughtPanic{typeName, message}, frames: stack}, nil } return nil, Errorf("could not parse panic: %v", text) } // The lines we're passing look like this: // // main.(*foo).destruct(0xc208067e98) // /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151 func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) { idx := strings.LastIndex(name, "(") if idx == -1 && !createdBy { return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name) } if idx != -1 { name = name[:idx] } pkg := "" if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { pkg += name[:lastslash] + "/" name = name[lastslash+1:] } if period := strings.Index(name, "."); period >= 0 { pkg += name[:period] name = name[period+1:] } name = strings.Replace(name, "·", ".", -1) if !strings.HasPrefix(line, "\t") { return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line) } idx = strings.LastIndex(line, ":") if idx == -1 { return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line) } file := line[1:idx] number := line[idx+1:] if idx = strings.Index(number, " +"); idx > -1 { number = number[:idx] } lno, err := strconv.ParseInt(number, 10, 32) if err != nil { return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line) } return &StackFrame{ File: file, LineNumber: int(lno), Package: pkg, Name: name, }, nil } bugsnag-go-2.2.0/errors/parse_panic_test.go000066400000000000000000000240401432154711400207340ustar00rootroot00000000000000package errors import ( "reflect" "testing" ) var createdBy = `panic: hello! goroutine 54 [running]: runtime.panic(0x35ce40, 0xc208039db0) /0/c/go/src/pkg/runtime/panic.c:279 +0xf5 github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.func·001() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:13 +0x74 net/http.(*Server).Serve(0xc20806c780, 0x910c88, 0xc20803e168, 0x0, 0x0) /0/c/go/src/pkg/net/http/server.go:1698 +0x91 created by github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.App.Index /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:14 +0x3e goroutine 16 [IO wait]: net.runtime_pollWait(0x911c30, 0x72, 0x0) /0/c/go/src/pkg/runtime/netpoll.goc:146 +0x66 net.(*pollDesc).Wait(0xc2080ba990, 0x72, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:84 +0x46 net.(*pollDesc).WaitRead(0xc2080ba990, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:89 +0x42 net.(*netFD).accept(0xc2080ba930, 0x58be30, 0x0, 0x9103f0, 0x23) /0/c/go/src/pkg/net/fd_unix.go:409 +0x343 net.(*TCPListener).AcceptTCP(0xc20803e168, 0x8, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:234 +0x5d net.(*TCPListener).Accept(0xc20803e168, 0x0, 0x0, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:244 +0x4b github.com/revel/revel.Run(0xe6d9) /0/go/src/github.com/revel/revel/server.go:113 +0x926 main.main() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/tmp/main.go:109 +0xe1a ` var normalSplit = `panic: hello! goroutine 54 [running]: runtime.panic(0x35ce40, 0xc208039db0) /0/c/go/src/pkg/runtime/panic.c:279 +0xf5 github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.func·001() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:13 +0x74 net/http.(*Server).Serve(0xc20806c780, 0x910c88, 0xc20803e168, 0x0, 0x0) /0/c/go/src/pkg/net/http/server.go:1698 +0x91 goroutine 16 [IO wait]: net.runtime_pollWait(0x911c30, 0x72, 0x0) /0/c/go/src/pkg/runtime/netpoll.goc:146 +0x66 net.(*pollDesc).Wait(0xc2080ba990, 0x72, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:84 +0x46 net.(*pollDesc).WaitRead(0xc2080ba990, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:89 +0x42 net.(*netFD).accept(0xc2080ba930, 0x58be30, 0x0, 0x9103f0, 0x23) /0/c/go/src/pkg/net/fd_unix.go:409 +0x343 net.(*TCPListener).AcceptTCP(0xc20803e168, 0x8, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:234 +0x5d net.(*TCPListener).Accept(0xc20803e168, 0x0, 0x0, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:244 +0x4b github.com/revel/revel.Run(0xe6d9) /0/go/src/github.com/revel/revel/server.go:113 +0x926 main.main() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/tmp/main.go:109 +0xe1a ` var lastGoroutine = `panic: hello! goroutine 16 [IO wait]: net.runtime_pollWait(0x911c30, 0x72, 0x0) /0/c/go/src/pkg/runtime/netpoll.goc:146 +0x66 net.(*pollDesc).Wait(0xc2080ba990, 0x72, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:84 +0x46 net.(*pollDesc).WaitRead(0xc2080ba990, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:89 +0x42 net.(*netFD).accept(0xc2080ba930, 0x58be30, 0x0, 0x9103f0, 0x23) /0/c/go/src/pkg/net/fd_unix.go:409 +0x343 net.(*TCPListener).AcceptTCP(0xc20803e168, 0x8, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:234 +0x5d net.(*TCPListener).Accept(0xc20803e168, 0x0, 0x0, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:244 +0x4b github.com/revel/revel.Run(0xe6d9) /0/go/src/github.com/revel/revel/server.go:113 +0x926 main.main() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/tmp/main.go:109 +0xe1a goroutine 54 [running]: runtime.panic(0x35ce40, 0xc208039db0) /0/c/go/src/pkg/runtime/panic.c:279 +0xf5 github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.func·001() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:13 +0x74 net/http.(*Server).Serve(0xc20806c780, 0x910c88, 0xc20803e168, 0x0, 0x0) /0/c/go/src/pkg/net/http/server.go:1698 +0x91 ` var stackOverflow = `fatal error: stack overflow runtime stack: runtime.throw(0x10cd82b, 0xe) /go/src/runtime/panic.go:1116 +0x72 runtime.newstack() /go/src/runtime/stack.go:1060 +0x78d runtime.morestack() /go/src/runtime/asm_amd64.s:449 +0x8f goroutine 1 [running]: main.stackExhaustion.func1(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/src/app/cases.go:42 +0x74 fp=0xc020161be0 sp=0xc020161bd8 pc=0x10a7774 main.stackExhaustion.func1(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/src/app/cases.go:43 +0x5f fp=0xc020163b30 sp=0xc020161be0 pc=0x10a775f main.stackExhaustion.func1(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/src/app/cases.go:43 +0x5f fp=0xc020165a80 sp=0xc020163b30 pc=0x10a775f main.stackExhaustion.func1(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/src/app/cases.go:43 +0x5f fp=0xc0201679d0 sp=0xc020165a80 pc=0x10a775f main.stackExhaustion.func1(0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...) /go/src/app/cases.go:43 +0x5f fp=0xc0201679d0 sp=0xc020165a80 pc=0x10a775f ...additional frames elided... ` var result = []StackFrame{ {File: "/0/c/go/src/pkg/runtime/panic.c", LineNumber: 279, Name: "panic", Package: "runtime"}, {File: "/0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go", LineNumber: 13, Name: "func.001", Package: "github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers"}, {File: "/0/c/go/src/pkg/net/http/server.go", LineNumber: 1698, Name: "(*Server).Serve", Package: "net/http"}, } var resultCreatedBy = append(result, StackFrame{File: "/0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go", LineNumber: 14, Name: "App.Index", Package: "github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers", ProgramCounter: 0x0}) func TestParsePanic(t *testing.T) { todo := map[string]string{ "createdBy": createdBy, "normalSplit": normalSplit, "lastGoroutine": lastGoroutine, } for key, val := range todo { Err, err := ParsePanic(val) if err != nil { t.Fatal(err) } if Err.TypeName() != "panic" { t.Errorf("Wrong type: %s", Err.TypeName()) } if Err.Error() != "hello!" { t.Errorf("Wrong message: %s", Err.TypeName()) } if Err.StackFrames()[0].Func() != nil { t.Errorf("Somehow managed to find a func...") } result := result if key == "createdBy" { result = resultCreatedBy } if !reflect.DeepEqual(Err.StackFrames(), result) { t.Errorf("Wrong stack for %s: %#v", key, Err.StackFrames()) } } } var concurrentMapReadWrite = `fatal error: concurrent map read and map write goroutine 1 [running]: runtime.throw(0x10766f5, 0x21) /usr/local/Cellar/go/1.15.5/libexec/src/runtime/panic.go:1116 +0x72 fp=0xc00003a6c8 sp=0xc00003a698 pc=0x102d592 runtime.mapaccess1_faststr(0x1066fc0, 0xc000060000, 0x10732e0, 0x1, 0xc000100088) /usr/local/Cellar/go/1.15.5/libexec/src/runtime/map_faststr.go:21 +0x465 fp=0xc00003a738 sp=0xc00003a6c8 pc=0x100e9c5 main.concurrentWrite() /myapps/go/fatalerror/main.go:14 +0x7a fp=0xc00003a778 sp=0xc00003a738 pc=0x105d83a main.main() /myapps/go/fatalerror/main.go:41 +0x25 fp=0xc00003a788 sp=0xc00003a778 pc=0x105d885 runtime.main() /usr/local/Cellar/go/1.15.5/libexec/src/runtime/proc.go:204 +0x209 fp=0xc00003a7e0 sp=0xc00003a788 pc=0x102fd49 runtime.goexit() /usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s:1374 +0x1 fp=0xc00003a7e8 sp=0xc00003a7e0 pc=0x105a4a1 goroutine 5 [runnable]: main.concurrentWrite.func1(0xc000060000) /myapps/go/fatalerror/main.go:10 +0x4c created by main.concurrentWrite /myapps/go/fatalerror/main.go:8 +0x4b ` func TestParseFatalError(t *testing.T) { Err, err := ParsePanic(concurrentMapReadWrite) if err != nil { t.Fatal(err) } if Err.TypeName() != "fatal error" { t.Errorf("Wrong type: %s", Err.TypeName()) } if Err.Error() != "concurrent map read and map write" { t.Errorf("Wrong message: '%s'", Err.Error()) } if Err.StackFrames()[0].Func() != nil { t.Errorf("Somehow managed to find a func...") } var result = []StackFrame{ StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/panic.go", LineNumber: 1116, Name: "throw", Package: "runtime"}, StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/map_faststr.go", LineNumber: 21, Name: "mapaccess1_faststr", Package: "runtime"}, StackFrame{File: "/myapps/go/fatalerror/main.go", LineNumber: 14, Name: "concurrentWrite", Package: "main"}, StackFrame{File: "/myapps/go/fatalerror/main.go", LineNumber: 41, Name: "main", Package: "main"}, StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/proc.go", LineNumber: 204, Name: "main", Package: "runtime"}, StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s", LineNumber: 1374, Name: "goexit", Package: "runtime"}, } if !reflect.DeepEqual(Err.StackFrames(), result) { t.Errorf("Wrong stack for concurrent write fatal error:") for i, frame := range result { t.Logf("[%d] %#v", i, frame) if len(Err.StackFrames()) > i { t.Logf(" %#v", Err.StackFrames()[i]) } } } } func TestParseStackOverflow(t *testing.T) { Err, err := ParsePanic(stackOverflow) if err != nil { t.Fatal(err) } if Err.TypeName() != "fatal error" { t.Errorf("Wrong type: %s", Err.TypeName()) } if Err.Error() != "stack overflow" { t.Errorf("Wrong message: '%s'", Err.Error()) } if Err.StackFrames()[0].Func() != nil { t.Errorf("Somehow managed to find a func...") } var result = []StackFrame{ {File: "/go/src/app/cases.go", LineNumber: 42, Name: "stackExhaustion.func1", Package: "main"}, {File: "/go/src/app/cases.go", LineNumber: 43, Name: "stackExhaustion.func1", Package: "main"}, {File: "/go/src/app/cases.go", LineNumber: 43, Name: "stackExhaustion.func1", Package: "main"}, {File: "/go/src/app/cases.go", LineNumber: 43, Name: "stackExhaustion.func1", Package: "main"}, {File: "/go/src/app/cases.go", LineNumber: 43, Name: "stackExhaustion.func1", Package: "main"}, } if !reflect.DeepEqual(Err.StackFrames(), result) { t.Errorf("Wrong stack:") for i, frame := range result { t.Logf("[%d] %#v", i, frame) if len(Err.StackFrames()) > i { t.Logf(" %#v", Err.StackFrames()[i]) } } } } bugsnag-go-2.2.0/errors/stackframe.go000066400000000000000000000050101432154711400175250ustar00rootroot00000000000000package errors import ( "bytes" "fmt" "io/ioutil" "runtime" "strings" ) // A StackFrame contains all necessary information about to generate a line // in a callstack. type StackFrame struct { File string LineNumber int Name string Package string ProgramCounter uintptr function *runtime.Func } // NewStackFrame popoulates a stack frame object from the program counter. func NewStackFrame(pc uintptr) (frame StackFrame) { frame = StackFrame{ProgramCounter: pc} if frame.Func() == nil { return } frame.Package, frame.Name = packageAndName(frame.Func()) // pc -1 because the program counters we use are usually return addresses, // and we want to show the line that corresponds to the function call frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) return } // Func returns the function that this stackframe corresponds to func (frame *StackFrame) Func() *runtime.Func { return frame.function } // String returns the stackframe formatted in the same way as go does // in runtime/debug.Stack() func (frame *StackFrame) String() string { str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) source, err := frame.SourceLine() if err != nil { return str } return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) } // SourceLine gets the line of code (from File and Line) of the original source if possible func (frame *StackFrame) SourceLine() (string, error) { data, err := ioutil.ReadFile(frame.File) if err != nil { return "", err } lines := bytes.Split(data, []byte{'\n'}) if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { return "???", nil } // -1 because line-numbers are 1 based, but our array is 0 based return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil } func packageAndName(fn *runtime.Func) (string, string) { name := fn.Name() pkg := "" // The name includes the path name to the package, which is unnecessary // since the file name is already included. Plus, it has center dots. // That is, we see // runtime/debug.*T·ptrmethod // and want // *T.ptrmethod // Since the package path might contains dots (e.g. code.google.com/...), // we first remove the path prefix if there is one. if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { pkg += name[:lastslash] + "/" name = name[lastslash+1:] } if period := strings.Index(name, "."); period >= 0 { pkg += name[:period] name = name[period+1:] } name = strings.Replace(name, "·", ".", -1) return pkg, name } bugsnag-go-2.2.0/event.go000066400000000000000000000155241432154711400152250ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "strings" "github.com/bugsnag/bugsnag-go/errors" ) // Context is the context of the error in Bugsnag. // This can be passed to Notify, Recover or AutoNotify as rawData. type Context struct { String string } // User represents the searchable user-data on Bugsnag. The Id is also used // to determine the number of users affected by a bug. This can be // passed to Notify, Recover or AutoNotify as rawData. type User struct { Id string `json:"id,omitempty"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` } // ErrorClass overrides the error class in Bugsnag. // This struct enables you to group errors as you like. type ErrorClass struct { Name string } // Sets the severity of the error on Bugsnag. These values can be // passed to Notify, Recover or AutoNotify as rawData. var ( SeverityError = severity{"error"} SeverityWarning = severity{"warning"} SeverityInfo = severity{"info"} ) // The severity tag type, private so that people can only use Error,Warning,Info type severity struct { String string } // The form of stacktrace that Bugsnag expects type StackFrame struct { Method string `json:"method"` File string `json:"file"` LineNumber int `json:"lineNumber"` InProject bool `json:"inProject,omitempty"` } type SeverityReason string const ( SeverityReasonCallbackSpecified SeverityReason = "userCallbackSetSeverity" SeverityReasonHandledError = "handledError" SeverityReasonHandledPanic = "handledPanic" SeverityReasonUnhandledError = "unhandledError" SeverityReasonUnhandledMiddlewareError = "unhandledErrorMiddleware" SeverityReasonUnhandledPanic = "unhandledPanic" SeverityReasonUserSpecified = "userSpecifiedSeverity" ) type HandledState struct { SeverityReason SeverityReason OriginalSeverity severity Unhandled bool Framework string } // Event represents a payload of data that gets sent to Bugsnag. // This is passed to each OnBeforeNotify hook. type Event struct { // The original error that caused this event, not sent to Bugsnag. Error *errors.Error // The rawData affecting this error, not sent to Bugsnag. RawData []interface{} // The error class to be sent to Bugsnag. This defaults to the type name of the Error, for // example *error.String ErrorClass string // The error message to be sent to Bugsnag. This defaults to the return value of Error.Error() Message string // The stacktrrace of the error to be sent to Bugsnag. Stacktrace []StackFrame // The context to be sent to Bugsnag. This should be set to the part of the app that was running, // e.g. for http requests, set it to the path. Context string // The severity of the error. Can be SeverityError, SeverityWarning or SeverityInfo. Severity severity // The grouping hash is used to override Bugsnag's grouping. Set this if you'd like all errors with // the same grouping hash to group together in the dashboard. GroupingHash string // User data to send to Bugsnag. This is searchable on the dashboard. User *User // Other MetaData to send to Bugsnag. Appears as a set of tabbed tables in the dashboard. MetaData MetaData // Ctx is the context of the session the event occurred in. This allows Bugsnag to associate the event with the session. Ctx context.Context // Request is the request information that populates the Request tab in the dashboard. Request *RequestJSON // The reason for the severity and original value handledState HandledState // True if the event was caused by an automatic event Unhandled bool } func newEvent(rawData []interface{}, notifier *Notifier) (*Event, *Configuration) { config := notifier.Config event := &Event{ RawData: append(notifier.RawData, rawData...), Severity: SeverityWarning, MetaData: make(MetaData), handledState: HandledState{ SeverityReason: SeverityReasonHandledError, OriginalSeverity: SeverityWarning, Unhandled: false, Framework: "", }, Unhandled: false, } var err *errors.Error var callbacks []func(*Event) for _, datum := range event.RawData { switch datum := datum.(type) { case error, errors.Error: err = errors.New(datum.(error), 1) event.Error = err // Only assign automatically if not explicitly set through ErrorClass already if event.ErrorClass == "" { event.ErrorClass = err.TypeName() } event.Message = err.Error() event.Stacktrace = make([]StackFrame, len(err.StackFrames())) case bool: config = config.merge(&Configuration{Synchronous: bool(datum)}) case severity: event.Severity = datum event.handledState.OriginalSeverity = datum event.handledState.SeverityReason = SeverityReasonUserSpecified case Context: event.Context = datum.String case context.Context: populateEventWithContext(datum, event) case *http.Request: populateEventWithRequest(datum, event) case Configuration: config = config.merge(&datum) case MetaData: event.MetaData.Update(datum) case User: event.User = &datum case ErrorClass: event.ErrorClass = datum.Name case HandledState: event.handledState = datum event.Severity = datum.OriginalSeverity event.Unhandled = datum.Unhandled case func(*Event): callbacks = append(callbacks, datum) } } event.Stacktrace = generateStacktrace(err, config) for _, callback := range callbacks { callback(event) if event.Severity != event.handledState.OriginalSeverity { event.handledState.SeverityReason = SeverityReasonCallbackSpecified } } return event, config } func generateStacktrace(err *errors.Error, config *Configuration) []StackFrame { stack := make([]StackFrame, len(err.StackFrames())) for i, frame := range err.StackFrames() { file := frame.File inProject := config.isProjectPackage(frame.Package) // remove $GOROOT and $GOHOME from other frames if idx := strings.Index(file, frame.Package); idx > -1 { file = file[idx:] } if inProject { file = config.stripProjectPackages(file) } stack[i] = StackFrame{ Method: frame.Name, File: file, LineNumber: frame.LineNumber, InProject: inProject, } } return stack } func populateEventWithContext(ctx context.Context, event *Event) { event.Ctx = ctx reqJSON, req := extractRequestInfo(ctx) if event.Request == nil { event.Request = reqJSON } populateEventWithRequest(req, event) } func populateEventWithRequest(req *http.Request, event *Event) { if req == nil { return } event.Request = extractRequestInfoFromReq(req) if event.Context == "" { event.Context = req.URL.Path } // Default user.id to IP so that the count of users affected works. if event.User == nil { ip := req.RemoteAddr if idx := strings.LastIndex(ip, ":"); idx != -1 { ip = ip[:idx] } event.User = &User{Id: ip} } } bugsnag-go-2.2.0/event_test.go000066400000000000000000000016071432154711400162610ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "net/http/httptest" "reflect" "strings" "testing" ) func TestPopulateEvent(t *testing.T) { event := new(Event) contexts := make(chan context.Context, 1) reqs := make(chan *http.Request, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { contexts <- AttachRequestData(r.Context(), r) reqs <- r })) defer ts.Close() http.Get(ts.URL + "/serenity?q=abcdef") ctx, req := <-contexts, <-reqs populateEventWithContext(ctx, event) for _, tc := range []struct{ e, c interface{} }{ {e: event.Ctx, c: ctx}, {e: event.Request, c: extractRequestInfoFromReq(req)}, {e: event.Context, c: req.URL.Path}, {e: event.User.Id, c: req.RemoteAddr[:strings.LastIndex(req.RemoteAddr, ":")]}, } { if !reflect.DeepEqual(tc.e, tc.c) { t.Errorf("Expected '%+v' and '%+v' to be equal", tc.e, tc.c) } } } bugsnag-go-2.2.0/examples/000077500000000000000000000000001432154711400153645ustar00rootroot00000000000000bugsnag-go-2.2.0/examples/README.md000066400000000000000000000013371432154711400166470ustar00rootroot00000000000000# Examples of working with bugsnag-go In this directory you can find example applications of the frameworks we support, and other examples of common use cases. The examples that expose a HTTP port will all listen on 9001. ## Use cases and frameworks * [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. This is an example of a panic within a goroutine which is sent to Bugsnag. * [Using net/http](http) (web server using the standard library) * [Using Gin](gin) (web framework) * [Using Negroni](negroni) (web framework) * [Using Martini](martini) (web framework) * [Using Revel](revelapp) (web framework) bugsnag-go-2.2.0/examples/http/000077500000000000000000000000001432154711400163435ustar00rootroot00000000000000bugsnag-go-2.2.0/examples/http/README.md000066400000000000000000000011741432154711400176250ustar00rootroot00000000000000# Example `net/http` application This package contains an example `net/http` application, with Bugsnag configured. ## Run the example 1. Change the API key in `main.go` to a project you've created in [Bugsnag](https://app.bugsnag.com). 1. Inside `bugsnag-go/examples/http` do: ```bash go get go run main.go ``` 1. The application is now running. You can now visit ``` http://localhost:9001/unhandled - to trigger an unhandled panic http://localhost:9001/handled - to trigger a handled error ``` 1. You should now see events for these exceptions in your [Bugsnag dashboard](https://app.bugsnag.com). bugsnag-go-2.2.0/examples/http/main.go000066400000000000000000000023271432154711400176220ustar00rootroot00000000000000package main import ( "fmt" "net/http" "os" "github.com/bugsnag/bugsnag-go/v2" ) // Insert your API key const apiKey = "YOUR-API-KEY-HERE" func main() { if len(apiKey) != 32 { fmt.Println("Please set the API key in main.go before running the example") return } bugsnag.Configure(bugsnag.Configuration{APIKey: apiKey}) http.HandleFunc("/unhandled", unhandledCrash) http.HandleFunc("/handled", handledError) fmt.Println("=============================================================================") fmt.Println("Visit http://localhost:9001/unhandled - To perform an unhandled crash") fmt.Println("Visit http://localhost:9001/handled - To create a manual error notification") fmt.Println("=============================================================================") fmt.Println("") http.ListenAndServe(":9001", bugsnag.Handler(nil)) } func unhandledCrash(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Write([]byte("OK\n")) // Invalid type assertion, will panic func(a interface{}) string { return a.(string) }(struct{}{}) } func handledError(w http.ResponseWriter, r *http.Request) { _, err := os.Open("nonexistent_file.txt") if err != nil { bugsnag.Notify(err, r.Context()) } } bugsnag-go-2.2.0/examples/using-goroutines/000077500000000000000000000000001432154711400207055ustar00rootroot00000000000000bugsnag-go-2.2.0/examples/using-goroutines/README.md000066400000000000000000000010331432154711400221610ustar00rootroot00000000000000# Managing panics from goroutines This package contains an example for how to manage panics in a separate goroutine with Bugsnag. ## Run the example 1. Change the API key in `main.go` to a project you've created in [Bugsnag](https://app.bugsnag.com). 1. Inside `bugsnag-go/examples/using-goroutines` do: ```bash go get go run main.go ``` 1. The application will run for a split second, starting a new goroutine, which panics. 1. You should now see events this panic in your [Bugsnag dashboard](https://app.bugsnag.com). bugsnag-go-2.2.0/examples/using-goroutines/main.go000066400000000000000000000021521432154711400221600ustar00rootroot00000000000000package main import ( "context" "fmt" "sync" "github.com/bugsnag/bugsnag-go/v2" ) // Insert your API key const apiKey = "YOUR-API-KEY-HERE" // The following example will cause two events in your dashboard: // One event because AutoNotify intercepted a panic. // The other because Bugsnag noticed your application was about to be taken // down by a panic. // To avoid taking down your application and the last event, replace // bugsnag.AutoNotify with bugsnag.Recover in the below example. func main() { if len(apiKey) != 32 { fmt.Println("Please set your API key in main.go before running example.") return } bugsnag.Configure(bugsnag.Configuration{APIKey: apiKey}) var wg sync.WaitGroup wg.Add(1) go func() { fmt.Println("Starting new go routine...") // Manually create a new Bugsnag session for this goroutine ctx := bugsnag.StartSession(context.Background()) defer wg.Done() // AutoNotify captures any panics, repanicking after error reports are sent defer bugsnag.AutoNotify(ctx) // Invalid type assertion, will panic func(a interface{}) { _ = a.(string) }(struct{}{}) }() wg.Wait() } bugsnag-go-2.2.0/features/000077500000000000000000000000001432154711400153645ustar00rootroot00000000000000bugsnag-go-2.2.0/features/apptype.feature000066400000000000000000000021051432154711400204210ustar00rootroot00000000000000Feature: Configuring app type Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "APP_TYPE" to "background-queue" And I have built the service "app" Scenario: An error report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "app.type" equals "background-queue" Scenario: An session report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I run the go service "app" with the test case "session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "app.type" equals "background-queue" bugsnag-go-2.2.0/features/appversion.feature000066400000000000000000000020571432154711400211330ustar00rootroot00000000000000Feature: Configuring app version Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "APP_VERSION" to "3.1.2" And I have built the service "app" Scenario: An error report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "app.version" equals "3.1.2" Scenario: An session report contains the configured app type when running a go app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I run the go service "app" with the test case "session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "app.version" equals "3.1.2" bugsnag-go-2.2.0/features/autonotify.feature000066400000000000000000000012051432154711400211400ustar00rootroot00000000000000Feature: Using auto notify Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered When I run the go service "app" with the test case "autonotify" Then I wait for 3 seconds And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "errorClass" equals "*errors.errorString" for request 1 And the exception "message" equals "Go routine killed with auto notify" for request 1 bugsnag-go-2.2.0/features/configuration.feature000066400000000000000000000142671432154711400216220ustar00rootroot00000000000000Feature: Configure integration with environment variables The library should be configurable using environment variables to support single-line and reusable configuration Background: Given I set environment variable "BUGSNAG_API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I set environment variable "BUGSNAG_NOTIFY_ENDPOINT" to the notify endpoint And I set environment variable "BUGSNAG_SESSIONS_ENDPOINT" to the sessions endpoint And I have built the service "autoconfigure" Scenario Outline: Adding content to handled events through env variables Given I set environment variable "" to "" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I run the go service "autoconfigure" with the test case "" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "" equals "" Examples: | testcase | variable | value | field | | panic | BUGSNAG_APP_VERSION | 1.4.34 | app.version | | panic | BUGSNAG_APP_TYPE | mailer-daemon | app.type | | panic | BUGSNAG_RELEASE_STAGE | beta1 | app.releaseStage | | panic | BUGSNAG_HOSTNAME | dream-machine-2 | device.hostname | | panic | BUGSNAG_METADATA_device_instance | kube2-33-A | metaData.device.instance | | panic | BUGSNAG_METADATA_framework_version | v3.1.0 | metaData.framework.version | | panic | BUGSNAG_METADATA_device_runtime_level | 1C | metaData.device.runtime_level | | panic | BUGSNAG_METADATA_Carrot | orange | metaData.custom.Carrot | | handled | BUGSNAG_APP_VERSION | 1.4.34 | app.version | | handled | BUGSNAG_APP_TYPE | mailer-daemon | app.type | | handled | BUGSNAG_RELEASE_STAGE | beta1 | app.releaseStage | | handled | BUGSNAG_HOSTNAME | dream-machine-2 | device.hostname | | handled | BUGSNAG_METADATA_device_instance | kube2-33-A | metaData.device.instance | | handled | BUGSNAG_METADATA_framework_version | v3.1.0 | metaData.framework.version | | handled | BUGSNAG_METADATA_device_runtime_level | 1C | metaData.device.runtime_level | | handled | BUGSNAG_METADATA_Carrot | orange | metaData.custom.Carrot | Scenario: Configuring project packages Given I set environment variable "BUGSNAG_PROJECT_PACKAGES" to "main,test" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I run the go service "autoconfigure" with the test case "panic" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the in-project frames of the stacktrace are: | file | method | lineNumber | | cases.go | explicitPanic | 22 | | main.go | main | 11 | Scenario: Configuring source root Given I set environment variable "BUGSNAG_SOURCE_ROOT" to the app directory And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I run the go service "autoconfigure" with the test case "panic" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the in-project frames of the stacktrace are: | file | method | lineNumber | | cases.go | explicitPanic | 22 | | main.go | main | 11 | Scenario: Delivering events filtering through notify release stages Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "prod,beta" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "beta" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I run the go service "autoconfigure" with the test case "panic" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" Scenario: Suppressing events through notify release stages Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "prod,beta" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "dev" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I run the go service "autoconfigure" with the test case "panic" Then 0 requests were received Scenario: Suppressing events using panic handler Given I set environment variable "BUGSNAG_DISABLE_PANIC_HANDLER" to "1" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I run the go service "autoconfigure" with the test case "panic" And I wait for 2 seconds Then 0 requests were received Scenario: Enabling synchronous event delivery Given I set environment variable "BUGSNAG_SYNCHRONOUS" to "1" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I run the go service "autoconfigure" with the test case "handled" Then 1 request was received Scenario: Filtering metadata Given I set environment variable "BUGSNAG_PARAMS_FILTERS" to "tomato,pears" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I run the go service "autoconfigure" with the test case "handled-metadata" Then I wait to receive a request And the event "metaData.fruit.Tomato" equals "[FILTERED]" And the event "metaData.snacks.Carrot" equals "4" bugsnag-go-2.2.0/features/endpoint.feature000066400000000000000000000013201432154711400205550ustar00rootroot00000000000000Feature: Configuring endpoint Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report is sent successfully using the notify endpoint only When I run the go service "app" with the test case "endpoint-notify" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" Scenario: Configuring Bugsnag will panic if the sessions endpoint is configured without the notify endpoint When I run the go service "app" with the test case "endpoint-session" And I wait for 3 second Then I should receive no requests bugsnag-go-2.2.0/features/fixtures/000077500000000000000000000000001432154711400172355ustar00rootroot00000000000000bugsnag-go-2.2.0/features/fixtures/app/000077500000000000000000000000001432154711400200155ustar00rootroot00000000000000bugsnag-go-2.2.0/features/fixtures/app/Dockerfile000066400000000000000000000012521432154711400220070ustar00rootroot00000000000000ARG GO_VERSION FROM golang:${GO_VERSION}-alpine RUN apk update && apk upgrade && apk add git bash ENV GOPATH /app COPY testbuild /app/src/github.com/bugsnag/bugsnag-go WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2 RUN go get ./... # Copy test scenarios COPY ./app /app/src/test WORKDIR /app/src/test # Ensure subsequent steps are re-run if the GO_VERSION variable changes ARG GO_VERSION # Create app module - avoid locking bugsnag dep by not checking it in # Skip on old versions of Go which pre-date modules RUN if [[ $GO_VERSION != '1.11' && $GO_VERSION != '1.12' ]]; then \ go mod init && go mod tidy; \ fi RUN chmod +x run.sh CMD ["/app/src/test/run.sh"] bugsnag-go-2.2.0/features/fixtures/app/main.go000066400000000000000000000171251432154711400212760ustar00rootroot00000000000000package main import ( "context" "flag" "fmt" "log" "os" "runtime" "strconv" "strings" "time" bugsnag "github.com/bugsnag/bugsnag-go/v2" ) func configureBasicBugsnag(testcase string) { config := bugsnag.Configuration{ APIKey: os.Getenv("API_KEY"), AppVersion: os.Getenv("APP_VERSION"), AppType: os.Getenv("APP_TYPE"), Hostname: os.Getenv("HOSTNAME"), } if notifyReleaseStages := os.Getenv("NOTIFY_RELEASE_STAGES"); notifyReleaseStages != "" { config.NotifyReleaseStages = strings.Split(notifyReleaseStages, ",") } if releaseStage := os.Getenv("RELEASE_STAGE"); releaseStage != "" { config.ReleaseStage = releaseStage } if filters := os.Getenv("PARAMS_FILTERS"); filters != "" { config.ParamsFilters = []string{filters} } sync, err := strconv.ParseBool(os.Getenv("SYNCHRONOUS")) if err == nil { config.Synchronous = sync } acs, err := strconv.ParseBool(os.Getenv("AUTO_CAPTURE_SESSIONS")) if err == nil { config.AutoCaptureSessions = acs } switch testcase { case "endpoint-notify": config.Endpoints = bugsnag.Endpoints{Notify: os.Getenv("BUGSNAG_ENDPOINT")} case "endpoint-session": config.Endpoints = bugsnag.Endpoints{Sessions: os.Getenv("BUGSNAG_ENDPOINT")} default: config.Endpoints = bugsnag.Endpoints{ Notify: os.Getenv("BUGSNAG_ENDPOINT"), Sessions: os.Getenv("BUGSNAG_ENDPOINT"), } } bugsnag.Configure(config) time.Sleep(200 * time.Millisecond) // Increase publish rate for testing bugsnag.DefaultSessionPublishInterval = time.Millisecond * 100 } func main() { test := flag.String("test", "handled", "what the app should send, either handled, unhandled, session, autonotify") flag.Parse() configureBasicBugsnag(*test) time.Sleep(100 * time.Millisecond) // Ensure tests are less flaky by ensuring the start-up session gets sent switch *test { case "unhandled": unhandledCrash() case "handled", "endpoint-legacy", "endpoint-notify", "endpoint-session": handledError() case "handled-with-callback": handledCallbackError() case "session": session() case "autonotify": autonotify() case "metadata": metadata() case "onbeforenotify": onBeforeNotify() case "filtered": filtered() case "recover": dontDie() case "session-and-error": sessionAndError() case "send-and-exit": sendAndExit() case "user": user() case "multiple-handled": multipleHandled() case "multiple-unhandled": multipleUnhandled() case "make-unhandled-with-callback": handledToUnhandled() case "nested-error": nestedHandledError() default: log.Println("Not a valid test flag: " + *test) os.Exit(1) } } func multipleHandled() { //Make the order of the below predictable bugsnag.Configure(bugsnag.Configuration{Synchronous: true}) ctx := bugsnag.StartSession(context.Background()) bugsnag.Notify(fmt.Errorf("oops"), ctx) bugsnag.Notify(fmt.Errorf("oops"), ctx) } func multipleUnhandled() { //Make the order of the below predictable notifier := bugsnag.New(bugsnag.Configuration{Synchronous: true}) notifier.FlushSessionsOnRepanic(false) ctx := bugsnag.StartSession(context.Background()) defer func() { recover() }() defer notifier.AutoNotify(ctx) defer notifier.AutoNotify(ctx) panic("oops") } func unhandledCrash() { // Invalid type assertion, will panic func(a interface{}) string { return a.(string) }(struct{}{}) } func handledError() { if _, err := os.Open("nonexistent_file.txt"); err != nil { if errClass := os.Getenv("ERROR_CLASS"); errClass != "" { bugsnag.Notify(err, bugsnag.ErrorClass{Name: errClass}) } else { bugsnag.Notify(err) } } // Give some time for the error to be sent before exiting time.Sleep(200 * time.Millisecond) } func session() { bugsnag.StartSession(context.Background()) // Give some time for the session to be sent before exiting time.Sleep(200 * time.Millisecond) } func autonotify() { go func() { defer bugsnag.AutoNotify() panic("Go routine killed with auto notify") }() // Give enough time for the panic to happen time.Sleep(100 * time.Millisecond) } func metadata() { customerData := map[string]string{"Name": "Joe Bloggs", "Age": "21"} bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ "Scheme": { "Customer": customerData, "Level": "Blue", }, }) time.Sleep(200 * time.Millisecond) } func filtered() { bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ "Account": { "Name": "Company XYZ", "Price(dollars)": "1 Million", }, }) time.Sleep(200 * time.Millisecond) } func onBeforeNotify() { bugsnag.OnBeforeNotify( func(event *bugsnag.Event, config *bugsnag.Configuration) error { if event.Message == "Ignore this error" { return fmt.Errorf("not sending errors to ignore") } // continue notifying as normal if event.Message == "Change error message" { event.Message = "Error message was changed" } return nil }) bugsnag.Notify(fmt.Errorf("Ignore this error")) time.Sleep(100 * time.Millisecond) bugsnag.Notify(fmt.Errorf("Don't ignore this error")) time.Sleep(100 * time.Millisecond) bugsnag.Notify(fmt.Errorf("Change error message")) time.Sleep(100 * time.Millisecond) } func dontDie() { go func() { defer bugsnag.Recover() panic("Go routine killed but recovered") }() time.Sleep(100 * time.Millisecond) } func sessionAndError() { ctx := bugsnag.StartSession(context.Background()) bugsnag.Notify(fmt.Errorf("oops"), ctx) time.Sleep(200 * time.Millisecond) } func sendAndExit() { bugsnag.Notify(fmt.Errorf("oops")) } func user() { bugsnag.Notify(fmt.Errorf("oops"), bugsnag.User{ Id: "test-user-id", Name: "test-user-name", Email: "test-user-email", }) time.Sleep(200 * time.Millisecond) } func handledCallbackError() { bugsnag.Notify(fmt.Errorf("Inadequent Prep Error"), func(event *bugsnag.Event) { event.Context = "nonfatal.go:14" event.Severity = bugsnag.SeverityInfo event.Stacktrace[1].File = ">insertion<" event.Stacktrace[1].LineNumber = 0 }) // Give some time for the error to be sent before exiting time.Sleep(200 * time.Millisecond) } func handledToUnhandled() { bugsnag.Notify(fmt.Errorf("unknown event"), func(event *bugsnag.Event) { event.Unhandled = true event.Severity = bugsnag.SeverityError }) // Give some time for the error to be sent before exiting time.Sleep(200 * time.Millisecond) } type customErr struct { msg string cause error callers []uintptr } func newCustomErr(msg string, cause error) error { callers := make([]uintptr, 8) runtime.Callers(2, callers) return customErr { msg: msg, cause: cause, callers: callers, } } func (err customErr) Error() string { return err.msg } func (err customErr) Unwrap() error { return err.cause } func (err customErr) Callers() []uintptr { return err.callers } func nestedHandledError() { if err := login("token " + os.Getenv("API_KEY")); err != nil { bugsnag.Notify(newCustomErr("terminate process", err)) // Give some time for the error to be sent before exiting time.Sleep(200 * time.Millisecond) } else { i := len(os.Getenv("API_KEY")) // Some nonsense to avoid inlining checkValue if val, err := checkValue(i); err != nil { fmt.Printf("err: %v, val: %d", err, val) } if val, err := checkValue(i-46); err != nil { fmt.Printf("err: %v, val: %d", err, val) } log.Fatalf("This test is broken - no error was generated.") } } func login(token string) error { val, err := checkValue(len(token) * -1) if err != nil { return newCustomErr("login failed", err) } fmt.Printf("val: %d", val) return nil } func checkValue(i int) (int, error) { if i < 0 { return 0, newCustomErr("invalid token", nil) } else if i % 2 == 0 { return i / 2, nil } else if i < 9 { return i * 3, nil } return i * 4, nil } bugsnag-go-2.2.0/features/fixtures/app/run.sh000077500000000000000000000010031432154711400211520ustar00rootroot00000000000000#!/usr/bin/env bash # SIGTERM or SIGINT trapped (likely SIGTERM from docker), pass it onto app # process function _term_or_init { kill -TERM "$APP_PID" 2>/dev/null wait $APP_PID } # The bugsnag notifier monitor process needs at least 300ms, in order to ensure # that it can send its notify function _exit { sleep 1 } trap _term_or_init SIGTERM SIGINT trap _exit EXIT PROC="${@:1}" $PROC & # Wait on the app process to ensure that this script is able to trap the SIGTERM # signal APP_PID=$! wait $APP_PID bugsnag-go-2.2.0/features/fixtures/autoconfigure/000077500000000000000000000000001432154711400221075ustar00rootroot00000000000000bugsnag-go-2.2.0/features/fixtures/autoconfigure/Dockerfile000066400000000000000000000013171432154711400241030ustar00rootroot00000000000000ARG GO_VERSION FROM golang:${GO_VERSION}-alpine RUN apk update && apk upgrade && apk add git bash ENV GOPATH /app COPY testbuild /app/src/github.com/bugsnag/bugsnag-go WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2 # Get bugsnag dependencies RUN go get ./... # Copy test scenarios COPY ./autoconfigure /app/src/test WORKDIR /app/src/test # Ensure subsequent steps are re-run if the GO_VERSION variable changes ARG GO_VERSION # Create app module - avoid locking bugsnag dep by not checking it in # Skip on old versions of Go which pre-date modules RUN if [[ $GO_VERSION != '1.11' && $GO_VERSION != '1.12' ]]; then \ go mod init && go mod tidy; \ fi RUN chmod +x run.sh CMD ["/app/src/test/run.sh"] bugsnag-go-2.2.0/features/fixtures/autoconfigure/cases.go000066400000000000000000000006551432154711400235420ustar00rootroot00000000000000package main import ( "fmt" "github.com/bugsnag/bugsnag-go/v2" ) func explicitPanic() { panic("PANIQ!") } func handledEvent() { bugsnag.Notify(fmt.Errorf("gone awry!")) } func handledMetadata() { bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { event.MetaData.Add("fruit", "Tomato", "beefsteak") event.MetaData.Add("snacks", "Carrot", "4") return nil }) handledEvent() } bugsnag-go-2.2.0/features/fixtures/autoconfigure/main.go000066400000000000000000000012071432154711400233620ustar00rootroot00000000000000package main import ( "flag" "fmt" "time" "github.com/bugsnag/bugsnag-go/v2" ) var testcase = flag.String("test", "", "the error scenario to run") func main() { bugsnag.Configure(bugsnag.Configuration{}) // Increase publish rate for testing bugsnag.DefaultSessionPublishInterval = time.Millisecond * 50 flag.Parse() switch *testcase { case "panic": explicitPanic() case "handled": handledEvent() case "handled-metadata": handledMetadata() case "no-op": // nothing to see here default: fmt.Printf("No test case found for '%s'\n", *testcase) } time.Sleep(time.Millisecond * 100) // time to send before termination } bugsnag-go-2.2.0/features/fixtures/autoconfigure/run.sh000077500000000000000000000010031432154711400232440ustar00rootroot00000000000000#!/usr/bin/env bash # SIGTERM or SIGINT trapped (likely SIGTERM from docker), pass it onto app # process function _term_or_init { kill -TERM "$APP_PID" 2>/dev/null wait $APP_PID } # The bugsnag notifier monitor process needs at least 300ms, in order to ensure # that it can send its notify function _exit { sleep 1 } trap _term_or_init SIGTERM SIGINT trap _exit EXIT PROC="${@:1}" $PROC & # Wait on the app process to ensure that this script is able to trap the SIGTERM # signal APP_PID=$! wait $APP_PID bugsnag-go-2.2.0/features/fixtures/docker-compose.yml000066400000000000000000000071671432154711400227050ustar00rootroot00000000000000version: '3.4' services: app: build: context: . dockerfile: app/Dockerfile args: - GO_VERSION environment: - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT - APP_VERSION - APP_TYPE - AUTO_CAPTURE_SESSIONS - HOSTNAME - NOTIFY_RELEASE_STAGES - RELEASE_STAGE - PARAMS_FILTERS - SYNCHRONOUS - SERVER_PORT restart: "no" autoconfigure: build: context: . dockerfile: autoconfigure/Dockerfile args: - GO_VERSION environment: - BUGSNAG_API_KEY - BUGSNAG_APP_TYPE - BUGSNAG_APP_VERSION - BUGSNAG_AUTO_CAPTURE_SESSIONS - BUGSNAG_DISABLE_PANIC_HANDLER - BUGSNAG_HOSTNAME - BUGSNAG_NOTIFY_ENDPOINT - BUGSNAG_NOTIFY_RELEASE_STAGES - BUGSNAG_PARAMS_FILTERS - BUGSNAG_PROJECT_PACKAGES - BUGSNAG_RELEASE_STAGE - BUGSNAG_SESSIONS_ENDPOINT - BUGSNAG_SOURCE_ROOT - BUGSNAG_SYNCHRONOUS - BUGSNAG_METADATA_Carrot - BUGSNAG_METADATA_device_instance - BUGSNAG_METADATA_device_runtime_level - BUGSNAG_METADATA_framework_version - BUGSNAG_METADATA_fruit_Tomato - BUGSNAG_METADATA_snacks_Carrot restart: "no" command: go run . nethttp: build: context: . dockerfile: net_http/Dockerfile args: - GO_VERSION ports: - "4512:4512" environment: - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT - APP_VERSION - APP_TYPE - HOSTNAME - NOTIFY_RELEASE_STAGES - RELEASE_STAGE - PARAMS_FILTERS - AUTO_CAPTURE_SESSIONS - SYNCHRONOUS - SERVER_PORT restart: "no" command: go run main.go gin: build: context: . dockerfile: gin/Dockerfile args: - GO_VERSION - GIN_VERSION ports: - "4511:4511" environment: - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT - APP_VERSION - APP_TYPE - HOSTNAME - NOTIFY_RELEASE_STAGES - RELEASE_STAGE - PARAMS_FILTERS - AUTO_CAPTURE_SESSIONS - SYNCHRONOUS - SERVER_PORT restart: "no" command: go run main.go martini: build: context: . dockerfile: martini/Dockerfile args: - GO_VERSION ports: - "4513:4513" environment: - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT - APP_VERSION - APP_TYPE - HOSTNAME - NOTIFY_RELEASE_STAGES - RELEASE_STAGE - PARAMS_FILTERS - AUTO_CAPTURE_SESSIONS - SYNCHRONOUS - SERVER_PORT restart: "no" command: go run main.go negroni: build: context: . dockerfile: negroni/Dockerfile args: - GO_VERSION - NEGRONI_VERSION ports: - "4514:4514" environment: - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT - APP_VERSION - APP_TYPE - HOSTNAME - NOTIFY_RELEASE_STAGES - RELEASE_STAGE - PARAMS_FILTERS - AUTO_CAPTURE_SESSIONS - SYNCHRONOUS - SERVER_PORT restart: "no" command: go run main.go revel: build: context: . dockerfile: revel/Dockerfile args: - GO_VERSION - REVEL_VERSION - REVEL_CMD_VERSION ports: - "4515:4515" environment: - API_KEY - ERROR_CLASS - BUGSNAG_ENDPOINT - APP_VERSION - APP_TYPE - HOSTNAME - NOTIFY_RELEASE_STAGES - RELEASE_STAGE - PARAMS_FILTERS - AUTO_CAPTURE_SESSIONS - SYNCHRONOUS - SERVER_PORT - USE_PROPERTIES_FILE_CONFIG restart: "no" command: ./test/run.sh bugsnag-go-2.2.0/features/fixtures/net_http/000077500000000000000000000000001432154711400210625ustar00rootroot00000000000000bugsnag-go-2.2.0/features/fixtures/net_http/Dockerfile000066400000000000000000000012371432154711400230570ustar00rootroot00000000000000ARG GO_VERSION FROM golang:${GO_VERSION}-alpine RUN apk update && \ apk upgrade && \ apk add git ENV GOPATH /app COPY testbuild /app/src/github.com/bugsnag/bugsnag-go WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2 # Get bugsnag dependencies RUN go get ./... # Copy test scenarios COPY ./net_http /app/src/test WORKDIR /app/src/test # Ensure subsequent steps are re-run if the GO_VERSION variable changes ARG GO_VERSION # Create app module - avoid locking bugsnag dep by not checking it in # Skip on old versions of Go which pre-date modules RUN if [[ $GO_VERSION != '1.11' && $GO_VERSION != '1.12' ]]; then \ go mod init && go mod tidy; \ fi bugsnag-go-2.2.0/features/fixtures/net_http/main.go000066400000000000000000000071421432154711400223410ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "net/http" "os" "strconv" "strings" "time" bugsnag "github.com/bugsnag/bugsnag-go/v2" ) func main() { configureBasicBugsnag() http.HandleFunc("/handled", handledError) http.HandleFunc("/autonotify-then-recover", unhandledCrash) http.HandleFunc("/session", session) http.HandleFunc("/autonotify", autonotify) http.HandleFunc("/onbeforenotify", onBeforeNotify) http.HandleFunc("/recover", dontdie) http.HandleFunc("/user", user) http.ListenAndServe(":"+os.Getenv("SERVER_PORT"), recoverWrap(bugsnag.Handler(nil))) } // Simple wrapper to send internal server error on panics func recoverWrap(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { r := recover() if r != nil { http.Error(w, "", http.StatusInternalServerError) } }() h.ServeHTTP(w, r) }) } func configureBasicBugsnag() { config := bugsnag.Configuration{ APIKey: os.Getenv("API_KEY"), Endpoints: bugsnag.Endpoints{ Notify: os.Getenv("BUGSNAG_ENDPOINT"), Sessions: os.Getenv("BUGSNAG_ENDPOINT"), }, AppVersion: os.Getenv("APP_VERSION"), AppType: os.Getenv("APP_TYPE"), Hostname: os.Getenv("HOSTNAME"), } if notifyReleaseStages := os.Getenv("NOTIFY_RELEASE_STAGES"); notifyReleaseStages != "" { config.NotifyReleaseStages = strings.Split(notifyReleaseStages, ",") } if releaseStage := os.Getenv("RELEASE_STAGE"); releaseStage != "" { config.ReleaseStage = releaseStage } if filters := os.Getenv("PARAMS_FILTERS"); filters != "" { config.ParamsFilters = []string{filters} } acs, err := strconv.ParseBool(os.Getenv("AUTO_CAPTURE_SESSIONS")) if err == nil { config.AutoCaptureSessions = acs } bugsnag.Configure(config) // Increase publish rate for testing bugsnag.DefaultSessionPublishInterval = time.Millisecond * 300 } func handledError(w http.ResponseWriter, r *http.Request) { if _, err := os.Open("nonexistent_file.txt"); err != nil { if errClass := os.Getenv("ERROR_CLASS"); errClass != "" { bugsnag.Notify(err, r.Context(), bugsnag.ErrorClass{Name: errClass}) } else { bugsnag.Notify(err, r.Context()) } } } func unhandledCrash(w http.ResponseWriter, r *http.Request) { // Invalid type assertion, will panic func(a interface{}) string { return a.(string) }(struct{}{}) } func session(w http.ResponseWriter, r *http.Request) { log.Println("single session") } func autonotify(w http.ResponseWriter, r *http.Request) { go func(ctx context.Context) { defer func() { recover() }() defer bugsnag.AutoNotify(ctx) panic("Go routine killed with auto notify") }(r.Context()) } func onBeforeNotify(w http.ResponseWriter, r *http.Request) { bugsnag.OnBeforeNotify( func(event *bugsnag.Event, config *bugsnag.Configuration) error { if event.Message == "Ignore this error" { return fmt.Errorf("not sending errors to ignore") } // continue notifying as normal if event.Message == "Change error message" { event.Message = "Error message was changed" } return nil }) bugsnag.Notify(fmt.Errorf("Ignore this error")) time.Sleep(100 * time.Millisecond) bugsnag.Notify(fmt.Errorf("Don't ignore this error")) time.Sleep(100 * time.Millisecond) bugsnag.Notify(fmt.Errorf("Change error message")) time.Sleep(100 * time.Millisecond) } func dontdie(w http.ResponseWriter, r *http.Request) { defer bugsnag.Recover(r.Context()) panic("Request killed but recovered") } func user(w http.ResponseWriter, r *http.Request) { bugsnag.Notify(fmt.Errorf("oops"), r.Context(), bugsnag.User{ Id: "test-user-id", Name: "test-user-name", Email: "test-user-email", }) } bugsnag-go-2.2.0/features/handled.feature000066400000000000000000000067311432154711400203470ustar00rootroot00000000000000Feature: Plain handled errors Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: A handled error sends a report When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledError" And the exception is a PathError for request 0 And the "file" of stack frame 0 equals "main.go" Scenario: A handled error sends a report with a custom name Given I set environment variable "ERROR_CLASS" to "MyCustomErrorClass" When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledError" And the exception "errorClass" equals "MyCustomErrorClass" And the "file" of stack frame 0 equals "main.go" Scenario: Sending an event using a callback to modify report contents When I run the go service "app" with the test case "handled-with-callback" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is false And the event "severity" equals "info" And the event "severityReason.type" equals "userCallbackSetSeverity" And the event "context" equals "nonfatal.go:14" And the "file" of stack frame 0 equals "main.go" And stack frame 0 contains a local function spanning 241 to 247 And the "file" of stack frame 1 equals ">insertion<" And the "lineNumber" of stack frame 1 equals 0 Scenario: Marking an error as unhandled in a callback When I run the go service "app" with the test case "make-unhandled-with-callback" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "userCallbackSetSeverity" And the event "severityReason.unhandledOverridden" is true And the "file" of stack frame 0 equals "main.go" And stack frame 0 contains a local function spanning 253 to 256 Scenario: Unwrapping the causes of a handled error When I run the go service "app" with the test case "nested-error" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is false And the event "severity" equals "warning" And the event "exceptions.0.message" equals "terminate process" And the "lineNumber" of stack frame 0 equals 291 And the "file" of stack frame 0 equals "main.go" And the "method" of stack frame 0 equals "nestedHandledError" And the event "exceptions.1.message" equals "login failed" And the event "exceptions.1.stacktrace.0.file" equals "main.go" And the event "exceptions.1.stacktrace.0.lineNumber" equals 311 And the event "exceptions.2.message" equals "invalid token" And the event "exceptions.2.stacktrace.0.file" equals "main.go" And the event "exceptions.2.stacktrace.0.lineNumber" equals 319 bugsnag-go-2.2.0/features/hostname.feature000066400000000000000000000021121432154711400205530ustar00rootroot00000000000000Feature: Configuring hostname Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report contains the configured hostname Given I set environment variable "HOSTNAME" to "server-1a" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I run the go service "app" with the test case "handled" And I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "device.hostname" equals "server-1a" Scenario: An session report contains the configured hostname Given I set environment variable "HOSTNAME" to "server-1a" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I run the go service "app" with the test case "session" And I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "device.hostname" equals "server-1a" bugsnag-go-2.2.0/features/metadata.feature000066400000000000000000000012611432154711400205210ustar00rootroot00000000000000Feature: Sending meta data Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report contains custom meta data When I run the go service "app" with the test case "metadata" And I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "metaData.Scheme.Customer.Name" equals "Joe Bloggs" And the event "metaData.Scheme.Customer.Age" equals "21" And the event "metaData.Scheme.Level" equals "Blue" bugsnag-go-2.2.0/features/multieventsession.feature000066400000000000000000000024171432154711400225450ustar00rootroot00000000000000Feature: Reporting multiple handled and unhandled errors in the same session Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: Handled errors know about previous reported handled errors When I run the go service "app" with the test case "multiple-handled" And I wait to receive 2 requests And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event handled sessions count equals 1 for request 0 And the event handled sessions count equals 2 for request 1 Scenario: Unhandled errors know about previous reported handled errors When I run the go service "app" with the test case "multiple-unhandled" And I wait to receive 2 requests And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event unhandled sessions count equals 1 for request 0 And the event unhandled sessions count equals 2 for request 1 bugsnag-go-2.2.0/features/net-http/000077500000000000000000000000001432154711400171275ustar00rootroot00000000000000bugsnag-go-2.2.0/features/net-http/appversion.feature000066400000000000000000000024201432154711400226700ustar00rootroot00000000000000Feature: Configuring app version Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "APP_VERSION" to "3.1.2" And I set environment variable "SERVER_PORT" to "4512" Scenario: A error report contains the configured app type when using a net http app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "app.version" equals "3.1.2" Scenario: A session report contains the configured app type when using a net http app Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "app.version" equals "3.1.2" bugsnag-go-2.2.0/features/net-http/autocapturesessions.feature000066400000000000000000000020161432154711400246260ustar00rootroot00000000000000Feature: Configure auto capture sessions Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" Scenario: A session is not sent if auto capture sessions is off Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/session" And I wait for 2 seconds Then I should receive no requests Scenario: A session is sent if auto capture sessions is on Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" bugsnag-go-2.2.0/features/net-http/autonotify.feature000066400000000000000000000027701432154711400227130ustar00rootroot00000000000000Feature: Using auto notify Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/autonotify-then-recover" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is true And the exception "errorClass" equals "*runtime.TypeAssertionError" And the exception "message" matches "interface conversion: interface ({} )?is struct {}, not string" Scenario: An error report is sent when a go routine crashes which is reported through auto notify Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/autonotify" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is true And the exception "errorClass" equals "*errors.errorString" And the exception "message" equals "Go routine killed with auto notify" bugsnag-go-2.2.0/features/net-http/handled.feature000066400000000000000000000031721432154711400221060ustar00rootroot00000000000000Feature: Handled errors Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: A handled error sends a report When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/handled" Then I wait to receive a request And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is false for request 0 And the event "severity" equals "warning" for request 0 And the event "severityReason.type" equals "handledError" for request 0 And the exception is a PathError for request 0 And the "file" of stack frame 0 equals "main.go" for request 0 Scenario: A handled error sends a report with a custom name Given I set environment variable "ERROR_CLASS" to "MyCustomErrorClass" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/handled" Then I wait to receive a request And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is false for request 0 And the event "severity" equals "warning" for request 0 And the event "severityReason.type" equals "handledError" for request 0 And the exception "errorClass" equals "MyCustomErrorClass" for request 0 And the "file" of stack frame 0 equals "main.go" for request 0 bugsnag-go-2.2.0/features/net-http/onbeforenotify.feature000066400000000000000000000016611432154711400235400ustar00rootroot00000000000000Feature: Configuring on before notify Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/onbeforenotify" Then I wait to receive 2 requests And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "message" equals "Don't ignore this error" for request 0 And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "message" equals "Error message was changed" for request 1 bugsnag-go-2.2.0/features/net-http/recover.feature000066400000000000000000000014431432154711400221530ustar00rootroot00000000000000Feature: Using recover Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: An error report is sent when request crashes but is recovered When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/recover" Then I wait to receive a request And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "errorClass" equals "*errors.errorString" for request 0 And the exception "message" equals "Request killed but recovered" for request 0 bugsnag-go-2.2.0/features/net-http/releasestage.feature000066400000000000000000000024101432154711400231450ustar00rootroot00000000000000Feature: Configuring release stage Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" And I set environment variable "RELEASE_STAGE" to "my-stage" Scenario: An error report is sent with configured release stage Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/handled" Then I wait to receive a request And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "app.releaseStage" equals "my-stage" for request 0 Scenario: A session report contains the configured app type Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "app.releaseStage" equals "my-stage" bugsnag-go-2.2.0/features/net-http/request.feature000066400000000000000000000016471432154711400222040ustar00rootroot00000000000000Feature: Capturing request information automatically Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" Scenario: An error report will automatically contain request information Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "request.clientIp" is not null And the event "request.headers.User-Agent" equals "Ruby" And the event "request.httpMethod" equals "GET" And the event "request.url" ends with "/handled" And the event "request.url" starts with "http://" bugsnag-go-2.2.0/features/net-http/user.feature000066400000000000000000000017151432154711400214660ustar00rootroot00000000000000Feature: Sending user data Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I set environment variable "SERVER_PORT" to "4512" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: An error report contains custom user data Given I set environment variable "USER_ID" to "test-user-id" And I set environment variable "USER_NAME" to "test-user-name" And I set environment variable "USER_EMAIL" to "test-user-email" When I start the service "nethttp" And I wait for the app to open port "4512" And I wait for 2 seconds And I open the URL "http://localhost:4512/user" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "user.id" equals "test-user-id" And the event "user.name" equals "test-user-name" And the event "user.email" equals "test-user-email" bugsnag-go-2.2.0/features/onbeforenotify.feature000066400000000000000000000014211432154711400217670ustar00rootroot00000000000000Feature: Configuring on before notify Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another When I run the go service "app" with the test case "onbeforenotify" Then I wait to receive 2 requests after the start up session And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "message" equals "Don't ignore this error" for request 0 And the request 1 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "message" equals "Error message was changed" for request 1 bugsnag-go-2.2.0/features/paramfilters.feature000066400000000000000000000030601432154711400214310ustar00rootroot00000000000000Feature: Configuring param filters Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report containing meta data is not filtered when the param filters are set but do not match Given I set environment variable "PARAMS_FILTERS" to "Name" When I run the go service "app" with the test case "filtered" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "metaData.Account.Price(dollars)" equals "1 Million" Scenario: An error report containing meta data is filtered when the param filters are set and completely match Given I set environment variable "PARAMS_FILTERS" to "Price(dollars)" When I run the go service "app" with the test case "filtered" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" Scenario: An error report containing meta data is filtered when the param filters are set and partially match Given I set environment variable "PARAMS_FILTERS" to "Price" When I run the go service "app" with the test case "filtered" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" bugsnag-go-2.2.0/features/plain_features/000077500000000000000000000000001432154711400203655ustar00rootroot00000000000000bugsnag-go-2.2.0/features/plain_features/panics.feature000066400000000000000000000021141432154711400232150ustar00rootroot00000000000000Feature: Panic handling Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" Scenario: Capturing a panic When I run the go service "app" with the test case "unhandled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "unhandled" is true And the event "severity" equals "error" And the event "severityReason.type" equals "unhandledPanic" And the exception "errorClass" equals "panic" And the exception "message" is one of: | interface conversion: interface is struct {}, not string | | interface conversion: interface {} is struct {}, not string | And the in-project frames of the stacktrace are: | file | method | | main.go | unhandledCrash.func1 | | main.go | unhandledCrash | bugsnag-go-2.2.0/features/recover.feature000066400000000000000000000012221432154711400204030ustar00rootroot00000000000000Feature: Using recover Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report is sent when a go routine crashes but recovers When I run the go service "app" with the test case "recover" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the exception "errorClass" equals "*errors.errorString" And the exception "message" equals "Go routine killed but recovered" bugsnag-go-2.2.0/features/releasestage.feature000066400000000000000000000077521432154711400214200ustar00rootroot00000000000000Feature: Configuring release stages and notify release stages Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report is sent when release stage matches notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I set environment variable "RELEASE_STAGE" to "stage2" When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "app.releaseStage" equals "stage2" Scenario: An error report is sent when no notify release stages are specified Given I set environment variable "RELEASE_STAGE" to "stage2" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "app.releaseStage" equals "stage2" Scenario: An error report is sent regardless of notify release stages if release stage is not set Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" When I run the go service "app" with the test case "handled" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" Scenario: An error report is not sent if the release stage does not match the notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I set environment variable "RELEASE_STAGE" to "stage4" When I run the go service "app" with the test case "handled" And I wait for 3 second Then I should receive no requests Scenario: An session report is sent when release stage matches notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" And I set environment variable "RELEASE_STAGE" to "stage2" When I run the go service "app" with the test case "session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "app.releaseStage" equals "stage2" Scenario: An session report is sent when no notify release stages are specified Given I set environment variable "RELEASE_STAGE" to "stage2" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I run the go service "app" with the test case "session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the payload field "app.releaseStage" equals "stage2" Scenario: An session report is sent regardless of notify release stages if release stage is not set Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" When I run the go service "app" with the test case "session" Then I wait to receive a request after the start up session And the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" Scenario: An session report is not sent if the release stage does not match the notify release stages Given I set environment variable "NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "true" And I set environment variable "RELEASE_STAGE" to "stage4" When I run the go service "app" with the test case "session" And I wait for 3 second Then I should receive no requests bugsnag-go-2.2.0/features/sessioncontext.feature000066400000000000000000000014701432154711400220330ustar00rootroot00000000000000Feature: Session data inside an error report using a session context Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report contains a session count when part of a session When I run the go service "app" with the test case "session-and-error" Then I wait to receive 2 requests after the start up session And the request 0 is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the request 1 is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event handled sessions count equals 1 for request 0 And the event unhandled sessions count equals 0 for request 0 And the number of sessions started equals 1 for request 1 bugsnag-go-2.2.0/features/steps/000077500000000000000000000000001432154711400165225ustar00rootroot00000000000000bugsnag-go-2.2.0/features/steps/go_steps.rb000066400000000000000000000116171432154711400207000ustar00rootroot00000000000000require 'net/http' Given("I set environment variable {string} to the app directory") do |key| step("I set environment variable \"#{key}\" to \"/app/src/test/\"") end Given("I set environment variable {string} to the notify endpoint") do |key| step("I set environment variable \"#{key}\" to \"http://#{current_ip}:#{MOCK_API_PORT}\"") end Given("I set environment variable {string} to the sessions endpoint") do |key| # they're the same picture dot gif # split them out for the future endpoint splitting work step("I set environment variable \"#{key}\" to \"http://#{current_ip}:#{MOCK_API_PORT}\"") end Then(/^the request(?: (\d+))? is a valid error report with api key "(.*)"$/) do |request_index, api_key| request_index ||= 0 steps %Q{ And the request #{request_index} is valid for the error reporting API And the "bugsnag-api-key" header equals "#{api_key}" for request #{request_index} And the payload field "apiKey" equals "#{api_key}" for request #{request_index} } end Then(/^the exception is a PathError for request (\d+)$/) do |request_index| body = find_request(request_index)[:body] error_class = body["events"][0]["exceptions"][0]["errorClass"] if ['1.11', '1.12', '1.13', '1.14', '1.15'].include? ENV['GO_VERSION'] assert_equal(error_class, '*os.PathError') else assert_equal(error_class, '*fs.PathError') end end Then(/^the request(?: (\d+))? is a valid session report with api key "(.*)"$/) do |request_index, api_key| request_index ||= 0 steps %Q{ And the request #{request_index} is valid for the session tracking API And the "bugsnag-api-key" header equals "#{api_key}" for request #{request_index} } end Then(/^the event unhandled sessions count equals (\d+) for request (\d+)$/) do |count, request_index| step "the payload field \"events.0.session.events.unhandled\" equals #{count} for request #{request_index}" end Then(/^the event handled sessions count equals (\d+) for request (\d+)$/) do |count, request_index| step "the payload field \"events.0.session.events.handled\" equals #{count} for request #{request_index}" end Then(/^the number of sessions started equals (\d+) for request (\d+)$/) do |count, request_index| step "the payload field \"sessionCounts.0.sessionsStarted\" equals #{count} for request #{request_index}" end When("I run the go service {string} with the test case {string}") do |service, testcase| run_service_with_command(service, "./run.sh go run . -test=\"#{testcase}\"") end Then(/^I wait to receive a request after the start up session$/) do step "I wait to receive 1 requests after the start up session" end Then(/^I wait to receive (\d+) requests after the start up session?$/) do |request_count| max_attempts = 50 attempts = 0 start_up_message_received = false start_up_message_removed = false received = false until (attempts >= max_attempts) || received attempts += 1 start_up_message_received ||= (stored_requests.size == 1) if start_up_message_received && !start_up_message_removed step 'the request is a valid session report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa"' stored_requests.shift start_up_message_removed = true next end received = (stored_requests.size == request_count) sleep 0.2 end raise "Requests not received in 10s (received #{stored_requests.size})" unless received # Wait an extra second to ensure there are no further requests sleep 1 assert_equal(request_count, stored_requests.size, "#{stored_requests.size} requests received") end Then(/^(\d+) requests? (?:was|were) received$/) do |request_count| sleep 1 assert_equal(request_count, stored_requests.size, "#{stored_requests.size} requests received") end Then("the in-project frames of the stacktrace are:") do |table| body = find_request(0)[:body] stacktrace = body["events"][0]["exceptions"][0]["stacktrace"] found = 0 # counts matching frames and ensures ordering is correct expected = table.hashes.length stacktrace.each do |frame| if found < expected and frame["inProject"] and frame["file"] == table.hashes[found]["file"] and frame["method"] == table.hashes[found]["method"] found = found + 1 end end assert_equal(found, expected, "expected #{expected} matching frames but found #{found}. stacktrace:\n#{stacktrace}") end Then("stack frame {int} contains a local function spanning {int} to {int}") do |frame, val, old_val| # Old versions of Go put the line number on the end of the function if ['1.7', '1.8'].include? ENV['GO_VERSION'] step "the \"lineNumber\" of stack frame #{frame} equals #{old_val}" else step "the \"lineNumber\" of stack frame #{frame} equals #{val}" end end Then("the exception {string} is one of:") do |key, table| body = find_request(0)[:body] exception = body["events"][0]["exceptions"][0] options = table.raw.flatten assert(options.include?(exception[key]), "expected '#{key}' to be one of #{options}") end bugsnag-go-2.2.0/features/support/000077500000000000000000000000001432154711400171005ustar00rootroot00000000000000bugsnag-go-2.2.0/features/support/env.rb000066400000000000000000000007121432154711400202150ustar00rootroot00000000000000require 'fileutils' require 'socket' require 'timeout' testBuildFolder = 'features/fixtures/testbuild' FileUtils.rm_rf(testBuildFolder) Dir.mkdir testBuildFolder # Copy the existing dir `find . -name '*.go' -o -name 'go.sum' -o -name 'go.mod' \ -not -path "./examples/*" \ -not -path "./testutil/*" \ -not -path "./v2/testutil/*" \ -not -path "./features/*" \ -not -name '*_test.go' | cpio -pdm #{testBuildFolder}` bugsnag-go-2.2.0/features/synchronous.feature000066400000000000000000000015731432154711400213410ustar00rootroot00000000000000Feature: Configuring synchronous flag Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" And I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report is sent asynchrously but exits immediately so is not sent Given I set environment variable "SYNCHRONOUS" to "false" When I run the go service "app" with the test case "send-and-exit" And I wait for 3 second Then I should receive no requests Scenario: An error report is report synchronously so it will send before exiting Given I set environment variable "SYNCHRONOUS" to "true" When I run the go service "app" with the test case "send-and-exit" Then I wait to receive 1 requests And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" bugsnag-go-2.2.0/features/user.feature000066400000000000000000000015301432154711400177160ustar00rootroot00000000000000Feature: Sending user data Background: Given I set environment variable "API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" Given I set environment variable "AUTO_CAPTURE_SESSIONS" to "false" And I configure the bugsnag endpoint And I have built the service "app" Scenario: An error report contains custom user data Given I set environment variable "USER_ID" to "test-user-id" And I set environment variable "USER_NAME" to "test-user-name" And I set environment variable "USER_EMAIL" to "test-user-email" When I run the go service "app" with the test case "user" Then I wait to receive a request And the request is a valid error report with api key "a35a2a72bd230ac0aa0f52715bbdc6aa" And the event "user.id" equals "test-user-id" And the event "user.name" equals "test-user-name" And the event "user.email" equals "test-user-email" bugsnag-go-2.2.0/gin/000077500000000000000000000000001432154711400143235ustar00rootroot00000000000000bugsnag-go-2.2.0/gin/bugsnaggin.go000066400000000000000000000025461432154711400170050ustar00rootroot00000000000000package bugsnaggin import ( "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/device" "github.com/gin-gonic/gin" ) const FrameworkName string = "Gin" // AutoNotify sends any panics to bugsnag, and then re-raises them. // You should use this after another middleware that // returns an error page to the client, for example gin.Recovery(). // The arguments can be any RawData to pass to Bugsnag, most usually // you'll pass a bugsnag.Configuration object. func AutoNotify(rawData ...interface{}) gin.HandlerFunc { // Configure bugsnag with the passed in configuration (for manual notifications) for _, datum := range rawData { if c, ok := datum.(bugsnag.Configuration); ok { bugsnag.Configure(c) } } device.AddVersion(FrameworkName, gin.Version) state := bugsnag.HandledState{ SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, OriginalSeverity: bugsnag.SeverityError, Unhandled: true, Framework: FrameworkName, } rawData = append(rawData, state) return func(c *gin.Context) { r := c.Copy().Request notifier := bugsnag.New(append(rawData, r)...) ctx := bugsnag.AttachRequestData(r.Context(), r) if notifier.Config.IsAutoCaptureSessions() { ctx = bugsnag.StartSession(ctx) } c.Request = r.WithContext(ctx) notifier.FlushSessionsOnRepanic(false) defer notifier.AutoNotify(ctx) c.Next() } } bugsnag-go-2.2.0/gin/gin_test.go000066400000000000000000000062131432154711400164700ustar00rootroot00000000000000package bugsnaggin_test import ( "fmt" "net/http" "os" "testing" "time" "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/gin" . "github.com/bugsnag/bugsnag-go/testutil" "github.com/gin-gonic/gin" ) func TestGin(t *testing.T) { ts, reports := Setup() defer ts.Close() g := gin.Default() userID := "1234abcd" g.Use(bugsnaggin.AutoNotify(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, }, bugsnag.User{Id: userID})) g.GET("/unhandled", performUnhandledCrash) g.GET("/handled", performHandledError) go g.Run(":9079") //This call blocks t.Run("AutoNotify", func(st *testing.T) { time.Sleep(1 * time.Second) _, err := http.Get("http://localhost:9079/unhandled") if err != nil { t.Error(err) } report := <-reports r, _ := simplejson.NewJson(report) hostname, _ := os.Hostname() AssertPayload(st, r, fmt.Sprintf(` { "apiKey":"166f5ad3590596f9aa8d601ea89af845", "events":[ { "app":{ "releaseStage":"" }, "context":"/unhandled", "device":{ "hostname": "%s" }, "exceptions":[ { "errorClass":"*errors.errorString", "message":"you shouldn't have done that", "stacktrace":[] } ], "payloadVersion":"4", "severity":"error", "severityReason":{ "type":"unhandledErrorMiddleware" }, "unhandled":true, "request": { "url": "http://localhost:9079/unhandled", "httpMethod": "GET", "referer": "", "headers": { "Accept-Encoding": "gzip" } }, "user":{ "id": "%s" } } ], "notifier":{ "name":"Bugsnag Go", "url":"https://github.com/bugsnag/bugsnag-go", "version": "%s" } } `, hostname, userID, bugsnag.VERSION)) }) t.Run("Manual notify", func(st *testing.T) { _, err := http.Get("http://localhost:9079/handled") if err != nil { t.Error(err) } report := <-reports r, _ := simplejson.NewJson(report) hostname, _ := os.Hostname() AssertPayload(st, r, fmt.Sprintf(` { "apiKey":"166f5ad3590596f9aa8d601ea89af845", "events":[ { "app":{ "releaseStage":"" }, "context":"/handled", "device":{ "hostname": "%s" }, "exceptions":[ { "errorClass":"*errors.errorString", "message":"Ooopsie", "stacktrace":[] } ], "payloadVersion":"4", "severity":"warning", "severityReason":{ "type":"handledError" }, "unhandled":false, "request": { "url": "http://localhost:9079/handled", "httpMethod": "GET", "referer": "", "headers": { "Accept-Encoding": "gzip" } }, "user":{ "id": "%s" } } ], "notifier":{ "name":"Bugsnag Go", "url":"https://github.com/bugsnag/bugsnag-go", "version": "%s" } } `, hostname, "987zyx", bugsnag.VERSION)) }) } func performHandledError(c *gin.Context) { ctx := c.Request.Context() bugsnag.Notify(fmt.Errorf("Ooopsie"), ctx, bugsnag.User{Id: "987zyx"}) } func performUnhandledCrash(c *gin.Context) { panic("you shouldn't have done that") } func crash(a interface{}) string { return a.(string) } bugsnag-go-2.2.0/headers/000077500000000000000000000000001432154711400151615ustar00rootroot00000000000000bugsnag-go-2.2.0/headers/prefixed.go000066400000000000000000000007531432154711400173230ustar00rootroot00000000000000package headers import "time" //PrefixedHeaders returns a map of Content-Type and the 'Bugsnag-' headers for //API key, payload version, and the time at which the request is being sent. func PrefixedHeaders(apiKey, payloadVersion string) map[string]string { return map[string]string{ "Content-Type": "application/json", "Bugsnag-Api-Key": apiKey, "Bugsnag-Payload-Version": payloadVersion, "Bugsnag-Sent-At": time.Now().UTC().Format(time.RFC3339), } } bugsnag-go-2.2.0/headers/prefixed_test.go000066400000000000000000000025641432154711400203640ustar00rootroot00000000000000package headers import ( "strings" "testing" "time" ) const APIKey = "abcd1234abcd1234" const testPayloadVersion = "3" func TestConstantBugsnagPrefixedHeaders(t *testing.T) { headers := PrefixedHeaders(APIKey, testPayloadVersion) testCases := []struct { header string expected string }{ {header: "Content-Type", expected: "application/json"}, {header: "Bugsnag-Api-Key", expected: APIKey}, {header: "Bugsnag-Payload-Version", expected: testPayloadVersion}, } for _, tc := range testCases { t.Run(tc.header, func(st *testing.T) { if got := headers[tc.header]; got != tc.expected { t.Errorf("Expected headers to contain %s header %s but was %s", tc.header, tc.expected, got) } }) } } func TestTimeDependentBugsnagPrefixedHeaders(t *testing.T) { headers := PrefixedHeaders(APIKey, testPayloadVersion) sentAtString := headers["Bugsnag-Sent-At"] if !strings.HasSuffix(sentAtString, "Z") { t.Errorf("Error when setting Bugsnag-Sent-At header: %s, doesn't end with a Z", sentAtString) } sentAt, err := time.Parse(time.RFC3339, sentAtString) if err != nil { t.Errorf("Error when attempting to parse Bugsnag-Sent-At header: %s", sentAtString) } if now := time.Now(); now.Sub(sentAt) > time.Second || now.Sub(sentAt) < -time.Second { t.Errorf("Expected Bugsnag-Sent-At header approx. %s but was %s", now.UTC().Format(time.RFC3339), sentAtString) } } bugsnag-go-2.2.0/json_tags.go000066400000000000000000000017601432154711400160700ustar00rootroot00000000000000// The code is stripped from: // http://golang.org/src/pkg/encoding/json/tags.go?m=text package bugsnag import ( "strings" ) // tagOptions is the string following a comma in a struct field's "json" // tag, or the empty string. It does not include the leading comma. type tagOptions string // parseTag splits a struct field's json tag into its name and // comma-separated options. func parseTag(tag string) (string, tagOptions) { if idx := strings.Index(tag, ","); idx != -1 { return tag[:idx], tagOptions(tag[idx+1:]) } return tag, tagOptions("") } // Contains reports whether a comma-separated list of options // contains a particular substr flag. substr must be surrounded by a // string boundary or commas. func (o tagOptions) Contains(optionName string) bool { if len(o) == 0 { return false } s := string(o) for s != "" { var next string i := strings.Index(s, ",") if i >= 0 { s, next = s[:i], s[i+1:] } if s == optionName { return true } s = next } return false } bugsnag-go-2.2.0/martini/000077500000000000000000000000001432154711400152115ustar00rootroot00000000000000bugsnag-go-2.2.0/martini/bugsnagmiddleware.go000066400000000000000000000050461432154711400212310ustar00rootroot00000000000000/* Package bugsnagmartini provides a martini middleware that sends panics to Bugsnag. You should use this middleware in combination with martini.Recover() if you want to send error messages to your clients: func main() { m := martini.New() // used to stop panics bubbling and return a 500 error. m.Use(martini.Recovery()) // used to send panics to Bugsnag. m.Use(bugsnagmartini.AutoNotify(bugsnag.Configuration{ APIKey: "YOUR_API_KEY_HERE", }) // ... } This middleware also makes bugsnag available to martini handlers via the context. func myHandler(w http.ResponseWriter, r *http.Request, bugsnag *bugsnag.Notifier) { // ... bugsnag.Notify(err) // ... } */ package bugsnagmartini import ( "net/http" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/device" "github.com/go-martini/martini" ) // FrameworkName is the name of the framework this middleware applies to const FrameworkName string = "Martini" // AutoNotify sends any panics to bugsnag, and then re-raises them. // You should use this after another middleware that // returns an error page to the client, for example martini.Recover(). // The arguments can be any RawData to pass to Bugsnag, most usually // you'll pass a bugsnag.Configuration object. func AutoNotify(rawData ...interface{}) martini.Handler { updateGlobalConfig(rawData...) device.AddVersion(FrameworkName, "v1.0") // The latest martini release from 2014 state := bugsnag.HandledState{ SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, OriginalSeverity: bugsnag.SeverityError, Unhandled: true, Framework: FrameworkName, } return func(r *http.Request, c martini.Context) { // Martini's request-based context for dependency injection means that we can // attach request data to the notifier (one notifier <=> one request) itself. // This means that request data will show up when doing just notifier.Notify(err) notifier := bugsnag.New(append(rawData, r, state)...) // In case users use bugsnag.Notify instead of the mapped notifier. ctx := bugsnag.AttachRequestData(r.Context(), r) if notifier.Config.IsAutoCaptureSessions() { ctx = bugsnag.StartSession(ctx) } notifier.FlushSessionsOnRepanic(false) c.Map(r.WithContext(ctx)) defer notifier.AutoNotify(ctx) c.Map(notifier) c.Next() } } func updateGlobalConfig(rawData ...interface{}) { for i, datum := range rawData { if c, ok := datum.(bugsnag.Configuration); ok { if c.ReleaseStage == "" { c.ReleaseStage = martini.Env } bugsnag.Configure(c) rawData[i] = nil } } } bugsnag-go-2.2.0/martini/martini_test.go000066400000000000000000000062441432154711400202500ustar00rootroot00000000000000package bugsnagmartini_test import ( "fmt" "net/http" "os" "testing" "time" simplejson "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/martini" . "github.com/bugsnag/bugsnag-go/testutil" "github.com/go-martini/martini" ) func performHandledError(notifier *bugsnag.Notifier, r *http.Request) { ctx := r.Context() notifier.Notify(fmt.Errorf("Ooopsie"), ctx, bugsnag.User{Id: "987zyx"}) } func performUnhandledCrash() { panic("something bad just happened") } func TestMartini(t *testing.T) { ts, reports := Setup() defer ts.Close() config := bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, } bugsnag.Configure(config) m := martini.Classic() userID := "1234abcd" m.Use(martini.Recovery()) m.Use(bugsnagmartini.AutoNotify(bugsnag.User{Id: userID})) m.Get("/unhandled", performUnhandledCrash) m.Get("/handled", performHandledError) go m.RunOnAddr(":9077") t.Run("AutoNotify", func(st *testing.T) { time.Sleep(1 * time.Second) _, err := http.Get("http://localhost:9077/unhandled") if err != nil { t.Error(err) } report := <-reports r, _ := simplejson.NewJson(report) hostname, _ := os.Hostname() AssertPayload(st, r, fmt.Sprintf(` { "apiKey": "%s", "events":[ { "app":{ "releaseStage":"" }, "context":"/unhandled", "device":{ "hostname": "%s" }, "exceptions":[ { "errorClass":"*errors.errorString", "message":"something bad just happened", "stacktrace":[] } ], "payloadVersion":"4", "severity":"error", "severityReason":{ "type":"unhandledErrorMiddleware" }, "unhandled":true, "request": { "httpMethod": "GET", "url": "http://localhost:9077/unhandled", "headers": { "Accept-Encoding": "gzip" } }, "user":{ "id": "%s" } } ], "notifier":{ "name":"Bugsnag Go", "url":"https://github.com/bugsnag/bugsnag-go", "version": "%s" } } `, TestAPIKey, hostname, userID, bugsnag.VERSION)) }) t.Run("Notify", func(st *testing.T) { time.Sleep(1 * time.Second) _, err := http.Get("http://localhost:9077/handled") if err != nil { t.Error(err) } report := <-reports r, _ := simplejson.NewJson(report) hostname, _ := os.Hostname() AssertPayload(st, r, fmt.Sprintf(` { "apiKey": "%s", "events":[ { "app":{ "releaseStage":"" }, "device":{ "hostname": "%s" }, "exceptions":[ { "errorClass":"*errors.errorString", "message":"Ooopsie", "stacktrace":[] } ], "payloadVersion":"4", "severity":"error", "severityReason":{ "type":"unhandledErrorMiddleware" }, "request": { "url": "http://localhost:9077/handled", "httpMethod": "GET", "headers": { "Accept-Encoding": "gzip" } }, "unhandled":true, "user":{ "id": "%s" } } ], "notifier":{ "name":"Bugsnag Go", "url":"https://github.com/bugsnag/bugsnag-go", "version": "%s" } } `, TestAPIKey, hostname, "987zyx", bugsnag.VERSION)) }) } func crash(a interface{}) string { return a.(string) } bugsnag-go-2.2.0/metadata.go000066400000000000000000000107051432154711400156600ustar00rootroot00000000000000package bugsnag import ( "fmt" "reflect" "strings" ) // MetaData is added to the Bugsnag dashboard in tabs. Each tab is // a map of strings -> values. You can pass MetaData to Notify, Recover // and AutoNotify as rawData. type MetaData map[string]map[string]interface{} // Update the meta-data with more information. Tabs are merged together such // that unique keys from both sides are preserved, and duplicate keys end up // with the provided values. func (meta MetaData) Update(other MetaData) { for name, tab := range other { if meta[name] == nil { meta[name] = make(map[string]interface{}) } for key, value := range tab { meta[name][key] = value } } } // Add creates a tab of Bugsnag meta-data. // If the tab doesn't yet exist it will be created. // If the key already exists, it will be overwritten. func (meta MetaData) Add(tab string, key string, value interface{}) { if meta[tab] == nil { meta[tab] = make(map[string]interface{}) } meta[tab][key] = value } // AddStruct creates a tab of Bugsnag meta-data. // The struct will be converted to an Object using the // reflect library so any private fields will not be exported. // As a safety measure, if you pass a non-struct the value will be // sent to Bugsnag under the "Extra data" tab. func (meta MetaData) AddStruct(tab string, obj interface{}) { val := sanitizer{}.Sanitize(obj) content, ok := val.(map[string]interface{}) if ok { meta[tab] = content } else { // Wasn't a struct meta.Add("Extra data", tab, obj) } } // Remove any values from meta-data that have keys matching the filters, // and any that are recursive data-structures func (meta MetaData) sanitize(filters []string) interface{} { return sanitizer{ Filters: filters, Seen: make([]interface{}, 0), }.Sanitize(meta) } // The sanitizer is used to remove filtered params and recursion from meta-data. type sanitizer struct { Filters []string Seen []interface{} } func (s sanitizer) Sanitize(data interface{}) interface{} { for _, s := range s.Seen { // TODO: we don't need deep equal here, just type-ignoring equality if reflect.DeepEqual(data, s) { return "[RECURSION]" } } // Sanitizers are passed by value, so we can modify s and it only affects // s.Seen for nested calls. s.Seen = append(s.Seen, data) t := reflect.TypeOf(data) v := reflect.ValueOf(data) if t == nil { return "" } switch t.Kind() { case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64: return data case reflect.String: return data case reflect.Interface, reflect.Ptr: if v.IsNil() { return "" } return s.Sanitize(v.Elem().Interface()) case reflect.Array, reflect.Slice: ret := make([]interface{}, v.Len()) for i := 0; i < v.Len(); i++ { ret[i] = s.Sanitize(v.Index(i).Interface()) } return ret case reflect.Map: return s.sanitizeMap(v) case reflect.Struct: return s.sanitizeStruct(v, t) // Things JSON can't serialize: // case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer: default: return "[" + t.String() + "]" } } func (s sanitizer) sanitizeMap(v reflect.Value) interface{} { ret := make(map[string]interface{}) for _, key := range v.MapKeys() { val := s.Sanitize(v.MapIndex(key).Interface()) newKey := fmt.Sprintf("%v", key.Interface()) if s.shouldRedact(newKey) { val = "[FILTERED]" } ret[newKey] = val } return ret } func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) interface{} { ret := make(map[string]interface{}) for i := 0; i < v.NumField(); i++ { val := v.Field(i) // Don't export private fields if !val.CanInterface() { continue } name := t.Field(i).Name var opts tagOptions // Parse JSON tags. Supports name and "omitempty" if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 { name, opts = parseTag(jsonTag) } if s.shouldRedact(name) { ret[name] = "[FILTERED]" } else { sanitized := s.Sanitize(val.Interface()) if str, ok := sanitized.(string); ok { if !(opts.Contains("omitempty") && len(str) == 0) { ret[name] = str } } else { ret[name] = sanitized } } } return ret } func (s sanitizer) shouldRedact(key string) bool { for _, filter := range s.Filters { if strings.Contains(strings.ToLower(key), strings.ToLower(filter)) { return true } } return false } bugsnag-go-2.2.0/metadata_test.go000066400000000000000000000074751432154711400167310ustar00rootroot00000000000000package bugsnag import ( "reflect" "testing" "unsafe" "github.com/bugsnag/bugsnag-go/errors" ) type _account struct { ID string Name string Plan struct { Premium bool } Password string secret string Email string `json:"email"` EmptyEmail string `json:"emptyemail,omitempty"` NotEmptyEmail string `json:"not_empty_email,omitempty"` } type _broken struct { Me *_broken Data string } var account = _account{} var notifier = New(Configuration{}) func TestMetaDataAdd(t *testing.T) { m := MetaData{ "one": { "key": "value", "override": false, }} m.Add("one", "override", true) m.Add("one", "new", "key") m.Add("new", "tab", account) m.AddStruct("lol", "not really a struct") m.AddStruct("account", account) if !reflect.DeepEqual(m, MetaData{ "one": { "key": "value", "override": true, "new": "key", }, "new": { "tab": account, }, "Extra data": { "lol": "not really a struct", }, "account": { "ID": "", "Name": "", "Plan": map[string]interface{}{ "Premium": false, }, "Password": "", "email": "", }, }) { t.Errorf("metadata.Add didn't work: %#v", m) } } func TestMetaDataUpdate(t *testing.T) { m := MetaData{ "one": { "key": "value", "override": false, }} m.Update(MetaData{ "one": { "override": true, "new": "key", }, "new": { "tab": account, }, }) if !reflect.DeepEqual(m, MetaData{ "one": { "key": "value", "override": true, "new": "key", }, "new": { "tab": account, }, }) { t.Errorf("metadata.Update didn't work: %#v", m) } } func TestMetaDataSanitize(t *testing.T) { var broken = _broken{} broken.Me = &broken broken.Data = "ohai" account.Name = "test" account.ID = "test" account.secret = "hush" account.Email = "example@example.com" account.EmptyEmail = "" account.NotEmptyEmail = "not_empty_email@example.com" m := MetaData{ "one": { "bool": true, "int": 7, "float": 7.1, "complex": complex(1, 1), "func": func() {}, "unsafe": unsafe.Pointer(broken.Me), "string": "string", "password": "secret", "array": []hash{{ "creditcard": "1234567812345678", "broken": broken, }}, "broken": broken, "account": account, }, } n := m.sanitize([]string{"password", "creditcard"}) if !reflect.DeepEqual(n, map[string]interface{}{ "one": map[string]interface{}{ "bool": true, "int": 7, "float": 7.1, "complex": "[complex128]", "string": "string", "unsafe": "[unsafe.Pointer]", "func": "[func()]", "password": "[FILTERED]", "array": []interface{}{map[string]interface{}{ "creditcard": "[FILTERED]", "broken": map[string]interface{}{ "Me": "[RECURSION]", "Data": "ohai", }, }}, "broken": map[string]interface{}{ "Me": "[RECURSION]", "Data": "ohai", }, "account": map[string]interface{}{ "ID": "test", "Name": "test", "Plan": map[string]interface{}{ "Premium": false, }, "Password": "[FILTERED]", "email": "example@example.com", "not_empty_email": "not_empty_email@example.com", }, }, }) { t.Errorf("metadata.Sanitize didn't work: %#v", n) } } func TestSanitizerSanitize(t *testing.T) { var ( nilPointer *int nilInterface = interface{}(nil) ) for n, tc := range []struct { input interface{} want interface{} }{ {nilPointer, ""}, {nilInterface, ""}, } { s := &sanitizer{} gotValue := s.Sanitize(tc.input) if got, want := gotValue, tc.want; got != want { t.Errorf("[%d] got %v, want %v", n, got, want) } } } func ExampleMetaData() { notifier.Notify(errors.Errorf("hi world"), MetaData{"Account": { "id": account.ID, "name": account.Name, "paying?": account.Plan.Premium, }}) } bugsnag-go-2.2.0/middleware.go000066400000000000000000000042731432154711400162200ustar00rootroot00000000000000package bugsnag import ( "net/http" ) type ( beforeFunc func(*Event, *Configuration) error // MiddlewareStacks keep middleware in the correct order. They are // called in reverse order, so if you add a new middleware it will // be called before all existing middleware. middlewareStack struct { before []beforeFunc } ) // AddMiddleware adds a new middleware to the outside of the existing ones, // when the middlewareStack is Run it will be run before all middleware that // have been added before. func (stack *middlewareStack) OnBeforeNotify(middleware beforeFunc) { stack.before = append(stack.before, middleware) } // Run causes all the middleware to be run. If they all permit it the next callback // will be called with all the middleware on the stack. func (stack *middlewareStack) Run(event *Event, config *Configuration, next func() error) error { // run all the before filters in reverse order for i := range stack.before { before := stack.before[len(stack.before)-i-1] severity := event.Severity err := stack.runBeforeFilter(before, event, config) if err != nil { return err } if event.Severity != severity { event.handledState.SeverityReason = SeverityReasonCallbackSpecified } } return next() } func (stack *middlewareStack) runBeforeFilter(f beforeFunc, event *Event, config *Configuration) error { defer func() { if err := recover(); err != nil { config.logf("bugsnag/middleware: unexpected panic: %v", err) } }() return f(event, config) } // catchMiddlewarePanic is used to log any panics that happen inside Middleware, // we wouldn't want to not notify Bugsnag in this case. func catchMiddlewarePanic(event *Event, config *Configuration, next func() error) { } // httpRequestMiddleware is added OnBeforeNotify by default. It takes information // from an http.Request passed in as rawData, and adds it to the Event. You can // use this as a template for writing your own Middleware. func httpRequestMiddleware(event *Event, config *Configuration) error { for _, datum := range event.RawData { if request, ok := datum.(*http.Request); ok { event.MetaData.Update(MetaData{ "request": { "params": request.URL.Query(), }, }) } } return nil } bugsnag-go-2.2.0/middleware_test.go000066400000000000000000000037571432154711400172650ustar00rootroot00000000000000package bugsnag import ( "bytes" "fmt" "github.com/bugsnag/bugsnag-go/errors" "log" "reflect" "testing" ) func TestMiddlewareOrder(t *testing.T) { err := fmt.Errorf("test") data := []interface{}{errors.New(err, 1)} event, config := newEvent(data, &defaultNotifier) result := make([]int, 0, 7) stack := middlewareStack{} stack.OnBeforeNotify(func(e *Event, c *Configuration) error { result = append(result, 2) return nil }) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { result = append(result, 1) return nil }) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { result = append(result, 0) return nil }) stack.Run(event, config, func() error { result = append(result, 3) return nil }) if !reflect.DeepEqual(result, []int{0, 1, 2, 3}) { t.Errorf("unexpected middleware order %v", result) } } func TestBeforeNotifyReturnErr(t *testing.T) { stack := middlewareStack{} err := fmt.Errorf("test") data := []interface{}{errors.New(err, 1)} event, config := newEvent(data, &defaultNotifier) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { return err }) called := false e := stack.Run(event, config, func() error { called = true return nil }) if e != err { t.Errorf("Middleware didn't return the error") } if called == true { t.Errorf("Notify was called when BeforeNotify returned False") } } func TestBeforeNotifyPanic(t *testing.T) { stack := middlewareStack{} err := fmt.Errorf("test") event, _ := newEvent([]interface{}{errors.New(err, 1)}, &defaultNotifier) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { panic("oops") }) called := false b := &bytes.Buffer{} stack.Run(event, &Configuration{Logger: log.New(b, log.Prefix(), 0)}, func() error { called = true return nil }) logged := b.String() if logged != "bugsnag/middleware: unexpected panic: oops\n" { t.Errorf("Logged: %s", logged) } if called == false { t.Errorf("Notify was not called when BeforeNotify panicked") } } bugsnag-go-2.2.0/negroni/000077500000000000000000000000001432154711400152075ustar00rootroot00000000000000bugsnag-go-2.2.0/negroni/bugsnagnegroni.go000066400000000000000000000026701432154711400205530ustar00rootroot00000000000000package bugsnagnegroni import ( "net/http" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/device" "github.com/urfave/negroni" ) // FrameworkName is the name of the framework this middleware applies to const FrameworkName string = "Negroni" type handler struct { rawData []interface{} } // AutoNotify sends any panics to bugsnag, and then re-raises them. func AutoNotify(rawData ...interface{}) negroni.Handler { updateGlobalConfig(rawData...) device.AddVersion(FrameworkName, "unknown") // Negroni exposes no version prop. state := bugsnag.HandledState{ SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, OriginalSeverity: bugsnag.SeverityError, Unhandled: true, Framework: FrameworkName, } rawData = append(rawData, state) return &handler{rawData: rawData} } func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { // Record a session if auto capture sessions is enabled ctx := bugsnag.AttachRequestData(r.Context(), r) if bugsnag.Config.IsAutoCaptureSessions() { ctx = bugsnag.StartSession(ctx) } request := r.WithContext(ctx) notifier := bugsnag.New(h.rawData...) notifier.FlushSessionsOnRepanic(false) defer notifier.AutoNotify(ctx) next(rw, request) } func updateGlobalConfig(rawData ...interface{}) { for i, datum := range rawData { if c, ok := datum.(bugsnag.Configuration); ok { bugsnag.Configure(c) rawData[i] = nil } } } bugsnag-go-2.2.0/negroni/negroni_test.go000066400000000000000000000063161432154711400202440ustar00rootroot00000000000000package bugsnagnegroni_test import ( "fmt" "net/http" "os" "testing" "time" simplejson "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/negroni" . "github.com/bugsnag/bugsnag-go/testutil" "github.com/urfave/negroni" ) const userID = "1234abcd" func TestNegroni(t *testing.T) { ts, reports := Setup() config := bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, } mux := http.NewServeMux() mux.HandleFunc("/unhandled", unhandledCrashHandler) mux.HandleFunc("/handled", handledCrashHandler) hostname, _ := os.Hostname() n := negroni.New() n.Use(negroni.NewRecovery()) n.Use(bugsnagnegroni.AutoNotify(config, bugsnag.User{Id: userID})) n.UseHandler(mux) go http.ListenAndServe(":9078", n) t.Run("AutoNotify", func(st *testing.T) { time.Sleep(500 * time.Millisecond) http.Get("http://localhost:9078/unhandled") report := <-reports r, _ := simplejson.NewJson(report) AssertPayload(st, r, fmt.Sprintf(` { "apiKey":"166f5ad3590596f9aa8d601ea89af845", "events":[ { "app":{ "releaseStage":"" }, "context":"/unhandled", "device":{ "hostname": "%s" }, "exceptions":[ { "errorClass":"*errors.errorString", "message":"something went terribly wrong", "stacktrace":[] } ], "payloadVersion":"4", "severity":"error", "severityReason":{ "type":"unhandledErrorMiddleware" }, "unhandled":true, "request": { "url": "http://localhost:9078/unhandled", "httpMethod": "GET", "referer": "", "headers": { "Accept-Encoding": "gzip" } }, "user":{ "id": "%s" } } ], "notifier":{ "name":"Bugsnag Go", "url":"https://github.com/bugsnag/bugsnag-go", "version": "%s" } } `, hostname, userID, bugsnag.VERSION)) }) t.Run("Notify", func(st *testing.T) { time.Sleep(500 * time.Millisecond) http.Get("http://localhost:9078/handled") report := <-reports r, _ := simplejson.NewJson(report) AssertPayload(st, r, fmt.Sprintf(` { "apiKey":"166f5ad3590596f9aa8d601ea89af845", "events":[ { "app":{ "releaseStage":"" }, "context":"/handled", "device":{ "hostname": "%s" }, "exceptions":[ { "errorClass":"*errors.errorString", "message":"Ooopsie", "stacktrace":[] } ], "payloadVersion":"4", "severity":"warning", "severityReason":{ "type":"handledError" }, "unhandled": false, "request": { "url": "http://localhost:9078/handled", "httpMethod": "GET", "referer": "", "headers": { "Accept-Encoding": "gzip" } }, "user":{ "id": "%s" } } ], "notifier":{ "name":"Bugsnag Go", "url":"https://github.com/bugsnag/bugsnag-go", "version": "%s" } } `, hostname, userID, bugsnag.VERSION)) }) } func unhandledCrashHandler(w http.ResponseWriter, req *http.Request) { panic("something went terribly wrong") } func handledCrashHandler(w http.ResponseWriter, req *http.Request) { bugsnag.Notify(fmt.Errorf("Ooopsie"), bugsnag.User{Id: userID}, req.Context()) } func crash(a interface{}) string { return a.(string) } bugsnag-go-2.2.0/notifier.go000066400000000000000000000121521432154711400157150ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/errors" ) var publisher reportPublisher = new(defaultReportPublisher) // Notifier sends errors to Bugsnag. type Notifier struct { Config *Configuration RawData []interface{} } // New creates a new notifier. // You can pass an instance of bugsnag.Configuration in rawData to change the configuration. // Other values of rawData will be passed to Notify. func New(rawData ...interface{}) *Notifier { config := Config.clone() for i, datum := range rawData { if c, ok := datum.(Configuration); ok { config.update(&c) rawData[i] = nil } } return &Notifier{ Config: config, RawData: rawData, } } // FlushSessionsOnRepanic takes a boolean that indicates whether sessions // should be flushed when AutoNotify repanics. In the case of a fatal panic the // sessions might not get sent to Bugsnag before the application shuts down. // Many frameworks will have their own error handler, and for these frameworks // there is no need to flush sessions as the application will survive the panic // and the sessions can be sent off later. The default value is true, so this // needs only be called if you wish to inform Bugsnag that there is an error // handler that will take care of panics that AutoNotify will re-raise. func (notifier *Notifier) FlushSessionsOnRepanic(shouldFlush bool) { notifier.Config.flushSessionsOnRepanic = shouldFlush } // Notify sends an error to Bugsnag. Any rawData you pass here will be sent to // Bugsnag after being converted to JSON. e.g. bugsnag.SeverityError, bugsnag.Context, // or bugsnag.MetaData. Any bools in rawData overrides the // notifier.Config.Synchronous flag. func (notifier *Notifier) Notify(err error, rawData ...interface{}) (e error) { if e := checkForEmptyError(err); e != nil { return e } // Stripping one stackframe to not include this function in the stacktrace // for a manual notification. skipFrames := 1 return notifier.NotifySync(errors.New(err, skipFrames), notifier.Config.Synchronous, rawData...) } // NotifySync sends an error to Bugsnag. A boolean parameter specifies whether // to send the report in the current context (by default false, i.e. // asynchronous). Any other rawData you pass here will be sent to Bugsnag after // being converted to JSON. E.g. bugsnag.SeverityError, bugsnag.Context, or // bugsnag.MetaData. func (notifier *Notifier) NotifySync(err error, sync bool, rawData ...interface{}) error { if e := checkForEmptyError(err); e != nil { return e } // Stripping one stackframe to not include this function in the stacktrace // for a manual notification. skipFrames := 1 event, config := newEvent(append(rawData, errors.New(err, skipFrames), sync), notifier) // Never block, start throwing away errors if we have too many. e := middleware.Run(event, config, func() error { return publisher.publishReport(&payload{event, config}) }) if e != nil { config.logf("bugsnag.Notify: %v", e) } return e } // AutoNotify notifies Bugsnag of any panics, then repanics. // It sends along any rawData that gets passed in. // Usage: // go func() { // defer AutoNotify() // // (possibly crashy code) // }() func (notifier *Notifier) AutoNotify(rawData ...interface{}) { if err := recover(); err != nil { severity := notifier.getDefaultSeverity(rawData, SeverityError) state := HandledState{SeverityReasonHandledPanic, severity, true, ""} rawData = notifier.appendStateIfNeeded(rawData, state) // We strip the following stackframes as they don't add much // information but would mess with the grouping algorithm // { "file": "github.com/bugsnag/bugsnag-go/notifier.go", "lineNumber": 116, "method": "(*Notifier).AutoNotify" }, // { "file": "runtime/asm_amd64.s", "lineNumber": 573, "method": "call32" }, skipFrames := 2 notifier.NotifySync(errors.New(err, skipFrames), true, rawData...) panic(err) } } // Recover logs any panics, then recovers. // It sends along any rawData that gets passed in. // Usage: defer Recover() func (notifier *Notifier) Recover(rawData ...interface{}) { if err := recover(); err != nil { severity := notifier.getDefaultSeverity(rawData, SeverityWarning) state := HandledState{SeverityReasonHandledPanic, severity, false, ""} rawData = notifier.appendStateIfNeeded(rawData, state) notifier.Notify(errors.New(err, 2), rawData...) } } func (notifier *Notifier) dontPanic() { if err := recover(); err != nil { notifier.Config.logf("bugsnag/notifier.Notify: panic! %s", err) } } // Get defined severity from raw data or a fallback value func (notifier *Notifier) getDefaultSeverity(rawData []interface{}, s severity) severity { allData := append(notifier.RawData, rawData...) for _, datum := range allData { if _, ok := datum.(severity); ok { return datum.(severity) } } for _, datum := range allData { if _, ok := datum.(HandledState); ok { return datum.(HandledState).OriginalSeverity } } return s } func (notifier *Notifier) appendStateIfNeeded(rawData []interface{}, h HandledState) []interface{} { for _, datum := range append(notifier.RawData, rawData...) { if _, ok := datum.(HandledState); ok { return rawData } } return append(rawData, h) } bugsnag-go-2.2.0/notifier_test.go000066400000000000000000000152271432154711400167620ustar00rootroot00000000000000package bugsnag_test import ( "fmt" "strings" "testing" simplejson "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/errors" . "github.com/bugsnag/bugsnag-go/testutil" ) var bugsnaggedReports chan []byte func notifierSetup(url string) *bugsnag.Notifier { return bugsnag.New(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: url, Sessions: url + "/sessions"}, }) } func crash(s interface{}) int { return s.(int) } func TestStackframesAreSkippedCorrectly(t *testing.T) { ts, reports := Setup() bugsnaggedReports = reports defer ts.Close() notifier := notifierSetup(ts.URL) bugsnag.Configure(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, }) t.Run("notifier.Notify", func(st *testing.T) { notifier.Notify(fmt.Errorf("oopsie")) assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func1", File: "notifier_test.go"}, }) }) t.Run("bugsnag.Notify", func(st *testing.T) { bugsnag.Notify(fmt.Errorf("oopsie")) assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func2", File: "notifier_test.go"}, }) }) t.Run("notifier.NotifySync", func(st *testing.T) { notifier.NotifySync(fmt.Errorf("oopsie"), true) assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func3", File: "notifier_test.go"}, }) }) t.Run("notifier.AutoNotify", func(st *testing.T) { func() { defer func() { recover() }() defer notifier.AutoNotify() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4", File: "notifier_test.go"}, }) }) t.Run("bugsnag.AutoNotify", func(st *testing.T) { func() { defer func() { recover() }() defer bugsnag.AutoNotify() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5", File: "notifier_test.go"}, }) }) // Expect the following frames to be present for *.Recover /* { "file": "runtime/panic.go", "method": "gopanic" }, { "file": "runtime/iface.go", "method": "panicdottypeE" }, { "file": "$GOPATH/src/github.com/bugsnag/bugsnag-go/notifier_test.go", "method": "TestStackframesAreSkippedCorrectly.func4.1" }, { "file": "$GOPATH/src/github.com/bugsnag/bugsnag-go/notifier_test.go", "method": "TestStackframesAreSkippedCorrectly.func4" }, { "file": "testing/testing.go", "method": "tRunner" }, { "file": "runtime/asm_amd64.s", "method": "goexit" } */ t.Run("notifier.Recover", func(st *testing.T) { func() { defer notifier.Recover() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6", File: "notifier_test.go"}, }) }) t.Run("bugsnag.Recover", func(st *testing.T) { func() { defer bugsnag.Recover() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7", File: "notifier_test.go"}, }) }) } func TestModifyingEventsWithCallbacks(t *testing.T) { server, eventQueue := Setup() defer server.Close() notifier := notifierSetup(server.URL) bugsnag.Configure(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: server.URL, Sessions: server.URL + "/sessions"}, }) t.Run("bugsnag.Notify change unhandled in block", func(st *testing.T) { notifier.Notify(fmt.Errorf("ahoy"), func(event *bugsnag.Event) { event.Unhandled = true }) json, _ := simplejson.NewJson(<-eventQueue) event := GetIndex(json, "events", 0) exception := GetIndex(event, "exceptions", 0) message := exception.Get("message").MustString() unhandled := event.Get("unhandled").MustBool() overridden := event.Get("severityReason").Get("unhandledOverridden").MustBool() if message != "ahoy" { st.Errorf("incorrect error message '%s'", message) } if !unhandled { st.Errorf("failed to change handled-ness in block") } if !overridden { st.Errorf("failed to set handledness change in block") } }) t.Run("bugsnag.Notify with block", func(st *testing.T) { notifier.Notify(fmt.Errorf("bnuuy"), bugsnag.Context{String: "should be overridden"}, func(event *bugsnag.Event) { event.Context = "known unknowns" }) json, _ := simplejson.NewJson(<-eventQueue) event := GetIndex(json, "events", 0) context := event.Get("context").MustString() exception := GetIndex(event, "exceptions", 0) class := exception.Get("errorClass").MustString() message := exception.Get("message").MustString() if class != "*errors.errorString" { st.Errorf("incorrect error class '%s'", class) } if message != "bnuuy" { st.Errorf("incorrect error message '%s'", message) } if context != "known unknowns" { st.Errorf("failed to change context in block. '%s'", context) } if event.Get("unhandled").MustBool() { st.Errorf("error is unexpectedly unhandled") } if overridden, err := event.Get("severityReason").Get("unhandledOverridden").Bool(); err == nil { // if err == nil, then the value existed in the payload. the expectation // is that unhandledOverridden is not sent when handled-ness is not changed. st.Errorf("error unexpectedly has unhandledOverridden: %v", overridden) } }) } func assertStackframesMatch(t *testing.T, expected []errors.StackFrame) { var lastmatch int = 0 var matched int = 0 event, _ := simplejson.NewJson(<-bugsnaggedReports) json := GetIndex(event, "events", 0) stacktrace := GetIndex(json, "exceptions", 0).Get("stacktrace") for i := 0; i < len(stacktrace.MustArray()); i++ { actualFrame := stacktrace.GetIndex(i) file := actualFrame.Get("file").MustString() method := actualFrame.Get("method").MustString() for index, expectedFrame := range expected { if index < lastmatch { continue } if strings.HasSuffix(file, expectedFrame.File) && expectedFrame.Name == method { lastmatch = index matched++ } } } if matched != len(expected) { s, _ := stacktrace.EncodePretty() t.Errorf("failed to find matches for %d frames: '%v'\ngot: '%v'", len(expected)-matched, expected[matched:], string(s)) } } bugsnag-go-2.2.0/panicwrap.go000066400000000000000000000016651432154711400160710ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/errors" "github.com/bugsnag/bugsnag-go/sessions" "github.com/bugsnag/panicwrap" ) // Forks and re-runs your program to add panic monitoring. This function does // not return on one process, instead listening on stderr of the other process, // which returns nil. // // Related: https://godoc.org/github.com/bugsnag/panicwrap#BasicMonitor func defaultPanicHandler() { defer defaultNotifier.dontPanic() ctx := sessions.SendStartupSession(&sessionTrackingConfig) err := panicwrap.BasicMonitor(func(output string) { toNotify, err := errors.ParsePanic(output) if err != nil { defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) } state := HandledState{SeverityReasonUnhandledPanic, SeverityError, true, ""} defaultNotifier.NotifySync(toNotify, true, state, ctx) }) if err != nil { defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) } } bugsnag-go-2.2.0/panicwrap_test.go000066400000000000000000000102031432154711400171140ustar00rootroot00000000000000package bugsnag import ( "context" "os" "os/exec" "strings" "testing" "time" "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go/sessions" "github.com/kardianos/osext" ) // Test the panic handler by launching a new process which runs the init() // method in this file and causing a handled panic func TestPanicHandlerHandledPanic(t *testing.T) { ts, reports := setup() defer ts.Close() startPanickingProcess(t, "handled", ts.URL) json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "ruh roh"}}, }) event := getIndex(json, "events", 0) assertValidSession(t, event, true) stacktrace := getIndex(event, "exceptions", 0).Get("stacktrace") found := false for i := 0; i < len(stacktrace.MustArray()); i++ { frame := stacktrace.GetIndex(i) if strings.HasSuffix(getString(frame, "file"), "panicwrap_test.go") && getInt(frame, "lineNumber") != 0 { found = true break } } if !found { s, _ := stacktrace.EncodePretty() t.Errorf("no stack frame found matching this file in stack trace: %v", string(s)) } } // Test the panic handler by launching a new process which runs the init() // method in this file and causing an unhandled panic func TestPanicHandlerUnhandledPanic(t *testing.T) { ts, reports := setup() defer ts.Close() startPanickingProcess(t, "unhandled", ts.URL) json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonUnhandledPanic}, Unhandled: true, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "panic", Message: "ruh roh"}}, }) } func startPanickingProcess(t *testing.T, variant string, endpoint string) { exePath, err := osext.Executable() if err != nil { t.Fatal(err) } // Use the same trick as panicwrap() to re-run ourselves. // In the init() block below, we will then panic. cmd := exec.Command(exePath, os.Args[1:]...) cmd.Env = append(os.Environ(), "BUGSNAG_API_KEY="+testAPIKey, "BUGSNAG_NOTIFY_ENDPOINT="+endpoint, "please_panic="+variant) // Gift for the debugging developer: // As these tests shell out we don't see, or even want to see, the output // of these tests by default. The following two lines may be uncommented // in order to see what this command would print to stdout and stderr. /* bytes, _ := cmd.CombinedOutput() fmt.Println(string(bytes)) */ if err = cmd.Start(); err != nil { t.Fatal(err) } if err = cmd.Wait(); err.Error() != "exit status 2" { t.Fatal(err) } } func init() { if os.Getenv("please_panic") == "handled" { Configure(Configuration{ APIKey: os.Getenv("BUGSNAG_API_KEY"), Endpoints: Endpoints{Notify: os.Getenv("BUGSNAG_NOTIFY_ENDPOINT")}, Hostname: "web1", ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"}}) go func() { ctx := StartSession(context.Background()) defer AutoNotify(ctx) panick() }() // Plenty of time to crash, it shouldn't need any of it. time.Sleep(1 * time.Second) } else if os.Getenv("please_panic") == "unhandled" { Configure(Configuration{ APIKey: os.Getenv("BUGSNAG_API_KEY"), Endpoints: Endpoints{Notify: os.Getenv("BUGSNAG_NOTIFY_ENDPOINT")}, Hostname: "web1", Synchronous: true, ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"}}) panick() } } func panick() { panic("ruh roh") } bugsnag-go-2.2.0/payload.go000066400000000000000000000071541432154711400155350ustar00rootroot00000000000000package bugsnag import ( "bytes" "encoding/json" "fmt" "net/http" "runtime" "sync" "time" "github.com/bugsnag/bugsnag-go/device" "github.com/bugsnag/bugsnag-go/headers" "github.com/bugsnag/bugsnag-go/sessions" ) const notifyPayloadVersion = "4" var sessionMutex sync.Mutex type payload struct { *Event *Configuration } type hash map[string]interface{} func (p *payload) deliver() error { if len(p.APIKey) != 32 { return fmt.Errorf("bugsnag/payload.deliver: invalid api key: '%s'", p.APIKey) } buf, err := p.MarshalJSON() if err != nil { return fmt.Errorf("bugsnag/payload.deliver: %v", err) } client := http.Client{ Transport: p.Transport, } req, err := http.NewRequest("POST", p.Endpoints.Notify, bytes.NewBuffer(buf)) if err != nil { return fmt.Errorf("bugsnag/payload.deliver unable to create request: %v", err) } for k, v := range headers.PrefixedHeaders(p.APIKey, notifyPayloadVersion) { req.Header.Add(k, v) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("bugsnag/payload.deliver: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("bugsnag/payload.deliver: Got HTTP %s", resp.Status) } return nil } func (p *payload) MarshalJSON() ([]byte, error) { return json.Marshal(reportJSON{ APIKey: p.APIKey, Events: []eventJSON{ eventJSON{ App: &appJSON{ ReleaseStage: p.ReleaseStage, Type: p.AppType, Version: p.AppVersion, }, Context: p.Context, Device: &deviceJSON{ Hostname: p.Hostname, OsName: runtime.GOOS, RuntimeVersions: device.GetRuntimeVersions(), }, Request: p.Request, Exceptions: p.exceptions(), GroupingHash: p.GroupingHash, Metadata: p.MetaData.sanitize(p.ParamsFilters), PayloadVersion: notifyPayloadVersion, Session: p.makeSession(), Severity: p.Severity.String, SeverityReason: p.severityReasonPayload(), Unhandled: p.Unhandled, User: p.User, }, }, Notifier: notifierJSON{ Name: "Bugsnag Go", URL: "https://github.com/bugsnag/bugsnag-go", Version: VERSION, }, }) } func (p *payload) makeSession() *sessionJSON { // If a context has not been applied to the payload then assume that no // session has started either if p.Ctx == nil { return nil } sessionMutex.Lock() defer sessionMutex.Unlock() session := sessions.IncrementEventCountAndGetSession(p.Ctx, p.Unhandled) if session != nil { s := *session return &sessionJSON{ ID: s.ID, StartedAt: s.StartedAt.UTC().Format(time.RFC3339), Events: sessions.EventCounts{ Handled: s.EventCounts.Handled, Unhandled: s.EventCounts.Unhandled, }, } } return nil } func (p *payload) severityReasonPayload() *severityReasonJSON { if reason := p.handledState.SeverityReason; reason != "" { json := &severityReasonJSON{ Type: reason, UnhandledOverridden: p.handledState.Unhandled != p.Unhandled, } if p.handledState.Framework != "" { json.Attributes = make(map[string]string, 1) json.Attributes["framework"] = p.handledState.Framework } return json } return nil } func (p *payload) exceptions() []exceptionJSON { exceptions := []exceptionJSON{ exceptionJSON{ ErrorClass: p.ErrorClass, Message: p.Message, Stacktrace: p.Stacktrace, }, } if p.Error == nil { return exceptions } cause := p.Error.Cause for cause != nil { exceptions = append(exceptions, exceptionJSON{ ErrorClass: cause.TypeName(), Message: cause.Error(), Stacktrace: generateStacktrace(cause, p.Configuration), }) cause = cause.Cause } return exceptions } bugsnag-go-2.2.0/payload_test.go000066400000000000000000000074561432154711400166010ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" "runtime" "strings" "testing" "github.com/bugsnag/bugsnag-go/errors" "github.com/bugsnag/bugsnag-go/sessions" ) 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"}}` // The large payload has a timestamp in it which makes it awkward to assert against. // Instead, assert that the timestamp property exist, along with the rest of the expected payload 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":"` 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"}}` func TestMarshalEmptyPayload(t *testing.T) { sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig) p := payload{&Event{Ctx: context.Background()}, &Configuration{}} bytes, _ := p.MarshalJSON() exp := fmt.Sprintf(expSmall, runtime.GOOS, runtime.Version()) if got := string(bytes[:]); got != exp { t.Errorf("Payload different to what was expected. \nGot: %s\nExp: %s", got, exp) } } func TestMarshalLargePayload(t *testing.T) { payload := makeLargePayload() bytes, _ := payload.MarshalJSON() got := string(bytes[:]) expPre := fmt.Sprintf(expLargePre, runtime.GOOS, runtime.Version()) if !strings.Contains(got, expPre) { t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expPre, got) } if !strings.Contains(got, expLargePost) { t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expLargePost, got) } } func makeLargePayload() *payload { stackframes := []StackFrame{ {Method: "doA", File: "a.go", LineNumber: 65, InProject: false}, {Method: "fetchB", File: "b.go", LineNumber: 99, InProject: true}, {Method: "incrementI", File: "i.go", LineNumber: 651, InProject: false}, } user := User{ Id: "1234baerg134", Name: "Kool Kidz on da bus", Email: "typo@busgang.com", } handledState := HandledState{ SeverityReason: SeverityReasonUnhandledError, OriginalSeverity: severity{String: "error"}, Unhandled: true, Framework: "gin", } ctx := context.Background() ctx = StartSession(ctx) event := Event{ Error: &errors.Error{}, RawData: nil, ErrorClass: "error class", Message: "error message goes here", Stacktrace: stackframes, Context: "/api/v2/albums", Severity: SeverityInfo, GroupingHash: "custom grouping hash", User: &user, Ctx: ctx, MetaData: map[string]map[string]interface{}{ "custom tab": map[string]interface{}{ "my key": "my value", }, }, Unhandled: true, handledState: handledState, } config := Configuration{ APIKey: testAPIKey, ReleaseStage: "mega-production", AppType: "gin", AppVersion: "1.5.3", Hostname: "super.duper.site", } return &payload{&event, &config} } bugsnag-go-2.2.0/report.go000066400000000000000000000047721432154711400154220ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/device" "github.com/bugsnag/bugsnag-go/sessions" uuid "github.com/google/uuid" ) type reportJSON struct { APIKey string `json:"apiKey"` Events []eventJSON `json:"events"` Notifier notifierJSON `json:"notifier"` } type notifierJSON struct { Name string `json:"name"` URL string `json:"url"` Version string `json:"version"` } type eventJSON struct { App *appJSON `json:"app"` Context string `json:"context,omitempty"` Device *deviceJSON `json:"device,omitempty"` Request *RequestJSON `json:"request,omitempty"` Exceptions []exceptionJSON `json:"exceptions"` GroupingHash string `json:"groupingHash,omitempty"` Metadata interface{} `json:"metaData"` PayloadVersion string `json:"payloadVersion"` Session *sessionJSON `json:"session,omitempty"` Severity string `json:"severity"` SeverityReason *severityReasonJSON `json:"severityReason,omitempty"` Unhandled bool `json:"unhandled"` User *User `json:"user,omitempty"` } type sessionJSON struct { StartedAt string `json:"startedAt"` ID uuid.UUID `json:"id"` Events sessions.EventCounts `json:"events"` } type appJSON struct { ReleaseStage string `json:"releaseStage"` Type string `json:"type,omitempty"` Version string `json:"version,omitempty"` } type exceptionJSON struct { ErrorClass string `json:"errorClass"` Message string `json:"message"` Stacktrace []StackFrame `json:"stacktrace"` } type severityReasonJSON struct { Type SeverityReason `json:"type,omitempty"` Attributes map[string]string `json:"attributes,omitempty"` UnhandledOverridden bool `json:"unhandledOverridden,omitempty"` } type deviceJSON struct { Hostname string `json:"hostname,omitempty"` OsName string `json:"osName,omitempty"` RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions,omitempty"` } // RequestJSON is the request information that populates the Request tab in the dashboard. type RequestJSON struct { ClientIP string `json:"clientIp,omitempty"` Headers map[string]string `json:"headers,omitempty"` HTTPMethod string `json:"httpMethod,omitempty"` URL string `json:"url,omitempty"` Referer string `json:"referer,omitempty"` } bugsnag-go-2.2.0/report_publisher.go000066400000000000000000000011361432154711400174660ustar00rootroot00000000000000package bugsnag import "fmt" type reportPublisher interface { publishReport(*payload) error } type defaultReportPublisher struct{} func (*defaultReportPublisher) publishReport(p *payload) error { p.logf("notifying bugsnag: %s", p.Message) if !p.notifyInReleaseStage() { return fmt.Errorf("not notifying in %s", p.ReleaseStage) } if p.Synchronous { return p.deliver() } go func(p *payload) { if err := p.deliver(); err != nil { // Ensure that any errors are logged if they occur in a goroutine. p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err) } }(p) return nil } bugsnag-go-2.2.0/request_extractor.go000066400000000000000000000057161432154711400176710ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "net/url" "strings" ) const requestContextKey requestKey = iota type requestKey int // AttachRequestData returns a child of the given context with the request // object attached for later extraction by the notifier in order to // automatically record request data func AttachRequestData(ctx context.Context, r *http.Request) context.Context { return context.WithValue(ctx, requestContextKey, r) } // extractRequestInfo looks for the request object that the notifier // automatically attaches to the context when using any of the supported // frameworks or bugsnag.HandlerFunc or bugsnag.Handler, and returns sub-object // supported by the notify API. func extractRequestInfo(ctx context.Context) (*RequestJSON, *http.Request) { if req := getRequestIfPresent(ctx); req != nil { return extractRequestInfoFromReq(req), req } return nil, nil } // extractRequestInfoFromReq extracts the request information the notify API // understands from the given HTTP request. Returns the sub-object supported by // the notify API. func extractRequestInfoFromReq(req *http.Request) *RequestJSON { return &RequestJSON{ ClientIP: req.RemoteAddr, HTTPMethod: req.Method, URL: sanitizeURL(req), Referer: req.Referer(), Headers: parseRequestHeaders(req.Header), } } // sanitizeURL will build up the URL matching the request. It will filter query parameters to remove sensitive fields. // The query part of the URL might appear differently (different order of parameters) if any filtering was done. func sanitizeURL(req *http.Request) string { scheme := "http" if req.TLS != nil { scheme = "https" } rawQuery := req.URL.RawQuery parsedQuery, err := url.ParseQuery(req.URL.RawQuery) if err != nil { return scheme + "://" + req.Host + req.RequestURI } changed := false for key, values := range parsedQuery { if contains(Config.ParamsFilters, key) { for i := range values { values[i] = "BUGSNAG_URL_FILTERED" changed = true } } } if changed { rawQuery = parsedQuery.Encode() rawQuery = strings.Replace(rawQuery, "BUGSNAG_URL_FILTERED", "[FILTERED]", -1) } u := url.URL{ Scheme: scheme, Host: req.Host, Path: req.URL.Path, RawQuery: rawQuery, } return u.String() } func parseRequestHeaders(header map[string][]string) map[string]string { headers := make(map[string]string) for k, v := range header { // Headers can have multiple values, in which case we report them as csv if contains(Config.ParamsFilters, k) { headers[k] = "[FILTERED]" } else { headers[k] = strings.Join(v, ",") } } return headers } func contains(slice []string, e string) bool { for _, s := range slice { if strings.Contains(strings.ToLower(e), strings.ToLower(s)) { return true } } return false } func getRequestIfPresent(ctx context.Context) *http.Request { if ctx == nil { return nil } val := ctx.Value(requestContextKey) if val == nil { return nil } return val.(*http.Request) } bugsnag-go-2.2.0/request_extractor_test.go000066400000000000000000000073461432154711400207310ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestRequestInformationGetsExtracted(t *testing.T) { contexts := make(chan context.Context, 1) hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = AttachRequestData(ctx, r) contexts <- ctx }) ts := httptest.NewServer(hf) defer ts.Close() http.Get(ts.URL + "/1234abcd?fish=bird") reqJSON, req := extractRequestInfo(<-contexts) if reqJSON.ClientIP == "" { t.Errorf("expected to find an IP address for the request but was blank") } if got, exp := reqJSON.HTTPMethod, "GET"; got != exp { t.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := req.URL.Path, "/1234abcd"; got != exp { t.Errorf("expected request URL to be '%s' but was '%s'", exp, got) } if got, exp := reqJSON.URL, "/1234abcd?fish=bird"; !strings.Contains(got, exp) { t.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) } if got, exp := reqJSON.Referer, ""; got != exp { t.Errorf("expected request referer to be '%s' but was '%s'", exp, got) } if got, exp := reqJSON.Headers["Accept-Encoding"], "gzip"; got != exp { t.Errorf("expected Accept-Encoding to be '%s' but was '%s'", exp, got) } if got, exp := reqJSON.Headers["User-Agent"], "Go-http-client"; !strings.Contains(got, exp) { t.Errorf("expected user agent to contain '%s' but was '%s'", exp, got) } } func TestRequestExtractorCanHandleAbsentContext(t *testing.T) { if got, _ := extractRequestInfo(nil); got != nil { //really just testing that nothing panics here t.Errorf("expected nil contexts to give nil sub-objects, but was '%s'", got) } if got, _ := extractRequestInfo(context.Background()); got != nil { //really just testing that nothing panics here t.Errorf("expected contexts without requst info to give nil sub-objects, but was '%s'", got) } } func TestExtractRequestInfoFromReq_RedactURL(t *testing.T) { testCases := []struct { in url.URL exp string }{ {in: url.URL{}, exp: "http://example.com"}, {in: url.URL{Path: "/"}, exp: "http://example.com/"}, {in: url.URL{Path: "/foo.html"}, exp: "http://example.com/foo.html"}, {in: url.URL{Path: "/foo.html", RawQuery: "q=something&bar=123"}, exp: "http://example.com/foo.html?q=something&bar=123"}, {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"}, // Invalid query string. {in: url.URL{Path: "/foo", RawQuery: "%"}, exp: "http://example.com/foo?%"}, // Query params contain secrets {in: url.URL{Path: "/foo.html", RawQuery: "access_token=something"}, exp: "http://example.com/foo.html?access_token=[FILTERED]"}, {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"}, } for _, tc := range testCases { requestURI := tc.in.Path if tc.in.RawQuery != "" { requestURI += "?" + tc.in.RawQuery } req := &http.Request{ Host: "example.com", URL: &tc.in, RequestURI: requestURI, } result := extractRequestInfoFromReq(req) if result.URL != tc.exp { t.Errorf("expected URL to be '%s' but was '%s'", tc.exp, result.URL) } } } func TestParseHeadersWillSanitiseIllegalParams(t *testing.T) { headers := make(map[string][]string) headers["password"] = []string{"correct horse battery staple"} headers["secret"] = []string{"I am Banksy"} headers["authorization"] = []string{"licence to kill -9"} headers["custom-made-secret"] = []string{"I'm the insider at Sotheby's"} for k, v := range parseRequestHeaders(headers) { if v != "[FILTERED]" { t.Errorf("expected '%s' to be [FILTERED], but was '%s'", k, v) } } } bugsnag-go-2.2.0/revel/000077500000000000000000000000001432154711400146635ustar00rootroot00000000000000bugsnag-go-2.2.0/revel/bugsnagrevel.go000066400000000000000000000114571432154711400177060ustar00rootroot00000000000000// Package bugsnagrevel adds Bugsnag to revel. // It lets you pass *revel.Controller into bugsnag.Notify(), // and provides a Filter to catch errors. package bugsnagrevel import ( "context" "net/http" "strings" "github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go/device" "github.com/revel/revel" ) // FrameworkName is the name of the framework this middleware applies to const FrameworkName string = "Revel" var errorHandlingState = bugsnag.HandledState{ SeverityReason: bugsnag.SeverityReasonUnhandledMiddlewareError, OriginalSeverity: bugsnag.SeverityError, Unhandled: true, Framework: FrameworkName, } // Filter should be added to the filter chain just after the PanicFilter. // It sends errors to Bugsnag automatically. Configuration is read out of // conf/app.conf, you should set bugsnag.apikey, and can also set // bugsnag.endpoints, bugsnag.releasestage, bugsnag.apptype, bugsnag.appversion, // bugsnag.projectroot, bugsnag.projectpackages if needed. func Filter(c *revel.Controller, fc []revel.Filter) { notifier := bugsnag.New() ctx := bugsnag.AttachRequestData(context.Background(), findProperHTTPRequest(c)) // Record a session if auto capture sessions is enabled if notifier.Config.IsAutoCaptureSessions() { ctx = bugsnag.StartSession(ctx) } c.Args["context"] = ctx defer notifier.AutoNotify(c, ctx, errorHandlingState) fc[0](c, fc[1:]) } // Add support to bugsnag for reading data out of *revel.Controllers func middleware(event *bugsnag.Event, config *bugsnag.Configuration) error { for _, datum := range event.RawData { if controller, ok := datum.(*revel.Controller); ok { // make the request visible to the builtin HttpMiddleware event.Context = controller.Action event.MetaData.AddStruct("Session", controller.Session) } } return nil } func findProperHTTPRequest(c *revel.Controller) *http.Request { var req *http.Request rawReq := c.Request.In.GetRaw() // This *should* always be a *http.Request, but the revel team must have // made this an interface{} for a reason, and we might as well be defensive // about it switch rawReq.(type) { case (*http.Request): req = rawReq.(*http.Request) // Find the *proper* http request. } return req } type bugsnagRevelLogger struct{} func (l *bugsnagRevelLogger) Printf(s string, params ...interface{}) { if strings.HasPrefix(s, "ERROR") { revel.AppLog.Errorf(s, params...) } else if strings.HasPrefix(s, "WARN") { revel.AppLog.Warnf(s, params...) } else { revel.AppLog.Infof(s, params...) } } func init() { // To ensure that users can disable the default panic handler (by calling // bugsnag.Configure before this function does) we must allow other // callbacks to execute before this function. order := 2 revel.OnAppStart(func() { bugsnag.OnBeforeNotify(middleware) c := revel.Config config := bugsnag.Config bugsnag.Configure(bugsnag.Configuration{ APIKey: c.StringDefault("bugsnag.apikey", ""), Endpoint: c.StringDefault("bugsnag.endpoint", ""), Endpoints: bugsnag.Endpoints{ Notify: c.StringDefault("bugsnag.endpoints.notify", ""), Sessions: c.StringDefault("bugsnag.endpoints.sessions", ""), }, ReleaseStage: c.StringDefault("bugsnag.releasestage", defaultReleaseStage()), AppType: c.StringDefault("bugsnag.apptype", defaultAppType()), AppVersion: c.StringDefault("bugsnag.appversion", config.AppVersion), AutoCaptureSessions: c.BoolDefault("bugsnag.autocapturesessions", config.IsAutoCaptureSessions()), Hostname: c.StringDefault("bugsnag.hostname", config.Hostname), NotifyReleaseStages: getCsvsOrDefault("bugsnag.notifyreleasestages", config.NotifyReleaseStages), ProjectPackages: getCsvsOrDefault("bugsnag.projectpackages", defaultProjectPackages()), SourceRoot: c.StringDefault("bugsnag.sourceroot", config.SourceRoot), ParamsFilters: getCsvsOrDefault("bugsnag.paramsfilters", config.ParamsFilters), Logger: new(bugsnagRevelLogger), Synchronous: c.BoolDefault("bugsnag.synchronous", config.Synchronous), }) device.AddVersion(FrameworkName, revel.Version) }, order) } func defaultProjectPackages() []string { pp := bugsnag.Config.ProjectPackages // Use the bugsnag.Config previously set (probably in init.go) if it is not // the default []string{"main*"} value if len(pp) == 1 && pp[0] == "main*" { return []string{revel.ImportPath + "/app/**"} } return pp } func defaultAppType() string { if at := bugsnag.Config.AppType; at != "" { return at } return FrameworkName } func defaultReleaseStage() string { if rs := bugsnag.Config.ReleaseStage; rs != "" { return rs } return revel.RunMode } func getCsvsOrDefault(propertyKey string, d []string) []string { if propString, ok := revel.Config.String(propertyKey); ok { return strings.Split(propString, ",") } return d } bugsnag-go-2.2.0/sessions/000077500000000000000000000000001432154711400154145ustar00rootroot00000000000000bugsnag-go-2.2.0/sessions/config.go000066400000000000000000000104321432154711400172100ustar00rootroot00000000000000package sessions import ( "log" "net/http" "sync" "time" ) // SessionTrackingConfiguration defines the configuration options relevant for session tracking. // These are likely a subset of the global bugsnag.Configuration. Users should // not modify this struct directly but rather call // `bugsnag.Configure(bugsnag.Configuration)` which will update this configuration in return. type SessionTrackingConfiguration struct { // PublishInterval defines how often the sessions are sent off to the session server. PublishInterval time.Duration // AutoCaptureSessions can be set to false to disable automatic session // tracking. If you want control over what is deemed a session, you can // switch off automatic session tracking with this configuration, and call // bugsnag.StartSession() when appropriate for your application. See the // official docs for instructions and examples of associating handled // errors with sessions and ensuring error rate accuracy on the Bugsnag // dashboard. This will default to true, but is stored as an interface to enable // us to detect when this option has not been set. AutoCaptureSessions interface{} // APIKey defines the API key for the Bugsnag project. Same value as for reporting errors. APIKey string // Endpoint is the URI of the session server to receive session payloads. Endpoint string // Version defines the current version of the notifier. Version string // ReleaseStage defines the release stage, e.g. "production" or "staging", // that this session occurred in. The release stage, in combination with // the app version make up the release that Bugsnag tracks. ReleaseStage string // Hostname defines the host of the server this application is running on. Hostname string // AppType defines the type of the application. AppType string // AppVersion defines the version of the application. AppVersion string // Transport defines the http.RoundTripper to be used for managing HTTP requests. Transport http.RoundTripper // The release stages to notify about sessions in. If you set this then // bugsnag-go will only send sessions to Bugsnag if the release stage // is listed here. NotifyReleaseStages []string // Logger is the logger that Bugsnag should log to. Uses the same defaults // as go's builtin logging package. This logger gets invoked when any error // occurs inside the library itself. Logger interface { Printf(format string, v ...interface{}) } mutex sync.Mutex } // Update modifies the values inside the receiver to match the non-default properties of the given config. // Existing properties will not be cleared when given empty fields. func (c *SessionTrackingConfiguration) Update(config *SessionTrackingConfiguration) { c.mutex.Lock() defer c.mutex.Unlock() if config.PublishInterval != 0 { c.PublishInterval = config.PublishInterval } if config.APIKey != "" { c.APIKey = config.APIKey } if config.Endpoint != "" { c.Endpoint = config.Endpoint } if config.Version != "" { c.Version = config.Version } if config.ReleaseStage != "" { c.ReleaseStage = config.ReleaseStage } if config.Hostname != "" { c.Hostname = config.Hostname } if config.AppType != "" { c.AppType = config.AppType } if config.AppVersion != "" { c.AppVersion = config.AppVersion } if config.Transport != nil { c.Transport = config.Transport } if config.Logger != nil { c.Logger = config.Logger } if config.NotifyReleaseStages != nil { c.NotifyReleaseStages = config.NotifyReleaseStages } if config.AutoCaptureSessions != nil { c.AutoCaptureSessions = config.AutoCaptureSessions } } func (c *SessionTrackingConfiguration) logf(fmt string, args ...interface{}) { if c != nil && c.Logger != nil { c.Logger.Printf(fmt, args...) } else { log.Printf(fmt, args...) } } // IsAutoCaptureSessions identifies whether or not the notifier should // automatically capture sessions as requests come in. It's a convenience // wrapper that allows automatic session capturing to be enabled by default. func (c *SessionTrackingConfiguration) IsAutoCaptureSessions() bool { if c.AutoCaptureSessions == nil { return true // enabled by default } if val, ok := c.AutoCaptureSessions.(bool); ok { return val } // It has been configured to *something* (although not a valid value) // assume the user wanted to disable this option. return false } bugsnag-go-2.2.0/sessions/config_test.go000066400000000000000000000051771432154711400202610ustar00rootroot00000000000000package sessions import ( "net/http" "reflect" "testing" "time" ) func TestConfigDoesNotChangeGivenBlankValues(t *testing.T) { c := testConfig() exp := testConfig() c.Update(&SessionTrackingConfiguration{}) tt := []struct { name string expected interface{} got interface{} }{ {"PublishInterval", exp.PublishInterval, c.PublishInterval}, {"APIKey", exp.APIKey, c.APIKey}, {"Endpoint", exp.Endpoint, c.Endpoint}, {"Version", exp.Version, c.Version}, {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, {"Hostname", exp.Hostname, c.Hostname}, {"AppType", exp.AppType, c.AppType}, {"AppVersion", exp.AppVersion, c.AppVersion}, {"Transport", exp.Transport, c.Transport}, {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, } for _, tc := range tt { if !reflect.DeepEqual(tc.got, tc.expected) { t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) } } } func TestConfigUpdatesGivenNonDefaultValues(t *testing.T) { c := testConfig() exp := SessionTrackingConfiguration{ PublishInterval: 40 * time.Second, APIKey: "api234", Endpoint: "https://docs.bugsnag.com/platforms/go/", Version: "2.7.3", ReleaseStage: "Production", Hostname: "Brian's Surface", AppType: "Revel API", AppVersion: "6.3.9", NotifyReleaseStages: []string{"staging", "production"}, } c.Update(&exp) tt := []struct { name string expected interface{} got interface{} }{ {"PublishInterval", exp.PublishInterval, c.PublishInterval}, {"APIKey", exp.APIKey, c.APIKey}, {"Endpoint", exp.Endpoint, c.Endpoint}, {"Version", exp.Version, c.Version}, {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, {"Hostname", exp.Hostname, c.Hostname}, {"AppType", exp.AppType, c.AppType}, {"AppVersion", exp.AppVersion, c.AppVersion}, {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, } for _, tc := range tt { if !reflect.DeepEqual(tc.got, tc.expected) { t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) } } } func testConfig() SessionTrackingConfiguration { return SessionTrackingConfiguration{ PublishInterval: 20 * time.Second, APIKey: "api123", Endpoint: "https://bugsnag.com/jobs", //If you like what you see... ;) Version: "1.6.2", ReleaseStage: "Staging", Hostname: "Russ's MacbookPro", AppType: "Gin API", AppVersion: "5.2.8", NotifyReleaseStages: []string{"staging", "production"}, Transport: http.DefaultTransport, } } bugsnag-go-2.2.0/sessions/integration_test.go000066400000000000000000000102461432154711400213300ustar00rootroot00000000000000package sessions_test import ( "context" "io/ioutil" "net/http" "net/http/httptest" "os" "runtime" "strings" "sync" "testing" "time" simplejson "github.com/bitly/go-simplejson" bugsnag "github.com/bugsnag/bugsnag-go" ) const testAPIKey = "166f5ad3590596f9aa8d601ea89af845" const testPublishInterval = time.Millisecond * 200 const sessionsCount = 50000 func init() { //Naughty injection to achieve a reasonable test duration. bugsnag.DefaultSessionPublishInterval = testPublishInterval } func get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } func getBool(j *simplejson.Json, path string) bool { return get(j, path).MustBool() } func getInt(j *simplejson.Json, path string) int { return get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return get(j, path).MustString() } func getIndex(j *simplejson.Json, path string, index int) *simplejson.Json { return get(j, path).GetIndex(index) } func getFirstString(j *simplejson.Json, path string) string { return getIndex(j, path, 0).MustString() } // Spins up a session server and checks that for every call to // bugsnag.StartSession() a session is being recorded. func TestStartSession(t *testing.T) { sessionsStarted := 0 mutex := sync.Mutex{} // Test server does all the checking of individual requests ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertCorrectHeaders(t, r) body, err := ioutil.ReadAll(r.Body) if err != nil { t.Error(err) } json, err := simplejson.NewJson(body) if err != nil { t.Error(err) } hostname, _ := os.Hostname() tt := []struct { prop string exp interface{} }{ {prop: "notifier.name", exp: "Bugsnag Go"}, {prop: "notifier.url", exp: "https://github.com/bugsnag/bugsnag-go"}, {prop: "notifier.version", exp: bugsnag.VERSION}, {prop: "app.releaseStage", exp: "production"}, {prop: "app.version", exp: ""}, {prop: "device.osName", exp: runtime.GOOS}, {prop: "device.hostname", exp: hostname}, {prop: "device.runtimeVersions.go", exp: runtime.Version()}, {prop: "device.runtimeVersions.gin", exp: ""}, {prop: "device.runtimeVersions.martini", exp: ""}, {prop: "device.runtimeVersions.negroni", exp: ""}, {prop: "device.runtimeVersions.revel", exp: ""}, } for _, tc := range tt { got := getString(json, tc.prop) if got != tc.exp { t.Errorf("Expected '%s' to be '%s' but was '%s'", tc.prop, tc.exp, got) } } sessionCounts := getIndex(json, "sessionCounts", 0) if got := getString(sessionCounts, "startedAt"); len(got) != 20 { t.Errorf("Expected 'sessionCounts.startedAt' to be valid timestamp but was %s", got) } mutex.Lock() defer mutex.Unlock() sessionsStarted += getInt(sessionCounts, "sessionsStarted") w.WriteHeader(http.StatusAccepted) })) defer ts.Close() time.Sleep(testPublishInterval * 2) //Allow server to start // Minimal config. API is mandatory, URLs point to the test server bugsnag.Configure(bugsnag.Configuration{ APIKey: testAPIKey, Endpoints: bugsnag.Endpoints{ Sessions: ts.URL, Notify: ts.URL, }, }) for i := 0; i < sessionsCount; i++ { bugsnag.StartSession(context.Background()) } time.Sleep(testPublishInterval * 2) //Allow all messages to be processed mutex.Lock() defer mutex.Unlock() // Don't expect an additional session from startup as the test server URL // would be different between processes if got, exp := sessionsStarted, sessionsCount; got != exp { t.Errorf("Expected %d sessions started, but was %d", exp, got) } } func assertCorrectHeaders(t *testing.T, req *http.Request) { testCases := []struct{ name, expected string }{ {name: "Bugsnag-Payload-Version", expected: "1.0"}, {name: "Content-Type", expected: "application/json"}, {name: "Bugsnag-Api-Key", expected: testAPIKey}, } for _, tc := range testCases { t.Run(tc.name, func(st *testing.T) { if got := req.Header[tc.name][0]; tc.expected != got { t.Errorf("Expected header '%s' to be '%s' but was '%s'", tc.name, tc.expected, got) } }) } name := "Bugsnag-Sent-At" if req.Header[name][0] == "" { t.Errorf("Expected header '%s' to be non-empty but was empty", name) } } bugsnag-go-2.2.0/sessions/payload.go000066400000000000000000000044251432154711400174010ustar00rootroot00000000000000package sessions import ( "runtime" "time" "github.com/bugsnag/bugsnag-go/device" ) // notifierPayload defines the .notifier subobject of the payload type notifierPayload struct { Name string `json:"name"` URL string `json:"url"` Version string `json:"version"` } // appPayload defines the .app subobject of the payload type appPayload struct { Type string `json:"type,omitempty"` ReleaseStage string `json:"releaseStage,omitempty"` Version string `json:"version,omitempty"` } // devicePayload defines the .device subobject of the payload type devicePayload struct { OsName string `json:"osName,omitempty"` Hostname string `json:"hostname,omitempty"` RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions"` } // sessionCountsPayload defines the .sessionCounts subobject of the payload type sessionCountsPayload struct { StartedAt string `json:"startedAt"` SessionsStarted int `json:"sessionsStarted"` } // sessionPayload defines the top level payload object type sessionPayload struct { Notifier *notifierPayload `json:"notifier"` App *appPayload `json:"app"` Device *devicePayload `json:"device"` SessionCounts []sessionCountsPayload `json:"sessionCounts"` } // makeSessionPayload creates a sessionPayload based off of the given sessions and config func makeSessionPayload(sessions []*Session, config *SessionTrackingConfiguration) *sessionPayload { releaseStage := config.ReleaseStage if releaseStage == "" { releaseStage = "production" } hostname := config.Hostname if hostname == "" { hostname = device.GetHostname() } return &sessionPayload{ Notifier: ¬ifierPayload{ Name: "Bugsnag Go", URL: "https://github.com/bugsnag/bugsnag-go", Version: config.Version, }, App: &appPayload{ Type: config.AppType, Version: config.AppVersion, ReleaseStage: releaseStage, }, Device: &devicePayload{ OsName: runtime.GOOS, Hostname: hostname, RuntimeVersions: device.GetRuntimeVersions(), }, SessionCounts: []sessionCountsPayload{ { //This timestamp assumes that we're sending these off once a minute StartedAt: sessions[0].StartedAt.UTC().Format(time.RFC3339), SessionsStarted: len(sessions), }, }, } } bugsnag-go-2.2.0/sessions/publisher.go000066400000000000000000000050511432154711400177410ustar00rootroot00000000000000package sessions import ( "bytes" "encoding/json" "fmt" "net/http" "github.com/bugsnag/bugsnag-go/headers" ) // sessionPayloadVersion defines the current version of the payload that's // being sent to the session server. const sessionPayloadVersion = "1.0" type sessionPublisher interface { publish(sessions []*Session) error } type httpClient interface { Do(*http.Request) (*http.Response, error) } type publisher struct { config *SessionTrackingConfiguration client httpClient } // publish builds a payload from the given sessions and publishes them to the // session server. Returns any errors that happened as part of publishing. func (p *publisher) publish(sessions []*Session) error { if p.config.Endpoint == "" { // Session tracking is disabled, likely because the notify endpoint was // changed without changing the sessions endpoint // We've already logged a warning in this case, so no need to spam the // log every minute return nil } if apiKey := p.config.APIKey; len(apiKey) != 32 { return fmt.Errorf("bugsnag/sessions/publisher.publish invalid API key: '%s'", apiKey) } nrs, rs := p.config.NotifyReleaseStages, p.config.ReleaseStage if rs != "" && (nrs != nil && !contains(nrs, rs)) { // Always send sessions if the release stage is not set, but don't send any // sessions when notify release stages don't match the current release stage return nil } if len(sessions) == 0 { return fmt.Errorf("bugsnag/sessions/publisher.publish requested publication of 0") } p.config.mutex.Lock() defer p.config.mutex.Unlock() payload := makeSessionPayload(sessions, p.config) buf, err := json.Marshal(payload) if err != nil { return fmt.Errorf("bugsnag/sessions/publisher.publish unable to marshal json: %v", err) } req, err := http.NewRequest("POST", p.config.Endpoint, bytes.NewBuffer(buf)) if err != nil { return fmt.Errorf("bugsnag/sessions/publisher.publish unable to create request: %v", err) } for k, v := range headers.PrefixedHeaders(p.config.APIKey, sessionPayloadVersion) { req.Header.Add(k, v) } res, err := p.client.Do(req) if err != nil { return fmt.Errorf("bugsnag/sessions/publisher.publish unable to deliver session: %v", err) } defer func(res *http.Response) { if err := res.Body.Close(); err != nil { p.config.logf("%v", err) } }(res) if res.StatusCode != 202 { return fmt.Errorf("bugsnag/session.publish expected 202 response status, got HTTP %s", res.Status) } return nil } func contains(coll []string, e string) bool { for _, s := range coll { if s == e { return true } } return false } bugsnag-go-2.2.0/sessions/publisher_test.go000066400000000000000000000163021432154711400210010ustar00rootroot00000000000000package sessions import ( "io" "io/ioutil" "net/http" "os" "runtime" "strings" "testing" "time" simplejson "github.com/bitly/go-simplejson" uuid "github.com/google/uuid" ) const ( sessionEndpoint = "http://localhost:9181" testAPIKey = "166f5ad3590596f9aa8d601ea89af845" ) type testHTTPClient struct { reqs []*http.Request } // A simple io.ReadCloser that we can inject as a body of a http.Request. type nopCloser struct { io.Reader } func (nopCloser) Close() error { return nil } func (c *testHTTPClient) Do(r *http.Request) (*http.Response, error) { c.reqs = append(c.reqs, r) return &http.Response{Body: nopCloser{}, StatusCode: 202}, nil } func get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } func getBool(j *simplejson.Json, path string) bool { return get(j, path).MustBool() } func getInt(j *simplejson.Json, path string) int { return get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return get(j, path).MustString() } func getIndex(j *simplejson.Json, path string, index int) *simplejson.Json { return get(j, path).GetIndex(index) } func getFirstString(j *simplejson.Json, path string) string { return getIndex(j, path, 0).MustString() } func TestSendsCorrectPayloadForSmallConfig(t *testing.T) { sessions, earliestTime := makeSessions() testClient := testHTTPClient{} publisher := publisher{ config: &SessionTrackingConfiguration{Endpoint: sessionEndpoint, Transport: http.DefaultTransport, APIKey: testAPIKey}, client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } req := testClient.reqs[0] assertCorrectHeaders(t, req) body, err := ioutil.ReadAll(req.Body) if err != nil { t.Fatal(err) } root, err := simplejson.NewJson(body) if err != nil { t.Fatal(err) } hostname, _ := os.Hostname() for prop, exp := range map[string]string{ "notifier.name": "Bugsnag Go", "notifier.url": "https://github.com/bugsnag/bugsnag-go", "notifier.version": "", "app.type": "", "app.releaseStage": "production", "app.version": "", "device.osName": runtime.GOOS, "device.hostname": hostname, } { t.Run(prop, func(st *testing.T) { if got := getString(root, prop); got != exp { t.Errorf("Expected property '%s' in JSON to be '%v' but was '%v'", prop, exp, got) } }) } sessionCounts := getIndex(root, "sessionCounts", 0) if got, exp := getString(sessionCounts, "startedAt"), earliestTime; got != exp { t.Errorf("Expected sessionCounts[0].startedAt to be '%s' but was '%s'", exp, got) } if got, exp := getInt(sessionCounts, "sessionsStarted"), len(sessions); got != exp { t.Errorf("Expected sessionCounts[0].sessionsStarted to be %d but was %d", exp, got) } } func TestSendsCorrectPayloadForBigConfig(t *testing.T) { sessions, earliestTime := makeSessions() testClient := testHTTPClient{} publisher := publisher{ config: makeHeavyConfig(), client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } req := testClient.reqs[0] assertCorrectHeaders(t, req) body, err := ioutil.ReadAll(req.Body) if err != nil { t.Fatal(err) } root, err := simplejson.NewJson(body) if err != nil { t.Fatal(err) } for prop, exp := range map[string]string{ "notifier.name": "Bugsnag Go", "notifier.url": "https://github.com/bugsnag/bugsnag-go", "notifier.version": "2.3.4-alpha", "app.type": "gin", "app.releaseStage": "development", "app.version": "1.2.3-beta", "device.osName": runtime.GOOS, "device.hostname": "gce-1234-us-west-1", } { t.Run(prop, func(st *testing.T) { if got := getString(root, prop); got != exp { t.Errorf("Expected property '%s' in JSON to be '%v' but was '%v'", prop, exp, got) } }) } sessionCounts := getIndex(root, "sessionCounts", 0) if got, exp := getString(sessionCounts, "startedAt"), earliestTime; got != exp { t.Errorf("Expected sessionCounts[0].startedAt to be '%s' but was '%s'", exp, got) } if got, exp := getInt(sessionCounts, "sessionsStarted"), len(sessions); got != exp { t.Errorf("Expected sessionCounts[0].sessionsStarted to be %d but was %d", exp, got) } } func TestNoSessionsSentWhenAPIKeyIsMissing(t *testing.T) { sessions, _ := makeSessions() config := makeHeavyConfig() config.APIKey = "labracadabrador" publisher := publisher{config: config, client: &testHTTPClient{}} if err := publisher.publish(sessions); err != nil { if got, exp := err.Error(), "bugsnag/sessions/publisher.publish invalid API key: 'labracadabrador'"; got != exp { t.Errorf(`Expected error message "%s" but got "%s"`, exp, got) } } else { t.Errorf("Expected error message but no errors were returned") } } func TestNoSessionsOutsideNotifyReleaseStages(t *testing.T) { sessions, _ := makeSessions() testClient := testHTTPClient{} config := makeHeavyConfig() config.NotifyReleaseStages = []string{"staging", "production"} publisher := publisher{ config: config, client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } if got := len(testClient.reqs); got != 0 { t.Errorf("Didn't expect any sessions being sent as as 'development' is outside of the notify release stages, but got %d sessions", got) } } func TestReleaseStageNotSetSendsSessionsRegardlessOfNotifyReleaseStages(t *testing.T) { sessions, _ := makeSessions() testClient := testHTTPClient{} config := makeHeavyConfig() config.NotifyReleaseStages = []string{"staging", "production"} config.ReleaseStage = "" publisher := publisher{ config: config, client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } if exp, got := 1, len(testClient.reqs); got != exp { t.Errorf("Expected %d sessions sent when the release stage is \"\" regardless of notify release stage, but got %d", exp, got) } } func makeHeavyConfig() *SessionTrackingConfiguration { return &SessionTrackingConfiguration{ AppType: "gin", APIKey: testAPIKey, AppVersion: "1.2.3-beta", Version: "2.3.4-alpha", Endpoint: sessionEndpoint, Transport: http.DefaultTransport, ReleaseStage: "development", Hostname: "gce-1234-us-west-1", NotifyReleaseStages: []string{"development"}, } } func makeSessions() ([]*Session, string) { earliestTime := time.Now().Add(-6 * time.Minute) return []*Session{ {StartedAt: earliestTime, ID: uuid.New()}, {StartedAt: earliestTime.Add(2 * time.Minute), ID: uuid.New()}, {StartedAt: earliestTime.Add(4 * time.Minute), ID: uuid.New()}, }, earliestTime.UTC().Format(time.RFC3339) } func assertCorrectHeaders(t *testing.T, req *http.Request) { testCases := []struct{ name, expected string }{ {name: "Bugsnag-Payload-Version", expected: "1.0"}, {name: "Content-Type", expected: "application/json"}, {name: "Bugsnag-Api-Key", expected: testAPIKey}, } for _, tc := range testCases { t.Run(tc.name, func(st *testing.T) { if got := req.Header[tc.name][0]; tc.expected != got { t.Errorf("Expected header '%s' to be '%s' but was '%s'", tc.name, tc.expected, got) } }) } name := "Bugsnag-Sent-At" if req.Header[name][0] == "" { t.Errorf("Expected header '%s' to be non-empty but was empty", name) } } bugsnag-go-2.2.0/sessions/session.go000066400000000000000000000010731432154711400174270ustar00rootroot00000000000000package sessions import ( "time" uuid "github.com/google/uuid" ) // EventCounts register how many handled/unhandled events have happened for // this session type EventCounts struct { Handled int `json:"handled"` Unhandled int `json:"unhandled"` } // Session represents a start time and a unique ID that identifies the session. type Session struct { StartedAt time.Time ID uuid.UUID EventCounts *EventCounts } func newSession() *Session { return &Session{ StartedAt: time.Now(), ID: uuid.New(), EventCounts: &EventCounts{}, } } bugsnag-go-2.2.0/sessions/startup.go000066400000000000000000000020561432154711400174500ustar00rootroot00000000000000package sessions import ( "context" "net/http" "os" "github.com/bugsnag/panicwrap" ) // SendStartupSession is called by Bugsnag on startup, which will send a // session to Bugsnag and return a context to represent the session of the main // goroutine. This is the session associated with any fatal panics that are // caught by panicwrap. func SendStartupSession(config *SessionTrackingConfiguration) context.Context { ctx := context.Background() session := newSession() if !config.IsAutoCaptureSessions() || isApplicationProcess() { return ctx } publisher := &publisher{ config: config, client: &http.Client{Transport: config.Transport}, } go publisher.publish([]*Session{session}) return context.WithValue(ctx, contextSessionKey, session) } // Checks to see if this is the application process, as opposed to the process // that monitors for panics func isApplicationProcess() bool { // Application process is run first, and this will only have been set when // the monitoring process runs return "" == os.Getenv(panicwrap.DEFAULT_COOKIE_KEY) } bugsnag-go-2.2.0/sessions/tracker.go000066400000000000000000000072501432154711400174020ustar00rootroot00000000000000package sessions import ( "context" "net/http" "os" "os/signal" "sync" "syscall" "time" ) const ( //contextSessionKey is a unique key for accessing and setting Bugsnag //session data on a context.Context object contextSessionKey ctxKey = 1 ) // ctxKey is a type alias that ensures uniqueness as a context.Context key type ctxKey int // SessionTracker exposes a method for starting sessions that are used for // gauging your application's health type SessionTracker interface { StartSession(context.Context) context.Context FlushSessions() } type sessionTracker struct { sessionChannel chan *Session sessions []*Session config *SessionTrackingConfiguration publisher sessionPublisher sessionsMutex sync.Mutex } // NewSessionTracker creates a new SessionTracker based on the provided config, func NewSessionTracker(config *SessionTrackingConfiguration) SessionTracker { publisher := publisher{ config: config, client: &http.Client{Transport: config.Transport}, } st := sessionTracker{ sessionChannel: make(chan *Session, 1), sessions: []*Session{}, config: config, publisher: &publisher, } go st.processSessions() return &st } // IncrementEventCountAndGetSession extracts a Bugsnag session from the given // context and increments the event count of unhandled or handled events and // returns the session func IncrementEventCountAndGetSession(ctx context.Context, unhandled bool) *Session { if s := ctx.Value(contextSessionKey); s != nil { if session, ok := s.(*Session); ok && !session.StartedAt.IsZero() { // It is not just getting back a default value ec := session.EventCounts if unhandled { ec.Unhandled++ } else { ec.Handled++ } return session } } return nil } func (s *sessionTracker) StartSession(ctx context.Context) context.Context { session := newSession() s.sessionChannel <- session return context.WithValue(ctx, contextSessionKey, session) } func (s *sessionTracker) interval() time.Duration { s.config.mutex.Lock() defer s.config.mutex.Unlock() return s.config.PublishInterval } func (s *sessionTracker) processSessions() { tic := time.Tick(s.interval()) shutdown := shutdownSignals() for { select { case session := <-s.sessionChannel: s.appendSession(session) case <-tic: s.publishCollectedSessions() case sig := <-shutdown: s.flushSessionsAndRepeatSignal(shutdown, sig.(syscall.Signal)) } } } func (s *sessionTracker) appendSession(session *Session) { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() s.sessions = append(s.sessions, session) } func (s *sessionTracker) publishCollectedSessions() { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() oldSessions := s.sessions s.sessions = nil if len(oldSessions) > 0 { go func(s *sessionTracker) { err := s.publisher.publish(oldSessions) if err != nil { s.config.logf("%v", err) } }(s) } } func (s *sessionTracker) flushSessionsAndRepeatSignal(shutdown chan<- os.Signal, sig syscall.Signal) { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() signal.Stop(shutdown) if len(s.sessions) > 0 { err := s.publisher.publish(s.sessions) if err != nil { s.config.logf("%v", err) } } if p, err := os.FindProcess(os.Getpid()); err != nil { s.config.logf("%v", err) } else { p.Signal(sig) } } func (s *sessionTracker) FlushSessions() { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() sessions := s.sessions s.sessions = nil if len(sessions) != 0 { if err := s.publisher.publish(sessions); err != nil { s.config.logf("%v", err) } } } func shutdownSignals() chan os.Signal { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) return c } bugsnag-go-2.2.0/sessions/tracker_test.go000066400000000000000000000045151432154711400204420ustar00rootroot00000000000000package sessions import ( "context" "sync" "testing" "time" ) type testPublisher struct { mutex sync.Mutex sessionsReceived [][]*Session } var pub = testPublisher{ mutex: sync.Mutex{}, sessionsReceived: [][]*Session{}, } func (pub *testPublisher) publish(sessions []*Session) error { pub.mutex.Lock() defer pub.mutex.Unlock() pub.sessionsReceived = append(pub.sessionsReceived, sessions) return nil } func TestStartSessionModifiesContext(t *testing.T) { type ctxKey string var k ctxKey k, v := "key", "val" st, c := makeSessionTracker() defer close(c) ctx := st.StartSession(context.WithValue(context.Background(), k, v)) if got, exp := ctx.Value(k), v; got != exp { t.Errorf("Changed pre-existing key '%s' with value '%s' into %s", k, v, got) } if got := ctx.Value(contextSessionKey); got == nil { t.Fatalf("No session information applied to context %v", ctx) } verifyValidSession(t, IncrementEventCountAndGetSession(ctx, true)) } func TestShouldOnlyWriteWhenReceivingSessions(t *testing.T) { st, c := makeSessionTracker() defer close(c) go st.processSessions() time.Sleep(10 * st.config.PublishInterval) // Would publish many times in this time period if there were sessions if got := pub.sessionsReceived; len(got) != 0 { t.Errorf("pub was invoked unexpectedly %d times with arguments: %v", len(got), got) } for i := 0; i < 50000; i++ { st.StartSession(context.Background()) } time.Sleep(st.config.PublishInterval * 2) var sessions []*Session pub.mutex.Lock() defer pub.mutex.Unlock() for _, s := range pub.sessionsReceived { for _, session := range s { verifyValidSession(t, session) sessions = append(sessions, session) } } if exp, got := 50000, len(sessions); exp != got { t.Errorf("Expected %d sessions but got %d", exp, got) } } func makeSessionTracker() (*sessionTracker, chan *Session) { c := make(chan *Session, 1) return &sessionTracker{ config: &SessionTrackingConfiguration{ PublishInterval: time.Millisecond * 10, //Publish very fast }, sessionChannel: c, sessions: []*Session{}, publisher: &pub, }, c } func verifyValidSession(t *testing.T, s *Session) { if (s.StartedAt == time.Time{}) { t.Errorf("Expected start time to be set but was nil") } if len(s.ID) != 16 { t.Errorf("Expected UUID to be a valid V4 UUID but was %s", s.ID) } } bugsnag-go-2.2.0/testutil/000077500000000000000000000000001432154711400154235ustar00rootroot00000000000000bugsnag-go-2.2.0/testutil/json.go000066400000000000000000000100641432154711400167240ustar00rootroot00000000000000// Package testutil can be .-imported to gain access to useful test functions. package testutil import ( "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" "time" simplejson "github.com/bitly/go-simplejson" ) // TestAPIKey is a fake API key that can be used for testing const TestAPIKey = "166f5ad3590596f9aa8d601ea89af845" // Setup sets up and returns a test event server for receiving the event payloads. // report payloads published to the returned server's URL will be put on the returned channel func Setup() (*httptest.Server, chan []byte) { reports := make(chan []byte, 10) return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "sessions") { return } body, _ := ioutil.ReadAll(r.Body) reports <- body })), reports } // Get travels through a JSON object and returns the specified node func Get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } // GetIndex returns the n-th element of the specified path inside the given JSON object func GetIndex(j *simplejson.Json, path string, n int) *simplejson.Json { return Get(j, path).GetIndex(n) } func getBool(j *simplejson.Json, path string) bool { return Get(j, path).MustBool() } func getInt(j *simplejson.Json, path string) int { return Get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return Get(j, path).MustString() } func getFirstString(j *simplejson.Json, path string) string { return GetIndex(j, path, 0).MustString() } // AssertPayload compares the payload that was received by the event-server to // the expected report JSON payload func AssertPayload(t *testing.T, report *simplejson.Json, expPretty string) { expReport, err := simplejson.NewJson([]byte(expPretty)) if err != nil { t.Fatal(err) } expEvent := GetIndex(expReport, "events", 0) expException := GetIndex(expEvent, "exceptions", 0) event := GetIndex(report, "events", 0) exception := GetIndex(event, "exceptions", 0) if exp, got := getBool(expEvent, "unhandled"), getBool(event, "unhandled"); got != exp { t.Errorf("expected 'unhandled' to be '%v' but got '%v'", exp, got) } for _, tc := range []struct { prop string got, exp *simplejson.Json }{ {got: report, exp: expReport, prop: "apiKey"}, {got: report, exp: expReport, prop: "notifier.name"}, {got: report, exp: expReport, prop: "notifier.version"}, {got: report, exp: expReport, prop: "notifier.url"}, {got: exception, exp: expException, prop: "message"}, {got: exception, exp: expException, prop: "errorClass"}, {got: event, exp: expEvent, prop: "user.id"}, {got: event, exp: expEvent, prop: "severity"}, {got: event, exp: expEvent, prop: "severityReason.type"}, {got: event, exp: expEvent, prop: "metaData.request.httpMethod"}, {got: event, exp: expEvent, prop: "metaData.request.url"}, {got: event, exp: expEvent, prop: "request.httpMethod"}, {got: event, exp: expEvent, prop: "request.url"}, {got: event, exp: expEvent, prop: "request.referer"}, {got: event, exp: expEvent, prop: "request.headers.Accept-Encoding"}, } { if got, exp := getString(tc.got, tc.prop), getString(tc.exp, tc.prop); got != exp { t.Errorf("expected '%s' to be '%s' but was '%s'", tc.prop, exp, got) } } assertValidSession(t, event, getBool(expEvent, "unhandled")) } func assertValidSession(t *testing.T, event *simplejson.Json, unhandled bool) { if sessionID := getString(event, "session.id"); len(sessionID) != 36 { t.Errorf("Expected a valid session ID to be set but was '%s'", sessionID) } if _, e := time.Parse(time.RFC3339, getString(event, "session.startedAt")); e != nil { t.Error(e) } expHandled, expUnhandled := 1, 0 if unhandled { expHandled, expUnhandled = expUnhandled, expHandled } if got := getInt(event, "session.events.unhandled"); got != expUnhandled { t.Errorf("Expected %d unhandled events in session but was %d", expUnhandled, got) } if got := getInt(event, "session.events.handled"); got != expHandled { t.Errorf("Expected %d handled events in session but was %d", expHandled, got) } } bugsnag-go-2.2.0/v2/000077500000000000000000000000001432154711400140755ustar00rootroot00000000000000bugsnag-go-2.2.0/v2/LICENSE.txt000066400000000000000000000020331432154711400157160ustar00rootroot00000000000000Copyright (c) 2014 Bugsnag Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bugsnag-go-2.2.0/v2/bugsnag.go000066400000000000000000000241461432154711400160610ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" "log" "net/http" "os" "path/filepath" "runtime" "sync" "time" "github.com/bugsnag/bugsnag-go/v2/device" "github.com/bugsnag/bugsnag-go/v2/errors" "github.com/bugsnag/bugsnag-go/v2/sessions" // Fixes a bug with SHA-384 intermediate certs on some platforms. // - https://github.com/bugsnag/bugsnag-go/issues/9 _ "crypto/sha512" ) // Version defines the version of this Bugsnag notifier const Version = "2.2.0" var panicHandlerOnce sync.Once var sessionTrackerOnce sync.Once var readEnvConfigOnce sync.Once var middleware middlewareStack // Config is the configuration for the default bugsnag notifier. var Config Configuration var sessionTrackingConfig sessions.SessionTrackingConfiguration // DefaultSessionPublishInterval defines how often sessions should be sent to // Bugsnag. // Deprecated: Exposed for developer sanity in testing. Modify at own risk. var DefaultSessionPublishInterval = 60 * time.Second var defaultNotifier = Notifier{&Config, nil} var sessionTracker sessions.SessionTracker // Configure Bugsnag. The only required setting is the APIKey, which can be // obtained by clicking on "Settings" in your Bugsnag dashboard. This function // is also responsible for installing the global panic handler, so it should be // called as early as possible in your initialization process. func Configure(config Configuration) { // Load configuration from the environment, if any readEnvConfigOnce.Do(Config.loadEnv) Config.update(&config) updateSessionConfig() // Only do once in case the user overrides the default panichandler, and // configures multiple times. panicHandlerOnce.Do(Config.PanicHandler) } // StartSession creates new context from the context.Context instance with // Bugsnag session data attached. Will start the session tracker if not already // started func StartSession(ctx context.Context) context.Context { sessionTrackerOnce.Do(startSessionTracking) return sessionTracker.StartSession(ctx) } // Notify sends an error.Error to Bugsnag along with the current stack trace. // If at all possible, it is recommended to pass in a context.Context, e.g. // from a http.Request or bugsnag.StartSession() as Bugsnag will be able to // extract additional information in some cases. The rawData is used to send // extra information along with the error. For example you can pass the current // http.Request to Bugsnag to see information about it in the dashboard, or set // the severity of the notification. For a detailed list of the information // that can be extracted, see // https://docs.bugsnag.com/platforms/go/reporting-handled-errors/ func Notify(err error, rawData ...interface{}) error { if e := checkForEmptyError(err); e != nil { return e } // Stripping one stackframe to not include this function in the stacktrace // for a manual notification. skipFrames := 1 return defaultNotifier.Notify(errors.New(err, skipFrames), rawData...) } // AutoNotify logs a panic on a goroutine and then repanics. // It should only be used in places that have existing panic handlers further // up the stack. // Although it's not strictly enforced, it's highly recommended to pass a // context.Context object that has at one-point been returned from // bugsnag.StartSession. Doing so ensures your stability score remains accurate, // and future versions of Bugsnag may extract more useful information from this // context. // The rawData is used to send extra information along with any // panics that are handled this way. // Usage: // go func() { // ctx := bugsnag.StartSession(context.Background()) // defer bugsnag.AutoNotify(ctx) // // (possibly crashy code) // }() // See also: bugsnag.Recover() func AutoNotify(rawData ...interface{}) { if err := recover(); err != nil { severity := defaultNotifier.getDefaultSeverity(rawData, SeverityError) state := HandledState{SeverityReasonHandledPanic, severity, true, ""} rawData = append([]interface{}{state}, rawData...) // We strip the following stackframes as they don't add much info // - runtime/$arch - e.g. runtime/asm_amd64.s#call32 // - runtime/panic.go#gopanic // Panics have their own stacktrace, so no stripping of the current stack skipFrames := 2 defaultNotifier.NotifySync(errors.New(err, skipFrames), true, rawData...) sessionTracker.FlushSessions() panic(err) } } // Recover logs a panic on a goroutine and then recovers. // Although it's not strictly enforced, it's highly recommended to pass a // context.Context object that has at one-point been returned from // bugsnag.StartSession. Doing so ensures your stability score remains accurate, // and future versions of Bugsnag may extract more useful information from this // context. // The rawData is used to send extra information along with // any panics that are handled this way // Usage: // go func() { // ctx := bugsnag.StartSession(context.Background()) // defer bugsnag.Recover(ctx) // // (possibly crashy code) // }() // If you wish that any panics caught by the call to Recover shall affect your // stability score (it does not by default): // go func() { // ctx := bugsnag.StartSession(context.Background()) // defer bugsnag.Recover(ctx, bugsnag.HandledState{Unhandled: true}) // // (possibly crashy code) // }() // See also: bugsnag.AutoNotify() func Recover(rawData ...interface{}) { if err := recover(); err != nil { severity := defaultNotifier.getDefaultSeverity(rawData, SeverityWarning) state := HandledState{SeverityReasonHandledPanic, severity, false, ""} rawData = append([]interface{}{state}, rawData...) // We strip the following stackframes as they don't add much info // - runtime/$arch - e.g. runtime/asm_amd64.s#call32 // - runtime/panic.go#gopanic // Panics have their own stacktrace, so no stripping of the current stack skipFrames := 2 defaultNotifier.Notify(errors.New(err, skipFrames), rawData...) } } // OnBeforeNotify adds a callback to be run before a notification is sent to // Bugsnag. It can be used to modify the event or its MetaData. Changes made // to the configuration are local to notifying about this event. To prevent the // event from being sent to Bugsnag return an error, this error will be // returned from bugsnag.Notify() and the event will not be sent. func OnBeforeNotify(callback func(event *Event, config *Configuration) error) { middleware.OnBeforeNotify(callback) } // Handler creates an http Handler that notifies Bugsnag any panics that // happen. It then repanics so that the default http Server panic handler can // handle the panic too. The rawData is used to send extra information along // with any panics that are handled this way. func Handler(h http.Handler, rawData ...interface{}) http.Handler { notifier := New(rawData...) if h == nil { h = http.DefaultServeMux } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { request := r // Record a session if auto notify session is enabled ctx := r.Context() if Config.IsAutoCaptureSessions() { ctx = StartSession(ctx) } ctx = AttachRequestData(ctx, request) request = r.WithContext(ctx) defer notifier.AutoNotify(ctx, request) h.ServeHTTP(w, request) }) } // HandlerFunc creates an http HandlerFunc that notifies Bugsnag about any // panics that happen. It then repanics so that the default http Server panic // handler can handle the panic too. The rawData is used to send extra // information along with any panics that are handled this way. If you have // already wrapped your http server using bugsnag.Handler() you don't also need // to wrap each HandlerFunc. func HandlerFunc(h http.HandlerFunc, rawData ...interface{}) http.HandlerFunc { notifier := New(rawData...) return func(w http.ResponseWriter, r *http.Request) { request := r // Record a session if auto notify session is enabled ctx := request.Context() if notifier.Config.IsAutoCaptureSessions() { ctx = StartSession(ctx) } ctx = AttachRequestData(ctx, request) request = request.WithContext(ctx) defer notifier.AutoNotify(ctx) h(w, request) } } // checkForEmptyError checks if the given error (to be reported to Bugsnag) is // nil. If it is, then log an error message and return another error wrapping // this error message. func checkForEmptyError(err error) error { if err != nil { return nil } msg := "attempted to notify Bugsnag without supplying an error. Bugsnag not notified" Config.Logger.Printf("ERROR: " + msg) return fmt.Errorf(msg) } func init() { // Set up builtin middlewarez OnBeforeNotify(httpRequestMiddleware) // Default configuration sourceRoot := "" if gopath := os.Getenv("GOPATH"); len(gopath) > 0 { sourceRoot = filepath.Join(gopath, "src") + "/" } else { sourceRoot = filepath.Join(runtime.GOROOT(), "src") + "/" } Config.update(&Configuration{ APIKey: "", Endpoints: Endpoints{ Notify: "https://notify.bugsnag.com", Sessions: "https://sessions.bugsnag.com", }, Hostname: device.GetHostname(), AppType: "", AppVersion: "", AutoCaptureSessions: true, ReleaseStage: "", ParamsFilters: []string{"password", "secret", "authorization", "cookie", "access_token"}, SourceRoot: sourceRoot, ProjectPackages: []string{"main*"}, NotifyReleaseStages: nil, Logger: log.New(os.Stdout, log.Prefix(), log.Flags()), PanicHandler: defaultPanicHandler, Transport: http.DefaultTransport, flushSessionsOnRepanic: true, }) updateSessionConfig() } func startSessionTracking() { if sessionTracker == nil { updateSessionConfig() sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig) } } func updateSessionConfig() { sessionTrackingConfig.Update(&sessions.SessionTrackingConfiguration{ APIKey: Config.APIKey, AutoCaptureSessions: Config.AutoCaptureSessions, Endpoint: Config.Endpoints.Sessions, Version: Version, PublishInterval: DefaultSessionPublishInterval, Transport: Config.Transport, ReleaseStage: Config.ReleaseStage, Hostname: Config.Hostname, AppType: Config.AppType, AppVersion: Config.AppVersion, NotifyReleaseStages: Config.NotifyReleaseStages, Logger: Config.Logger, }) } bugsnag-go-2.2.0/v2/bugsnag_example_test.go000066400000000000000000000075131432154711400206320ustar00rootroot00000000000000package bugsnag_test import ( "context" "fmt" "net" "net/http" "time" "github.com/bugsnag/bugsnag-go/v2" ) var exampleAPIKey = "166f5ad3590596f9aa8d601ea89af845" func ExampleAutoNotify() { bugsnag.Configure(bugsnag.Configuration{APIKey: exampleAPIKey}) createAccount := func(ctx context.Context) { fmt.Println("Creating account and passing context around...") } ctx := bugsnag.StartSession(context.Background()) defer bugsnag.AutoNotify(ctx) createAccount(ctx) // Output: // Creating account and passing context around... } func ExampleRecover() { bugsnag.Configure(bugsnag.Configuration{APIKey: exampleAPIKey}) panicFunc := func() { fmt.Println("About to panic") panic("Oh noes") } // Will recover when panicFunc panics func() { ctx := bugsnag.StartSession(context.Background()) defer bugsnag.Recover(ctx) panicFunc() }() fmt.Println("Panic recovered") // Output: About to panic // Panic recovered } func ExampleConfigure() { bugsnag.Configure(bugsnag.Configuration{ APIKey: "YOUR_API_KEY_HERE", ReleaseStage: "production", // See bugsnag.Configuration for other fields }) } func ExampleHandler() { handleReq := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Handling HTTP request") } // Set up your http handlers as usual http.HandleFunc("/", handleReq) // use bugsnag.Handler(nil) to wrap the default http handlers // so that Bugsnag is automatically notified about panics. http.ListenAndServe(":1234", bugsnag.Handler(nil)) } func ExampleHandler_customServer() { handleReq := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Handling GET") } // If you're using a custom server, set the handlers explicitly. http.HandleFunc("/", handleReq) srv := http.Server{ Addr: ":1234", ReadTimeout: 10 * time.Second, // use bugsnag.Handler(nil) to wrap the default http handlers // so that Bugsnag is automatically notified about panics. Handler: bugsnag.Handler(nil), } srv.ListenAndServe() } func ExampleHandler_customHandlers() { handleReq := func(w http.ResponseWriter, r *http.Request) { fmt.Println("Handling GET") } // If you're using custom handlers, wrap the handlers explicitly. handler := http.NewServeMux() http.HandleFunc("/", handleReq) // use bugsnag.Handler(handler) to wrap the handlers so that Bugsnag is // automatically notified about panics http.ListenAndServe(":1234", bugsnag.Handler(handler)) } func ExampleNotify() { ctx := context.Background() ctx = bugsnag.StartSession(ctx) _, err := net.Listen("tcp", ":80") if err != nil { bugsnag.Notify(err, ctx) } } func ExampleNotify_details() { ctx := context.Background() ctx = bugsnag.StartSession(ctx) _, err := net.Listen("tcp", ":80") if err != nil { bugsnag.Notify(err, ctx, // show as low-severity bugsnag.SeverityInfo, // set the context bugsnag.Context{String: "createlistener"}, // pass the user id in to count users affected. bugsnag.User{Id: "123456789"}, // custom meta-data tab bugsnag.MetaData{ "Listen": { "Protocol": "tcp", "Port": "80", }, }, ) } } func ExampleOnBeforeNotify() { type Job struct { Retry bool UserID string UserEmail string } bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { // Search all the RawData for any *Job pointers that we're passed in // to bugsnag.Notify() and friends. for _, datum := range event.RawData { if job, ok := datum.(*Job); ok { // don't notify bugsnag about errors in retries if job.Retry { return fmt.Errorf("bugsnag middleware: not notifying about job retry") } // add the job as a tab on Bugsnag.com event.MetaData.AddStruct("Job", job) // set the user correctly event.User = &bugsnag.User{Id: job.UserID, Email: job.UserEmail} } } // continue notifying as normal return nil }) } bugsnag-go-2.2.0/v2/bugsnag_test.go000066400000000000000000000504071432154711400171170ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" "io/ioutil" "log" "net" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go/v2/sessions" ) // The line numbers of this method are used in tests. // If you move this function you'll have to change tests func crashyHandler(w http.ResponseWriter, r *http.Request) { c := make(chan int) close(c) c <- 1 } type _recurse struct { Recurse *_recurse } const ( unhandled = true handled = false ) var testAPIKey = "166f5ad3590596f9aa8d601ea89af845" type logger struct{ msg string } func (l *logger) Printf(format string, v ...interface{}) { l.msg = format } // setup sets up a simple sessionTracker and returns a test event server for receiving the event payloads. // report payloads published to the returned server's URL will be put on the returned channel func setup() (*httptest.Server, chan []byte) { reports := make(chan []byte, 10) sessionTracker = &testSessionTracker{} return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) reports <- body })), reports } type testSessionTracker struct{} func (t *testSessionTracker) StartSession(context.Context) context.Context { return context.Background() } func (t *testSessionTracker) IncrementEventCountAndGetSession(context.Context, bool) *sessions.Session { return &sessions.Session{} } func (t *testSessionTracker) FlushSessions() {} func TestConfigure(t *testing.T) { Configure(Configuration{ APIKey: testAPIKey, }) if Config.APIKey != testAPIKey { t.Errorf("Setting APIKey didn't work") } if New().Config.APIKey != testAPIKey { t.Errorf("Setting APIKey didn't work for new notifiers") } } func TestNotify(t *testing.T) { ts, reports := setup() defer ts.Close() sessionTracker = nil startSessionTracking() recurse := _recurse{} recurse.Recurse = &recurse OnBeforeNotify(func(event *Event, config *Configuration) error { if event.Context == "testing" { event.GroupingHash = "lol" } return nil }) md := MetaData{"test": {"password": "sneaky", "value": "able", "broken": complex(1, 2), "recurse": recurse}} user := User{Id: "123", Name: "Conrad", Email: "me@cirw.in"} config := generateSampleConfig(ts.URL) Notify(fmt.Errorf("hello world"), StartSession(context.Background()), config, user, ErrorClass{Name: "ExpectedErrorClass"}, Context{"testing"}, md) json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } event := getIndex(json, "events", 0) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "testing", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "lol", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "warning", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledError}, Unhandled: false, Request: &RequestJSON{}, User: &User{Id: "123", Name: "Conrad", Email: "me@cirw.in"}, Exceptions: []exceptionJSON{{ErrorClass: "ExpectedErrorClass", Message: "hello world"}}, }) assertValidSession(t, event, handled) for k, exp := range map[string]string{ "metaData.test.password": "[FILTERED]", "metaData.test.value": "able", "metaData.test.broken": "[complex128]", "metaData.test.recurse.Recurse": "[RECURSION]", } { if got := getString(event, k); got != exp { t.Errorf("Expected %s to be '%s' but was '%s'", k, exp, got) } } exception := getIndex(event, "exceptions", 0) verifyExistsInStackTrace(t, exception, &StackFrame{File: "bugsnag_test.go", Method: "TestNotify", LineNumber: 98, InProject: true}) } type testPublisher struct { sync bool } func (tp *testPublisher) publishReport(p *payload) error { tp.sync = p.Synchronous return nil } func TestNotifySyncThenAsync(t *testing.T) { ts, _ := setup() defer ts.Close() Configure(generateSampleConfig(ts.URL)) //async by default pub := new(testPublisher) publisher = pub defer func() { publisher = new(defaultReportPublisher) }() Notify(fmt.Errorf("oopsie")) if pub.sync { t.Errorf("Expected notify to be async by default") } defaultNotifier.NotifySync(fmt.Errorf("oopsie"), true) if !pub.sync { t.Errorf("Expected notify to be sent synchronously when calling NotifySync with true") } Notify(fmt.Errorf("oopsie")) if pub.sync { t.Errorf("Expected notify to be sent asynchronously when calling Notify regardless of previous NotifySync call") } } func TestHandlerFunc(t *testing.T) { eventserver, reports := setup() defer eventserver.Close() Configure(generateSampleConfig(eventserver.URL)) t.Run("unhandled", func(st *testing.T) { sessionTracker = nil startSessionTracking() ts := httptest.NewServer(HandlerFunc(crashyHandler)) defer ts.Close() http.Get(ts.URL + "/unhandled") json, _ := simplejson.NewJson(<-reports) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "/unhandled", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, Request: &RequestJSON{ Headers: map[string]string{"Accept-Encoding": "gzip"}, HTTPMethod: "GET", URL: ts.URL + "/unhandled", }, User: &User{Id: "127.0.0.1", Name: "", Email: ""}, Exceptions: []exceptionJSON{{ErrorClass: "runtime.plainError", Message: "send on closed channel"}}, }) event := getIndex(json, "events", 0) if got, exp := getString(event, "request.headers.Accept-Encoding"), "gzip"; got != exp { st.Errorf("expected Accept-Encoding header to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.httpMethod"), "GET"; got != exp { st.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.url"), "/unhandled"; !strings.Contains(got, exp) { st.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) } assertValidSession(st, event, unhandled) }) t.Run("handled", func(st *testing.T) { ts := httptest.NewServer(HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Notify(fmt.Errorf("oopsie"), r.Context()) })) defer ts.Close() http.Get(ts.URL + "/handled") json, _ := simplejson.NewJson(<-reports) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "/handled", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 1, Unhandled: 0}}, Severity: "warning", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledError}, Unhandled: false, Request: &RequestJSON{ Headers: map[string]string{"Accept-Encoding": "gzip"}, HTTPMethod: "GET", URL: ts.URL + "/handled", }, User: &User{Id: "127.0.0.1", Name: "", Email: ""}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "oopsie"}}, }) event := getIndex(json, "events", 0) if got, exp := getString(event, "request.headers.Accept-Encoding"), "gzip"; got != exp { st.Errorf("expected Accept-Encoding header to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.httpMethod"), "GET"; got != exp { st.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.url"), "/handled"; !strings.Contains(got, exp) { st.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) } assertValidSession(st, event, handled) }) } func TestHandler(t *testing.T) { ts, reports := setup() defer ts.Close() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer l.Close() mux := http.NewServeMux() mux.HandleFunc("/", crashyHandler) go (&http.Server{ Addr: l.Addr().String(), Handler: Handler(mux, generateSampleConfig(ts.URL), SeverityInfo), ErrorLog: log.New(ioutil.Discard, log.Prefix(), 0), }).Serve(l) sessionTracker = nil startSessionTracking() http.Get("http://" + l.Addr().String() + "/ok?foo=bar") json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "/ok", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "info", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, User: &User{Id: "127.0.0.1", Name: "", Email: ""}, Request: &RequestJSON{ Headers: map[string]string{"Accept-Encoding": "gzip"}, HTTPMethod: "GET", URL: "http://" + l.Addr().String() + "/ok?foo=bar", }, Exceptions: []exceptionJSON{{ErrorClass: "runtime.plainError", Message: "send on closed channel"}}, }) event := getIndex(json, "events", 0) if got, exp := getString(event, "request.headers.Accept-Encoding"), "gzip"; got != exp { t.Errorf("expected Accept-Encoding header to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.httpMethod"), "GET"; got != exp { t.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := getString(event, "request.url"), "/ok?foo=bar"; !strings.Contains(got, exp) { t.Errorf("expected request URL to be '%s' but was '%s'", exp, got) } assertValidSession(t, event, unhandled) if got, exp := getFirstString(event, "metaData.request.params.foo"), "bar"; got != exp { t.Errorf("Expected metadata params 'foo' to be '%s' but was '%s'", exp, got) } exception := getIndex(event, "exceptions", 0) verifyExistsInStackTrace(t, exception, &StackFrame{File: "bugsnag_test.go", Method: "crashyHandler", InProject: true, LineNumber: 24}) } func TestAutoNotify(t *testing.T) { ts, reports := setup() defer ts.Close() var panicked error func() { defer func() { p := recover() switch p.(type) { case error: panicked = p.(error) default: t.Fatalf("Unexpected panic happened. Expected 'eggs' Error but was a(n) <%T> with value <%+v>", p, p) } }() defer AutoNotify(StartSession(context.Background()), generateSampleConfig(ts.URL)) panic(fmt.Errorf("eggs")) }() if panicked.Error() != "eggs" { t.Errorf("didn't re-panic") } json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, User: &User{}, Request: &RequestJSON{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "eggs"}}, }) } func TestRecover(t *testing.T) { ts, reports := setup() defer ts.Close() var panicked interface{} func() { defer func() { panicked = recover() }() defer Recover(StartSession(context.Background()), generateSampleConfig(ts.URL)) panic("ham") }() if panicked != nil { t.Errorf("Did not expect a panic but repanicked") } json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "warning", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: false, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "ham"}}, }) } func TestRecoverCustomHandledState(t *testing.T) { ts, reports := setup() defer ts.Close() var panicked interface{} func() { defer func() { panicked = recover() }() handledState := HandledState{ SeverityReason: SeverityReasonHandledPanic, OriginalSeverity: SeverityError, Unhandled: true, } defer Recover(handledState, StartSession(context.Background()), generateSampleConfig(ts.URL)) panic("at the disco?") }() if panicked != nil { t.Errorf("Did not expect a panic but repanicked") } json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "at the disco?"}}, }) } func TestSeverityReasonNotifyCallback(t *testing.T) { ts, reports := setup() defer ts.Close() OnBeforeNotify(func(event *Event, config *Configuration) error { event.Severity = SeverityInfo return nil }) Notify(fmt.Errorf("hello world"), generateSampleConfig(ts.URL), StartSession(context.Background())) json, _ := simplejson.NewJson(<-reports) assertPayload(t, json, eventJSON{ App: &appJSON{ReleaseStage: "test", Type: "foo", Version: "1.2.3"}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "info", SeverityReason: &severityReasonJSON{Type: SeverityReasonCallbackSpecified}, Unhandled: false, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "hello world"}}, }) } func TestNotifyWithoutError(t *testing.T) { ts, reports := setup() defer ts.Close() config := generateSampleConfig(ts.URL) config.Synchronous = true l := logger{} config.Logger = &l Configure(config) Notify(nil, StartSession(context.Background())) select { case r := <-reports: t.Fatalf("Unexpected request made to bugsnag: %+v", string(r)) default: for _, exp := range []string{"ERROR", "error", "Bugsnag", "not notified"} { if got := l.msg; !strings.Contains(got, exp) { t.Errorf("Expected to see '%s' in logged message but logged message was '%s'", exp, got) } } } } func TestConfigureTwice(t *testing.T) { Configure(Configuration{}) if !Config.IsAutoCaptureSessions() { t.Errorf("Expected auto capture sessions to be enabled by default") } Configure(Configuration{AutoCaptureSessions: false}) if Config.IsAutoCaptureSessions() { t.Errorf("Expected auto capture sessions to be disabled when configured") } Configure(Configuration{AutoCaptureSessions: true}) if !Config.IsAutoCaptureSessions() { t.Errorf("Expected auto capture sessions to be enabled when configured") } } func generateSampleConfig(endpoint string) Configuration { return Configuration{ APIKey: testAPIKey, Endpoints: Endpoints{Notify: endpoint}, ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"}, Logger: log.New(ioutil.Discard, log.Prefix(), log.Flags()), ReleaseStage: "test", AppType: "foo", AppVersion: "1.2.3", Hostname: "web1", } } func get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } func getBool(j *simplejson.Json, path string) bool { return get(j, path).MustBool() } func getInt(j *simplejson.Json, path string) int { return get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return get(j, path).MustString() } func getIndex(j *simplejson.Json, path string, index int) *simplejson.Json { return get(j, path).GetIndex(index) } func getFirstString(j *simplejson.Json, path string) string { return getIndex(j, path, 0).MustString() } // assertPayload compares the payload that was received by the event-server to // the expected report JSON payload func assertPayload(t *testing.T, report *simplejson.Json, exp eventJSON) { expException := exp.Exceptions[0] event := getIndex(report, "events", 0) exception := getIndex(event, "exceptions", 0) for _, tc := range []struct { prop string exp, got interface{} }{ {prop: "API Key", exp: testAPIKey, got: getString(report, "apiKey")}, {prop: "notifier name", exp: "Bugsnag Go", got: getString(report, "notifier.name")}, {prop: "notifier version", exp: Version, got: getString(report, "notifier.version")}, {prop: "notifier url", exp: "https://github.com/bugsnag/bugsnag-go", got: getString(report, "notifier.url")}, {prop: "exception message", exp: expException.Message, got: getString(exception, "message")}, {prop: "exception error class", exp: expException.ErrorClass, got: getString(exception, "errorClass")}, {prop: "unhandled", exp: exp.Unhandled, got: getBool(event, "unhandled")}, {prop: "app version", exp: exp.App.Version, got: getString(event, "app.version")}, {prop: "app release stage", exp: exp.App.ReleaseStage, got: getString(event, "app.releaseStage")}, {prop: "app type", exp: exp.App.Type, got: getString(event, "app.type")}, {prop: "user id", exp: exp.User.Id, got: getString(event, "user.id")}, {prop: "user name", exp: exp.User.Name, got: getString(event, "user.name")}, {prop: "user email", exp: exp.User.Email, got: getString(event, "user.email")}, {prop: "context", exp: exp.Context, got: getString(event, "context")}, {prop: "device hostname", exp: exp.Device.Hostname, got: getString(event, "device.hostname")}, {prop: "grouping hash", exp: exp.GroupingHash, got: getString(event, "groupingHash")}, {prop: "payload version", exp: "4", got: getString(event, "payloadVersion")}, {prop: "severity", exp: exp.Severity, got: getString(event, "severity")}, {prop: "severity reason type", exp: string(exp.SeverityReason.Type), got: getString(event, "severityReason.type")}, {prop: "request header 'Accept-Encoding'", exp: string(exp.Request.Headers["Accept-Encoding"]), got: getString(event, "request.headers.Accept-Encoding")}, {prop: "request HTTP method", exp: string(exp.Request.HTTPMethod), got: getString(event, "request.httpMethod")}, {prop: "request URL", exp: string(exp.Request.URL), got: getString(event, "request.url")}, } { if tc.got != tc.exp { t.Errorf("Wrong %s: expected '%v' but got '%v'", tc.prop, tc.exp, tc.got) } } } func assertValidSession(t *testing.T, event *simplejson.Json, unhandled bool) { if sessionID := getString(event, "session.id"); len(sessionID) != 36 { t.Errorf("Expected a valid session ID to be set but was '%s'", sessionID) } if _, e := time.Parse(time.RFC3339, getString(event, "session.startedAt")); e != nil { t.Error(e) } expHandled, expUnhandled := 1, 0 if unhandled { expHandled, expUnhandled = expUnhandled, expHandled } if got := getInt(event, "session.events.unhandled"); got != expUnhandled { t.Errorf("Expected %d unhandled events in session but was %d", expUnhandled, got) } if got := getInt(event, "session.events.handled"); got != expHandled { t.Errorf("Expected %d handled events in session but was %d", expHandled, got) } } func verifyExistsInStackTrace(t *testing.T, exception *simplejson.Json, exp *StackFrame) { isFile := func(frame *simplejson.Json) bool { return strings.HasSuffix(getString(frame, "file"), exp.File) } isMethod := func(frame *simplejson.Json) bool { return getString(frame, "method") == exp.Method } isLineNumber := func(frame *simplejson.Json) bool { return getInt(frame, "lineNumber") == exp.LineNumber } arr, _ := exception.Get("stacktrace").Array() for i := 0; i < len(arr); i++ { frame := getIndex(exception, "stacktrace", i) if isFile(frame) && isMethod(frame) && isLineNumber(frame) { return } } t.Errorf("Could not find expected stackframe %v in exception '%v'", exp, exception) } bugsnag-go-2.2.0/v2/configuration.go000066400000000000000000000265111432154711400173000ustar00rootroot00000000000000package bugsnag import ( "log" "net/http" "os" "path/filepath" "runtime" "strings" ) // Endpoints hold the HTTP endpoints of the notifier. type Endpoints struct { Sessions string Notify string } // Configuration sets up and customizes communication with the Bugsnag API. type Configuration struct { // Your Bugsnag API key, e.g. "c9d60ae4c7e70c4b6c4ebd3e8056d2b8". You can // find this by clicking Settings on https://bugsnag.com/. APIKey string // Endpoints define the HTTP endpoints that the notifier should notify // about crashes and sessions. These default to notify.bugsnag.com for // error reports and sessions.bugsnag.com for sessions. // If you are using bugsnag on-premise you will have to set these to your // Event Server and Session Server endpoints. If the notify endpoint is set // but the sessions endpoint is not, session tracking will be disabled // automatically to avoid leaking session information outside of your // server configuration, and a warning will be logged. Endpoints Endpoints // The current release stage. This defaults to "production" and is used to // filter errors in the Bugsnag dashboard. ReleaseStage string // A specialized type of the application, such as the worker queue or web // framework used, like "rails", "mailman", or "celery" AppType string // The currently running version of the app. This is used to filter errors // in the Bugsnag dasboard. If you set this then Bugsnag will only re-open // resolved errors if they happen in different app versions. AppVersion string // AutoCaptureSessions can be set to false to disable automatic session // tracking. If you want control over what is deemed a session, you can // switch off automatic session tracking with this configuration, and call // bugsnag.StartSession() when appropriate for your application. See the // official docs for instructions and examples of associating handled // errors with sessions and ensuring error rate accuracy on the Bugsnag // dashboard. This will default to true, but is stored as an interface to enable // us to detect when this option has not been set. AutoCaptureSessions interface{} // The hostname of the current server. This defaults to the return value of // os.Hostname() and is graphed in the Bugsnag dashboard. Hostname string // The Release stages to notify in. If you set this then bugsnag-go will // only send notifications to Bugsnag if the ReleaseStage is listed here. NotifyReleaseStages []string // packages that are part of your app. Bugsnag uses this to determine how // to group errors and how to display them on your dashboard. You should // include any packages that are part of your app, and exclude libraries // and helpers. You can list wildcards here, and they'll be expanded using // filepath.Glob. For matching subpackages within a package you may use the // `**` notation. The default value is []string{"main*"} ProjectPackages []string // The SourceRoot is the directory where the application is built, and the // assumed prefix of lines on the stacktrace originating in the parent // application. When set, the prefix is trimmed from callstack file names // before ProjectPackages for better readability and to better group errors // on the Bugsnag dashboard. The default value is $GOPATH/src or $GOROOT/src // if $GOPATH is unset. At runtime, $GOROOT is the root used during the Go // build. SourceRoot string // Any meta-data that matches these filters will be marked as [FILTERED] // before sending a Notification to Bugsnag. It defaults to // []string{"password", "secret"} so that request parameters like password, // password_confirmation and auth_secret will not be sent to Bugsnag. ParamsFilters []string // The PanicHandler is used by Bugsnag to catch unhandled panics in your // application. The default panicHandler uses mitchellh's panicwrap library, // and you can disable this feature by passing an empty: func() {} PanicHandler func() // The logger that Bugsnag should log to. Uses the same defaults as go's // builtin logging package. bugsnag-go logs whenever it notifies Bugsnag // of an error, and when any error occurs inside the library itself. Logger interface { Printf(format string, v ...interface{}) // limited to the functions used } // The http Transport to use, defaults to the default http Transport. This // can be configured if you are in an environment // that has stringent conditions on making http requests. Transport http.RoundTripper // Whether bugsnag should notify synchronously. This defaults to false which // causes bugsnag-go to spawn a new goroutine for each notification. Synchronous bool // Whether the notifier should send all sessions recorded so far to Bugsnag // when repanicking to ensure that no session information is lost in a // fatal crash. flushSessionsOnRepanic bool // TODO: remember to update the update() function when modifying this struct } func (config *Configuration) update(other *Configuration) *Configuration { if other.APIKey != "" { config.APIKey = other.APIKey } if other.Hostname != "" { config.Hostname = other.Hostname } if other.AppType != "" { config.AppType = other.AppType } if other.AppVersion != "" { config.AppVersion = other.AppVersion } if other.SourceRoot != "" { config.SourceRoot = other.SourceRoot // Use '/' as the separator as Go stacktraces are printed with '/' as // the separator regardless of os.PathSeparator. if runtime.GOOS == "windows" { config.SourceRoot = strings.Replace(config.SourceRoot, "\\", "/", -1) } } if other.ReleaseStage != "" { config.ReleaseStage = other.ReleaseStage } if other.ParamsFilters != nil { config.ParamsFilters = other.ParamsFilters } if other.ProjectPackages != nil { config.ProjectPackages = other.ProjectPackages // Use '/' as the separator as Go stacktraces are printed with '/' as // the separator regardless of os.PathSeparator. if runtime.GOOS == "windows" { for idx, pkg := range config.ProjectPackages { config.ProjectPackages[idx] = strings.Replace(pkg, "\\", "/", -1) } } } if other.Logger != nil { config.Logger = other.Logger } if other.NotifyReleaseStages != nil { config.NotifyReleaseStages = other.NotifyReleaseStages } if other.PanicHandler != nil { config.PanicHandler = other.PanicHandler } if other.Transport != nil { config.Transport = other.Transport } if other.Synchronous { config.Synchronous = true } if other.AutoCaptureSessions != nil { config.AutoCaptureSessions = other.AutoCaptureSessions } config.updateEndpoints(&other.Endpoints) return config } // IsAutoCaptureSessions identifies whether or not the notifier should // automatically capture sessions as requests come in. It's a convenience // wrapper that allows automatic session capturing to be enabled by default. func (config *Configuration) IsAutoCaptureSessions() bool { if config.AutoCaptureSessions == nil { return true // enabled by default } if val, ok := config.AutoCaptureSessions.(bool); ok { return val } // It has been configured to *something* (although not a valid value) // assume the user wanted to disable this option. return false } func (config *Configuration) updateEndpoints(endpoints *Endpoints) { if endpoints.Notify != "" { config.Endpoints.Notify = endpoints.Notify if endpoints.Sessions == "" { config.Logger.Printf("WARNING: Bugsnag notify endpoint configured without also configuring the sessions endpoint. No sessions will be recorded") config.Endpoints.Sessions = "" } } if endpoints.Sessions != "" { if endpoints.Notify == "" { panic("FATAL: Bugsnag sessions endpoint configured without also changing the notify endpoint. Bugsnag cannot identify where to report errors") } config.Endpoints.Sessions = endpoints.Sessions } } func (config *Configuration) merge(other *Configuration) *Configuration { return config.clone().update(other) } func (config *Configuration) clone() *Configuration { clone := *config return &clone } func (config *Configuration) isProjectPackage(_pkg string) bool { sep := string(filepath.Separator) // filepath functions only work if the contents of the paths use the system // file separator format := func(s string) string { return strings.Replace(s, "/", sep, -1) } pkg := format(_pkg) for _, _p := range config.ProjectPackages { p := format(_p) if d, f := filepath.Split(p); f == "**" { if strings.HasPrefix(pkg, d) { return true } } if match, _ := filepath.Match(p, pkg); match { return true } } return false } func (config *Configuration) stripProjectPackages(file string) string { trimmedFile := file if strings.HasPrefix(trimmedFile, config.SourceRoot) { trimmedFile = strings.TrimPrefix(trimmedFile, config.SourceRoot) } for _, p := range config.ProjectPackages { if len(p) > 2 && p[len(p)-2] == '/' && p[len(p)-1] == '*' { p = p[:len(p)-1] } else if p[len(p)-1] == '*' && p[len(p)-2] == '*' { p = p[:len(p)-2] } else { p = p + "/" } if strings.HasPrefix(trimmedFile, p) { return strings.TrimPrefix(trimmedFile, p) } } return trimmedFile } func (config *Configuration) logf(fmt string, args ...interface{}) { if config != nil && config.Logger != nil { config.Logger.Printf(fmt, args...) } else { log.Printf(fmt, args...) } } func (config *Configuration) notifyInReleaseStage() bool { if config.NotifyReleaseStages == nil { return true } if config.ReleaseStage == "" { return true } for _, r := range config.NotifyReleaseStages { if r == config.ReleaseStage { return true } } return false } func (config *Configuration) loadEnv() { envConfig := Configuration{} if apiKey := os.Getenv("BUGSNAG_API_KEY"); apiKey != "" { envConfig.APIKey = apiKey } if endpoint := os.Getenv("BUGSNAG_SESSIONS_ENDPOINT"); endpoint != "" { envConfig.Endpoints.Sessions = endpoint } if endpoint := os.Getenv("BUGSNAG_NOTIFY_ENDPOINT"); endpoint != "" { envConfig.Endpoints.Notify = endpoint } if stage := os.Getenv("BUGSNAG_RELEASE_STAGE"); stage != "" { envConfig.ReleaseStage = stage } if appVersion := os.Getenv("BUGSNAG_APP_VERSION"); appVersion != "" { envConfig.AppVersion = appVersion } if hostname := os.Getenv("BUGSNAG_HOSTNAME"); hostname != "" { envConfig.Hostname = hostname } if sourceRoot := os.Getenv("BUGSNAG_SOURCE_ROOT"); sourceRoot != "" { envConfig.SourceRoot = sourceRoot } if appType := os.Getenv("BUGSNAG_APP_TYPE"); appType != "" { envConfig.AppType = appType } if stages := os.Getenv("BUGSNAG_NOTIFY_RELEASE_STAGES"); stages != "" { envConfig.NotifyReleaseStages = strings.Split(stages, ",") } if packages := os.Getenv("BUGSNAG_PROJECT_PACKAGES"); packages != "" { envConfig.ProjectPackages = strings.Split(packages, ",") } if synchronous := os.Getenv("BUGSNAG_SYNCHRONOUS"); synchronous != "" { envConfig.Synchronous = synchronous == "1" } if disablePanics := os.Getenv("BUGSNAG_DISABLE_PANIC_HANDLER"); disablePanics == "1" { envConfig.PanicHandler = func() {} } if autoSessions := os.Getenv("BUGSNAG_AUTO_CAPTURE_SESSIONS"); autoSessions != "" { envConfig.AutoCaptureSessions = autoSessions == "1" } if filters := os.Getenv("BUGSNAG_PARAMS_FILTERS"); filters != "" { envConfig.ParamsFilters = strings.Split(filters, ",") } metadata := loadEnvMetadata(os.Environ()) OnBeforeNotify(func(event *Event, config *Configuration) error { for _, m := range metadata { event.MetaData.Add(m.tab, m.key, m.value) } return nil }) config.update(&envConfig) } bugsnag-go-2.2.0/v2/configuration_test.go000066400000000000000000000244171432154711400203420ustar00rootroot00000000000000package bugsnag import ( "log" "os" "runtime" "strings" "testing" ) func TestNotifyReleaseStages(t *testing.T) { notify := " " var tt = []struct { releaseStage string notifyReleaseStages []string expected bool }{ { releaseStage: "production", expected: true, }, { releaseStage: "production", notifyReleaseStages: []string{"development", "production"}, expected: true, }, { releaseStage: "staging", notifyReleaseStages: []string{"development", "production"}, expected: false, }, { notifyReleaseStages: []string{"development", "production"}, expected: true, }, } for _, tc := range tt { rs, nrs, exp := tc.releaseStage, tc.notifyReleaseStages, tc.expected config := &Configuration{ReleaseStage: rs, NotifyReleaseStages: nrs} if config.notifyInReleaseStage() != exp { if !exp { notify = " not " } t.Errorf("expected%sto notify when release stage is '%s' and notify release stages are '%+v'", notify, rs, nrs) } } } func TestIsProjectPackage(t *testing.T) { Configure(Configuration{ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com/b/*", "example.com/c/*/*", "example.com/d/**", "example.com/e", }}) var testCases = []struct { Path string Included bool }{ {"", false}, {"main", true}, {"runtime", false}, {"star", true}, {"sta", false}, {"starred", true}, {"star/foo", false}, {"example.com/a", true}, {"example.com/b", false}, {"example.com/b/", true}, {"example.com/b/foo", true}, {"example.com/b/foo/bar", false}, {"example.com/c/foo/bar", true}, {"example.com/c/foo/bar/baz", false}, {"example.com/d/foo/bar", true}, {"example.com/d/foo/bar/baz", true}, {"example.com/e", true}, } for _, s := range testCases { if Config.isProjectPackage(s.Path) != s.Included { t.Error("literal project package doesn't work:", s.Path, s.Included) } } } func TestStripProjectPackage(t *testing.T) { gopath := os.Getenv("GOPATH") Configure(Configuration{ ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com/b/*", "example.com/c/**", }, SourceRoot: gopath + "/src/", }) // on windows, source lines always use '/' but GOPATH may use '\' depending // on user settings. adjustedGopath := strings.Replace(gopath, "\\", "/", -1) var testCases = []struct { File string Stripped string }{ {"main.go", "main.go"}, {"runtime.go", "runtime.go"}, {"star.go", "star.go"}, {"example.com/a/foo.go", "foo.go"}, {"example.com/b/foo/bar.go", "foo/bar.go"}, {"example.com/b/foo.go", "foo.go"}, {"example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"example.com/c/a/b/foo.go", "a/b/foo.go"}, {adjustedGopath + "/src/runtime.go", "runtime.go"}, {adjustedGopath + "/src/example.com/a/foo.go", "foo.go"}, {adjustedGopath + "/src/example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {adjustedGopath + "/src/example.com/c/a/b/foo.go", "a/b/foo.go"}, } for _, tc := range testCases { if s := Config.stripProjectPackages(tc.File); s != tc.Stripped { t.Error("stripProjectPackage did not remove expected path:", tc.File, tc.Stripped, "was:", s) } } } func TestStripCustomSourceRoot(t *testing.T) { Configure(Configuration{ ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com/b/*", "example.com/c/**", }, SourceRoot: "/Users/bob/code/go/src/", }) var testCases = []struct { File string Stripped string }{ {"main.go", "main.go"}, {"runtime.go", "runtime.go"}, {"star.go", "star.go"}, {"example.com/a/foo.go", "foo.go"}, {"example.com/b/foo/bar.go", "foo/bar.go"}, {"example.com/b/foo.go", "foo.go"}, {"example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"example.com/c/a/b/foo.go", "a/b/foo.go"}, {"/Users/bob/code/go/src/runtime.go", "runtime.go"}, {"/Users/bob/code/go/src/example.com/a/foo.go", "foo.go"}, {"/Users/bob/code/go/src/example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"/Users/bob/code/go/src/example.com/c/a/b/foo.go", "a/b/foo.go"}, } for _, tc := range testCases { if s := Config.stripProjectPackages(tc.File); s != tc.Stripped { t.Error("stripProjectPackage did not remove expected path:", tc.File, tc.Stripped, "was:", s) } } } func TestStripCustomWindowsSourceRoot(t *testing.T) { if runtime.GOOS != "windows" { t.Skip("not compatible with non-windows builds") return } Configure(Configuration{ ProjectPackages: []string{ "main", "star*", "example.com/a", "example.com\\b\\*", "example.com/c/**", }, SourceRoot: "C:\\Users\\bob\\code\\go\\src\\", }) var testCases = []struct { File string Stripped string }{ {"main.go", "main.go"}, {"runtime.go", "runtime.go"}, {"star.go", "star.go"}, {"example.com/a/foo.go", "foo.go"}, {"example.com/b/foo/bar.go", "foo/bar.go"}, {"example.com/b/foo.go", "foo.go"}, {"example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"example.com/c/a/b/foo.go", "a/b/foo.go"}, {"C:/Users/bob/code/go/src/runtime.go", "runtime.go"}, {"C:/Users/bob/code/go/src/example.com/a/foo.go", "foo.go"}, {"C:/Users/bob/code/go/src/example.com/x/a/b/foo.go", "example.com/x/a/b/foo.go"}, {"C:/Users/bob/code/go/src/example.com/c/a/b/foo.go", "a/b/foo.go"}, } for _, tc := range testCases { if s := Config.stripProjectPackages(tc.File); s != tc.Stripped { t.Error("stripProjectPackage did not remove expected path:", tc.File, tc.Stripped, "was:", s) } } } type CustomTestLogger struct { loggedMessages []string } func (logger *CustomTestLogger) Printf(format string, v ...interface{}) { logger.loggedMessages = append(logger.loggedMessages, format) } func TestConfiguringCustomLogger(t *testing.T) { l1 := log.New(os.Stdout, "", log.Lshortfile) l2 := &CustomTestLogger{} var testCases = []struct { config Configuration notify bool msg string }{ { config: Configuration{ReleaseStage: "production", NotifyReleaseStages: []string{"development", "production"}, Logger: l1}, }, { config: Configuration{ReleaseStage: "production", NotifyReleaseStages: []string{"development", "production"}, Logger: l2}, }, } for _, testCase := range testCases { Configure(testCase.config) // call printf just to illustrate it is present as the compiler does most of the hard work testCase.config.Logger.Printf("hello %s", "bugsnag") } } func TestEndpointDeprecationWarning(t *testing.T) { defaultNotify := "https://notify.bugsnag.com/" defaultSessions := "https://sessions.bugsnag.com/" setUp := func() (*Configuration, *CustomTestLogger) { logger := &CustomTestLogger{} return &Configuration{ Endpoints: Endpoints{ Notify: defaultNotify, Sessions: defaultSessions, }, Logger: logger, }, logger } t.Run("Setting Endpoints.Notify without setting Endpoints.Sessions gives session disabled warning", func(st *testing.T) { c, logger := setUp() config := Configuration{ Endpoints: Endpoints{ Notify: "https://notify.whatever.com/", }, } keywords := []string{"WARNING", "Bugsnag", "notify", "No sessions"} c.update(&config) if got := len(logger.loggedMessages); got != 1 { st.Errorf("Expected exactly one logged message but got %d", got) } got := logger.loggedMessages[0] for _, exp := range keywords { if !strings.Contains(got, exp) { st.Errorf("Expected logger message containing '%s' when configuring but got %s.", exp, got) } } if got, exp := c.Endpoints.Notify, config.Endpoints.Notify; got != exp { st.Errorf("Expected notify endpoint to be '%s' but was '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, ""; got != exp { st.Errorf("Expected sessions endpoint to be '%s' but was '%s'", exp, got) } }) t.Run("Setting Endpoints.Sessions without setting Endpoints.Notify should panic", func(st *testing.T) { c, _ := setUp() defer func() { if err := recover(); err != nil { got := err.(string) for _, exp := range []string{"FATAL", "Bugsnag", "notify", "sessions"} { if !strings.Contains(got, exp) { st.Errorf("Expected panic error containing '%s' when configuring but got %s.", exp, got) } } } else { st.Errorf("Expected a panic to happen but didn't") } }() c.update(&Configuration{ Endpoints: Endpoints{ Sessions: "https://sessions.whatever.com/", }, }) }) t.Run("Should not complain if both Endpoints.Notify and Endpoints.Sessions are configured", func(st *testing.T) { notifyEndpoint, sessionsEndpoint := "https://notify.whatever.com", "https://sessions.whatever.com" config := Configuration{ Endpoints: Endpoints{ Notify: notifyEndpoint, Sessions: sessionsEndpoint, }, } c, logger := setUp() c.update(&config) if len(logger.loggedMessages) != 0 { st.Errorf("Did not expect any messages to be logged but logged: %v", logger.loggedMessages) } if got, exp := c.Endpoints.Notify, notifyEndpoint; got != exp { st.Errorf("Expected Notify endpoint: '%s', but was: '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, sessionsEndpoint; got != exp { st.Errorf("Expected Sessions endpoint: '%s', but was: '%s'", exp, got) } }) t.Run("Should not complain if Endpoints are not configured", func(st *testing.T) { c, logger := setUp() c.update(&Configuration{}) if len(logger.loggedMessages) != 0 { st.Errorf("Did not expect any messages to be logged but logged: %v", logger.loggedMessages) } if got, exp := c.Endpoints.Notify, defaultNotify; got != exp { st.Errorf("Expected Notify endpoint: '%s', but was: '%s'", exp, got) } if got, exp := c.Endpoints.Sessions, defaultSessions; got != exp { st.Errorf("Expected Sessions endpoint: '%s', but was: '%s'", exp, got) } }) } func TestIsAutoCaptureSessions(t *testing.T) { defaultConfig := Configuration{} if !defaultConfig.IsAutoCaptureSessions() { t.Errorf("Expected automatic session tracking to be enabled by default, but was disabled") } enabledConfig := Configuration{AutoCaptureSessions: true} if !enabledConfig.IsAutoCaptureSessions() { t.Errorf("Expected automatic session tracking to be enabled when so configured, but was disabled") } disabledConfig := Configuration{AutoCaptureSessions: false} if disabledConfig.IsAutoCaptureSessions() { t.Errorf("Expected automatic session tracking to be disabled when so configured, but enabled") } } bugsnag-go-2.2.0/v2/device/000077500000000000000000000000001432154711400153345ustar00rootroot00000000000000bugsnag-go-2.2.0/v2/device/hostname.go000066400000000000000000000005371432154711400175060ustar00rootroot00000000000000package device import "os" var hostname string // GetHostname returns the hostname of the current device. Caches the hostname // between calls to ensure this is performant. Returns a blank string in case // that the hostname cannot be identified. func GetHostname() string { if hostname == "" { hostname, _ = os.Hostname() } return hostname } bugsnag-go-2.2.0/v2/device/runtimeversions.go000066400000000000000000000026741432154711400211500ustar00rootroot00000000000000package device import ( "runtime" ) // Cached runtime versions that can be updated globally by framework // integrations through AddVersion. var versions *RuntimeVersions // RuntimeVersions define the various versions of Go and any framework that may // be in use. // As a user of the notifier you're unlikely to need to modify this struct. // As such, the authors reserve the right to introduce breaking changes to the // properties in this struct. In particular the framework versions are liable // to change in new versions of the notifier in minor/patch versions. type RuntimeVersions struct { Go string `json:"go"` Gin string `json:"gin,omitempty"` Martini string `json:"martini,omitempty"` Negroni string `json:"negroni,omitempty"` Revel string `json:"revel,omitempty"` } // GetRuntimeVersions retrieves the recorded runtime versions in a goroutine-safe manner. func GetRuntimeVersions() *RuntimeVersions { if versions == nil { versions = &RuntimeVersions{Go: runtime.Version()} } return versions } // AddVersion permits a framework to register its version, assuming it's one of // the officially supported frameworks. func AddVersion(framework, version string) { if versions == nil { versions = &RuntimeVersions{Go: runtime.Version()} } switch framework { case "Martini": versions.Martini = version case "Gin": versions.Gin = version case "Negroni": versions.Negroni = version case "Revel": versions.Revel = version } } bugsnag-go-2.2.0/v2/device/runtimeversions_test.go000066400000000000000000000024001432154711400221720ustar00rootroot00000000000000package device import ( "runtime" "testing" ) func TestPristineRuntimeVersions(t *testing.T) { versions = nil // reset global variable rv := GetRuntimeVersions() for _, tc := range []struct{ name, got, exp string }{ {name: "Go", got: rv.Go, exp: runtime.Version()}, {name: "Gin", got: rv.Gin, exp: ""}, {name: "Martini", got: rv.Martini, exp: ""}, {name: "Negroni", got: rv.Negroni, exp: ""}, {name: "Revel", got: rv.Revel, exp: ""}, } { if tc.got != tc.exp { t.Errorf("expected pristine '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) } } } func TestModifiedRuntimeVersions(t *testing.T) { versions = nil // reset global variable rv := GetRuntimeVersions() AddVersion("Gin", "1.2.1") AddVersion("Martini", "1.0.0") AddVersion("Negroni", "1.0.2") AddVersion("Revel", "0.20.1") for _, tc := range []struct{ name, got, exp string }{ {name: "Go", got: rv.Go, exp: runtime.Version()}, {name: "Gin", got: rv.Gin, exp: "1.2.1"}, {name: "Martini", got: rv.Martini, exp: "1.0.0"}, {name: "Negroni", got: rv.Negroni, exp: "1.0.2"}, {name: "Revel", got: rv.Revel, exp: "0.20.1"}, } { if tc.got != tc.exp { t.Errorf("expected modified '%s' runtime version to be '%s' but was '%s'", tc.name, tc.exp, tc.got) } } } bugsnag-go-2.2.0/v2/doc.go000066400000000000000000000045641432154711400152020ustar00rootroot00000000000000/* Package bugsnag captures errors in real-time and reports them to Bugsnag (http://bugsnag.com). Using bugsnag-go is a three-step process. 1. As early as possible in your program configure the notifier with your APIKey. This sets up handling of panics that would otherwise crash your app. func init() { bugsnag.Configure(bugsnag.Configuration{ APIKey: "YOUR_API_KEY_HERE", }) } 2. Add bugsnag to places that already catch panics. For example you should add it to the HTTP server when you call ListenAndServer: http.ListenAndServe(":8080", bugsnag.Handler(nil)) If that's not possible, you can also wrap each HTTP handler manually: http.HandleFunc("/" bugsnag.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { ... }) 3. To notify Bugsnag of an error that is not a panic, pass it to bugsnag.Notify. This will also log the error message using the configured Logger. if err != nil { bugsnag.Notify(err) } For detailed integration instructions see https://bugsnag.com/docs/notifiers/go. Configuration The only required configuration is the Bugsnag API key which can be obtained by clicking "Settings" on the top of https://bugsnag.com/ after signing up. We also recommend you set the ReleaseStage, AppType, and AppVersion if these make sense for your deployment workflow. RawData If you need to attach extra data to Bugsnag notifications you can do that using the rawData mechanism. Most of the functions that send errors to Bugsnag allow you to pass in any number of interface{} values as rawData. The rawData can consist of the Severity, Context, User or MetaData types listed below, and there is also builtin support for *http.Requests. bugsnag.Notify(err, bugsnag.SeverityError) If you want to add custom tabs to your bugsnag dashboard you can pass any value in as rawData, and then process it into the event's metadata using a bugsnag.OnBeforeNotify() hook. bugsnag.Notify(err, account) bugsnag.OnBeforeNotify(func (e *bugsnag.Event, c *bugsnag.Configuration) { for datum := range e.RawData { if account, ok := datum.(Account); ok { e.MetaData.Add("account", "name", account.Name) e.MetaData.Add("account", "url", account.URL) } } }) If necessary you can pass Configuration in as rawData, or modify the Configuration object passed into OnBeforeNotify hooks. Configuration passed in this way only affects the current notification. */ package bugsnag bugsnag-go-2.2.0/v2/env_metadata.go000066400000000000000000000021521432154711400170540ustar00rootroot00000000000000package bugsnag import ( "fmt" "strings" ) const metadataPrefix string = "BUGSNAG_METADATA_" const metadataPrefixLen int = len(metadataPrefix) const metadataDefaultTab string = "custom" type envMetadata struct { tab string key string value string } func loadEnvMetadata(environ []string) []envMetadata { metadata := make([]envMetadata, 0) for _, value := range environ { key, value, err := parseEnvironmentPair(value) if err != nil { continue } if keypath, err := parseMetadataKeypath(key); err == nil { tab, key := splitTabKeyValues(keypath) metadata = append(metadata, envMetadata{tab, key, value}) } } return metadata } func splitTabKeyValues(keypath string) (string, string) { key_components := strings.SplitN(keypath, "_", 2) if len(key_components) > 1 { return key_components[0], key_components[1] } return metadataDefaultTab, keypath } func parseMetadataKeypath(key string) (string, error) { if strings.HasPrefix(key, metadataPrefix) && len(key) > metadataPrefixLen { return strings.TrimPrefix(key, metadataPrefix), nil } return "", fmt.Errorf("No metadata prefix found") } bugsnag-go-2.2.0/v2/env_metadata_test.go000066400000000000000000000043041432154711400201140ustar00rootroot00000000000000package bugsnag import "testing" func TestParseMetadataKeypath(t *testing.T) { type output struct { keypath string err string } cases := map[string]output{ "": {"", "No metadata prefix found"}, "BUGSNAG_METADATA_": {"", "No metadata prefix found"}, "BUGSNAG_METADATA_key": {"key", ""}, "BUGSNAG_METADATA_device_foo": {"device_foo", ""}, "BUGSNAG_METADATA_device_foo_two": {"device_foo_two", ""}, } for input, expected := range cases { keypath, err := parseMetadataKeypath(input) if len(expected.err) > 0 && (err == nil || err.Error() != expected.err) { t.Errorf("expected error with message '%s', got '%v'", expected.err, err) } if expected.keypath != keypath { t.Errorf("expected keypath '%s', got '%s'", expected.keypath, keypath) } } } func TestLoadEnvMetadata(t *testing.T) { cases := map[string]envMetadata{ "": {"", "", ""}, "BUGSNAG_METADATA_Orange=tomato_paste": {"custom", "Orange", "tomato_paste"}, "BUGSNAG_METADATA_true_orange=tomato_paste": {"true", "orange", "tomato_paste"}, "BUGSNAG_METADATA_color_Orange=tomato_paste": {"color", "Orange", "tomato_paste"}, "BUGSNAG_METADATA_color_Orange_hue=tomato_paste": {"color", "Orange_hue", "tomato_paste"}, "BUGSNAG_METADATA_crayonColor_Magenta=tomato_paste": {"crayonColor", "Magenta", "tomato_paste"}, "BUGSNAG_METADATA_crayonColor_Magenta_hue=tomato_paste": {"crayonColor", "Magenta_hue", "tomato_paste"}, } for input, expected := range cases { metadata := loadEnvMetadata([]string{input}) if len(expected.tab) == 0 { for _, m := range metadata { t.Errorf("erroneously added a value for '%s' to tab '%s':'%s'", input, m.tab, m.key) } } else { if len(metadata) != 1 { t.Fatalf("wrong number of metadata elements: %d %v", len(metadata), metadata) } m := metadata[0] if m.tab != expected.tab { t.Errorf("wrong tab '%s'", expected.tab) continue } if m.key != expected.key { t.Errorf("wrong key '%s'", expected.key) continue } if m.value != expected.value { t.Errorf("incorrect value added to keypath: '%s'", m.value) } } } } bugsnag-go-2.2.0/v2/environment.go000066400000000000000000000004411432154711400167670ustar00rootroot00000000000000package bugsnag import ( "fmt" "strings" ) func parseEnvironmentPair(pair string) (string, string, error) { components := strings.SplitN(pair, "=", 2) if len(components) < 2 { return "", "", fmt.Errorf("Not a '='-delimited key pair") } return components[0], components[1], nil } bugsnag-go-2.2.0/v2/environment_test.go000066400000000000000000000014121432154711400200250ustar00rootroot00000000000000package bugsnag import ( "fmt" "testing" ) func TestParsePairs(t *testing.T) { type output struct { key, value string err error } cases := map[string]output{ "":{"", "", fmt.Errorf("Not a '='-delimited key pair")}, "key=value":{"key", "value", nil}, "key=value=bar":{"key", "value=bar", nil}, "something":{"", "", fmt.Errorf("Not a '='-delimited key pair")}, } for input, expected := range cases { key, value, err := parseEnvironmentPair(input) if expected.err != nil && (err == nil || err.Error() != expected.err.Error()) { t.Errorf("expected error '%v', got '%v'", expected.err, err) } if key != expected.key || value != expected.value { t.Errorf("expected pair '%s'='%s', got '%s'='%s'", expected.key, expected.value, key, value) } } } bugsnag-go-2.2.0/v2/errors/000077500000000000000000000000001432154711400154115ustar00rootroot00000000000000bugsnag-go-2.2.0/v2/errors/README.md000066400000000000000000000003701432154711400166700ustar00rootroot00000000000000Adds stacktraces to errors in golang. This was made to help build the Bugsnag notifier but can be used standalone if you like to have stacktraces on errors. See [Godoc](https://godoc.org/github.com/bugsnag/bugsnag-go/v2/errors) for the API docs. bugsnag-go-2.2.0/v2/errors/error.go000066400000000000000000000107701432154711400170760ustar00rootroot00000000000000// Package errors provides errors that have stack-traces. package errors import ( "bytes" "fmt" "github.com/pkg/errors" "reflect" "runtime" ) // The maximum number of stackframes on any error. var MaxStackDepth = 50 // Error is an error with an attached stacktrace. It can be used // wherever the builtin error interface is expected. type Error struct { Err error Cause *Error stack []uintptr frames []StackFrame } // ErrorWithCallers allows passing in error objects that // also have caller information attached. type ErrorWithCallers interface { Error() string Callers() []uintptr } // ErrorWithStackFrames allows the stack to be rebuilt from the stack frames, thus // allowing to use the Error type when the program counter is not available. type ErrorWithStackFrames interface { Error() string StackFrames() []StackFrame } type errorWithStack interface { StackTrace() errors.StackTrace Error() string } type errorWithCause interface { Unwrap() error } // New makes an Error from the given value. If that value is already an // error then it will be used directly, if not, it will be passed to // fmt.Errorf("%v"). The skip parameter indicates how far up the stack // to start the stacktrace. 0 is from the current call, 1 from its caller, etc. func New(e interface{}, skip int) *Error { var err error switch e := e.(type) { case *Error: return e case ErrorWithCallers: return &Error{ Err: e, stack: e.Callers(), Cause: unwrapCause(e), } case errorWithStack: trace := e.StackTrace() stack := make([]uintptr, len(trace)) for i, ptr := range trace { stack[i] = uintptr(ptr) - 1 } return &Error{ Err: e, Cause: unwrapCause(e), stack: stack, } case ErrorWithStackFrames: stack := make([]uintptr, len(e.StackFrames())) for i, frame := range e.StackFrames() { stack[i] = frame.ProgramCounter } return &Error{ Err: e, Cause: unwrapCause(e), stack: stack, frames: e.StackFrames(), } case error: err = e default: err = fmt.Errorf("%v", e) } stack := make([]uintptr, MaxStackDepth) length := runtime.Callers(2+skip, stack[:]) return &Error{ Err: err, Cause: unwrapCause(err), stack: stack[:length], } } // Errorf creates a new error with the given message. You can use it // as a drop-in replacement for fmt.Errorf() to provide descriptive // errors in return values. func Errorf(format string, a ...interface{}) *Error { return New(fmt.Errorf(format, a...), 1) } // Error returns the underlying error's message. func (err *Error) Error() string { return err.Err.Error() } // Callers returns the raw stack frames as returned by runtime.Callers() func (err *Error) Callers() []uintptr { return err.stack[:] } // Stack returns the callstack formatted the same way that go does // in runtime/debug.Stack() func (err *Error) Stack() []byte { buf := bytes.Buffer{} for _, frame := range err.StackFrames() { buf.WriteString(frame.String()) } return buf.Bytes() } // StackFrames returns an array of frames containing information about the // stack. func (err *Error) StackFrames() []StackFrame { if err.frames == nil { callers := runtime.CallersFrames(err.stack) err.frames = make([]StackFrame, 0, len(err.stack)) for frame, more := callers.Next(); more; frame, more = callers.Next() { if frame.Func == nil { // Ignore fully inlined functions continue } pkg, name := packageAndName(frame.Func) err.frames = append(err.frames, StackFrame{ function: frame.Func, File: frame.File, LineNumber: frame.Line, Name: name, Package: pkg, ProgramCounter: frame.PC, }) } } return err.frames } // TypeName returns the type this error. e.g. *errors.stringError. func (err *Error) TypeName() string { if p, ok := err.Err.(uncaughtPanic); ok { return p.typeName } if name := reflect.TypeOf(err.Err).String(); len(name) > 0 { return name } return "error" } func unwrapCause(err interface{}) *Error { if causer, ok := err.(errorWithCause); ok { cause := causer.Unwrap() if cause == nil { return nil } else if hasStack(cause) { // avoid generating a (duplicate) stack from the current frame return New(cause, 0) } else { return &Error{ Err: cause, Cause: unwrapCause(cause), stack: []uintptr{}, } } } return nil } func hasStack(err error) bool { if _, ok := err.(errorWithStack); ok { return true } if _, ok := err.(ErrorWithStackFrames); ok { return true } if _, ok := err.(ErrorWithCallers); ok { return true } return false } bugsnag-go-2.2.0/v2/errors/error_fmt_wrap_test.go000066400000000000000000000026571432154711400220410ustar00rootroot00000000000000// +build go1.13 package errors import ( "fmt" "runtime" "testing" ) func TestUnwrapErrorsCause(t *testing.T) { _, _, line, ok := runtime.Caller(0) // grab line immediately before error generators err1 := fmt.Errorf("invalid token") err2 := fmt.Errorf("login failed: %w", err1) err3 := fmt.Errorf("terminate process: %w", err2) unwrapped := New(err3, 0) if !ok { t.Fatalf("Something has gone wrong with loading the current stack") } if unwrapped.Error() != "terminate process: login failed: invalid token" { t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) } assertStacksMatch(t, []StackFrame{ StackFrame{Name: "TestUnwrapErrorsCause", File: "errors/error_fmt_wrap_test.go", LineNumber: line + 4}, }, unwrapped.StackFrames()) if unwrapped.Cause == nil { t.Fatalf("Failed to capture cause error") } if unwrapped.Cause.Error() != "login failed: invalid token" { t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error()) } if len(unwrapped.Cause.StackFrames()) > 0 { t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.StackFrames()) } if unwrapped.Cause.Cause == nil { t.Fatalf("Failed to capture nested cause error") } if len(unwrapped.Cause.Cause.StackFrames()) > 0 { t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames()) } if unwrapped.Cause.Cause.Cause != nil { t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause) } } bugsnag-go-2.2.0/v2/errors/error_test.go000066400000000000000000000167741432154711400201470ustar00rootroot00000000000000package errors import ( "bytes" "fmt" "io" "runtime" "strings" "testing" "github.com/pkg/errors" ) // fixture functions doing work to avoid inlining func a(i int) error { if b(i + 5) && b(i + 6) { return nil } return fmt.Errorf("not gonna happen") } func b(i int) bool { return c(i+2) > 12 } // panicking function! func c(i int) int { if i > 3 { panic('a') } return i * i } func TestParsePanicStack(t *testing.T) { defer func() { err := New(recover(), 0) if err.Error() != "97" { t.Errorf("Received incorrect error, expected 'a' got '%s'", err.Error()) } if err.TypeName() != "*errors.errorString" { t.Errorf("Error type was '%s'", err.TypeName()) } for index, frame := range err.StackFrames() { if frame.Func() == nil { t.Errorf("Failed to remove nil frame %d", index) } } expected := []StackFrame{ StackFrame{Name: "TestParsePanicStack.func1", File: "errors/error_test.go"}, StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, } assertStacksMatch(t, expected, err.StackFrames()) }() a(1) } func TestParseGeneratedStack(t *testing.T) { err := New(fmt.Errorf("e_too_many_colander"), 0) expected := []StackFrame{ StackFrame{Name: "TestParseGeneratedStack", File: "errors/error_test.go"}, } if err.Error() != "e_too_many_colander" { t.Errorf("Error name was '%s'", err.Error()) } if err.TypeName() != "*errors.errorString" { t.Errorf("Error type was '%s'", err.TypeName()) } for index, frame := range err.StackFrames() { if frame.Func() == nil { t.Errorf("Failed to remove nil frame %d", index) } } assertStacksMatch(t, expected, err.StackFrames()) } func TestSkipWorks(t *testing.T) { defer func() { err := New(recover(), 1) if err.Error() != "97" { t.Errorf("Received incorrect error, expected 'a' got '%s'", err.Error()) } for index, frame := range err.StackFrames() { if frame.Name == "TestSkipWorks.func1" { t.Errorf("Failed to skip frame") } if frame.Func() == nil { t.Errorf("Failed to remove inlined frame %d", index) } } expected := []StackFrame{ StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, } assertStacksMatch(t, expected, err.StackFrames()) }() a(4) } func checkFramesMatch(expected StackFrame, actual StackFrame) bool { if actual.Name != expected.Name { return false } // Not using exact match as it would change depending on whether // the package is being tested within or outside of the $GOPATH if expected.File != "" && !strings.HasSuffix(actual.File, expected.File) { return false } if expected.Package != "" && actual.Package != expected.Package { return false } if expected.LineNumber != 0 && actual.LineNumber != expected.LineNumber { return false } return true } func assertStacksMatch(t *testing.T, expected []StackFrame, actual []StackFrame) { var lastmatch int = 0 var matched int = 0 // loop over the actual stacktrace, checking off expected frames as they // are found. Each one might be in the middle of the stack, but the order // should remain the same. for _, actualFrame := range actual { for index, expectedFrame := range expected { if index < lastmatch { continue } if checkFramesMatch(expectedFrame, actualFrame) { lastmatch = index matched += 1 break } } } if matched != len(expected) { t.Fatalf("failed to find matches for %d frames: '%v'\ngot: '%v'", len(expected)-matched, expected[matched:], actual) } } type testErrorWithStackFrames struct { Err *Error } func (tews *testErrorWithStackFrames) StackFrames() []StackFrame { return tews.Err.StackFrames() } func (tews *testErrorWithStackFrames) Error() string { return tews.Err.Error() } func TestNewError(t *testing.T) { e := func() error { return New("hi", 1) }() if e.Error() != "hi" { t.Errorf("Constructor with a string failed") } if New(fmt.Errorf("yo"), 0).Error() != "yo" { t.Errorf("Constructor with an error failed") } if New(e, 0) != e { t.Errorf("Constructor with an Error failed") } if New(nil, 0).Error() != "" { t.Errorf("Constructor with nil failed") } err := New("foo", 0) tews := &testErrorWithStackFrames{ Err: err, } if bytes.Compare(New(tews, 0).Stack(), err.Stack()) != 0 { t.Errorf("Constructor with ErrorWithStackFrames failed") } } func TestUnwrapPkgError(t *testing.T) { _, _, line, ok := runtime.Caller(0) // grab line immediately before error generator top := func() error { err := fmt.Errorf("OH NO") return errors.Wrap(err, "failed") // the correct line for the top of the stack } unwrapped := New(top(), 0) // if errors.StackTrace detection fails, this line will be top of stack if !ok { t.Fatalf("Something has gone wrong with loading the current stack") } if unwrapped.Error() != "failed: OH NO" { t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) } expected := []StackFrame{ StackFrame{Name: "TestUnwrapPkgError.func1", File: "errors/error_test.go", LineNumber: line + 3}, StackFrame{Name: "TestUnwrapPkgError", File: "errors/error_test.go", LineNumber: line + 5}, } assertStacksMatch(t, expected, unwrapped.StackFrames()) } type customErr struct { msg string cause error callers []uintptr } func newCustomErr(msg string, cause error) error { callers := make([]uintptr, 8) runtime.Callers(2, callers) return customErr{ msg: msg, cause: cause, callers: callers, } } func (err customErr) Error() string { return err.msg } func (err customErr) Unwrap() error { return err.cause } func (err customErr) Callers() []uintptr { return err.callers } func TestUnwrapCustomCause(t *testing.T) { _, _, line, ok := runtime.Caller(0) // grab line immediately before error generators err1 := fmt.Errorf("invalid token") err2 := newCustomErr("login failed", err1) err3 := newCustomErr("terminate process", err2) unwrapped := New(err3, 0) if !ok { t.Fatalf("Something has gone wrong with loading the current stack") } if unwrapped.Error() != "terminate process" { t.Errorf("Failed to unwrap error: %s", unwrapped.Error()) } if unwrapped.Cause == nil { t.Fatalf("Failed to capture cause error") } assertStacksMatch(t, []StackFrame{ StackFrame{Name: "TestUnwrapCustomCause", File: "errors/error_test.go", LineNumber: line + 3}, }, unwrapped.StackFrames()) if unwrapped.Cause.Error() != "login failed" { t.Errorf("Failed to unwrap cause error: %s", unwrapped.Cause.Error()) } if unwrapped.Cause.Cause == nil { t.Fatalf("Failed to capture nested cause error") } assertStacksMatch(t, []StackFrame{ StackFrame{Name: "TestUnwrapCustomCause", File: "errors/error_test.go", LineNumber: line + 2}, }, unwrapped.Cause.StackFrames()) if unwrapped.Cause.Cause.Error() != "invalid token" { t.Errorf("Failed to unwrap nested cause error: %s", unwrapped.Cause.Cause.Error()) } if len(unwrapped.Cause.Cause.StackFrames()) > 0 { t.Errorf("Did not expect cause to have a stack: %v", unwrapped.Cause.Cause.StackFrames()) } if unwrapped.Cause.Cause.Cause != nil { t.Fatalf("Extra cause detected: %v", unwrapped.Cause.Cause.Cause) } } func ExampleErrorf() { for i := 1; i <= 2; i++ { if i%2 == 1 { e := Errorf("can only halve even numbers, got %d", i) fmt.Printf("Error: %+v", e) } } // Output: // Error: can only halve even numbers, got 1 } func ExampleNew() { // Wrap io.EOF with the current stack-trace and return it e := New(io.EOF, 0) fmt.Printf("%+v", e) // Output: // EOF } func ExampleNew_skip() { defer func() { if err := recover(); err != nil { // skip 1 frame (the deferred function) and then return the wrapped err err = New(err, 1) } }() } bugsnag-go-2.2.0/v2/errors/error_unwrap.go000066400000000000000000000003311432154711400204620ustar00rootroot00000000000000// +build go1.13 package errors import ( "github.com/pkg/errors" ) // Unwrap returns the result of calling errors.Unwrap on the underlying error func (err *Error) Unwrap() error { return errors.Unwrap(err.Err) } bugsnag-go-2.2.0/v2/errors/error_unwrap_test.go000066400000000000000000000011601432154711400215220ustar00rootroot00000000000000// +build go1.13 package errors import ( "fmt" "testing" "github.com/pkg/errors" ) func TestFindingErrorInChain(t *testing.T) { baseErr := errors.New("base error") wrappedErr := errors.Wrap(baseErr, "failed") err := New(wrappedErr, 0) if !errors.Is(err, baseErr) { t.Errorf("Failed to find base error: %s", err.Error()) } } func TestErrorUnwrapping(t *testing.T) { baseErr := errors.New("base error") wrappedErr := fmt.Errorf("failed: %w", baseErr) err := New(wrappedErr, 0) unwrapped := errors.Unwrap(err) if unwrapped != baseErr { t.Errorf("Failed to find base error: %s", unwrapped.Error()) } } bugsnag-go-2.2.0/v2/errors/parse_panic.go000066400000000000000000000062451432154711400202330ustar00rootroot00000000000000package errors import ( "strconv" "strings" ) type uncaughtPanic struct { typeName string message string } func (p uncaughtPanic) Error() string { return p.message } // ParsePanic allows you to get an error object from the output of a go program // that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap. func ParsePanic(text string) (*Error, error) { lines := strings.Split(text, "\n") prefixes := []string{"panic:", "fatal error:"} state := "start" var message string var typeName string var stack []StackFrame for i := 0; i < len(lines); i++ { line := lines[i] if state == "start" { for _, prefix := range prefixes { if strings.HasPrefix(line, prefix) { message = strings.TrimSpace(strings.TrimPrefix(line, prefix)) typeName = prefix[:len(prefix) - 1] state = "seek" break } } if state == "start" { return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line) } } else if state == "seek" { if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") { state = "parsing" } } else if state == "parsing" { if line == "" { state = "done" break } createdBy := false if strings.HasPrefix(line, "created by ") { line = strings.TrimPrefix(line, "created by ") createdBy = true } i++ if i >= len(lines) { return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line) } frame, err := parsePanicFrame(line, lines[i], createdBy) if err != nil { return nil, err } stack = append(stack, *frame) if createdBy { state = "done" break } } } if state == "done" || state == "parsing" { return &Error{Err: uncaughtPanic{typeName, message}, frames: stack}, nil } return nil, Errorf("could not parse panic: %v", text) } // The lines we're passing look like this: // // main.(*foo).destruct(0xc208067e98) // /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151 func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) { idx := strings.LastIndex(name, "(") if idx == -1 && !createdBy { return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name) } if idx != -1 { name = name[:idx] } pkg := "" if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { pkg += name[:lastslash] + "/" name = name[lastslash+1:] } if period := strings.Index(name, "."); period >= 0 { pkg += name[:period] name = name[period+1:] } name = strings.Replace(name, "·", ".", -1) if !strings.HasPrefix(line, "\t") { return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line) } idx = strings.LastIndex(line, ":") if idx == -1 { return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line) } file := line[1:idx] number := line[idx+1:] if idx = strings.Index(number, " +"); idx > -1 { number = number[:idx] } lno, err := strconv.ParseInt(number, 10, 32) if err != nil { return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line) } return &StackFrame{ File: file, LineNumber: int(lno), Package: pkg, Name: name, }, nil } bugsnag-go-2.2.0/v2/errors/parse_panic_test.go000066400000000000000000000175711432154711400212760ustar00rootroot00000000000000package errors import ( "reflect" "testing" ) var createdBy = `panic: hello! goroutine 54 [running]: runtime.panic(0x35ce40, 0xc208039db0) /0/c/go/src/pkg/runtime/panic.c:279 +0xf5 github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.func·001() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:13 +0x74 net/http.(*Server).Serve(0xc20806c780, 0x910c88, 0xc20803e168, 0x0, 0x0) /0/c/go/src/pkg/net/http/server.go:1698 +0x91 created by github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.App.Index /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:14 +0x3e goroutine 16 [IO wait]: net.runtime_pollWait(0x911c30, 0x72, 0x0) /0/c/go/src/pkg/runtime/netpoll.goc:146 +0x66 net.(*pollDesc).Wait(0xc2080ba990, 0x72, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:84 +0x46 net.(*pollDesc).WaitRead(0xc2080ba990, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:89 +0x42 net.(*netFD).accept(0xc2080ba930, 0x58be30, 0x0, 0x9103f0, 0x23) /0/c/go/src/pkg/net/fd_unix.go:409 +0x343 net.(*TCPListener).AcceptTCP(0xc20803e168, 0x8, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:234 +0x5d net.(*TCPListener).Accept(0xc20803e168, 0x0, 0x0, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:244 +0x4b github.com/revel/revel.Run(0xe6d9) /0/go/src/github.com/revel/revel/server.go:113 +0x926 main.main() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/tmp/main.go:109 +0xe1a ` var normalSplit = `panic: hello! goroutine 54 [running]: runtime.panic(0x35ce40, 0xc208039db0) /0/c/go/src/pkg/runtime/panic.c:279 +0xf5 github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.func·001() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:13 +0x74 net/http.(*Server).Serve(0xc20806c780, 0x910c88, 0xc20803e168, 0x0, 0x0) /0/c/go/src/pkg/net/http/server.go:1698 +0x91 goroutine 16 [IO wait]: net.runtime_pollWait(0x911c30, 0x72, 0x0) /0/c/go/src/pkg/runtime/netpoll.goc:146 +0x66 net.(*pollDesc).Wait(0xc2080ba990, 0x72, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:84 +0x46 net.(*pollDesc).WaitRead(0xc2080ba990, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:89 +0x42 net.(*netFD).accept(0xc2080ba930, 0x58be30, 0x0, 0x9103f0, 0x23) /0/c/go/src/pkg/net/fd_unix.go:409 +0x343 net.(*TCPListener).AcceptTCP(0xc20803e168, 0x8, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:234 +0x5d net.(*TCPListener).Accept(0xc20803e168, 0x0, 0x0, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:244 +0x4b github.com/revel/revel.Run(0xe6d9) /0/go/src/github.com/revel/revel/server.go:113 +0x926 main.main() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/tmp/main.go:109 +0xe1a ` var lastGoroutine = `panic: hello! goroutine 16 [IO wait]: net.runtime_pollWait(0x911c30, 0x72, 0x0) /0/c/go/src/pkg/runtime/netpoll.goc:146 +0x66 net.(*pollDesc).Wait(0xc2080ba990, 0x72, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:84 +0x46 net.(*pollDesc).WaitRead(0xc2080ba990, 0x0, 0x0) /0/c/go/src/pkg/net/fd_poll_runtime.go:89 +0x42 net.(*netFD).accept(0xc2080ba930, 0x58be30, 0x0, 0x9103f0, 0x23) /0/c/go/src/pkg/net/fd_unix.go:409 +0x343 net.(*TCPListener).AcceptTCP(0xc20803e168, 0x8, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:234 +0x5d net.(*TCPListener).Accept(0xc20803e168, 0x0, 0x0, 0x0, 0x0) /0/c/go/src/pkg/net/tcpsock_posix.go:244 +0x4b github.com/revel/revel.Run(0xe6d9) /0/go/src/github.com/revel/revel/server.go:113 +0x926 main.main() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/tmp/main.go:109 +0xe1a goroutine 54 [running]: runtime.panic(0x35ce40, 0xc208039db0) /0/c/go/src/pkg/runtime/panic.c:279 +0xf5 github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers.func·001() /0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go:13 +0x74 net/http.(*Server).Serve(0xc20806c780, 0x910c88, 0xc20803e168, 0x0, 0x0) /0/c/go/src/pkg/net/http/server.go:1698 +0x91 ` var result = []StackFrame{ StackFrame{File: "/0/c/go/src/pkg/runtime/panic.c", LineNumber: 279, Name: "panic", Package: "runtime"}, StackFrame{File: "/0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go", LineNumber: 13, Name: "func.001", Package: "github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers"}, StackFrame{File: "/0/c/go/src/pkg/net/http/server.go", LineNumber: 1698, Name: "(*Server).Serve", Package: "net/http"}, } var resultCreatedBy = append(result, StackFrame{File: "/0/go/src/github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers/app.go", LineNumber: 14, Name: "App.Index", Package: "github.com/loopj/bugsnag-example-apps/go/revelapp/app/controllers", ProgramCounter: 0x0}) func TestParsePanic(t *testing.T) { todo := map[string]string{ "createdBy": createdBy, "normalSplit": normalSplit, "lastGoroutine": lastGoroutine, } for key, val := range todo { Err, err := ParsePanic(val) if err != nil { t.Fatal(err) } if Err.TypeName() != "panic" { t.Errorf("Wrong type: %s", Err.TypeName()) } if Err.Error() != "hello!" { t.Errorf("Wrong message: %s", Err.TypeName()) } if Err.StackFrames()[0].Func() != nil { t.Errorf("Somehow managed to find a func...") } result := result if key == "createdBy" { result = resultCreatedBy } if !reflect.DeepEqual(Err.StackFrames(), result) { t.Errorf("Wrong stack for %s: %#v", key, Err.StackFrames()) } } } var concurrentMapReadWrite = `fatal error: concurrent map read and map write goroutine 1 [running]: runtime.throw(0x10766f5, 0x21) /usr/local/Cellar/go/1.15.5/libexec/src/runtime/panic.go:1116 +0x72 fp=0xc00003a6c8 sp=0xc00003a698 pc=0x102d592 runtime.mapaccess1_faststr(0x1066fc0, 0xc000060000, 0x10732e0, 0x1, 0xc000100088) /usr/local/Cellar/go/1.15.5/libexec/src/runtime/map_faststr.go:21 +0x465 fp=0xc00003a738 sp=0xc00003a6c8 pc=0x100e9c5 main.concurrentWrite() /myapps/go/fatalerror/main.go:14 +0x7a fp=0xc00003a778 sp=0xc00003a738 pc=0x105d83a main.main() /myapps/go/fatalerror/main.go:41 +0x25 fp=0xc00003a788 sp=0xc00003a778 pc=0x105d885 runtime.main() /usr/local/Cellar/go/1.15.5/libexec/src/runtime/proc.go:204 +0x209 fp=0xc00003a7e0 sp=0xc00003a788 pc=0x102fd49 runtime.goexit() /usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s:1374 +0x1 fp=0xc00003a7e8 sp=0xc00003a7e0 pc=0x105a4a1 goroutine 5 [runnable]: main.concurrentWrite.func1(0xc000060000) /myapps/go/fatalerror/main.go:10 +0x4c created by main.concurrentWrite /myapps/go/fatalerror/main.go:8 +0x4b ` func TestParseFatalError(t *testing.T) { Err, err := ParsePanic(concurrentMapReadWrite) if err != nil { t.Fatal(err) } if Err.TypeName() != "fatal error" { t.Errorf("Wrong type: %s", Err.TypeName()) } if Err.Error() != "concurrent map read and map write" { t.Errorf("Wrong message: '%s'", Err.Error()) } if Err.StackFrames()[0].Func() != nil { t.Errorf("Somehow managed to find a func...") } var result = []StackFrame{ StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/panic.go", LineNumber: 1116, Name: "throw", Package: "runtime"}, StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/map_faststr.go", LineNumber: 21, Name: "mapaccess1_faststr", Package: "runtime"}, StackFrame{File: "/myapps/go/fatalerror/main.go", LineNumber: 14, Name: "concurrentWrite", Package: "main"}, StackFrame{File: "/myapps/go/fatalerror/main.go", LineNumber: 41, Name: "main", Package: "main"}, StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/proc.go", LineNumber: 204, Name: "main", Package: "runtime"}, StackFrame{File: "/usr/local/Cellar/go/1.15.5/libexec/src/runtime/asm_amd64.s", LineNumber: 1374, Name: "goexit", Package: "runtime"}, } if !reflect.DeepEqual(Err.StackFrames(), result) { t.Errorf("Wrong stack for concurrent write fatal error:") for i, frame := range result { t.Logf("[%d] %#v", i, frame) if len(Err.StackFrames()) > i { t.Logf(" %#v", Err.StackFrames()[i]) } } } } bugsnag-go-2.2.0/v2/errors/stackframe.go000066400000000000000000000050101432154711400200540ustar00rootroot00000000000000package errors import ( "bytes" "fmt" "io/ioutil" "runtime" "strings" ) // A StackFrame contains all necessary information about to generate a line // in a callstack. type StackFrame struct { File string LineNumber int Name string Package string ProgramCounter uintptr function *runtime.Func } // NewStackFrame popoulates a stack frame object from the program counter. func NewStackFrame(pc uintptr) (frame StackFrame) { frame = StackFrame{ProgramCounter: pc} if frame.Func() == nil { return } frame.Package, frame.Name = packageAndName(frame.Func()) // pc -1 because the program counters we use are usually return addresses, // and we want to show the line that corresponds to the function call frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1) return } // Func returns the function that this stackframe corresponds to func (frame *StackFrame) Func() *runtime.Func { return frame.function } // String returns the stackframe formatted in the same way as go does // in runtime/debug.Stack() func (frame *StackFrame) String() string { str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter) source, err := frame.SourceLine() if err != nil { return str } return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source) } // SourceLine gets the line of code (from File and Line) of the original source if possible func (frame *StackFrame) SourceLine() (string, error) { data, err := ioutil.ReadFile(frame.File) if err != nil { return "", err } lines := bytes.Split(data, []byte{'\n'}) if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) { return "???", nil } // -1 because line-numbers are 1 based, but our array is 0 based return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil } func packageAndName(fn *runtime.Func) (string, string) { name := fn.Name() pkg := "" // The name includes the path name to the package, which is unnecessary // since the file name is already included. Plus, it has center dots. // That is, we see // runtime/debug.*T·ptrmethod // and want // *T.ptrmethod // Since the package path might contains dots (e.g. code.google.com/...), // we first remove the path prefix if there is one. if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 { pkg += name[:lastslash] + "/" name = name[lastslash+1:] } if period := strings.Index(name, "."); period >= 0 { pkg += name[:period] name = name[period+1:] } name = strings.Replace(name, "·", ".", -1) return pkg, name } bugsnag-go-2.2.0/v2/event.go000066400000000000000000000155271432154711400155570ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "strings" "github.com/bugsnag/bugsnag-go/v2/errors" ) // Context is the context of the error in Bugsnag. // This can be passed to Notify, Recover or AutoNotify as rawData. type Context struct { String string } // User represents the searchable user-data on Bugsnag. The Id is also used // to determine the number of users affected by a bug. This can be // passed to Notify, Recover or AutoNotify as rawData. type User struct { Id string `json:"id,omitempty"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` } // ErrorClass overrides the error class in Bugsnag. // This struct enables you to group errors as you like. type ErrorClass struct { Name string } // Sets the severity of the error on Bugsnag. These values can be // passed to Notify, Recover or AutoNotify as rawData. var ( SeverityError = severity{"error"} SeverityWarning = severity{"warning"} SeverityInfo = severity{"info"} ) // The severity tag type, private so that people can only use Error,Warning,Info type severity struct { String string } // The form of stacktrace that Bugsnag expects type StackFrame struct { Method string `json:"method"` File string `json:"file"` LineNumber int `json:"lineNumber"` InProject bool `json:"inProject,omitempty"` } type SeverityReason string const ( SeverityReasonCallbackSpecified SeverityReason = "userCallbackSetSeverity" SeverityReasonHandledError = "handledError" SeverityReasonHandledPanic = "handledPanic" SeverityReasonUnhandledError = "unhandledError" SeverityReasonUnhandledMiddlewareError = "unhandledErrorMiddleware" SeverityReasonUnhandledPanic = "unhandledPanic" SeverityReasonUserSpecified = "userSpecifiedSeverity" ) type HandledState struct { SeverityReason SeverityReason OriginalSeverity severity Unhandled bool Framework string } // Event represents a payload of data that gets sent to Bugsnag. // This is passed to each OnBeforeNotify hook. type Event struct { // The original error that caused this event, not sent to Bugsnag. Error *errors.Error // The rawData affecting this error, not sent to Bugsnag. RawData []interface{} // The error class to be sent to Bugsnag. This defaults to the type name of the Error, for // example *error.String ErrorClass string // The error message to be sent to Bugsnag. This defaults to the return value of Error.Error() Message string // The stacktrrace of the error to be sent to Bugsnag. Stacktrace []StackFrame // The context to be sent to Bugsnag. This should be set to the part of the app that was running, // e.g. for http requests, set it to the path. Context string // The severity of the error. Can be SeverityError, SeverityWarning or SeverityInfo. Severity severity // The grouping hash is used to override Bugsnag's grouping. Set this if you'd like all errors with // the same grouping hash to group together in the dashboard. GroupingHash string // User data to send to Bugsnag. This is searchable on the dashboard. User *User // Other MetaData to send to Bugsnag. Appears as a set of tabbed tables in the dashboard. MetaData MetaData // Ctx is the context of the session the event occurred in. This allows Bugsnag to associate the event with the session. Ctx context.Context // Request is the request information that populates the Request tab in the dashboard. Request *RequestJSON // The reason for the severity and original value handledState HandledState // True if the event was caused by an automatic event Unhandled bool } func newEvent(rawData []interface{}, notifier *Notifier) (*Event, *Configuration) { config := notifier.Config event := &Event{ RawData: append(notifier.RawData, rawData...), Severity: SeverityWarning, MetaData: make(MetaData), handledState: HandledState{ SeverityReason: SeverityReasonHandledError, OriginalSeverity: SeverityWarning, Unhandled: false, Framework: "", }, Unhandled: false, } var err *errors.Error var callbacks []func(*Event) for _, datum := range event.RawData { switch datum := datum.(type) { case error, errors.Error: err = errors.New(datum.(error), 1) event.Error = err // Only assign automatically if not explicitly set through ErrorClass already if event.ErrorClass == "" { event.ErrorClass = err.TypeName() } event.Message = err.Error() event.Stacktrace = make([]StackFrame, len(err.StackFrames())) case bool: config = config.merge(&Configuration{Synchronous: bool(datum)}) case severity: event.Severity = datum event.handledState.OriginalSeverity = datum event.handledState.SeverityReason = SeverityReasonUserSpecified case Context: event.Context = datum.String case context.Context: populateEventWithContext(datum, event) case *http.Request: populateEventWithRequest(datum, event) case Configuration: config = config.merge(&datum) case MetaData: event.MetaData.Update(datum) case User: event.User = &datum case ErrorClass: event.ErrorClass = datum.Name case HandledState: event.handledState = datum event.Severity = datum.OriginalSeverity event.Unhandled = datum.Unhandled case func(*Event): callbacks = append(callbacks, datum) } } event.Stacktrace = generateStacktrace(err, config) for _, callback := range callbacks { callback(event) if event.Severity != event.handledState.OriginalSeverity { event.handledState.SeverityReason = SeverityReasonCallbackSpecified } } return event, config } func generateStacktrace(err *errors.Error, config *Configuration) []StackFrame { stack := make([]StackFrame, len(err.StackFrames())) for i, frame := range err.StackFrames() { file := frame.File inProject := config.isProjectPackage(frame.Package) // remove $GOROOT and $GOHOME from other frames if idx := strings.Index(file, frame.Package); idx > -1 { file = file[idx:] } if inProject { file = config.stripProjectPackages(file) } stack[i] = StackFrame{ Method: frame.Name, File: file, LineNumber: frame.LineNumber, InProject: inProject, } } return stack } func populateEventWithContext(ctx context.Context, event *Event) { event.Ctx = ctx reqJSON, req := extractRequestInfo(ctx) if event.Request == nil { event.Request = reqJSON } populateEventWithRequest(req, event) } func populateEventWithRequest(req *http.Request, event *Event) { if req == nil { return } event.Request = extractRequestInfoFromReq(req) if event.Context == "" { event.Context = req.URL.Path } // Default user.id to IP so that the count of users affected works. if event.User == nil { ip := req.RemoteAddr if idx := strings.LastIndex(ip, ":"); idx != -1 { ip = ip[:idx] } event.User = &User{Id: ip} } } bugsnag-go-2.2.0/v2/event_test.go000066400000000000000000000016071432154711400166100ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "net/http/httptest" "reflect" "strings" "testing" ) func TestPopulateEvent(t *testing.T) { event := new(Event) contexts := make(chan context.Context, 1) reqs := make(chan *http.Request, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { contexts <- AttachRequestData(r.Context(), r) reqs <- r })) defer ts.Close() http.Get(ts.URL + "/serenity?q=abcdef") ctx, req := <-contexts, <-reqs populateEventWithContext(ctx, event) for _, tc := range []struct{ e, c interface{} }{ {e: event.Ctx, c: ctx}, {e: event.Request, c: extractRequestInfoFromReq(req)}, {e: event.Context, c: req.URL.Path}, {e: event.User.Id, c: req.RemoteAddr[:strings.LastIndex(req.RemoteAddr, ":")]}, } { if !reflect.DeepEqual(tc.e, tc.c) { t.Errorf("Expected '%+v' and '%+v' to be equal", tc.e, tc.c) } } } bugsnag-go-2.2.0/v2/go.mod000066400000000000000000000006071432154711400152060ustar00rootroot00000000000000module github.com/bugsnag/bugsnag-go/v2 go 1.15 require ( github.com/bitly/go-simplejson v0.5.0 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/bugsnag/panicwrap v1.3.4 github.com/google/uuid v1.3.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/pkg/errors v0.9.1 ) bugsnag-go-2.2.0/v2/go.sum000066400000000000000000000027701432154711400152360ustar00rootroot00000000000000github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU= github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= bugsnag-go-2.2.0/v2/headers/000077500000000000000000000000001432154711400155105ustar00rootroot00000000000000bugsnag-go-2.2.0/v2/headers/prefixed.go000066400000000000000000000007531432154711400176520ustar00rootroot00000000000000package headers import "time" //PrefixedHeaders returns a map of Content-Type and the 'Bugsnag-' headers for //API key, payload version, and the time at which the request is being sent. func PrefixedHeaders(apiKey, payloadVersion string) map[string]string { return map[string]string{ "Content-Type": "application/json", "Bugsnag-Api-Key": apiKey, "Bugsnag-Payload-Version": payloadVersion, "Bugsnag-Sent-At": time.Now().UTC().Format(time.RFC3339), } } bugsnag-go-2.2.0/v2/headers/prefixed_test.go000066400000000000000000000025641432154711400207130ustar00rootroot00000000000000package headers import ( "strings" "testing" "time" ) const APIKey = "abcd1234abcd1234" const testPayloadVersion = "3" func TestConstantBugsnagPrefixedHeaders(t *testing.T) { headers := PrefixedHeaders(APIKey, testPayloadVersion) testCases := []struct { header string expected string }{ {header: "Content-Type", expected: "application/json"}, {header: "Bugsnag-Api-Key", expected: APIKey}, {header: "Bugsnag-Payload-Version", expected: testPayloadVersion}, } for _, tc := range testCases { t.Run(tc.header, func(st *testing.T) { if got := headers[tc.header]; got != tc.expected { t.Errorf("Expected headers to contain %s header %s but was %s", tc.header, tc.expected, got) } }) } } func TestTimeDependentBugsnagPrefixedHeaders(t *testing.T) { headers := PrefixedHeaders(APIKey, testPayloadVersion) sentAtString := headers["Bugsnag-Sent-At"] if !strings.HasSuffix(sentAtString, "Z") { t.Errorf("Error when setting Bugsnag-Sent-At header: %s, doesn't end with a Z", sentAtString) } sentAt, err := time.Parse(time.RFC3339, sentAtString) if err != nil { t.Errorf("Error when attempting to parse Bugsnag-Sent-At header: %s", sentAtString) } if now := time.Now(); now.Sub(sentAt) > time.Second || now.Sub(sentAt) < -time.Second { t.Errorf("Expected Bugsnag-Sent-At header approx. %s but was %s", now.UTC().Format(time.RFC3339), sentAtString) } } bugsnag-go-2.2.0/v2/json_tags.go000066400000000000000000000017601432154711400164170ustar00rootroot00000000000000// The code is stripped from: // http://golang.org/src/pkg/encoding/json/tags.go?m=text package bugsnag import ( "strings" ) // tagOptions is the string following a comma in a struct field's "json" // tag, or the empty string. It does not include the leading comma. type tagOptions string // parseTag splits a struct field's json tag into its name and // comma-separated options. func parseTag(tag string) (string, tagOptions) { if idx := strings.Index(tag, ","); idx != -1 { return tag[:idx], tagOptions(tag[idx+1:]) } return tag, tagOptions("") } // Contains reports whether a comma-separated list of options // contains a particular substr flag. substr must be surrounded by a // string boundary or commas. func (o tagOptions) Contains(optionName string) bool { if len(o) == 0 { return false } s := string(o) for s != "" { var next string i := strings.Index(s, ",") if i >= 0 { s, next = s[:i], s[i+1:] } if s == optionName { return true } s = next } return false } bugsnag-go-2.2.0/v2/metadata.go000066400000000000000000000107051432154711400162070ustar00rootroot00000000000000package bugsnag import ( "fmt" "reflect" "strings" ) // MetaData is added to the Bugsnag dashboard in tabs. Each tab is // a map of strings -> values. You can pass MetaData to Notify, Recover // and AutoNotify as rawData. type MetaData map[string]map[string]interface{} // Update the meta-data with more information. Tabs are merged together such // that unique keys from both sides are preserved, and duplicate keys end up // with the provided values. func (meta MetaData) Update(other MetaData) { for name, tab := range other { if meta[name] == nil { meta[name] = make(map[string]interface{}) } for key, value := range tab { meta[name][key] = value } } } // Add creates a tab of Bugsnag meta-data. // If the tab doesn't yet exist it will be created. // If the key already exists, it will be overwritten. func (meta MetaData) Add(tab string, key string, value interface{}) { if meta[tab] == nil { meta[tab] = make(map[string]interface{}) } meta[tab][key] = value } // AddStruct creates a tab of Bugsnag meta-data. // The struct will be converted to an Object using the // reflect library so any private fields will not be exported. // As a safety measure, if you pass a non-struct the value will be // sent to Bugsnag under the "Extra data" tab. func (meta MetaData) AddStruct(tab string, obj interface{}) { val := sanitizer{}.Sanitize(obj) content, ok := val.(map[string]interface{}) if ok { meta[tab] = content } else { // Wasn't a struct meta.Add("Extra data", tab, obj) } } // Remove any values from meta-data that have keys matching the filters, // and any that are recursive data-structures func (meta MetaData) sanitize(filters []string) interface{} { return sanitizer{ Filters: filters, Seen: make([]interface{}, 0), }.Sanitize(meta) } // The sanitizer is used to remove filtered params and recursion from meta-data. type sanitizer struct { Filters []string Seen []interface{} } func (s sanitizer) Sanitize(data interface{}) interface{} { for _, s := range s.Seen { // TODO: we don't need deep equal here, just type-ignoring equality if reflect.DeepEqual(data, s) { return "[RECURSION]" } } // Sanitizers are passed by value, so we can modify s and it only affects // s.Seen for nested calls. s.Seen = append(s.Seen, data) t := reflect.TypeOf(data) v := reflect.ValueOf(data) if t == nil { return "" } switch t.Kind() { case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64: return data case reflect.String: return data case reflect.Interface, reflect.Ptr: if v.IsNil() { return "" } return s.Sanitize(v.Elem().Interface()) case reflect.Array, reflect.Slice: ret := make([]interface{}, v.Len()) for i := 0; i < v.Len(); i++ { ret[i] = s.Sanitize(v.Index(i).Interface()) } return ret case reflect.Map: return s.sanitizeMap(v) case reflect.Struct: return s.sanitizeStruct(v, t) // Things JSON can't serialize: // case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer: default: return "[" + t.String() + "]" } } func (s sanitizer) sanitizeMap(v reflect.Value) interface{} { ret := make(map[string]interface{}) for _, key := range v.MapKeys() { val := s.Sanitize(v.MapIndex(key).Interface()) newKey := fmt.Sprintf("%v", key.Interface()) if s.shouldRedact(newKey) { val = "[FILTERED]" } ret[newKey] = val } return ret } func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) interface{} { ret := make(map[string]interface{}) for i := 0; i < v.NumField(); i++ { val := v.Field(i) // Don't export private fields if !val.CanInterface() { continue } name := t.Field(i).Name var opts tagOptions // Parse JSON tags. Supports name and "omitempty" if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 { name, opts = parseTag(jsonTag) } if s.shouldRedact(name) { ret[name] = "[FILTERED]" } else { sanitized := s.Sanitize(val.Interface()) if str, ok := sanitized.(string); ok { if !(opts.Contains("omitempty") && len(str) == 0) { ret[name] = str } } else { ret[name] = sanitized } } } return ret } func (s sanitizer) shouldRedact(key string) bool { for _, filter := range s.Filters { if strings.Contains(strings.ToLower(key), strings.ToLower(filter)) { return true } } return false } bugsnag-go-2.2.0/v2/metadata_test.go000066400000000000000000000075001432154711400172450ustar00rootroot00000000000000package bugsnag import ( "reflect" "testing" "unsafe" "github.com/bugsnag/bugsnag-go/v2/errors" ) type _account struct { ID string Name string Plan struct { Premium bool } Password string secret string Email string `json:"email"` EmptyEmail string `json:"emptyemail,omitempty"` NotEmptyEmail string `json:"not_empty_email,omitempty"` } type _broken struct { Me *_broken Data string } var account = _account{} var notifier = New(Configuration{}) func TestMetaDataAdd(t *testing.T) { m := MetaData{ "one": { "key": "value", "override": false, }} m.Add("one", "override", true) m.Add("one", "new", "key") m.Add("new", "tab", account) m.AddStruct("lol", "not really a struct") m.AddStruct("account", account) if !reflect.DeepEqual(m, MetaData{ "one": { "key": "value", "override": true, "new": "key", }, "new": { "tab": account, }, "Extra data": { "lol": "not really a struct", }, "account": { "ID": "", "Name": "", "Plan": map[string]interface{}{ "Premium": false, }, "Password": "", "email": "", }, }) { t.Errorf("metadata.Add didn't work: %#v", m) } } func TestMetaDataUpdate(t *testing.T) { m := MetaData{ "one": { "key": "value", "override": false, }} m.Update(MetaData{ "one": { "override": true, "new": "key", }, "new": { "tab": account, }, }) if !reflect.DeepEqual(m, MetaData{ "one": { "key": "value", "override": true, "new": "key", }, "new": { "tab": account, }, }) { t.Errorf("metadata.Update didn't work: %#v", m) } } func TestMetaDataSanitize(t *testing.T) { var broken = _broken{} broken.Me = &broken broken.Data = "ohai" account.Name = "test" account.ID = "test" account.secret = "hush" account.Email = "example@example.com" account.EmptyEmail = "" account.NotEmptyEmail = "not_empty_email@example.com" m := MetaData{ "one": { "bool": true, "int": 7, "float": 7.1, "complex": complex(1, 1), "func": func() {}, "unsafe": unsafe.Pointer(broken.Me), "string": "string", "password": "secret", "array": []hash{{ "creditcard": "1234567812345678", "broken": broken, }}, "broken": broken, "account": account, }, } n := m.sanitize([]string{"password", "creditcard"}) if !reflect.DeepEqual(n, map[string]interface{}{ "one": map[string]interface{}{ "bool": true, "int": 7, "float": 7.1, "complex": "[complex128]", "string": "string", "unsafe": "[unsafe.Pointer]", "func": "[func()]", "password": "[FILTERED]", "array": []interface{}{map[string]interface{}{ "creditcard": "[FILTERED]", "broken": map[string]interface{}{ "Me": "[RECURSION]", "Data": "ohai", }, }}, "broken": map[string]interface{}{ "Me": "[RECURSION]", "Data": "ohai", }, "account": map[string]interface{}{ "ID": "test", "Name": "test", "Plan": map[string]interface{}{ "Premium": false, }, "Password": "[FILTERED]", "email": "example@example.com", "not_empty_email": "not_empty_email@example.com", }, }, }) { t.Errorf("metadata.Sanitize didn't work: %#v", n) } } func TestSanitizerSanitize(t *testing.T) { var ( nilPointer *int nilInterface = interface{}(nil) ) for n, tc := range []struct { input interface{} want interface{} }{ {nilPointer, ""}, {nilInterface, ""}, } { s := &sanitizer{} gotValue := s.Sanitize(tc.input) if got, want := gotValue, tc.want; got != want { t.Errorf("[%d] got %v, want %v", n, got, want) } } } func ExampleMetaData() { notifier.Notify(errors.Errorf("hi world"), MetaData{"Account": { "id": account.ID, "name": account.Name, "paying?": account.Plan.Premium, }}) } bugsnag-go-2.2.0/v2/middleware.go000066400000000000000000000037331432154711400165470ustar00rootroot00000000000000package bugsnag import ( "net/http" ) type ( beforeFunc func(*Event, *Configuration) error // MiddlewareStacks keep middleware in the correct order. They are // called in reverse order, so if you add a new middleware it will // be called before all existing middleware. middlewareStack struct { before []beforeFunc } ) // AddMiddleware adds a new middleware to the outside of the existing ones, // when the middlewareStack is Run it will be run before all middleware that // have been added before. func (stack *middlewareStack) OnBeforeNotify(middleware beforeFunc) { stack.before = append(stack.before, middleware) } // Run causes all the middleware to be run. If they all permit it the next callback // will be called with all the middleware on the stack. func (stack *middlewareStack) Run(event *Event, config *Configuration, next func() error) error { // run all the before filters in reverse order for i := range stack.before { before := stack.before[len(stack.before)-i-1] severity := event.Severity err := stack.runBeforeFilter(before, event, config) if err != nil { return err } if event.Severity != severity { event.handledState.SeverityReason = SeverityReasonCallbackSpecified } } return next() } func (stack *middlewareStack) runBeforeFilter(f beforeFunc, event *Event, config *Configuration) error { defer func() { if err := recover(); err != nil { config.logf("bugsnag/middleware: unexpected panic: %v", err) } }() return f(event, config) } // httpRequestMiddleware is added OnBeforeNotify by default. It takes information // from an http.Request passed in as rawData, and adds it to the Event. You can // use this as a template for writing your own Middleware. func httpRequestMiddleware(event *Event, config *Configuration) error { for _, datum := range event.RawData { if request, ok := datum.(*http.Request); ok { event.MetaData.Update(MetaData{ "request": { "params": request.URL.Query(), }, }) } } return nil } bugsnag-go-2.2.0/v2/middleware_test.go000066400000000000000000000037621432154711400176100ustar00rootroot00000000000000package bugsnag import ( "bytes" "fmt" "github.com/bugsnag/bugsnag-go/v2/errors" "log" "reflect" "testing" ) func TestMiddlewareOrder(t *testing.T) { err := fmt.Errorf("test") data := []interface{}{errors.New(err, 1)} event, config := newEvent(data, &defaultNotifier) result := make([]int, 0, 7) stack := middlewareStack{} stack.OnBeforeNotify(func(e *Event, c *Configuration) error { result = append(result, 2) return nil }) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { result = append(result, 1) return nil }) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { result = append(result, 0) return nil }) stack.Run(event, config, func() error { result = append(result, 3) return nil }) if !reflect.DeepEqual(result, []int{0, 1, 2, 3}) { t.Errorf("unexpected middleware order %v", result) } } func TestBeforeNotifyReturnErr(t *testing.T) { stack := middlewareStack{} err := fmt.Errorf("test") data := []interface{}{errors.New(err, 1)} event, config := newEvent(data, &defaultNotifier) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { return err }) called := false e := stack.Run(event, config, func() error { called = true return nil }) if e != err { t.Errorf("Middleware didn't return the error") } if called == true { t.Errorf("Notify was called when BeforeNotify returned False") } } func TestBeforeNotifyPanic(t *testing.T) { stack := middlewareStack{} err := fmt.Errorf("test") event, _ := newEvent([]interface{}{errors.New(err, 1)}, &defaultNotifier) stack.OnBeforeNotify(func(e *Event, c *Configuration) error { panic("oops") }) called := false b := &bytes.Buffer{} stack.Run(event, &Configuration{Logger: log.New(b, log.Prefix(), 0)}, func() error { called = true return nil }) logged := b.String() if logged != "bugsnag/middleware: unexpected panic: oops\n" { t.Errorf("Logged: %s", logged) } if called == false { t.Errorf("Notify was not called when BeforeNotify panicked") } } bugsnag-go-2.2.0/v2/notifier.go000066400000000000000000000121551432154711400162470ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/v2/errors" ) var publisher reportPublisher = new(defaultReportPublisher) // Notifier sends errors to Bugsnag. type Notifier struct { Config *Configuration RawData []interface{} } // New creates a new notifier. // You can pass an instance of bugsnag.Configuration in rawData to change the configuration. // Other values of rawData will be passed to Notify. func New(rawData ...interface{}) *Notifier { config := Config.clone() for i, datum := range rawData { if c, ok := datum.(Configuration); ok { config.update(&c) rawData[i] = nil } } return &Notifier{ Config: config, RawData: rawData, } } // FlushSessionsOnRepanic takes a boolean that indicates whether sessions // should be flushed when AutoNotify repanics. In the case of a fatal panic the // sessions might not get sent to Bugsnag before the application shuts down. // Many frameworks will have their own error handler, and for these frameworks // there is no need to flush sessions as the application will survive the panic // and the sessions can be sent off later. The default value is true, so this // needs only be called if you wish to inform Bugsnag that there is an error // handler that will take care of panics that AutoNotify will re-raise. func (notifier *Notifier) FlushSessionsOnRepanic(shouldFlush bool) { notifier.Config.flushSessionsOnRepanic = shouldFlush } // Notify sends an error to Bugsnag. Any rawData you pass here will be sent to // Bugsnag after being converted to JSON. e.g. bugsnag.SeverityError, bugsnag.Context, // or bugsnag.MetaData. Any bools in rawData overrides the // notifier.Config.Synchronous flag. func (notifier *Notifier) Notify(err error, rawData ...interface{}) (e error) { if e := checkForEmptyError(err); e != nil { return e } // Stripping one stackframe to not include this function in the stacktrace // for a manual notification. skipFrames := 1 return notifier.NotifySync(errors.New(err, skipFrames), notifier.Config.Synchronous, rawData...) } // NotifySync sends an error to Bugsnag. A boolean parameter specifies whether // to send the report in the current context (by default false, i.e. // asynchronous). Any other rawData you pass here will be sent to Bugsnag after // being converted to JSON. E.g. bugsnag.SeverityError, bugsnag.Context, or // bugsnag.MetaData. func (notifier *Notifier) NotifySync(err error, sync bool, rawData ...interface{}) error { if e := checkForEmptyError(err); e != nil { return e } // Stripping one stackframe to not include this function in the stacktrace // for a manual notification. skipFrames := 1 event, config := newEvent(append(rawData, errors.New(err, skipFrames), sync), notifier) // Never block, start throwing away errors if we have too many. e := middleware.Run(event, config, func() error { return publisher.publishReport(&payload{event, config}) }) if e != nil { config.logf("bugsnag.Notify: %v", e) } return e } // AutoNotify notifies Bugsnag of any panics, then repanics. // It sends along any rawData that gets passed in. // Usage: // go func() { // defer AutoNotify() // // (possibly crashy code) // }() func (notifier *Notifier) AutoNotify(rawData ...interface{}) { if err := recover(); err != nil { severity := notifier.getDefaultSeverity(rawData, SeverityError) state := HandledState{SeverityReasonHandledPanic, severity, true, ""} rawData = notifier.appendStateIfNeeded(rawData, state) // We strip the following stackframes as they don't add much // information but would mess with the grouping algorithm // { "file": "github.com/bugsnag/bugsnag-go/notifier.go", "lineNumber": 116, "method": "(*Notifier).AutoNotify" }, // { "file": "runtime/asm_amd64.s", "lineNumber": 573, "method": "call32" }, skipFrames := 2 notifier.NotifySync(errors.New(err, skipFrames), true, rawData...) panic(err) } } // Recover logs any panics, then recovers. // It sends along any rawData that gets passed in. // Usage: defer Recover() func (notifier *Notifier) Recover(rawData ...interface{}) { if err := recover(); err != nil { severity := notifier.getDefaultSeverity(rawData, SeverityWarning) state := HandledState{SeverityReasonHandledPanic, severity, false, ""} rawData = notifier.appendStateIfNeeded(rawData, state) notifier.Notify(errors.New(err, 2), rawData...) } } func (notifier *Notifier) dontPanic() { if err := recover(); err != nil { notifier.Config.logf("bugsnag/notifier.Notify: panic! %s", err) } } // Get defined severity from raw data or a fallback value func (notifier *Notifier) getDefaultSeverity(rawData []interface{}, s severity) severity { allData := append(notifier.RawData, rawData...) for _, datum := range allData { if _, ok := datum.(severity); ok { return datum.(severity) } } for _, datum := range allData { if _, ok := datum.(HandledState); ok { return datum.(HandledState).OriginalSeverity } } return s } func (notifier *Notifier) appendStateIfNeeded(rawData []interface{}, h HandledState) []interface{} { for _, datum := range append(notifier.RawData, rawData...) { if _, ok := datum.(HandledState); ok { return rawData } } return append(rawData, h) } bugsnag-go-2.2.0/v2/notifier_test.go000066400000000000000000000152401432154711400173040ustar00rootroot00000000000000package bugsnag_test import ( "fmt" "strings" "testing" simplejson "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go/v2" "github.com/bugsnag/bugsnag-go/v2/errors" . "github.com/bugsnag/bugsnag-go/v2/testutil" ) var bugsnaggedReports chan []byte func notifierSetup(url string) *bugsnag.Notifier { return bugsnag.New(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: url, Sessions: url + "/sessions"}, }) } func crash(s interface{}) int { return s.(int) } func TestStackframesAreSkippedCorrectly(t *testing.T) { ts, reports := Setup() bugsnaggedReports = reports defer ts.Close() notifier := notifierSetup(ts.URL) bugsnag.Configure(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: ts.URL, Sessions: ts.URL + "/sessions"}, }) t.Run("notifier.Notify", func(st *testing.T) { notifier.Notify(fmt.Errorf("oopsie")) assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func1", File: "notifier_test.go"}, }) }) t.Run("bugsnag.Notify", func(st *testing.T) { bugsnag.Notify(fmt.Errorf("oopsie")) assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func2", File: "notifier_test.go"}, }) }) t.Run("notifier.NotifySync", func(st *testing.T) { notifier.NotifySync(fmt.Errorf("oopsie"), true) assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func3", File: "notifier_test.go"}, }) }) t.Run("notifier.AutoNotify", func(st *testing.T) { func() { defer func() { recover() }() defer notifier.AutoNotify() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4", File: "notifier_test.go"}, }) }) t.Run("bugsnag.AutoNotify", func(st *testing.T) { func() { defer func() { recover() }() defer bugsnag.AutoNotify() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5", File: "notifier_test.go"}, }) }) // Expect the following frames to be present for *.Recover /* { "file": "runtime/panic.go", "method": "gopanic" }, { "file": "runtime/iface.go", "method": "panicdottypeE" }, { "file": "$GOPATH/src/github.com/bugsnag/bugsnag-go/notifier_test.go", "method": "TestStackframesAreSkippedCorrectly.func4.1" }, { "file": "$GOPATH/src/github.com/bugsnag/bugsnag-go/notifier_test.go", "method": "TestStackframesAreSkippedCorrectly.func4" }, { "file": "testing/testing.go", "method": "tRunner" }, { "file": "runtime/asm_amd64.s", "method": "goexit" } */ t.Run("notifier.Recover", func(st *testing.T) { func() { defer notifier.Recover() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6", File: "notifier_test.go"}, }) }) t.Run("bugsnag.Recover", func(st *testing.T) { func() { defer bugsnag.Recover() crash("NaN") }() assertStackframesMatch(t, []errors.StackFrame{ errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7", File: "notifier_test.go"}, }) }) } func TestModifyingEventsWithCallbacks(t *testing.T) { server, eventQueue := Setup() defer server.Close() notifier := notifierSetup(server.URL) bugsnag.Configure(bugsnag.Configuration{ APIKey: TestAPIKey, Endpoints: bugsnag.Endpoints{Notify: server.URL, Sessions: server.URL + "/sessions"}, }) t.Run("bugsnag.Notify change unhandled in block", func(st *testing.T) { notifier.Notify(fmt.Errorf("ahoy"), func(event *bugsnag.Event) { event.Unhandled = true }) json, _ := simplejson.NewJson(<-eventQueue) event := GetIndex(json, "events", 0) exception := GetIndex(event, "exceptions", 0) message := exception.Get("message").MustString() unhandled := event.Get("unhandled").MustBool() overridden := event.Get("severityReason").Get("unhandledOverridden").MustBool() if message != "ahoy" { st.Errorf("incorrect error message '%s'", message) } if !unhandled { st.Errorf("failed to change handled-ness in block") } if !overridden { st.Errorf("failed to set handledness change in block") } }) t.Run("bugsnag.Notify with block", func(st *testing.T) { notifier.Notify(fmt.Errorf("bnuuy"), bugsnag.Context{String: "should be overridden"}, func(event *bugsnag.Event) { event.Context = "known unknowns" }) json, _ := simplejson.NewJson(<-eventQueue) event := GetIndex(json, "events", 0) context := event.Get("context").MustString() exception := GetIndex(event, "exceptions", 0) class := exception.Get("errorClass").MustString() message := exception.Get("message").MustString() if class != "*errors.errorString" { st.Errorf("incorrect error class '%s'", class) } if message != "bnuuy" { st.Errorf("incorrect error message '%s'", message) } if context != "known unknowns" { st.Errorf("failed to change context in block. '%s'", context) } if event.Get("unhandled").MustBool() { st.Errorf("error is unexpectedly unhandled") } if overridden, err := event.Get("severityReason").Get("unhandledOverridden").Bool(); err == nil { // if err == nil, then the value existed in the payload. the expectation // is that unhandledOverridden is not sent when handled-ness is not changed. st.Errorf("error unexpectedly has unhandledOverridden: %v", overridden) } }) } func assertStackframesMatch(t *testing.T, expected []errors.StackFrame) { var lastmatch int = 0 var matched int = 0 event, _ := simplejson.NewJson(<-bugsnaggedReports) json := GetIndex(event, "events", 0) stacktrace := GetIndex(json, "exceptions", 0).Get("stacktrace") for i := 0; i < len(stacktrace.MustArray()); i++ { actualFrame := stacktrace.GetIndex(i) file := actualFrame.Get("file").MustString() method := actualFrame.Get("method").MustString() for index, expectedFrame := range expected { if index < lastmatch { continue } if strings.HasSuffix(file, expectedFrame.File) && expectedFrame.Name == method { lastmatch = index matched++ } } } if matched != len(expected) { s, _ := stacktrace.EncodePretty() t.Errorf("failed to find matches for %d frames: '%v'\ngot: '%v'", len(expected)-matched, expected[matched:], string(s)) } } bugsnag-go-2.2.0/v2/panicwrap.go000066400000000000000000000016731432154711400164170ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/v2/errors" "github.com/bugsnag/bugsnag-go/v2/sessions" "github.com/bugsnag/panicwrap" ) // Forks and re-runs your program to add panic monitoring. This function does // not return on one process, instead listening on stderr of the other process, // which returns nil. // // Related: https://godoc.org/github.com/bugsnag/panicwrap#BasicMonitor func defaultPanicHandler() { defer defaultNotifier.dontPanic() ctx := sessions.SendStartupSession(&sessionTrackingConfig) err := panicwrap.BasicMonitor(func(output string) { toNotify, err := errors.ParsePanic(output) if err != nil { defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) } state := HandledState{SeverityReasonUnhandledPanic, SeverityError, true, ""} defaultNotifier.NotifySync(toNotify, true, state, ctx) }) if err != nil { defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err) } } bugsnag-go-2.2.0/v2/panicwrap_test.go000066400000000000000000000103131432154711400174450ustar00rootroot00000000000000package bugsnag import ( "context" "os" "os/exec" "runtime" "strings" "testing" "time" "github.com/bitly/go-simplejson" "github.com/bugsnag/bugsnag-go/v2/sessions" ) // Test the panic handler by launching a new process which runs the init() // method in this file and causing a handled panic func TestPanicHandlerHandledPanic(t *testing.T) { ts, reports := setup() defer ts.Close() startPanickingProcess(t, "handled", ts.URL) json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonHandledPanic}, Unhandled: true, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "*errors.errorString", Message: "ruh roh"}}, }) event := getIndex(json, "events", 0) assertValidSession(t, event, true) stacktrace := getIndex(event, "exceptions", 0).Get("stacktrace") found := false for i := 0; i < len(stacktrace.MustArray()); i++ { frame := stacktrace.GetIndex(i) if strings.HasSuffix(getString(frame, "file"), "panicwrap_test.go") && getInt(frame, "lineNumber") != 0 { found = true break } } if !found { s, _ := stacktrace.EncodePretty() t.Errorf("no stack frame found matching this file in stack trace: %v", string(s)) } } // Test the panic handler by launching a new process which runs the init() // method in this file and causing an unhandled panic func TestPanicHandlerUnhandledPanic(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("not compatible with windows builds") return } ts, reports := setup() defer ts.Close() startPanickingProcess(t, "unhandled", ts.URL) json, err := simplejson.NewJson(<-reports) if err != nil { t.Fatal(err) } assertPayload(t, json, eventJSON{ App: &appJSON{}, Context: "", Device: &deviceJSON{Hostname: "web1"}, GroupingHash: "", Session: &sessionJSON{Events: sessions.EventCounts{Handled: 0, Unhandled: 1}}, Severity: "error", SeverityReason: &severityReasonJSON{Type: SeverityReasonUnhandledPanic}, Unhandled: true, Request: &RequestJSON{}, User: &User{}, Exceptions: []exceptionJSON{{ErrorClass: "panic", Message: "ruh roh"}}, }) } func startPanickingProcess(t *testing.T, variant string, endpoint string) { exePath, err := os.Executable() if err != nil { t.Fatal(err) } // Use the same trick as panicwrap() to re-run ourselves. // In the init() block below, we will then panic. cmd := exec.Command(exePath, os.Args[1:]...) cmd.Env = append(os.Environ(), "BUGSNAG_API_KEY="+testAPIKey, "BUGSNAG_NOTIFY_ENDPOINT="+endpoint, "please_panic="+variant) // Gift for the debugging developer: // As these tests shell out we don't see, or even want to see, the output // of these tests by default. The following two lines may be uncommented // in order to see what this command would print to stdout and stderr. /* bytes, _ := cmd.CombinedOutput() fmt.Println(string(bytes)) */ if err = cmd.Start(); err != nil { t.Fatal(err) } if err = cmd.Wait(); err.Error() != "exit status 2" { t.Fatal(err) } } func init() { if os.Getenv("please_panic") == "handled" { Configure(Configuration{ APIKey: os.Getenv("BUGSNAG_API_KEY"), Endpoints: Endpoints{Notify: os.Getenv("BUGSNAG_NOTIFY_ENDPOINT")}, Hostname: "web1", ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"}}) go func() { ctx := StartSession(context.Background()) defer AutoNotify(ctx) panick() }() // Plenty of time to crash, it shouldn't need any of it. time.Sleep(1 * time.Second) } else if os.Getenv("please_panic") == "unhandled" { Configure(Configuration{ APIKey: os.Getenv("BUGSNAG_API_KEY"), Endpoints: Endpoints{Notify: os.Getenv("BUGSNAG_NOTIFY_ENDPOINT")}, Hostname: "web1", Synchronous: true, ProjectPackages: []string{"github.com/bugsnag/bugsnag-go"}}) panick() } } func panick() { panic("ruh roh") } bugsnag-go-2.2.0/v2/payload.go000066400000000000000000000071651432154711400160660ustar00rootroot00000000000000package bugsnag import ( "bytes" "encoding/json" "fmt" "net/http" "runtime" "sync" "time" "github.com/bugsnag/bugsnag-go/v2/device" "github.com/bugsnag/bugsnag-go/v2/headers" "github.com/bugsnag/bugsnag-go/v2/sessions" ) const notifyPayloadVersion = "4" var sessionMutex sync.Mutex type payload struct { *Event *Configuration } type hash map[string]interface{} func (p *payload) deliver() error { if len(p.APIKey) != 32 { return fmt.Errorf("bugsnag/payload.deliver: invalid api key: '%s'", p.APIKey) } buf, err := p.MarshalJSON() if err != nil { return fmt.Errorf("bugsnag/payload.deliver: %v", err) } client := http.Client{ Transport: p.Transport, } req, err := http.NewRequest("POST", p.Endpoints.Notify, bytes.NewBuffer(buf)) if err != nil { return fmt.Errorf("bugsnag/payload.deliver unable to create request: %v", err) } for k, v := range headers.PrefixedHeaders(p.APIKey, notifyPayloadVersion) { req.Header.Add(k, v) } resp, err := client.Do(req) if err != nil { return fmt.Errorf("bugsnag/payload.deliver: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("bugsnag/payload.deliver: Got HTTP %s", resp.Status) } return nil } func (p *payload) MarshalJSON() ([]byte, error) { return json.Marshal(reportJSON{ APIKey: p.APIKey, Events: []eventJSON{ eventJSON{ App: &appJSON{ ReleaseStage: p.ReleaseStage, Type: p.AppType, Version: p.AppVersion, }, Context: p.Context, Device: &deviceJSON{ Hostname: p.Hostname, OsName: runtime.GOOS, RuntimeVersions: device.GetRuntimeVersions(), }, Request: p.Request, Exceptions: p.exceptions(), GroupingHash: p.GroupingHash, Metadata: p.MetaData.sanitize(p.ParamsFilters), PayloadVersion: notifyPayloadVersion, Session: p.makeSession(), Severity: p.Severity.String, SeverityReason: p.severityReasonPayload(), Unhandled: p.Unhandled, User: p.User, }, }, Notifier: notifierJSON{ Name: "Bugsnag Go", URL: "https://github.com/bugsnag/bugsnag-go", Version: Version, }, }) } func (p *payload) makeSession() *sessionJSON { // If a context has not been applied to the payload then assume that no // session has started either if p.Ctx == nil { return nil } sessionMutex.Lock() defer sessionMutex.Unlock() session := sessions.IncrementEventCountAndGetSession(p.Ctx, p.Unhandled) if session != nil { s := *session return &sessionJSON{ ID: s.ID, StartedAt: s.StartedAt.UTC().Format(time.RFC3339), Events: sessions.EventCounts{ Handled: s.EventCounts.Handled, Unhandled: s.EventCounts.Unhandled, }, } } return nil } func (p *payload) severityReasonPayload() *severityReasonJSON { if reason := p.handledState.SeverityReason; reason != "" { json := &severityReasonJSON{ Type: reason, UnhandledOverridden: p.handledState.Unhandled != p.Unhandled, } if p.handledState.Framework != "" { json.Attributes = make(map[string]string, 1) json.Attributes["framework"] = p.handledState.Framework } return json } return nil } func (p *payload) exceptions() []exceptionJSON { exceptions := []exceptionJSON{ exceptionJSON{ ErrorClass: p.ErrorClass, Message: p.Message, Stacktrace: p.Stacktrace, }, } if p.Error == nil { return exceptions } cause := p.Error.Cause for cause != nil { exceptions = append(exceptions, exceptionJSON{ ErrorClass: cause.TypeName(), Message: cause.Error(), Stacktrace: generateStacktrace(cause, p.Configuration), }) cause = cause.Cause } return exceptions } bugsnag-go-2.2.0/v2/payload_test.go000066400000000000000000000075101432154711400171170ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" "runtime" "strings" "testing" "github.com/bugsnag/bugsnag-go/v2/errors" "github.com/bugsnag/bugsnag-go/v2/sessions" ) 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 + `"}}` // The large payload has a timestamp in it which makes it awkward to assert against. // Instead, assert that the timestamp property exist, along with the rest of the expected payload 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":"` 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 + `"}}` func TestMarshalEmptyPayload(t *testing.T) { sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig) p := payload{&Event{Ctx: context.Background()}, &Configuration{}} bytes, _ := p.MarshalJSON() exp := fmt.Sprintf(expSmall, runtime.GOOS, runtime.Version()) if got := string(bytes[:]); got != exp { t.Errorf("Payload different to what was expected. \nGot: %s\nExp: %s", got, exp) } } func TestMarshalLargePayload(t *testing.T) { payload := makeLargePayload() bytes, _ := payload.MarshalJSON() got := string(bytes[:]) expPre := fmt.Sprintf(expLargePre, runtime.GOOS, runtime.Version()) if !strings.Contains(got, expPre) { t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expPre, got) } if !strings.Contains(got, expLargePost) { t.Errorf("Expected large payload to contain\n'%s'\n but was\n'%s'", expLargePost, got) } } func makeLargePayload() *payload { stackframes := []StackFrame{ {Method: "doA", File: "a.go", LineNumber: 65, InProject: false}, {Method: "fetchB", File: "b.go", LineNumber: 99, InProject: true}, {Method: "incrementI", File: "i.go", LineNumber: 651, InProject: false}, } user := User{ Id: "1234baerg134", Name: "Kool Kidz on da bus", Email: "typo@busgang.com", } handledState := HandledState{ SeverityReason: SeverityReasonUnhandledError, OriginalSeverity: severity{String: "error"}, Unhandled: true, Framework: "gin", } ctx := context.Background() ctx = StartSession(ctx) event := Event{ Error: &errors.Error{}, RawData: nil, ErrorClass: "error class", Message: "error message goes here", Stacktrace: stackframes, Context: "/api/v2/albums", Severity: SeverityInfo, GroupingHash: "custom grouping hash", User: &user, Ctx: ctx, MetaData: map[string]map[string]interface{}{ "custom tab": map[string]interface{}{ "my key": "my value", }, }, Unhandled: true, handledState: handledState, } config := Configuration{ APIKey: testAPIKey, ReleaseStage: "mega-production", AppType: "gin", AppVersion: "1.5.3", Hostname: "super.duper.site", } return &payload{&event, &config} } bugsnag-go-2.2.0/v2/report.go000066400000000000000000000050001432154711400157320ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/v2/device" "github.com/bugsnag/bugsnag-go/v2/sessions" uuid "github.com/google/uuid" ) type reportJSON struct { APIKey string `json:"apiKey"` Events []eventJSON `json:"events"` Notifier notifierJSON `json:"notifier"` } type notifierJSON struct { Name string `json:"name"` URL string `json:"url"` Version string `json:"version"` } type eventJSON struct { App *appJSON `json:"app"` Context string `json:"context,omitempty"` Device *deviceJSON `json:"device,omitempty"` Request *RequestJSON `json:"request,omitempty"` Exceptions []exceptionJSON `json:"exceptions"` GroupingHash string `json:"groupingHash,omitempty"` Metadata interface{} `json:"metaData"` PayloadVersion string `json:"payloadVersion"` Session *sessionJSON `json:"session,omitempty"` Severity string `json:"severity"` SeverityReason *severityReasonJSON `json:"severityReason,omitempty"` Unhandled bool `json:"unhandled"` User *User `json:"user,omitempty"` } type sessionJSON struct { StartedAt string `json:"startedAt"` ID uuid.UUID `json:"id"` Events sessions.EventCounts `json:"events"` } type appJSON struct { ReleaseStage string `json:"releaseStage"` Type string `json:"type,omitempty"` Version string `json:"version,omitempty"` } type exceptionJSON struct { ErrorClass string `json:"errorClass"` Message string `json:"message"` Stacktrace []StackFrame `json:"stacktrace"` } type severityReasonJSON struct { Type SeverityReason `json:"type,omitempty"` Attributes map[string]string `json:"attributes,omitempty"` UnhandledOverridden bool `json:"unhandledOverridden,omitempty"` } type deviceJSON struct { Hostname string `json:"hostname,omitempty"` OsName string `json:"osName,omitempty"` RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions,omitempty"` } // RequestJSON is the request information that populates the Request tab in the dashboard. type RequestJSON struct { ClientIP string `json:"clientIp,omitempty"` Headers map[string]string `json:"headers,omitempty"` HTTPMethod string `json:"httpMethod,omitempty"` URL string `json:"url,omitempty"` Referer string `json:"referer,omitempty"` } bugsnag-go-2.2.0/v2/report_publisher.go000066400000000000000000000011361432154711400200150ustar00rootroot00000000000000package bugsnag import "fmt" type reportPublisher interface { publishReport(*payload) error } type defaultReportPublisher struct{} func (*defaultReportPublisher) publishReport(p *payload) error { p.logf("notifying bugsnag: %s", p.Message) if !p.notifyInReleaseStage() { return fmt.Errorf("not notifying in %s", p.ReleaseStage) } if p.Synchronous { return p.deliver() } go func(p *payload) { if err := p.deliver(); err != nil { // Ensure that any errors are logged if they occur in a goroutine. p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err) } }(p) return nil } bugsnag-go-2.2.0/v2/request_extractor.go000066400000000000000000000057161432154711400202200ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "net/url" "strings" ) const requestContextKey requestKey = iota type requestKey int // AttachRequestData returns a child of the given context with the request // object attached for later extraction by the notifier in order to // automatically record request data func AttachRequestData(ctx context.Context, r *http.Request) context.Context { return context.WithValue(ctx, requestContextKey, r) } // extractRequestInfo looks for the request object that the notifier // automatically attaches to the context when using any of the supported // frameworks or bugsnag.HandlerFunc or bugsnag.Handler, and returns sub-object // supported by the notify API. func extractRequestInfo(ctx context.Context) (*RequestJSON, *http.Request) { if req := getRequestIfPresent(ctx); req != nil { return extractRequestInfoFromReq(req), req } return nil, nil } // extractRequestInfoFromReq extracts the request information the notify API // understands from the given HTTP request. Returns the sub-object supported by // the notify API. func extractRequestInfoFromReq(req *http.Request) *RequestJSON { return &RequestJSON{ ClientIP: req.RemoteAddr, HTTPMethod: req.Method, URL: sanitizeURL(req), Referer: req.Referer(), Headers: parseRequestHeaders(req.Header), } } // sanitizeURL will build up the URL matching the request. It will filter query parameters to remove sensitive fields. // The query part of the URL might appear differently (different order of parameters) if any filtering was done. func sanitizeURL(req *http.Request) string { scheme := "http" if req.TLS != nil { scheme = "https" } rawQuery := req.URL.RawQuery parsedQuery, err := url.ParseQuery(req.URL.RawQuery) if err != nil { return scheme + "://" + req.Host + req.RequestURI } changed := false for key, values := range parsedQuery { if contains(Config.ParamsFilters, key) { for i := range values { values[i] = "BUGSNAG_URL_FILTERED" changed = true } } } if changed { rawQuery = parsedQuery.Encode() rawQuery = strings.Replace(rawQuery, "BUGSNAG_URL_FILTERED", "[FILTERED]", -1) } u := url.URL{ Scheme: scheme, Host: req.Host, Path: req.URL.Path, RawQuery: rawQuery, } return u.String() } func parseRequestHeaders(header map[string][]string) map[string]string { headers := make(map[string]string) for k, v := range header { // Headers can have multiple values, in which case we report them as csv if contains(Config.ParamsFilters, k) { headers[k] = "[FILTERED]" } else { headers[k] = strings.Join(v, ",") } } return headers } func contains(slice []string, e string) bool { for _, s := range slice { if strings.Contains(strings.ToLower(e), strings.ToLower(s)) { return true } } return false } func getRequestIfPresent(ctx context.Context) *http.Request { if ctx == nil { return nil } val := ctx.Value(requestContextKey) if val == nil { return nil } return val.(*http.Request) } bugsnag-go-2.2.0/v2/request_extractor_test.go000066400000000000000000000073461432154711400212600ustar00rootroot00000000000000package bugsnag import ( "context" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestRequestInformationGetsExtracted(t *testing.T) { contexts := make(chan context.Context, 1) hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = AttachRequestData(ctx, r) contexts <- ctx }) ts := httptest.NewServer(hf) defer ts.Close() http.Get(ts.URL + "/1234abcd?fish=bird") reqJSON, req := extractRequestInfo(<-contexts) if reqJSON.ClientIP == "" { t.Errorf("expected to find an IP address for the request but was blank") } if got, exp := reqJSON.HTTPMethod, "GET"; got != exp { t.Errorf("expected HTTP method to be '%s' but was '%s'", exp, got) } if got, exp := req.URL.Path, "/1234abcd"; got != exp { t.Errorf("expected request URL to be '%s' but was '%s'", exp, got) } if got, exp := reqJSON.URL, "/1234abcd?fish=bird"; !strings.Contains(got, exp) { t.Errorf("expected request URL to contain '%s' but was '%s'", exp, got) } if got, exp := reqJSON.Referer, ""; got != exp { t.Errorf("expected request referer to be '%s' but was '%s'", exp, got) } if got, exp := reqJSON.Headers["Accept-Encoding"], "gzip"; got != exp { t.Errorf("expected Accept-Encoding to be '%s' but was '%s'", exp, got) } if got, exp := reqJSON.Headers["User-Agent"], "Go-http-client"; !strings.Contains(got, exp) { t.Errorf("expected user agent to contain '%s' but was '%s'", exp, got) } } func TestRequestExtractorCanHandleAbsentContext(t *testing.T) { if got, _ := extractRequestInfo(nil); got != nil { //really just testing that nothing panics here t.Errorf("expected nil contexts to give nil sub-objects, but was '%s'", got) } if got, _ := extractRequestInfo(context.Background()); got != nil { //really just testing that nothing panics here t.Errorf("expected contexts without requst info to give nil sub-objects, but was '%s'", got) } } func TestExtractRequestInfoFromReq_RedactURL(t *testing.T) { testCases := []struct { in url.URL exp string }{ {in: url.URL{}, exp: "http://example.com"}, {in: url.URL{Path: "/"}, exp: "http://example.com/"}, {in: url.URL{Path: "/foo.html"}, exp: "http://example.com/foo.html"}, {in: url.URL{Path: "/foo.html", RawQuery: "q=something&bar=123"}, exp: "http://example.com/foo.html?q=something&bar=123"}, {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"}, // Invalid query string. {in: url.URL{Path: "/foo", RawQuery: "%"}, exp: "http://example.com/foo?%"}, // Query params contain secrets {in: url.URL{Path: "/foo.html", RawQuery: "access_token=something"}, exp: "http://example.com/foo.html?access_token=[FILTERED]"}, {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"}, } for _, tc := range testCases { requestURI := tc.in.Path if tc.in.RawQuery != "" { requestURI += "?" + tc.in.RawQuery } req := &http.Request{ Host: "example.com", URL: &tc.in, RequestURI: requestURI, } result := extractRequestInfoFromReq(req) if result.URL != tc.exp { t.Errorf("expected URL to be '%s' but was '%s'", tc.exp, result.URL) } } } func TestParseHeadersWillSanitiseIllegalParams(t *testing.T) { headers := make(map[string][]string) headers["password"] = []string{"correct horse battery staple"} headers["secret"] = []string{"I am Banksy"} headers["authorization"] = []string{"licence to kill -9"} headers["custom-made-secret"] = []string{"I'm the insider at Sotheby's"} for k, v := range parseRequestHeaders(headers) { if v != "[FILTERED]" { t.Errorf("expected '%s' to be [FILTERED], but was '%s'", k, v) } } } bugsnag-go-2.2.0/v2/sessions/000077500000000000000000000000001432154711400157435ustar00rootroot00000000000000bugsnag-go-2.2.0/v2/sessions/config.go000066400000000000000000000104321432154711400175370ustar00rootroot00000000000000package sessions import ( "log" "net/http" "sync" "time" ) // SessionTrackingConfiguration defines the configuration options relevant for session tracking. // These are likely a subset of the global bugsnag.Configuration. Users should // not modify this struct directly but rather call // `bugsnag.Configure(bugsnag.Configuration)` which will update this configuration in return. type SessionTrackingConfiguration struct { // PublishInterval defines how often the sessions are sent off to the session server. PublishInterval time.Duration // AutoCaptureSessions can be set to false to disable automatic session // tracking. If you want control over what is deemed a session, you can // switch off automatic session tracking with this configuration, and call // bugsnag.StartSession() when appropriate for your application. See the // official docs for instructions and examples of associating handled // errors with sessions and ensuring error rate accuracy on the Bugsnag // dashboard. This will default to true, but is stored as an interface to enable // us to detect when this option has not been set. AutoCaptureSessions interface{} // APIKey defines the API key for the Bugsnag project. Same value as for reporting errors. APIKey string // Endpoint is the URI of the session server to receive session payloads. Endpoint string // Version defines the current version of the notifier. Version string // ReleaseStage defines the release stage, e.g. "production" or "staging", // that this session occurred in. The release stage, in combination with // the app version make up the release that Bugsnag tracks. ReleaseStage string // Hostname defines the host of the server this application is running on. Hostname string // AppType defines the type of the application. AppType string // AppVersion defines the version of the application. AppVersion string // Transport defines the http.RoundTripper to be used for managing HTTP requests. Transport http.RoundTripper // The release stages to notify about sessions in. If you set this then // bugsnag-go will only send sessions to Bugsnag if the release stage // is listed here. NotifyReleaseStages []string // Logger is the logger that Bugsnag should log to. Uses the same defaults // as go's builtin logging package. This logger gets invoked when any error // occurs inside the library itself. Logger interface { Printf(format string, v ...interface{}) } mutex sync.Mutex } // Update modifies the values inside the receiver to match the non-default properties of the given config. // Existing properties will not be cleared when given empty fields. func (c *SessionTrackingConfiguration) Update(config *SessionTrackingConfiguration) { c.mutex.Lock() defer c.mutex.Unlock() if config.PublishInterval != 0 { c.PublishInterval = config.PublishInterval } if config.APIKey != "" { c.APIKey = config.APIKey } if config.Endpoint != "" { c.Endpoint = config.Endpoint } if config.Version != "" { c.Version = config.Version } if config.ReleaseStage != "" { c.ReleaseStage = config.ReleaseStage } if config.Hostname != "" { c.Hostname = config.Hostname } if config.AppType != "" { c.AppType = config.AppType } if config.AppVersion != "" { c.AppVersion = config.AppVersion } if config.Transport != nil { c.Transport = config.Transport } if config.Logger != nil { c.Logger = config.Logger } if config.NotifyReleaseStages != nil { c.NotifyReleaseStages = config.NotifyReleaseStages } if config.AutoCaptureSessions != nil { c.AutoCaptureSessions = config.AutoCaptureSessions } } func (c *SessionTrackingConfiguration) logf(fmt string, args ...interface{}) { if c != nil && c.Logger != nil { c.Logger.Printf(fmt, args...) } else { log.Printf(fmt, args...) } } // IsAutoCaptureSessions identifies whether or not the notifier should // automatically capture sessions as requests come in. It's a convenience // wrapper that allows automatic session capturing to be enabled by default. func (c *SessionTrackingConfiguration) IsAutoCaptureSessions() bool { if c.AutoCaptureSessions == nil { return true // enabled by default } if val, ok := c.AutoCaptureSessions.(bool); ok { return val } // It has been configured to *something* (although not a valid value) // assume the user wanted to disable this option. return false } bugsnag-go-2.2.0/v2/sessions/config_test.go000066400000000000000000000051771432154711400206100ustar00rootroot00000000000000package sessions import ( "net/http" "reflect" "testing" "time" ) func TestConfigDoesNotChangeGivenBlankValues(t *testing.T) { c := testConfig() exp := testConfig() c.Update(&SessionTrackingConfiguration{}) tt := []struct { name string expected interface{} got interface{} }{ {"PublishInterval", exp.PublishInterval, c.PublishInterval}, {"APIKey", exp.APIKey, c.APIKey}, {"Endpoint", exp.Endpoint, c.Endpoint}, {"Version", exp.Version, c.Version}, {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, {"Hostname", exp.Hostname, c.Hostname}, {"AppType", exp.AppType, c.AppType}, {"AppVersion", exp.AppVersion, c.AppVersion}, {"Transport", exp.Transport, c.Transport}, {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, } for _, tc := range tt { if !reflect.DeepEqual(tc.got, tc.expected) { t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) } } } func TestConfigUpdatesGivenNonDefaultValues(t *testing.T) { c := testConfig() exp := SessionTrackingConfiguration{ PublishInterval: 40 * time.Second, APIKey: "api234", Endpoint: "https://docs.bugsnag.com/platforms/go/", Version: "2.7.3", ReleaseStage: "Production", Hostname: "Brian's Surface", AppType: "Revel API", AppVersion: "6.3.9", NotifyReleaseStages: []string{"staging", "production"}, } c.Update(&exp) tt := []struct { name string expected interface{} got interface{} }{ {"PublishInterval", exp.PublishInterval, c.PublishInterval}, {"APIKey", exp.APIKey, c.APIKey}, {"Endpoint", exp.Endpoint, c.Endpoint}, {"Version", exp.Version, c.Version}, {"ReleaseStage", exp.ReleaseStage, c.ReleaseStage}, {"Hostname", exp.Hostname, c.Hostname}, {"AppType", exp.AppType, c.AppType}, {"AppVersion", exp.AppVersion, c.AppVersion}, {"NotifyReleaseStages", exp.NotifyReleaseStages, c.NotifyReleaseStages}, } for _, tc := range tt { if !reflect.DeepEqual(tc.got, tc.expected) { t.Errorf("Expected '%s' to be '%v' but was '%v'", tc.name, tc.expected, tc.got) } } } func testConfig() SessionTrackingConfiguration { return SessionTrackingConfiguration{ PublishInterval: 20 * time.Second, APIKey: "api123", Endpoint: "https://bugsnag.com/jobs", //If you like what you see... ;) Version: "1.6.2", ReleaseStage: "Staging", Hostname: "Russ's MacbookPro", AppType: "Gin API", AppVersion: "5.2.8", NotifyReleaseStages: []string{"staging", "production"}, Transport: http.DefaultTransport, } } bugsnag-go-2.2.0/v2/sessions/integration_test.go000066400000000000000000000101031432154711400216470ustar00rootroot00000000000000package sessions_test import ( "context" "io/ioutil" "net/http" "net/http/httptest" "os" "runtime" "strings" "sync" "testing" "time" simplejson "github.com/bitly/go-simplejson" bugsnag "github.com/bugsnag/bugsnag-go/v2" ) const testAPIKey = "166f5ad3590596f9aa8d601ea89af845" const testPublishInterval = time.Millisecond * 200 const sessionsCount = 50000 func init() { //Naughty injection to achieve a reasonable test duration. bugsnag.DefaultSessionPublishInterval = testPublishInterval } func get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } func getInt(j *simplejson.Json, path string) int { return get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return get(j, path).MustString() } func getIndex(j *simplejson.Json, path string, index int) *simplejson.Json { return get(j, path).GetIndex(index) } // Spins up a session server and checks that for every call to // bugsnag.StartSession() a session is being recorded. func TestStartSession(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("not compatible with windows builds") return } sessionsStarted := 0 mutex := sync.Mutex{} // Test server does all the checking of individual requests ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertCorrectHeaders(t, r) body, err := ioutil.ReadAll(r.Body) if err != nil { t.Error(err) } json, err := simplejson.NewJson(body) if err != nil { t.Error(err) } hostname, _ := os.Hostname() tt := []struct { prop string exp interface{} }{ {prop: "notifier.name", exp: "Bugsnag Go"}, {prop: "notifier.url", exp: "https://github.com/bugsnag/bugsnag-go"}, {prop: "notifier.version", exp: bugsnag.Version}, {prop: "app.releaseStage", exp: "production"}, {prop: "app.version", exp: ""}, {prop: "device.osName", exp: runtime.GOOS}, {prop: "device.hostname", exp: hostname}, {prop: "device.runtimeVersions.go", exp: runtime.Version()}, {prop: "device.runtimeVersions.gin", exp: ""}, {prop: "device.runtimeVersions.martini", exp: ""}, {prop: "device.runtimeVersions.negroni", exp: ""}, {prop: "device.runtimeVersions.revel", exp: ""}, } for _, tc := range tt { got := getString(json, tc.prop) if got != tc.exp { t.Errorf("Expected '%s' to be '%s' but was '%s'", tc.prop, tc.exp, got) } } sessionCounts := getIndex(json, "sessionCounts", 0) if got := getString(sessionCounts, "startedAt"); len(got) != 20 { t.Errorf("Expected 'sessionCounts.startedAt' to be valid timestamp but was %s", got) } mutex.Lock() defer mutex.Unlock() sessionsStarted += getInt(sessionCounts, "sessionsStarted") w.WriteHeader(http.StatusAccepted) })) defer ts.Close() time.Sleep(testPublishInterval * 2) //Allow server to start // Minimal config. API is mandatory, URLs point to the test server bugsnag.Configure(bugsnag.Configuration{ APIKey: testAPIKey, Endpoints: bugsnag.Endpoints{ Sessions: ts.URL, Notify: ts.URL, }, }) for i := 0; i < sessionsCount; i++ { bugsnag.StartSession(context.Background()) } time.Sleep(testPublishInterval * 2) //Allow all messages to be processed mutex.Lock() defer mutex.Unlock() // Don't expect an additional session from startup as the test server URL // would be different between processes if got, exp := sessionsStarted, sessionsCount; got != exp { t.Errorf("Expected %d sessions started, but was %d", exp, got) } } func assertCorrectHeaders(t *testing.T, req *http.Request) { testCases := []struct{ name, expected string }{ {name: "Bugsnag-Payload-Version", expected: "1.0"}, {name: "Content-Type", expected: "application/json"}, {name: "Bugsnag-Api-Key", expected: testAPIKey}, } for _, tc := range testCases { t.Run(tc.name, func(st *testing.T) { if got := req.Header[tc.name][0]; tc.expected != got { t.Errorf("Expected header '%s' to be '%s' but was '%s'", tc.name, tc.expected, got) } }) } name := "Bugsnag-Sent-At" if req.Header[name][0] == "" { t.Errorf("Expected header '%s' to be non-empty but was empty", name) } } bugsnag-go-2.2.0/v2/sessions/payload.go000066400000000000000000000044301432154711400177240ustar00rootroot00000000000000package sessions import ( "runtime" "time" "github.com/bugsnag/bugsnag-go/v2/device" ) // notifierPayload defines the .notifier subobject of the payload type notifierPayload struct { Name string `json:"name"` URL string `json:"url"` Version string `json:"version"` } // appPayload defines the .app subobject of the payload type appPayload struct { Type string `json:"type,omitempty"` ReleaseStage string `json:"releaseStage,omitempty"` Version string `json:"version,omitempty"` } // devicePayload defines the .device subobject of the payload type devicePayload struct { OsName string `json:"osName,omitempty"` Hostname string `json:"hostname,omitempty"` RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions"` } // sessionCountsPayload defines the .sessionCounts subobject of the payload type sessionCountsPayload struct { StartedAt string `json:"startedAt"` SessionsStarted int `json:"sessionsStarted"` } // sessionPayload defines the top level payload object type sessionPayload struct { Notifier *notifierPayload `json:"notifier"` App *appPayload `json:"app"` Device *devicePayload `json:"device"` SessionCounts []sessionCountsPayload `json:"sessionCounts"` } // makeSessionPayload creates a sessionPayload based off of the given sessions and config func makeSessionPayload(sessions []*Session, config *SessionTrackingConfiguration) *sessionPayload { releaseStage := config.ReleaseStage if releaseStage == "" { releaseStage = "production" } hostname := config.Hostname if hostname == "" { hostname = device.GetHostname() } return &sessionPayload{ Notifier: ¬ifierPayload{ Name: "Bugsnag Go", URL: "https://github.com/bugsnag/bugsnag-go", Version: config.Version, }, App: &appPayload{ Type: config.AppType, Version: config.AppVersion, ReleaseStage: releaseStage, }, Device: &devicePayload{ OsName: runtime.GOOS, Hostname: hostname, RuntimeVersions: device.GetRuntimeVersions(), }, SessionCounts: []sessionCountsPayload{ { //This timestamp assumes that we're sending these off once a minute StartedAt: sessions[0].StartedAt.UTC().Format(time.RFC3339), SessionsStarted: len(sessions), }, }, } } bugsnag-go-2.2.0/v2/sessions/publisher.go000066400000000000000000000050541432154711400202730ustar00rootroot00000000000000package sessions import ( "bytes" "encoding/json" "fmt" "net/http" "github.com/bugsnag/bugsnag-go/v2/headers" ) // sessionPayloadVersion defines the current version of the payload that's // being sent to the session server. const sessionPayloadVersion = "1.0" type sessionPublisher interface { publish(sessions []*Session) error } type httpClient interface { Do(*http.Request) (*http.Response, error) } type publisher struct { config *SessionTrackingConfiguration client httpClient } // publish builds a payload from the given sessions and publishes them to the // session server. Returns any errors that happened as part of publishing. func (p *publisher) publish(sessions []*Session) error { if p.config.Endpoint == "" { // Session tracking is disabled, likely because the notify endpoint was // changed without changing the sessions endpoint // We've already logged a warning in this case, so no need to spam the // log every minute return nil } if apiKey := p.config.APIKey; len(apiKey) != 32 { return fmt.Errorf("bugsnag/sessions/publisher.publish invalid API key: '%s'", apiKey) } nrs, rs := p.config.NotifyReleaseStages, p.config.ReleaseStage if rs != "" && (nrs != nil && !contains(nrs, rs)) { // Always send sessions if the release stage is not set, but don't send any // sessions when notify release stages don't match the current release stage return nil } if len(sessions) == 0 { return fmt.Errorf("bugsnag/sessions/publisher.publish requested publication of 0") } p.config.mutex.Lock() defer p.config.mutex.Unlock() payload := makeSessionPayload(sessions, p.config) buf, err := json.Marshal(payload) if err != nil { return fmt.Errorf("bugsnag/sessions/publisher.publish unable to marshal json: %v", err) } req, err := http.NewRequest("POST", p.config.Endpoint, bytes.NewBuffer(buf)) if err != nil { return fmt.Errorf("bugsnag/sessions/publisher.publish unable to create request: %v", err) } for k, v := range headers.PrefixedHeaders(p.config.APIKey, sessionPayloadVersion) { req.Header.Add(k, v) } res, err := p.client.Do(req) if err != nil { return fmt.Errorf("bugsnag/sessions/publisher.publish unable to deliver session: %v", err) } defer func(res *http.Response) { if err := res.Body.Close(); err != nil { p.config.logf("%v", err) } }(res) if res.StatusCode != 202 { return fmt.Errorf("bugsnag/session.publish expected 202 response status, got HTTP %s", res.Status) } return nil } func contains(coll []string, e string) bool { for _, s := range coll { if s == e { return true } } return false } bugsnag-go-2.2.0/v2/sessions/publisher_test.go000066400000000000000000000160011432154711400213240ustar00rootroot00000000000000package sessions import ( "io" "io/ioutil" "net/http" "os" "runtime" "strings" "testing" "time" simplejson "github.com/bitly/go-simplejson" uuid "github.com/google/uuid" ) const ( sessionEndpoint = "http://localhost:9181" testAPIKey = "166f5ad3590596f9aa8d601ea89af845" ) type testHTTPClient struct { reqs []*http.Request } // A simple io.ReadCloser that we can inject as a body of a http.Request. type nopCloser struct { io.Reader } func (nopCloser) Close() error { return nil } func (c *testHTTPClient) Do(r *http.Request) (*http.Response, error) { c.reqs = append(c.reqs, r) return &http.Response{Body: nopCloser{}, StatusCode: 202}, nil } func get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } func getInt(j *simplejson.Json, path string) int { return get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return get(j, path).MustString() } func getIndex(j *simplejson.Json, path string, index int) *simplejson.Json { return get(j, path).GetIndex(index) } func TestSendsCorrectPayloadForSmallConfig(t *testing.T) { sessions, earliestTime := makeSessions() testClient := testHTTPClient{} publisher := publisher{ config: &SessionTrackingConfiguration{Endpoint: sessionEndpoint, Transport: http.DefaultTransport, APIKey: testAPIKey}, client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } req := testClient.reqs[0] assertCorrectHeaders(t, req) body, err := ioutil.ReadAll(req.Body) if err != nil { t.Fatal(err) } root, err := simplejson.NewJson(body) if err != nil { t.Fatal(err) } hostname, _ := os.Hostname() for prop, exp := range map[string]string{ "notifier.name": "Bugsnag Go", "notifier.url": "https://github.com/bugsnag/bugsnag-go", "notifier.version": "", "app.type": "", "app.releaseStage": "production", "app.version": "", "device.osName": runtime.GOOS, "device.hostname": hostname, } { t.Run(prop, func(st *testing.T) { if got := getString(root, prop); got != exp { t.Errorf("Expected property '%s' in JSON to be '%v' but was '%v'", prop, exp, got) } }) } sessionCounts := getIndex(root, "sessionCounts", 0) if got, exp := getString(sessionCounts, "startedAt"), earliestTime; got != exp { t.Errorf("Expected sessionCounts[0].startedAt to be '%s' but was '%s'", exp, got) } if got, exp := getInt(sessionCounts, "sessionsStarted"), len(sessions); got != exp { t.Errorf("Expected sessionCounts[0].sessionsStarted to be %d but was %d", exp, got) } } func TestSendsCorrectPayloadForBigConfig(t *testing.T) { sessions, earliestTime := makeSessions() testClient := testHTTPClient{} publisher := publisher{ config: makeHeavyConfig(), client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } req := testClient.reqs[0] assertCorrectHeaders(t, req) body, err := ioutil.ReadAll(req.Body) if err != nil { t.Fatal(err) } root, err := simplejson.NewJson(body) if err != nil { t.Fatal(err) } for prop, exp := range map[string]string{ "notifier.name": "Bugsnag Go", "notifier.url": "https://github.com/bugsnag/bugsnag-go", "notifier.version": "2.3.4-alpha", "app.type": "gin", "app.releaseStage": "development", "app.version": "1.2.3-beta", "device.osName": runtime.GOOS, "device.hostname": "gce-1234-us-west-1", } { t.Run(prop, func(st *testing.T) { if got := getString(root, prop); got != exp { t.Errorf("Expected property '%s' in JSON to be '%v' but was '%v'", prop, exp, got) } }) } sessionCounts := getIndex(root, "sessionCounts", 0) if got, exp := getString(sessionCounts, "startedAt"), earliestTime; got != exp { t.Errorf("Expected sessionCounts[0].startedAt to be '%s' but was '%s'", exp, got) } if got, exp := getInt(sessionCounts, "sessionsStarted"), len(sessions); got != exp { t.Errorf("Expected sessionCounts[0].sessionsStarted to be %d but was %d", exp, got) } } func TestNoSessionsSentWhenAPIKeyIsMissing(t *testing.T) { sessions, _ := makeSessions() config := makeHeavyConfig() config.APIKey = "labracadabrador" publisher := publisher{config: config, client: &testHTTPClient{}} if err := publisher.publish(sessions); err != nil { if got, exp := err.Error(), "bugsnag/sessions/publisher.publish invalid API key: 'labracadabrador'"; got != exp { t.Errorf(`Expected error message "%s" but got "%s"`, exp, got) } } else { t.Errorf("Expected error message but no errors were returned") } } func TestNoSessionsOutsideNotifyReleaseStages(t *testing.T) { sessions, _ := makeSessions() testClient := testHTTPClient{} config := makeHeavyConfig() config.NotifyReleaseStages = []string{"staging", "production"} publisher := publisher{ config: config, client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } if got := len(testClient.reqs); got != 0 { t.Errorf("Didn't expect any sessions being sent as as 'development' is outside of the notify release stages, but got %d sessions", got) } } func TestReleaseStageNotSetSendsSessionsRegardlessOfNotifyReleaseStages(t *testing.T) { sessions, _ := makeSessions() testClient := testHTTPClient{} config := makeHeavyConfig() config.NotifyReleaseStages = []string{"staging", "production"} config.ReleaseStage = "" publisher := publisher{ config: config, client: &testClient, } err := publisher.publish(sessions) if err != nil { t.Error(err) } if exp, got := 1, len(testClient.reqs); got != exp { t.Errorf("Expected %d sessions sent when the release stage is \"\" regardless of notify release stage, but got %d", exp, got) } } func makeHeavyConfig() *SessionTrackingConfiguration { return &SessionTrackingConfiguration{ AppType: "gin", APIKey: testAPIKey, AppVersion: "1.2.3-beta", Version: "2.3.4-alpha", Endpoint: sessionEndpoint, Transport: http.DefaultTransport, ReleaseStage: "development", Hostname: "gce-1234-us-west-1", NotifyReleaseStages: []string{"development"}, } } func makeSessions() ([]*Session, string) { earliestTime := time.Now().Add(-6 * time.Minute) return []*Session{ {StartedAt: earliestTime, ID: uuid.New()}, {StartedAt: earliestTime.Add(2 * time.Minute), ID: uuid.New()}, {StartedAt: earliestTime.Add(4 * time.Minute), ID: uuid.New()}, }, earliestTime.UTC().Format(time.RFC3339) } func assertCorrectHeaders(t *testing.T, req *http.Request) { testCases := []struct{ name, expected string }{ {name: "Bugsnag-Payload-Version", expected: "1.0"}, {name: "Content-Type", expected: "application/json"}, {name: "Bugsnag-Api-Key", expected: testAPIKey}, } for _, tc := range testCases { t.Run(tc.name, func(st *testing.T) { if got := req.Header[tc.name][0]; tc.expected != got { t.Errorf("Expected header '%s' to be '%s' but was '%s'", tc.name, tc.expected, got) } }) } name := "Bugsnag-Sent-At" if req.Header[name][0] == "" { t.Errorf("Expected header '%s' to be non-empty but was empty", name) } } bugsnag-go-2.2.0/v2/sessions/session.go000066400000000000000000000010731432154711400177560ustar00rootroot00000000000000package sessions import ( "time" uuid "github.com/google/uuid" ) // EventCounts register how many handled/unhandled events have happened for // this session type EventCounts struct { Handled int `json:"handled"` Unhandled int `json:"unhandled"` } // Session represents a start time and a unique ID that identifies the session. type Session struct { StartedAt time.Time ID uuid.UUID EventCounts *EventCounts } func newSession() *Session { return &Session{ StartedAt: time.Now(), ID: uuid.New(), EventCounts: &EventCounts{}, } } bugsnag-go-2.2.0/v2/sessions/startup.go000066400000000000000000000020561432154711400177770ustar00rootroot00000000000000package sessions import ( "context" "net/http" "os" "github.com/bugsnag/panicwrap" ) // SendStartupSession is called by Bugsnag on startup, which will send a // session to Bugsnag and return a context to represent the session of the main // goroutine. This is the session associated with any fatal panics that are // caught by panicwrap. func SendStartupSession(config *SessionTrackingConfiguration) context.Context { ctx := context.Background() session := newSession() if !config.IsAutoCaptureSessions() || isApplicationProcess() { return ctx } publisher := &publisher{ config: config, client: &http.Client{Transport: config.Transport}, } go publisher.publish([]*Session{session}) return context.WithValue(ctx, contextSessionKey, session) } // Checks to see if this is the application process, as opposed to the process // that monitors for panics func isApplicationProcess() bool { // Application process is run first, and this will only have been set when // the monitoring process runs return "" == os.Getenv(panicwrap.DEFAULT_COOKIE_KEY) } bugsnag-go-2.2.0/v2/sessions/tracker.go000066400000000000000000000072501432154711400177310ustar00rootroot00000000000000package sessions import ( "context" "net/http" "os" "os/signal" "sync" "syscall" "time" ) const ( //contextSessionKey is a unique key for accessing and setting Bugsnag //session data on a context.Context object contextSessionKey ctxKey = 1 ) // ctxKey is a type alias that ensures uniqueness as a context.Context key type ctxKey int // SessionTracker exposes a method for starting sessions that are used for // gauging your application's health type SessionTracker interface { StartSession(context.Context) context.Context FlushSessions() } type sessionTracker struct { sessionChannel chan *Session sessions []*Session config *SessionTrackingConfiguration publisher sessionPublisher sessionsMutex sync.Mutex } // NewSessionTracker creates a new SessionTracker based on the provided config, func NewSessionTracker(config *SessionTrackingConfiguration) SessionTracker { publisher := publisher{ config: config, client: &http.Client{Transport: config.Transport}, } st := sessionTracker{ sessionChannel: make(chan *Session, 1), sessions: []*Session{}, config: config, publisher: &publisher, } go st.processSessions() return &st } // IncrementEventCountAndGetSession extracts a Bugsnag session from the given // context and increments the event count of unhandled or handled events and // returns the session func IncrementEventCountAndGetSession(ctx context.Context, unhandled bool) *Session { if s := ctx.Value(contextSessionKey); s != nil { if session, ok := s.(*Session); ok && !session.StartedAt.IsZero() { // It is not just getting back a default value ec := session.EventCounts if unhandled { ec.Unhandled++ } else { ec.Handled++ } return session } } return nil } func (s *sessionTracker) StartSession(ctx context.Context) context.Context { session := newSession() s.sessionChannel <- session return context.WithValue(ctx, contextSessionKey, session) } func (s *sessionTracker) interval() time.Duration { s.config.mutex.Lock() defer s.config.mutex.Unlock() return s.config.PublishInterval } func (s *sessionTracker) processSessions() { tic := time.Tick(s.interval()) shutdown := shutdownSignals() for { select { case session := <-s.sessionChannel: s.appendSession(session) case <-tic: s.publishCollectedSessions() case sig := <-shutdown: s.flushSessionsAndRepeatSignal(shutdown, sig.(syscall.Signal)) } } } func (s *sessionTracker) appendSession(session *Session) { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() s.sessions = append(s.sessions, session) } func (s *sessionTracker) publishCollectedSessions() { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() oldSessions := s.sessions s.sessions = nil if len(oldSessions) > 0 { go func(s *sessionTracker) { err := s.publisher.publish(oldSessions) if err != nil { s.config.logf("%v", err) } }(s) } } func (s *sessionTracker) flushSessionsAndRepeatSignal(shutdown chan<- os.Signal, sig syscall.Signal) { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() signal.Stop(shutdown) if len(s.sessions) > 0 { err := s.publisher.publish(s.sessions) if err != nil { s.config.logf("%v", err) } } if p, err := os.FindProcess(os.Getpid()); err != nil { s.config.logf("%v", err) } else { p.Signal(sig) } } func (s *sessionTracker) FlushSessions() { s.sessionsMutex.Lock() defer s.sessionsMutex.Unlock() sessions := s.sessions s.sessions = nil if len(sessions) != 0 { if err := s.publisher.publish(sessions); err != nil { s.config.logf("%v", err) } } } func shutdownSignals() chan os.Signal { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) return c } bugsnag-go-2.2.0/v2/sessions/tracker_test.go000066400000000000000000000045151432154711400207710ustar00rootroot00000000000000package sessions import ( "context" "sync" "testing" "time" ) type testPublisher struct { mutex sync.Mutex sessionsReceived [][]*Session } var pub = testPublisher{ mutex: sync.Mutex{}, sessionsReceived: [][]*Session{}, } func (pub *testPublisher) publish(sessions []*Session) error { pub.mutex.Lock() defer pub.mutex.Unlock() pub.sessionsReceived = append(pub.sessionsReceived, sessions) return nil } func TestStartSessionModifiesContext(t *testing.T) { type ctxKey string var k ctxKey k, v := "key", "val" st, c := makeSessionTracker() defer close(c) ctx := st.StartSession(context.WithValue(context.Background(), k, v)) if got, exp := ctx.Value(k), v; got != exp { t.Errorf("Changed pre-existing key '%s' with value '%s' into %s", k, v, got) } if got := ctx.Value(contextSessionKey); got == nil { t.Fatalf("No session information applied to context %v", ctx) } verifyValidSession(t, IncrementEventCountAndGetSession(ctx, true)) } func TestShouldOnlyWriteWhenReceivingSessions(t *testing.T) { st, c := makeSessionTracker() defer close(c) go st.processSessions() time.Sleep(10 * st.config.PublishInterval) // Would publish many times in this time period if there were sessions if got := pub.sessionsReceived; len(got) != 0 { t.Errorf("pub was invoked unexpectedly %d times with arguments: %v", len(got), got) } for i := 0; i < 50000; i++ { st.StartSession(context.Background()) } time.Sleep(st.config.PublishInterval * 2) var sessions []*Session pub.mutex.Lock() defer pub.mutex.Unlock() for _, s := range pub.sessionsReceived { for _, session := range s { verifyValidSession(t, session) sessions = append(sessions, session) } } if exp, got := 50000, len(sessions); exp != got { t.Errorf("Expected %d sessions but got %d", exp, got) } } func makeSessionTracker() (*sessionTracker, chan *Session) { c := make(chan *Session, 1) return &sessionTracker{ config: &SessionTrackingConfiguration{ PublishInterval: time.Millisecond * 10, //Publish very fast }, sessionChannel: c, sessions: []*Session{}, publisher: &pub, }, c } func verifyValidSession(t *testing.T, s *Session) { if (s.StartedAt == time.Time{}) { t.Errorf("Expected start time to be set but was nil") } if len(s.ID) != 16 { t.Errorf("Expected UUID to be a valid V4 UUID but was %s", s.ID) } } bugsnag-go-2.2.0/v2/testutil/000077500000000000000000000000001432154711400157525ustar00rootroot00000000000000bugsnag-go-2.2.0/v2/testutil/json.go000066400000000000000000000077121432154711400172610ustar00rootroot00000000000000// Package testutil can be .-imported to gain access to useful test functions. package testutil import ( "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" "time" simplejson "github.com/bitly/go-simplejson" ) // TestAPIKey is a fake API key that can be used for testing const TestAPIKey = "166f5ad3590596f9aa8d601ea89af845" // Setup sets up and returns a test event server for receiving the event payloads. // report payloads published to the returned server's URL will be put on the returned channel func Setup() (*httptest.Server, chan []byte) { reports := make(chan []byte, 10) return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "sessions") { return } body, _ := ioutil.ReadAll(r.Body) reports <- body })), reports } // Get travels through a JSON object and returns the specified node func Get(j *simplejson.Json, path string) *simplejson.Json { return j.GetPath(strings.Split(path, ".")...) } // GetIndex returns the n-th element of the specified path inside the given JSON object func GetIndex(j *simplejson.Json, path string, n int) *simplejson.Json { return Get(j, path).GetIndex(n) } func getBool(j *simplejson.Json, path string) bool { return Get(j, path).MustBool() } func getInt(j *simplejson.Json, path string) int { return Get(j, path).MustInt() } func getString(j *simplejson.Json, path string) string { return Get(j, path).MustString() } // AssertPayload compares the payload that was received by the event-server to // the expected report JSON payload func AssertPayload(t *testing.T, report *simplejson.Json, expPretty string) { expReport, err := simplejson.NewJson([]byte(expPretty)) if err != nil { t.Fatal(err) } expEvent := GetIndex(expReport, "events", 0) expException := GetIndex(expEvent, "exceptions", 0) event := GetIndex(report, "events", 0) exception := GetIndex(event, "exceptions", 0) if exp, got := getBool(expEvent, "unhandled"), getBool(event, "unhandled"); got != exp { t.Errorf("expected 'unhandled' to be '%v' but got '%v'", exp, got) } for _, tc := range []struct { prop string got, exp *simplejson.Json }{ {got: report, exp: expReport, prop: "apiKey"}, {got: report, exp: expReport, prop: "notifier.name"}, {got: report, exp: expReport, prop: "notifier.version"}, {got: report, exp: expReport, prop: "notifier.url"}, {got: exception, exp: expException, prop: "message"}, {got: exception, exp: expException, prop: "errorClass"}, {got: event, exp: expEvent, prop: "user.id"}, {got: event, exp: expEvent, prop: "severity"}, {got: event, exp: expEvent, prop: "severityReason.type"}, {got: event, exp: expEvent, prop: "metaData.request.httpMethod"}, {got: event, exp: expEvent, prop: "metaData.request.url"}, {got: event, exp: expEvent, prop: "request.httpMethod"}, {got: event, exp: expEvent, prop: "request.url"}, {got: event, exp: expEvent, prop: "request.referer"}, {got: event, exp: expEvent, prop: "request.headers.Accept-Encoding"}, } { if got, exp := getString(tc.got, tc.prop), getString(tc.exp, tc.prop); got != exp { t.Errorf("expected '%s' to be '%s' but was '%s'", tc.prop, exp, got) } } assertValidSession(t, event, getBool(expEvent, "unhandled")) } func assertValidSession(t *testing.T, event *simplejson.Json, unhandled bool) { if sessionID := getString(event, "session.id"); len(sessionID) != 36 { t.Errorf("Expected a valid session ID to be set but was '%s'", sessionID) } if _, e := time.Parse(time.RFC3339, getString(event, "session.startedAt")); e != nil { t.Error(e) } expHandled, expUnhandled := 1, 0 if unhandled { expHandled, expUnhandled = expUnhandled, expHandled } if got := getInt(event, "session.events.unhandled"); got != expUnhandled { t.Errorf("Expected %d unhandled events in session but was %d", expUnhandled, got) } if got := getInt(event, "session.events.handled"); got != expHandled { t.Errorf("Expected %d handled events in session but was %d", expHandled, got) } }