pax_global_header00006660000000000000000000000064147054320610014514gustar00rootroot0000000000000052 comment=be3f0e2f00bab2c010346ca44ff53cfaa864198b bugsnag-go-2.5.1/000077500000000000000000000000001470543206100135525ustar00rootroot00000000000000bugsnag-go-2.5.1/.github/000077500000000000000000000000001470543206100151125ustar00rootroot00000000000000bugsnag-go-2.5.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001470543206100172755ustar00rootroot00000000000000bugsnag-go-2.5.1/.github/ISSUE_TEMPLATE/A.md000066400000000000000000000007621470543206100200040ustar00rootroot00000000000000--- 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.5.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000020261470543206100217670ustar00rootroot00000000000000--- 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.5.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000013251470543206100230230ustar00rootroot00000000000000--- 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.5.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000003441470543206100207140ustar00rootroot00000000000000## Goal ## Design ## Changeset ## Testing bugsnag-go-2.5.1/.github/support.md000066400000000000000000000021531470543206100171510ustar00rootroot00000000000000## 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.5.1/.github/workflows/000077500000000000000000000000001470543206100171475ustar00rootroot00000000000000bugsnag-go-2.5.1/.github/workflows/license-audit.yml000066400000000000000000000017561470543206100224310ustar00rootroot00000000000000name: 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.5.1/.github/workflows/test-package.yml000066400000000000000000000037451470543206100222530ustar00rootroot00000000000000name: 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', '1.17', '1.18', '1.19', '1.20', '1.21', '1.22'] 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' - name: set GO111MODULE run: | bash -c 'echo "GO111MODULE=on" >> $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 $(go list ./... | grep -v /features/) - name: vet package # go1.12 vet shows spurious 'unknown identifier' issues if: matrix.go-version != '1.12' run: go vet $(go list ./... | grep -v /features/) - name: install integration dependencies if: matrix.os == 'ubuntu' run: | sudo apt-get update sudo apt-get install libcurl4-openssl-dev - name: install Ruby if: matrix.os == 'ubuntu' uses: ruby/setup-ruby@v1 with: ruby-version: '3.2' bundler-cache: true working-directory: go/src/github.com/bugsnag/bugsnag-go # relative to $GITHUB_WORKSPACE - name: maze tests working-directory: go/src/github.com/bugsnag/bugsnag-go if: matrix.os == 'ubuntu' env: GO_VERSION: ${{ matrix.go-version }} run: bundle exec maze-runner --color --format progressbugsnag-go-2.5.1/.gitignore000066400000000000000000000002041470543206100155360ustar00rootroot00000000000000# Ignore maze runner generated files maze_output vendor # ignore the gemfile to prevent testing against stale versions Gemfile.lockbugsnag-go-2.5.1/CHANGELOG.md000066400000000000000000000313751470543206100153740ustar00rootroot00000000000000# Changelog ## 2.5.1 (2024-10-21) ### Bug fixes * Move start of delivery goroutine to configure, don't wait on signals in delivery [#250](https://github.com/bugsnag/bugsnag-go/pull/250) ## 2.5.0 (2024-08-27) ### Enhancements * Limit resource usage while sending events asynchronously \ Added MainContext configuration option for providing context from main app [#231](https://github.com/bugsnag/bugsnag-go/pull/231) ## 2.4.0 (2024-04-15) ### Enhancements * Sanitize for metadata should also handler json and []byte [#226](https://github.com/bugsnag/bugsnag-go/pull/226) [Chris Duncan](https://github.com/veqryn) ## 2.3.1 (2024-03-18) ### Bug fixes * Handle empty pointers to complex structs in metadata.Add [#221](https://github.com/bugsnag/bugsnag-go/pull/221) ## 2.3.0 (2024-03-05) ### Bug fixes * Start showing inlined functions in stack trace [#208](https://github.com/bugsnag/bugsnag-go/pull/208) * Handle complex structs in metadata [#215](https://github.com/bugsnag/bugsnag-go/pull/215) [Chris Duncan](https://github.com/veqryn) * Stop trimming everything before "main.go" on main packages [#217](https://github.com/bugsnag/bugsnag-go/pull/217) [Chris Duncan](https://github.com/veqryn) ## 2.2.1 (2022-02-21) ### Bug fixes * Fix middleware panic on nil *http.Request [#212](https://github.com/bugsnag/bugsnag-go/pull/212) ## 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.5.1/CONTRIBUTING.md000066400000000000000000000063031470543206100160050ustar00rootroot00000000000000Contributing ============ - [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.5.1/Gemfile000066400000000000000000000001031470543206100150370ustar00rootroot00000000000000source 'https://rubygems.org' gem "bugsnag-maze-runner", "~> 9.14"bugsnag-go-2.5.1/LICENSE.txt000066400000000000000000000020331470543206100153730ustar00rootroot00000000000000Copyright (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.5.1/LOCAL_TESTING.md000066400000000000000000000011611470543206100161220ustar00rootroot00000000000000 ## Unit tests * Install old golang version (do not install just 1.11 - it's not compatible with running newer modules): ``` ASDF_GOLANG_OVERWRITE_ARCH=amd64 asdf install golang 1.11.13 ``` * If you see error below use `CGO_ENABLED=0`. ``` # crypto/x509 malformed DWARF TagVariable entry ``` ## Local testing with maze runner * Maze runner tests require * Specyfing `GO_VERSION` env variable to set a golang version for docker container. * Ruby 2.7. * Running docker. * Commands to run tests ``` bundle install bundle exec bugsnag-maze-runner bundle exec bugsnag-maze-runner -c features/ ```bugsnag-go-2.5.1/Makefile000066400000000000000000000030461470543206100152150ustar00rootroot00000000000000TEST?=./... 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.5.1/README.md000066400000000000000000000046001470543206100150310ustar00rootroot00000000000000
SmartBear BugSnag logo

Error monitoring and reporting for Go

[![Documentation](https://img.shields.io/badge/documentation-latest-blue.svg)](https://docs.bugsnag.com/performance/go/) [![Go Reference](https://pkg.go.dev/badge/github.com/bugsnag/bugsnag-go.svg)](https://pkg.go.dev/github.com/bugsnag/bugsnag-go) [![Build status](https://github.com/bugsnag/bugsnag-go/actions/workflows/test-package.yml/badge.svg?branch=master)](https://buildkite.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: * [Gin](https://docs.bugsnag.com/platforms/go/gin/) * [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 * [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 error reporter for Go is free software released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details. bugsnag-go-2.5.1/UPGRADING.md000066400000000000000000000030051470543206100154120ustar00rootroot00000000000000# 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.5.1/bugsnag.go000066400000000000000000000237431470543206100155400ustar00rootroot00000000000000package 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.5.1/bugsnag_example_test.go000066400000000000000000000075101470543206100203040ustar00rootroot00000000000000package 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.5.1/bugsnag_test.go000066400000000000000000000504041470543206100165710ustar00rootroot00000000000000package 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.5.1/configuration.go000066400000000000000000000221161470543206100167520ustar00rootroot00000000000000package 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 dashboard. 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.5.1/configuration_test.go000066400000000000000000000233461470543206100200170ustar00rootroot00000000000000package 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.5.1/device/000077500000000000000000000000001470543206100150115ustar00rootroot00000000000000bugsnag-go-2.5.1/device/hostname.go000066400000000000000000000005371470543206100171630ustar00rootroot00000000000000package 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.5.1/device/runtimeversions.go000066400000000000000000000026741470543206100206250ustar00rootroot00000000000000package 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.5.1/device/runtimeversions_test.go000066400000000000000000000024001470543206100216470ustar00rootroot00000000000000package 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.5.1/doc.go000066400000000000000000000045731470543206100146570ustar00rootroot00000000000000/* 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://docs.bugsnag.com/platforms/go. # Configuration The only required configuration is the BugSnag API key which can be obtained by clicking "Project Settings" on the top of your BugSnag dashboard 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 events, 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.5.1/errors/000077500000000000000000000000001470543206100150665ustar00rootroot00000000000000bugsnag-go-2.5.1/errors/README.md000066400000000000000000000003651470543206100163510ustar00rootroot00000000000000Adds 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.5.1/errors/error.go000066400000000000000000000107701470543206100165530ustar00rootroot00000000000000// 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.5.1/errors/error_test.go000066400000000000000000000221471470543206100176130ustar00rootroot00000000000000package 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.5.1/errors/parse_panic.go000066400000000000000000000063071470543206100177070ustar00rootroot00000000000000package 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.5.1/errors/parse_panic_test.go000066400000000000000000000240401470543206100207400ustar00rootroot00000000000000package 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.5.1/errors/stackframe.go000066400000000000000000000050101470543206100175310ustar00rootroot00000000000000package 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.5.1/event.go000066400000000000000000000155241470543206100152310ustar00rootroot00000000000000package 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.5.1/event_test.go000066400000000000000000000016071470543206100162650ustar00rootroot00000000000000package 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.5.1/examples/000077500000000000000000000000001470543206100153705ustar00rootroot00000000000000bugsnag-go-2.5.1/examples/README.md000066400000000000000000000013371470543206100166530ustar00rootroot00000000000000# 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.5.1/examples/http/000077500000000000000000000000001470543206100163475ustar00rootroot00000000000000bugsnag-go-2.5.1/examples/http/README.md000066400000000000000000000012011470543206100176200ustar00rootroot00000000000000# 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 mod tidy 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.5.1/examples/http/go.mod000066400000000000000000000005061470543206100174560ustar00rootroot00000000000000module github/bugsnag/bugsnag-go/example/http go 1.19 require github.com/bugsnag/bugsnag-go/v2 v2.2.0 require ( github.com/bugsnag/panicwrap v1.3.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/pkg/errors v0.9.1 // indirect ) bugsnag-go-2.5.1/examples/http/go.sum000066400000000000000000000026521470543206100175070ustar00rootroot00000000000000github.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/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bugsnag/bugsnag-go/v2 v2.2.0 h1:y4JJ6xNJiK4jbmq/BLXe09MGUNRp/r1Zpye6RKcPJJ8= github.com/bugsnag/bugsnag-go/v2 v2.2.0/go.mod h1:Aoi1ax1kGbbkArShzXUQjxp6jM8gMh4qOtHLis/jY1E= 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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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.5.1/examples/http/main.go000066400000000000000000000023271470543206100176260ustar00rootroot00000000000000package 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.5.1/examples/using-goroutines/000077500000000000000000000000001470543206100207115ustar00rootroot00000000000000bugsnag-go-2.5.1/examples/using-goroutines/README.md000066400000000000000000000010331470543206100221650ustar00rootroot00000000000000# 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.5.1/examples/using-goroutines/main.go000066400000000000000000000021521470543206100221640ustar00rootroot00000000000000package 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.5.1/features/000077500000000000000000000000001470543206100153705ustar00rootroot00000000000000bugsnag-go-2.5.1/features/apptype.feature000066400000000000000000000013661470543206100204350ustar00rootroot00000000000000Feature: Configuring app type Background: Given I set environment variable "BUGSNAG_APP_TYPE" to "background-queue" Scenario: An error report contains the configured app type when running a go app Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error 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 "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "SendSessionScenario" And I wait to receive 2 sessions And the session payload field "app.type" equals "background-queue" bugsnag-go-2.5.1/features/appversion.feature000066400000000000000000000013351470543206100211350ustar00rootroot00000000000000Feature: Configuring app version Background: And I set environment variable "BUGSNAG_APP_VERSION" to "3.1.2" Scenario: An error report contains the configured app type when running a go app Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error And the event "app.version" equals "3.1.2" Scenario: A session report contains the configured app type when running a go app Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "SendSessionScenario" And I wait to receive 2 sessions And the session payload field "app.version" equals "3.1.2"bugsnag-go-2.5.1/features/autonotify.feature000066400000000000000000000010211470543206100211400ustar00rootroot00000000000000Feature: Using auto notify Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered When I start the service "app" And I run "AutonotifyPanicScenario" And I wait to receive 2 errors And the exception "errorClass" equals "*errors.errorString" And the exception "message" equals "Go routine killed with auto notify" And I discard the oldest error And the exception "errorClass" equals "panic" And the exception "message" equals "Go routine killed with auto notify [recovered]"bugsnag-go-2.5.1/features/configuration.feature000066400000000000000000000143351470543206100216220ustar00rootroot00000000000000Feature: 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" 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 start the service "app" And I run "" And I wait to receive an error And the event "" equals "" Examples: | testcase | variable | value | field | | AutoconfigPanicScenario | BUGSNAG_APP_VERSION | 1.4.34 | app.version | | AutoconfigPanicScenario | BUGSNAG_APP_TYPE | mailer-daemon | app.type | | AutoconfigPanicScenario | BUGSNAG_RELEASE_STAGE | beta1 | app.releaseStage | | AutoconfigPanicScenario | BUGSNAG_HOSTNAME | dream-machine-2 | device.hostname | | AutoconfigPanicScenario | BUGSNAG_METADATA_device_instance | kube2-33-A | metaData.device.instance | | AutoconfigPanicScenario | BUGSNAG_METADATA_framework_version | v3.1.0 | metaData.framework.version | | AutoconfigPanicScenario | BUGSNAG_METADATA_device_runtime_level | 1C | metaData.device.runtime_level | | AutoconfigPanicScenario | BUGSNAG_METADATA_Carrot | orange | metaData.custom.Carrot | | AutoconfigHandledScenario | BUGSNAG_APP_VERSION | 1.4.34 | app.version | | AutoconfigHandledScenario | BUGSNAG_APP_TYPE | mailer-daemon | app.type | | AutoconfigHandledScenario | BUGSNAG_RELEASE_STAGE | beta1 | app.releaseStage | | AutoconfigHandledScenario | BUGSNAG_HOSTNAME | dream-machine-2 | device.hostname | | AutoconfigHandledScenario | BUGSNAG_METADATA_device_instance | kube2-33-A | metaData.device.instance | | AutoconfigHandledScenario | BUGSNAG_METADATA_framework_version | v3.1.0 | metaData.framework.version | | AutoconfigHandledScenario | BUGSNAG_METADATA_device_runtime_level | 1C | metaData.device.runtime_level | | AutoconfigHandledScenario | 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 start the service "app" And I run "AutoconfigPanicScenario" And I wait to receive an error And the "file" of stack frame 0 equals "features/fixtures/app/autoconfig_scenario.go" And the "method" of stack frame 0 equals "AutoconfigPanicScenario.func1" And the "lineNumber" of stack frame 0 equals 11 And the "file" of stack frame 1 equals "features/fixtures/app/main.go" And the "method" of stack frame 1 equals "main" And the "lineNumber" of stack frame 1 equals 65 Scenario: Configuring source root Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "AutoconfigPanicScenario" And I wait to receive an error And the "file" of stack frame 0 equals "autoconfig_scenario.go" And the "method" of stack frame 0 equals "AutoconfigPanicScenario.func1" And the "lineNumber" of stack frame 0 equals 11 And the "file" of stack frame 1 equals "main.go" And the "method" of stack frame 1 equals "main" And the "lineNumber" of stack frame 1 equals 65 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" When I start the service "app" And I run "AutoconfigPanicScenario" And I wait to receive an error 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" When I start the service "app" And I run "AutoconfigPanicScenario" Then I should receive no errors 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" When I start the service "app" And I run "AutoconfigPanicScenario" Then I should receive no errors 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 start the service "app" And I run "AutoconfigHandledScenario" And I wait to receive an error 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 start the service "app" And I run "AutoconfigMetadataScenario" And I wait to receive an error And the event "metaData.fruit.Tomato" equals "[FILTERED]" And the event "metaData.snacks.Carrot" equals "4"bugsnag-go-2.5.1/features/fixtures/000077500000000000000000000000001470543206100172415ustar00rootroot00000000000000bugsnag-go-2.5.1/features/fixtures/app/000077500000000000000000000000001470543206100200215ustar00rootroot00000000000000bugsnag-go-2.5.1/features/fixtures/app/Dockerfile000066400000000000000000000022441470543206100220150ustar00rootroot00000000000000ARG GO_VERSION FROM golang:${GO_VERSION}-alpine RUN apk update && apk upgrade && apk add git bash build-base ENV GOPATH /app ENV GO111MODULE="on" COPY features /app/src/features COPY v2 /app/src/github.com/bugsnag/bugsnag-go/v2 WORKDIR /app/src/github.com/bugsnag/bugsnag-go/v2 # Ensure subsequent steps are re-run if the GO_VERSION variable changes ARG GO_VERSION # Get bugsnag dependencies using a conditional call to run go get or go install based on the go version RUN if [[ $(echo -e "1.11\n$GO_VERSION\n1.16" | sort -V | head -2 | tail -1) == "$GO_VERSION" ]]; then \ echo "Version is between 1.11 and 1.16, running go get"; \ go get ./...; \ else \ echo "Version is greater than 1.16, running go install"; \ go install ./...; \ fi WORKDIR /app/src/features/fixtures/app # Create app module - avoid locking bugsnag dep by not checking it in # Skip on old versions of Go which pre-date modules RUN go mod init && go mod tidy && \ echo "replace github.com/bugsnag/bugsnag-go/v2 => /app/src/github.com/bugsnag/bugsnag-go/v2" >> go.mod && \ go mod tidy RUN chmod +x run.sh CMD ["/app/src/features/fixtures/app/run.sh"]bugsnag-go-2.5.1/features/fixtures/app/autoconfig_scenario.go000066400000000000000000000013201470543206100243650ustar00rootroot00000000000000package main import ( "fmt" bugsnag "github.com/bugsnag/bugsnag-go/v2" ) func AutoconfigPanicScenario(command Command) func() { scenarioFunc := func() { panic("PANIQ!") } return scenarioFunc } func AutoconfigHandledScenario(command Command) func() { scenarioFunc := func() { bugsnag.Notify(fmt.Errorf("gone awry!")) } return scenarioFunc } func AutoconfigMetadataScenario(command Command) func() { scenarioFunc := func() { bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error { event.MetaData.Add("fruit", "Tomato", "beefsteak") event.MetaData.Add("snacks", "Carrot", "4") return nil }) bugsnag.Notify(fmt.Errorf("gone awry!")) } return scenarioFunc }bugsnag-go-2.5.1/features/fixtures/app/command.go000066400000000000000000000020421470543206100217640ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "net/http" "time" ) const DEFAULT_MAZE_ADDRESS = "http://localhost:9339" type Command struct { Action string `json:"action,omitempty"` ScenarioName string `json:"scenario_name,omitempty"` APIKey string `json:"api_key,omitempty"` NotifyEndpoint string `json:"notify_endpoint,omitempty"` SessionsEndpoint string `json:"sessions_endpoint,omitempty"` UUID string `json:"uuid,omitempty"` RunUUID string `json:"run_uuid,omitempty"` } func GetCommand(mazeAddress string) Command { var command Command mazeURL := fmt.Sprintf("%+v/command", mazeAddress) client := http.Client{Timeout: 2 * time.Second} res, err := client.Get(mazeURL) if err != nil { fmt.Printf("[Bugsnag] Error while receiving command: %+v\n", err) return command } if res != nil { err = json.NewDecoder(res.Body).Decode(&command) res.Body.Close() if err != nil { fmt.Printf("[Bugsnag] Error while decoding command: %+v\n", err) return command } } return command } bugsnag-go-2.5.1/features/fixtures/app/handled_scenario.go000066400000000000000000000052001470543206100236270ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "os" "github.com/bugsnag/bugsnag-go/v2" ) func HandledErrorScenario(command Command) func() { scenarioFunc := func() { 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) } } } return scenarioFunc } func MultipleHandledErrorsScenario(command Command) func() { //Make the order of the below predictable bugsnag.Configure(bugsnag.Configuration{Synchronous: true}) scenarioFunc := func() { ctx := bugsnag.StartSession(context.Background()) bugsnag.Notify(fmt.Errorf("oops"), ctx) bugsnag.Notify(fmt.Errorf("oops"), ctx) } return scenarioFunc } func NestedHandledErrorScenario(command Command) func() { scenarioFunc := func() { if err := Login("token " + os.Getenv("API_KEY")); err != nil { bugsnag.Notify(NewCustomErr("terminate process", err)) } 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\n", err, val) } if val, err := CheckValue(i - 46); err != nil { fmt.Printf("err: %v, val: %d\n", err, val) } log.Fatalf("This test is broken - no error was generated.") } } return scenarioFunc } func HandledCallbackErrorScenario(command Command) func() { scenarioFunc := func() { 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 }) } return scenarioFunc } func HandledToUnhandledScenario(command Command) func() { scenarioFunc := func() { bugsnag.Notify(fmt.Errorf("unknown event"), func(event *bugsnag.Event) { event.Unhandled = true event.Severity = bugsnag.SeverityError }) } return scenarioFunc } func OnBeforeNotifyScenario(command Command) func() { bugsnag.Configure(bugsnag.Configuration{Synchronous: true}) scenarioFunc := func() { 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")) bugsnag.Notify(fmt.Errorf("don't ignore this error")) bugsnag.Notify(fmt.Errorf("change error message")) } return scenarioFunc } bugsnag-go-2.5.1/features/fixtures/app/main.go000066400000000000000000000044411470543206100212770ustar00rootroot00000000000000package main import ( "fmt" "os" "time" "github.com/bugsnag/bugsnag-go/v2" ) var scenariosMap = map[string]func(Command) func(){ "UnhandledCrashScenario": UnhandledCrashScenario, "HandledErrorScenario": HandledErrorScenario, "MultipleUnhandledErrorsScenario": MultipleUnhandledErrorsScenario, "MultipleHandledErrorsScenario": MultipleHandledErrorsScenario, "NestedHandledErrorScenario": NestedHandledErrorScenario, "MetadataScenario": MetadataScenario, "FilteredMetadataScenario": FilteredMetadataScenario, "HandledCallbackErrorScenario": HandledCallbackErrorScenario, "SendSessionScenario": SendSessionScenario, "HandledToUnhandledScenario": HandledToUnhandledScenario, "SetUserScenario": SetUserScenario, "RecoverAfterPanicScenario": RecoverAfterPanicScenario, "AutonotifyPanicScenario": AutonotifyPanicScenario, "SessionAndErrorScenario": SessionAndErrorScenario, "OnBeforeNotifyScenario": OnBeforeNotifyScenario, "AutoconfigPanicScenario": AutoconfigPanicScenario, "AutoconfigHandledScenario": AutoconfigHandledScenario, "AutoconfigMetadataScenario": AutoconfigMetadataScenario, "HttpServerScenario": HttpServerScenario, } func main() { addr := os.Getenv("DEFAULT_MAZE_ADDRESS") if addr == "" { addr = DEFAULT_MAZE_ADDRESS } endpoints := bugsnag.Endpoints{ Notify: fmt.Sprintf("%+v/notify", addr), Sessions: fmt.Sprintf("%+v/sessions", addr), } // HAS TO RUN FIRST BECAUSE OF PANIC WRAP // https://github.com/bugsnag/panicwrap/blob/master/panicwrap.go#L177-L203 bugsnag.Configure(bugsnag.Configuration{ APIKey: "166f5ad3590596f9aa8d601ea89af845", Endpoints: endpoints, }) // Increase publish rate for testing bugsnag.DefaultSessionPublishInterval = time.Millisecond * 50 // Listening to the OS Signals ticker := time.NewTicker(1 * time.Second) for { select { case <-ticker.C: command := GetCommand(addr) fmt.Printf("[Bugsnag] Received command: %+v\n", command) if command.Action != "run-scenario" { continue } prepareScenarioFunc, ok := scenariosMap[command.ScenarioName] if ok { scenarioFunc := prepareScenarioFunc(command) scenarioFunc() time.Sleep(200 * time.Millisecond) } } } } bugsnag-go-2.5.1/features/fixtures/app/metadata_scenario.go000066400000000000000000000011771470543206100240210ustar00rootroot00000000000000package main import ( "fmt" "github.com/bugsnag/bugsnag-go/v2" ) func MetadataScenario(command Command) func() { scenarioFunc := func() { customerData := map[string]string{"Name": "Joe Bloggs", "Age": "21"} bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ "Scheme": { "Customer": customerData, "Level": "Blue", }, }) } return scenarioFunc } func FilteredMetadataScenario(command Command) func() { scenarioFunc := func() { bugsnag.Notify(fmt.Errorf("oops"), bugsnag.MetaData{ "Account": { "Name": "Company XYZ", "Price(dollars)": "1 Million", }, }) } return scenarioFunc } bugsnag-go-2.5.1/features/fixtures/app/nethttp_scenario.go000066400000000000000000000052311470543206100237220ustar00rootroot00000000000000package main import ( "context" "fmt" "log" "net/http" "os" "time" "github.com/bugsnag/bugsnag-go/v2" ) func HttpServerScenario(Command) func() { scenarioFunc := func() { 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(":4512", recoverWrap(bugsnag.Handler(nil))) } return scenarioFunc } // 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 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.5.1/features/fixtures/app/panic_scenario.go000066400000000000000000000006471470543206100233340ustar00rootroot00000000000000package main import ( "github.com/bugsnag/bugsnag-go/v2" ) func AutonotifyPanicScenario(command Command) func() { scenarioFunc := func() { defer bugsnag.AutoNotify() panic("Go routine killed with auto notify") } return scenarioFunc } func RecoverAfterPanicScenario(command Command) func() { scenarioFunc := func() { defer bugsnag.Recover() panic("Go routine killed but recovered") } return scenarioFunc }bugsnag-go-2.5.1/features/fixtures/app/run.sh000077500000000000000000000010141470543206100211600ustar00rootroot00000000000000#!/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 go run .bugsnag-go-2.5.1/features/fixtures/app/session_scenario.go000066400000000000000000000006561470543206100237250ustar00rootroot00000000000000package main import ( "context" "fmt" "github.com/bugsnag/bugsnag-go/v2" ) func SendSessionScenario(command Command) func() { scenarioFunc := func() { bugsnag.StartSession(context.Background()) } return scenarioFunc } func SessionAndErrorScenario(command Command) func() { scenarioFunc := func() { ctx := bugsnag.StartSession(context.Background()) bugsnag.Notify(fmt.Errorf("oops"), ctx) } return scenarioFunc }bugsnag-go-2.5.1/features/fixtures/app/unhandled_scenario.go000066400000000000000000000014071470543206100241770ustar00rootroot00000000000000package main import ( "context" "fmt" "github.com/bugsnag/bugsnag-go/v2" ) //go:noinline func UnhandledCrashScenario(command Command) func() { scenarioFunc := func() { fmt.Printf("Calling panic\n") // Invalid type assertion, will panic func(a interface{}) string { return a.(string) }(struct{}{}) } return scenarioFunc } func MultipleUnhandledErrorsScenario(command Command) func() { scenarioFunc := func() { //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") } return scenarioFunc }bugsnag-go-2.5.1/features/fixtures/app/user_scenario.go000066400000000000000000000004641470543206100232150ustar00rootroot00000000000000package main import ( "fmt" "github.com/bugsnag/bugsnag-go/v2" ) func SetUserScenario(command Command) func() { scenarioFunc := func() { bugsnag.Notify(fmt.Errorf("oops"), bugsnag.User{ Id: "test-user-id", Name: "test-user-name", Email: "test-user-email", }) } return scenarioFunc }bugsnag-go-2.5.1/features/fixtures/app/utils.go000066400000000000000000000015551470543206100215160ustar00rootroot00000000000000package main import ( "fmt" "runtime" ) 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 Login(token string) error { val, err := CheckValue(len(token) * -1) if err != nil { return NewCustomErr("login failed", err) } fmt.Printf("val: %d\n", 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.5.1/features/fixtures/docker-compose.yml000066400000000000000000000060771470543206100227100ustar00rootroot00000000000000services: app: build: context: ../../ dockerfile: ./features/fixtures/app/Dockerfile args: - GO_VERSION ports: - "4512:4512" environment: - DEFAULT_MAZE_ADDRESS - ERROR_CLASS - 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" 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 - BUGSNAG_SOURCE_ROOT - BUGSNAG_PROJECT_PACKAGES 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 - BUGSNAG_SOURCE_ROOT - BUGSNAG_PROJECT_PACKAGES 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 - BUGSNAG_SOURCE_ROOT - BUGSNAG_PROJECT_PACKAGES 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 - BUGSNAG_SOURCE_ROOT - BUGSNAG_PROJECT_PACKAGES restart: "no" command: ./test/run.sh bugsnag-go-2.5.1/features/handled.feature000066400000000000000000000060251470543206100203470ustar00rootroot00000000000000Feature: Plain handled errors Background: Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" Scenario: A handled error sends a report When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledError" And the exception "errorClass" matches "\*os.PathError|\*fs.PathError" And the "file" of stack frame 0 equals "handled_scenario.go" 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 "app" And I run "HandledErrorScenario" And I wait to receive an error 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 "handled_scenario.go" Scenario: Sending an event using a callback to modify report contents When I start the service "app" And I run "HandledCallbackErrorScenario" And I wait to receive an error 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 "handled_scenario.go" And the "lineNumber" of stack frame 0 equals 59 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 start the service "app" And I run "HandledToUnhandledScenario" And I wait to receive an error 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 "handled_scenario.go" And the "lineNumber" of stack frame 0 equals 72 Scenario: Unwrapping the causes of a handled error When I start the service "app" And I run "NestedHandledErrorScenario" And I wait to receive an error And the event "unhandled" is false And the event "severity" equals "warning" And the event "exceptions.0.message" equals "terminate process" And the "lineNumber" of stack frame 0 equals 40 And the "file" of stack frame 0 equals "handled_scenario.go" And the "method" of stack frame 0 equals "NestedHandledErrorScenario.func1" And the event "exceptions.1.message" equals "login failed" And the event "exceptions.1.stacktrace.0.file" equals "utils.go" And the event "exceptions.1.stacktrace.0.lineNumber" equals 39 And the event "exceptions.2.message" equals "invalid token" And the event "exceptions.2.stacktrace.0.file" equals "utils.go" And the event "exceptions.2.stacktrace.0.lineNumber" equals 47bugsnag-go-2.5.1/features/hostname.feature000066400000000000000000000013651470543206100205700ustar00rootroot00000000000000Feature: Configuring hostname Scenario: An error report contains the configured hostname Given I set environment variable "BUGSNAG_HOSTNAME" to "server-1a" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error And the event "device.hostname" equals "server-1a" Scenario: A session report contains the configured hostname Given I set environment variable "BUGSNAG_HOSTNAME" to "server-1a" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "SendSessionScenario" And I wait to receive 2 sessions And the session payload field "device.hostname" equals "server-1a"bugsnag-go-2.5.1/features/metadata.feature000066400000000000000000000006551470543206100205330ustar00rootroot00000000000000Feature: Sending meta data Scenario: An error report contains custom meta data When I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I start the service "app" And I run "MetadataScenario" And I wait to receive an error 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.5.1/features/multieventsession.feature000066400000000000000000000014251470543206100225470ustar00rootroot00000000000000Feature: Reporting multiple handled and unhandled errors in the same session Background: Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" Scenario: Handled errors know about previous reported handled errors When I start the service "app" And I run "MultipleHandledErrorsScenario" And I wait to receive 2 errors And the event handled sessions count equals 1 And I discard the oldest error And the event handled sessions count equals 2 Scenario: Unhandled errors know about previous reported handled errors When I start the service "app" And I run "MultipleUnhandledErrorsScenario" And I wait to receive 2 errors And the event unhandled sessions count equals 1 And I discard the oldest error And the event unhandled sessions count equals 2bugsnag-go-2.5.1/features/net-http/000077500000000000000000000000001470543206100171335ustar00rootroot00000000000000bugsnag-go-2.5.1/features/net-http/appversion.feature000066400000000000000000000017421470543206100227020ustar00rootroot00000000000000Feature: Configuring app version Background: And I set environment variable "BUGSNAG_APP_VERSION" to "3.1.2" Scenario: A error report contains the configured app type when using a net http app Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/handled" And I wait to receive an error And I should receive no sessions 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 "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/session" And I wait to receive a session And the session payload field "app.version" equals "3.1.2" bugsnag-go-2.5.1/features/net-http/autonotify.feature000066400000000000000000000022471470543206100227160ustar00rootroot00000000000000Feature: Using auto notify Scenario: An error report is sent when an AutoNotified crash occurs which later gets recovered Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/autonotify-then-recover" Then I wait to receive an error 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 "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/autonotify" Then I wait to receive an error 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.5.1/features/net-http/handled.feature000066400000000000000000000023661470543206100221160ustar00rootroot00000000000000Feature: Handled errors Background: Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" Scenario: A handled error sends a report When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/handled" Then I wait to receive an error And the event "unhandled" is false And the event "severity" equals "warning" And the event "severityReason.type" equals "handledError" And the exception "errorClass" matches "\*os.PathError|\*fs.PathError" And the "file" of stack frame 0 equals "nethttp_scenario.go" 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 "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/handled" Then I wait to receive an error 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 "nethttp_scenario.go"bugsnag-go-2.5.1/features/net-http/onbeforenotify.feature000066400000000000000000000010061470543206100235350ustar00rootroot00000000000000Feature: Configuring on before notify Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/onbeforenotify" Then I wait to receive 2 errors And the exception "message" equals "Don't ignore this error" And I discard the oldest error And the exception "message" equals "Error message was changed"bugsnag-go-2.5.1/features/net-http/recover.feature000066400000000000000000000006641470543206100221630ustar00rootroot00000000000000Feature: Using recover Scenario: An error report is sent when request crashes but is recovered When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/recover" Then I wait to receive an error And the exception "errorClass" equals "*errors.errorString" And the exception "message" equals "Request killed but recovered"bugsnag-go-2.5.1/features/net-http/releasestage.feature000066400000000000000000000016531470543206100231610ustar00rootroot00000000000000Feature: Configuring release stage Background: Given I set environment variable "BUGSNAG_RELEASE_STAGE" to "my-stage" Scenario: An error report is sent with configured release stage Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/handled" Then I wait to receive an error And the event "app.releaseStage" equals "my-stage" Scenario: A session report contains the configured app type Given I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/session" Then I wait to receive a session And the session payload field "app.releaseStage" equals "my-stage"bugsnag-go-2.5.1/features/net-http/request.feature000066400000000000000000000011241470543206100221760ustar00rootroot00000000000000Feature: Capturing request information automatically Scenario: An error report will automatically contain request information When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/handled" Then I wait to receive an error 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.5.1/features/net-http/user.feature000066400000000000000000000006711470543206100214720ustar00rootroot00000000000000Feature: Sending user data Scenario: An error report contains custom user data When I start the service "app" And I run "HttpServerScenario" And I wait for the host "localhost" to open port "4512" And I open the URL "http://localhost:4512/user" Then I wait to receive an error 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.5.1/features/onbeforenotify.feature000066400000000000000000000006231470543206100217760ustar00rootroot00000000000000Feature: Configuring on before notify Scenario: Send three bugsnags and use on before notify to drop one and modify the message of another When I start the service "app" And I run "OnBeforeNotifyScenario" And I wait to receive 2 errors And the exception "message" equals "don't ignore this error" And I discard the oldest error And the exception "message" equals "error message was changed"bugsnag-go-2.5.1/features/paramfilters.feature000066400000000000000000000021331470543206100214350ustar00rootroot00000000000000Feature: Configuring param filters 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 "BUGSNAG_PARAMS_FILTERS" to "Name" When I start the service "app" And I run "FilteredMetadataScenario" And I wait to receive an error 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 "BUGSNAG_PARAMS_FILTERS" to "Price(dollars)" When I start the service "app" And I run "FilteredMetadataScenario" And I wait to receive an error 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 "BUGSNAG_PARAMS_FILTERS" to "Price" When I start the service "app" And I run "FilteredMetadataScenario" And I wait to receive an error And the event "metaData.Account.Price(dollars)" equals "[FILTERED]" bugsnag-go-2.5.1/features/plain_features/000077500000000000000000000000001470543206100203715ustar00rootroot00000000000000bugsnag-go-2.5.1/features/plain_features/panics.feature000066400000000000000000000017061470543206100232270ustar00rootroot00000000000000Feature: Panic handling Background: Given I set environment variable "BUGSNAG_SOURCE_ROOT" to "/app/src/features/fixtures/app/" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" Scenario: Capturing a panic When I start the service "app" And I run "UnhandledCrashScenario" And I wait to receive an error 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" matches "^interface conversion: interface.*?is struct {}, not string" And the "file" of stack frame 0 equals "unhandled_scenario.go" And the "method" of stack frame 0 equals "UnhandledCrashScenario.func1.1" And the "file" of stack frame 1 equals "unhandled_scenario.go" And the "method" of stack frame 1 equals "UnhandledCrashScenario.func1"bugsnag-go-2.5.1/features/recover.feature000066400000000000000000000005171470543206100204150ustar00rootroot00000000000000Feature: Using recover Scenario: An error report is sent when a go routine crashes but recovers When I start the service "app" And I run "RecoverAfterPanicScenario" And I wait to receive an error And the exception "errorClass" equals "*errors.errorString" And the exception "message" equals "Go routine killed but recovered"bugsnag-go-2.5.1/features/releasestage.feature000066400000000000000000000065001470543206100214120ustar00rootroot00000000000000Feature: Configuring release stages and notify release stages Scenario: An error report is sent when release stage matches notify release stages Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error 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 "BUGSNAG_RELEASE_STAGE" to "stage2" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error 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 "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" When I start the service "app" And I run "HandledErrorScenario" And I wait to receive an error Scenario: An error report is not sent if the release stage does not match the notify release stages Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "0" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage4" When I start the service "app" And I run "HandledErrorScenario" And I should receive no errors Scenario: A session report is sent when release stage matches notify release stages Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" When I start the service "app" And I run "SendSessionScenario" And I wait to receive 2 sessions And the session payload field "app.releaseStage" equals "stage2" Scenario: A session report is sent when no notify release stages are specified Given I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage2" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "SendSessionScenario" And I wait to receive 2 sessions And the session payload field "app.releaseStage" equals "stage2" Scenario: A session report is sent regardless of notify release stages if release stage is not set Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" When I start the service "app" And I run "SendSessionScenario" And I wait to receive 2 sessions Scenario: A session report is not sent if the release stage does not match the notify release stages Given I set environment variable "BUGSNAG_NOTIFY_RELEASE_STAGES" to "stage1,stage2,stage3" And I set environment variable "BUGSNAG_AUTO_CAPTURE_SESSIONS" to "1" And I set environment variable "BUGSNAG_RELEASE_STAGE" to "stage4" When I start the service "app" And I run "SendSessionScenario" And I should receive no sessions bugsnag-go-2.5.1/features/sessioncontext.feature000066400000000000000000000006251470543206100220400ustar00rootroot00000000000000Feature: Session data inside an error report using a session context Scenario: An error report contains a session count when part of a session When I start the service "app" And I run "SessionAndErrorScenario" Then I wait to receive 1 error # one session is created on start And I wait to receive 2 session And I discard the oldest session And the session payload has a valid sessions arraybugsnag-go-2.5.1/features/steps/000077500000000000000000000000001470543206100165265ustar00rootroot00000000000000bugsnag-go-2.5.1/features/steps/go_steps.rb000066400000000000000000000026531470543206100207040ustar00rootroot00000000000000require 'net/http' require 'os' When('I run {string}') do |scenario_name| execute_command 'run-scenario', scenario_name end When('I configure the base endpoint') do steps %( When I set environment variable "DEFAULT_MAZE_ADDRESS" to "http://#{local_ip}:9339" ) end Then('the event unhandled sessions count equals {int}') do |count| step "the error payload field \"events.0.session.events.unhandled\" equals #{count}" end Then('the event handled sessions count equals {int}') do |count| step "the error payload field \"events.0.session.events.handled\" equals #{count}" end def execute_command(action, scenario_name = '') address = $address ? $address : "#{local_ip}:9339" command = { action: action, scenario_name: scenario_name, notify_endpoint: "http://#{address}/notify", sessions_endpoint: "http://#{address}/sessions", api_key: $api_key, } $logger.debug("Queuing command: #{command}") Maze::Server.commands.add command # Ensure fixture has read the command count = 900 sleep 0.1 until Maze::Server.commands.remaining.empty? || (count -= 1) < 1 raise 'Test fixture did not GET /command' unless Maze::Server.commands.remaining.empty? end def local_ip if OS.mac? 'host.docker.internal' else ip_addr = `ifconfig | grep -Eo 'inet (addr:)?([0-9]*\\\.){3}[0-9]*' | grep -v '127.0.0.1'` ip_list = /((?:[0-9]*\.){3}[0-9]*)/.match(ip_addr) ip_list.captures.first end end bugsnag-go-2.5.1/features/support/000077500000000000000000000000001470543206100171045ustar00rootroot00000000000000bugsnag-go-2.5.1/features/support/env.rb000066400000000000000000000025011470543206100202170ustar00rootroot00000000000000Before do Maze.config.enforce_bugsnag_integrity = false $address = nil $api_key = "166f5ad3590596f9aa8d601ea89af845" steps %( When I configure the base endpoint ) end Maze.config.add_validator('error') do |validator| validator.validate_header('bugsnag-api-key') { |value| value.eql?($api_key) } validator.validate_header('content-type') { |value| value.eql?('application/json') } validator.validate_header('bugsnag-payload-version') { |value| value.eql?('4') } validator.validate_header('bugsnag-sent-at') { |value| Date.iso8601(value) } validator.element_has_value('notifier.name', 'Bugsnag Go') validator.each_element_exists(['notifier.url', 'notifier.version', 'events']) validator.each_event_contains_each(['severity', 'severityReason.type', 'unhandled', 'exceptions']) end Maze.config.add_validator('session') do |validator| validator.validate_header('bugsnag-api-key') { |value| value.eql?($api_key) } validator.validate_header('content-type') { |value| value.eql?('application/json') } validator.validate_header('bugsnag-payload-version') { |value| value.eql?('1.0') } validator.validate_header('bugsnag-sent-at') { |value| Date.iso8601(value) } validator.element_has_value('notifier.name', 'Bugsnag Go') validator.each_element_exists(['notifier.url', 'notifier.version', 'app', 'device']) end bugsnag-go-2.5.1/features/user.feature000066400000000000000000000005111470543206100177200ustar00rootroot00000000000000Feature: Sending user data Scenario: An error report contains custom user data When I start the service "app" And I run "SetUserScenario" And I wait to receive an error 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.5.1/gin/000077500000000000000000000000001470543206100143275ustar00rootroot00000000000000bugsnag-go-2.5.1/gin/bugsnaggin.go000066400000000000000000000025461470543206100170110ustar00rootroot00000000000000package 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.5.1/gin/gin_test.go000066400000000000000000000062131470543206100164740ustar00rootroot00000000000000package 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.5.1/headers/000077500000000000000000000000001470543206100151655ustar00rootroot00000000000000bugsnag-go-2.5.1/headers/prefixed.go000066400000000000000000000007531470543206100173270ustar00rootroot00000000000000package 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.5.1/headers/prefixed_test.go000066400000000000000000000025641470543206100203700ustar00rootroot00000000000000package 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.5.1/json_tags.go000066400000000000000000000017601470543206100160740ustar00rootroot00000000000000// 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.5.1/martini/000077500000000000000000000000001470543206100152155ustar00rootroot00000000000000bugsnag-go-2.5.1/martini/bugsnagmiddleware.go000066400000000000000000000050461470543206100212350ustar00rootroot00000000000000/* 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.5.1/martini/martini_test.go000066400000000000000000000062441470543206100202540ustar00rootroot00000000000000package 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.5.1/metadata.go000066400000000000000000000107051470543206100156640ustar00rootroot00000000000000package 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.5.1/metadata_test.go000066400000000000000000000074751470543206100167350ustar00rootroot00000000000000package 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.5.1/middleware.go000066400000000000000000000043151470543206100162210ustar00rootroot00000000000000package 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 && request != nil { event.MetaData.Update(MetaData{ "request": { "params": request.URL.Query(), }, }) } } return nil } bugsnag-go-2.5.1/middleware_test.go000066400000000000000000000037571470543206100172710ustar00rootroot00000000000000package 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.5.1/negroni/000077500000000000000000000000001470543206100152135ustar00rootroot00000000000000bugsnag-go-2.5.1/negroni/bugsnagnegroni.go000066400000000000000000000026701470543206100205570ustar00rootroot00000000000000package 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.5.1/negroni/negroni_test.go000066400000000000000000000063161470543206100202500ustar00rootroot00000000000000package 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.5.1/notifier.go000066400000000000000000000121521470543206100157210ustar00rootroot00000000000000package 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.5.1/notifier_test.go000066400000000000000000000152271470543206100167660ustar00rootroot00000000000000package 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.5.1/panicwrap.go000066400000000000000000000016651470543206100160750ustar00rootroot00000000000000package 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.5.1/panicwrap_test.go000066400000000000000000000102031470543206100171200ustar00rootroot00000000000000package 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.5.1/payload.go000066400000000000000000000071541470543206100155410ustar00rootroot00000000000000package 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.5.1/payload_test.go000066400000000000000000000074561470543206100166050ustar00rootroot00000000000000package 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.5.1/report.go000066400000000000000000000047721470543206100154260ustar00rootroot00000000000000package 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.5.1/report_publisher.go000066400000000000000000000011361470543206100174720ustar00rootroot00000000000000package 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.5.1/request_extractor.go000066400000000000000000000057161470543206100176750ustar00rootroot00000000000000package 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.5.1/request_extractor_test.go000066400000000000000000000073461470543206100207350ustar00rootroot00000000000000package 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.5.1/revel/000077500000000000000000000000001470543206100146675ustar00rootroot00000000000000bugsnag-go-2.5.1/revel/bugsnagrevel.go000066400000000000000000000114571470543206100177120ustar00rootroot00000000000000// 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.5.1/sessions/000077500000000000000000000000001470543206100154205ustar00rootroot00000000000000bugsnag-go-2.5.1/sessions/config.go000066400000000000000000000104321470543206100172140ustar00rootroot00000000000000package 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.5.1/sessions/config_test.go000066400000000000000000000051771470543206100202650ustar00rootroot00000000000000package 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.5.1/sessions/integration_test.go000066400000000000000000000102461470543206100213340ustar00rootroot00000000000000package 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.5.1/sessions/payload.go000066400000000000000000000044251470543206100174050ustar00rootroot00000000000000package 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.5.1/sessions/publisher.go000066400000000000000000000050511470543206100177450ustar00rootroot00000000000000package 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.5.1/sessions/publisher_test.go000066400000000000000000000163021470543206100210050ustar00rootroot00000000000000package 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.5.1/sessions/session.go000066400000000000000000000010731470543206100174330ustar00rootroot00000000000000package 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.5.1/sessions/startup.go000066400000000000000000000020561470543206100174540ustar00rootroot00000000000000package 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.5.1/sessions/tracker.go000066400000000000000000000072501470543206100174060ustar00rootroot00000000000000package 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.5.1/sessions/tracker_test.go000066400000000000000000000045151470543206100204460ustar00rootroot00000000000000package 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.5.1/testutil/000077500000000000000000000000001470543206100154275ustar00rootroot00000000000000bugsnag-go-2.5.1/testutil/json.go000066400000000000000000000100641470543206100167300ustar00rootroot00000000000000// 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.5.1/v2/000077500000000000000000000000001470543206100141015ustar00rootroot00000000000000bugsnag-go-2.5.1/v2/LICENSE.txt000066400000000000000000000020331470543206100157220ustar00rootroot00000000000000Copyright (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.5.1/v2/bugsnag.go000066400000000000000000000243221470543206100160610ustar00rootroot00000000000000package 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.5.1" 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() // start delivery goroutine later than the module import time go publisher.delivery() // 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.5.1/v2/bugsnag_example_test.go000066400000000000000000000075131470543206100206360ustar00rootroot00000000000000package 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.5.1/v2/bugsnag_test.go000066400000000000000000000511251470543206100171210ustar00rootroot00000000000000package 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" ) // ATTENTION - tests in this file are changing global state variables // like default config or default report publisher // TAKE CARE to reset them to default after testcase! // 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: 102, InProject: true}) } type testPublisher struct { sync bool } func (tp *testPublisher) publishReport(p *payload) error { tp.sync = p.Synchronous return nil } func (tp *testPublisher) setMainProgramContext(context.Context) { } func (tp *testPublisher) delivery() { } 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 = newPublisher() }() 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)) // NOTE - this testcase will print a panic in verbose mode 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: 28}) } 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.5.1/v2/configuration.go000066400000000000000000000271641470543206100173110ustar00rootroot00000000000000package bugsnag import ( "context" "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 // Context created in the main program // Used in event delivery - after this context is marked Done // the event sending goroutine will switch to a graceful shutdown // and will try to send any remaining events. MainContext context.Context // 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.MainContext != nil { config.MainContext = other.MainContext publisher.setMainProgramContext(other.MainContext) } 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 := strings.TrimPrefix(file, 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.5.1/v2/configuration_test.go000066400000000000000000000244171470543206100203460ustar00rootroot00000000000000package 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.5.1/v2/device/000077500000000000000000000000001470543206100153405ustar00rootroot00000000000000bugsnag-go-2.5.1/v2/device/hostname.go000066400000000000000000000005371470543206100175120ustar00rootroot00000000000000package 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.5.1/v2/device/runtimeversions.go000066400000000000000000000026741470543206100211540ustar00rootroot00000000000000package 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.5.1/v2/device/runtimeversions_test.go000066400000000000000000000024001470543206100221760ustar00rootroot00000000000000package 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.5.1/v2/doc.go000066400000000000000000000045731470543206100152060ustar00rootroot00000000000000/* 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://docs.bugsnag.com/platforms/go. # Configuration The only required configuration is the BugSnag API key which can be obtained by clicking "Project Settings" on the top of your BugSnag dashboard 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 events, 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.5.1/v2/env_metadata.go000066400000000000000000000021521470543206100170600ustar00rootroot00000000000000package 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.5.1/v2/env_metadata_test.go000066400000000000000000000043041470543206100201200ustar00rootroot00000000000000package 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.5.1/v2/environment.go000066400000000000000000000004411470543206100167730ustar00rootroot00000000000000package 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.5.1/v2/environment_test.go000066400000000000000000000014121470543206100200310ustar00rootroot00000000000000package 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.5.1/v2/errors/000077500000000000000000000000001470543206100154155ustar00rootroot00000000000000bugsnag-go-2.5.1/v2/errors/README.md000066400000000000000000000003701470543206100166740ustar00rootroot00000000000000Adds 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.5.1/v2/errors/error.go000066400000000000000000000122401470543206100170740ustar00rootroot00000000000000// Package errors provides errors that have stack-traces. package errors import ( "bytes" "fmt" "reflect" "runtime" "github.com/pkg/errors" ) // 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 { // We do not modify the uintptr representation of the stack frame // stack is processed by runtime.CallersFrames and then by Next() on Frames slice // it's already doing uintptr-1 // refer to: https://github.com/golang/go/blob/897b3da2e079b9b940b309747305a5379fffa6ec/src/runtime/symtab.go#L108 stack[i] = uintptr(ptr) } 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() { processedStackFrame := StackFrame{ function: frame.Func, File: frame.File, LineNumber: frame.Line, ProgramCounter: frame.PC, } frameFunc := frame.Func if frameFunc == nil { newFrameFunc := runtime.FuncForPC(frame.PC) if newFrameFunc != nil { file, line := newFrameFunc.FileLine(frame.PC) // Unwrap fully inlined functions processedStackFrame.File = file processedStackFrame.LineNumber = line processedStackFrame.function = newFrameFunc frameFunc = newFrameFunc } } pkg, name := packageAndName(frameFunc) processedStackFrame.Name = name processedStackFrame.Package = pkg err.frames = append(err.frames, processedStackFrame) } } 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.5.1/v2/errors/error_fmt_wrap_test.go000066400000000000000000000026571470543206100220450ustar00rootroot00000000000000// +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.5.1/v2/errors/error_test.go000066400000000000000000000207401470543206100201370ustar00rootroot00000000000000package 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: "gopanic"}, } golangVersion := runtime.Version() // TODO remove this after dropping support for Golang 1.11 // Golang version < 1.12 cannot unwrap inlined functions correctly. if strings.HasPrefix(golangVersion, "go1.11") { expected = append(expected, StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 29}, StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 29}, StackFrame{Name: "a", File: "errors/error_test.go", LineNumber: 16}, ) } else { // For versions >= 1.12 inlined functions show normally expected = append(expected, StackFrame{Name: "c", File: "errors/error_test.go", LineNumber: 29}, StackFrame{Name: "b", File: "errors/error_test.go", LineNumber: 23}, 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 TestExampleErrorf(t *testing.T) { errorStr := "" for i := 1; i <= 2; i++ { if i%2 == 1 { e := Errorf("can only halve even numbers, got %d", i) errorStr += fmt.Sprintf("Error: %+v", e) } } expected := "Error: can only halve even numbers, got 1" if expected != errorStr { t.Errorf("Actual error does not match expected") } } func TestExampleNew(t *testing.T) { // Wrap io.EOF with the current stack-trace and return it e := New(io.EOF, 0) errorStr := fmt.Sprintf("%+v", e) expected := "EOF" if expected != errorStr { t.Errorf("Actual error does not match expected") } } 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.5.1/v2/errors/error_types_test.go000066400000000000000000000117371470543206100213710ustar00rootroot00000000000000package errors import ( "fmt" "runtime" "testing" pkgerror "github.com/pkg/errors" ) const ( bugsnagType = "bugsnagError" callersType = "callersType" stackFramesType = "stackFramesType" stackType = "stackType" internalType = "internalType" stringType = "stringType" ) // Prepare to test inlining func AInternal() interface{} { return fmt.Errorf("pure golang error") } func BInternal() interface{} { return AInternal() } func CInternal() interface{} { return BInternal() } func AString() interface{} { defer func() interface{} { return recover() }(); panic("panic") } func BString() interface{} { return AString() } func CString() interface{} { return BString() } func AStack() interface{} { return pkgerror.Errorf("from package") } func BStack() interface{} { return AStack() } func CStack() interface{} { return BStack() } func ACallers() interface{} { return newCustomErr("oh no an error", fmt.Errorf("parent error")) } func BCallers() interface{} { return ACallers() } func CCallers() interface{} { return BCallers() } func AFrames() interface{} { return &testErrorWithStackFrames{Err: New("foo", 0)} } func BFrames() interface{} { return AFrames() } func CFrames() interface{} { return BFrames() } // Golang internal errors don't have stacktrace // StackFrames are going to report only the line where internal golang error was wrapped in Bugsnag error func TestInternalError(t *testing.T) { err := CInternal() typeAssert(t, err, internalType) _, _, line, _ := runtime.Caller(0) // grab line immediately before error generator bgError := New(err, 0) actualStack := bgError.StackFrames() expected := []StackFrame{ {Name: "TestInternalError", File: "errors/error_types_test.go", LineNumber: line + 1}, } assertStacksMatch(t, expected, actualStack) } // Errors from panic contain only the message about panic // Same as above - StackFrames are going to contain only line numer of wrapping func TestStringError(t *testing.T) { err := CString() typeAssert(t, err, stringType) _, _, line, _ := runtime.Caller(0) // grab line immediately before error generator bgError := New(err, 0) actualStack := bgError.StackFrames() expected := []StackFrame{ {Name: "TestStringError", File: "errors/error_types_test.go", LineNumber: line + 1}, } assertStacksMatch(t, expected, actualStack) } // Errors from pkg/errors have their own stack // Inlined functions should be visible in StackFrames func TestStackError(t *testing.T) { _, _, line, _ := runtime.Caller(0) // grab line immediately before error generator err := CStack() typeAssert(t, err, stackType) bgError := New(err, 0) actualStack := bgError.StackFrames() expected := []StackFrame{ {Name: "AStack", File: "errors/error_types_test.go", LineNumber: 29}, {Name: "BStack", File: "errors/error_types_test.go", LineNumber: 30}, {Name: "CStack", File: "errors/error_types_test.go", LineNumber: 31}, {Name: "TestStackError", File: "errors/error_types_test.go", LineNumber: line + 1}, } assertStacksMatch(t, expected, actualStack) } // Errors implementing Callers() interface should have their own stack // Inlined functions should be visible in StackFrames func TestCallersError(t *testing.T) { _, _, line, _ := runtime.Caller(0) // grab line immediately before error generator err := CCallers() typeAssert(t, err, callersType) bgError := New(err, 0) actualStack := bgError.StackFrames() expected := []StackFrame{ {Name: "ACallers", File: "errors/error_types_test.go", LineNumber: 33}, {Name: "BCallers", File: "errors/error_types_test.go", LineNumber: 34}, {Name: "CCallers", File: "errors/error_types_test.go", LineNumber: 35}, {Name: "TestCallersError", File: "errors/error_types_test.go", LineNumber: line + 1}, } assertStacksMatch(t, expected, actualStack) } // Errors with StackFrames are explicitly adding stacktrace to error // Inlined functions should be visible in StackFrames func TestFramesError(t *testing.T) { _, _, line, _ := runtime.Caller(0) // grab line immediately before error generator err := CFrames() typeAssert(t, err, stackFramesType) bgError := New(err, 0) actualStack := bgError.StackFrames() expected := []StackFrame{ {Name: "AFrames", File: "errors/error_types_test.go", LineNumber: 37}, {Name: "BFrames", File: "errors/error_types_test.go", LineNumber: 38}, {Name: "CFrames", File: "errors/error_types_test.go", LineNumber: 39}, {Name: "TestFramesError", File: "errors/error_types_test.go", LineNumber: line + 1}, } assertStacksMatch(t, expected, actualStack) } func typeAssert(t *testing.T, err interface{}, expectedType string) { actualType := checkType(err) if actualType != expectedType { t.Errorf("Types don't match. Actual: %+v and expected: %+v\n", actualType, expectedType) } } func checkType(err interface{}) string { switch err.(type) { case *Error: return bugsnagType case ErrorWithCallers: return callersType case errorWithStack: return stackType case ErrorWithStackFrames: return stackFramesType case error: return internalType default: return stringType } } bugsnag-go-2.5.1/v2/errors/error_unwrap.go000066400000000000000000000003311470543206100204660ustar00rootroot00000000000000// +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.5.1/v2/errors/error_unwrap_test.go000066400000000000000000000011601470543206100215260ustar00rootroot00000000000000// +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.5.1/v2/errors/parse_panic.go000066400000000000000000000062451470543206100202370ustar00rootroot00000000000000package 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.5.1/v2/errors/parse_panic_test.go000066400000000000000000000175711470543206100213020ustar00rootroot00000000000000package 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.5.1/v2/errors/stackframe.go000066400000000000000000000050541470543206100200700ustar00rootroot00000000000000package 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) { if fn == nil { return "", "" } 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.5.1/v2/event.go000066400000000000000000000170531470543206100155570ustar00rootroot00000000000000package 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) // This will trim path before package name for external packages and golang default packages // Excluding main package as it's special case // This will NOT trim paths for packages in current module because path won't contain the package name // Example: path is "/user/name/work/internal/internal.go" and module package name is "example.com/mymodule/internal" if idx := strings.Index(file, frame.Package); idx > -1 && frame.Package != "main" { file = file[idx:] } // This should trim path for main and other current module packages with correct config // If input path is "/user/name/work/internal/internal.go" // SourceRoot is "/user/name/work" and ProjectPackages are []string{"main*", "example.com/mymodule/**"} // Then when package name is "example.com/mymodule/internal" // The path will be trimmed to "/internal/internal.go" 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.5.1/v2/event_test.go000066400000000000000000000016071470543206100166140ustar00rootroot00000000000000package 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.5.1/v2/go.mod000066400000000000000000000004221470543206100152050ustar00rootroot00000000000000module github.com/bugsnag/bugsnag-go/v2 go 1.15 require ( github.com/bitly/go-simplejson v0.5.1 github.com/bugsnag/panicwrap v1.3.4 github.com/google/uuid v1.6.0 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/pkg/errors v0.9.1 ) bugsnag-go-2.5.1/v2/go.sum000066400000000000000000000016111470543206100152330ustar00rootroot00000000000000github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= 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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= bugsnag-go-2.5.1/v2/headers/000077500000000000000000000000001470543206100155145ustar00rootroot00000000000000bugsnag-go-2.5.1/v2/headers/prefixed.go000066400000000000000000000007621470543206100176560ustar00rootroot00000000000000package 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.5.1/v2/headers/prefixed_test.go000066400000000000000000000025641470543206100207170ustar00rootroot00000000000000package 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.5.1/v2/json_tags.go000066400000000000000000000017601470543206100164230ustar00rootroot00000000000000// 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.5.1/v2/metadata.go000066400000000000000000000121401470543206100162060ustar00rootroot00000000000000package bugsnag import ( "encoding" "encoding/json" "fmt" "reflect" "strings" "time" ) // 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, val) } } // 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) } // 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 "" } // Handle nil pointers and interfaces specifically if t.Kind() == reflect.Interface || t.Kind() == reflect.Ptr { if v.IsNil() { return "" } } // Handle certain well known interfaces and types, in preferred order switch dataT := data.(type) { case error: return dataT.Error() case time.Time: return dataT.Format(time.RFC3339Nano) case fmt.Stringer: // This also covers time.Duration return dataT.String() case encoding.TextMarshaler: if b, err := dataT.MarshalText(); err == nil { return string(b) } case json.Marshaler: if b, err := dataT.MarshalJSON(); err == nil { return string(b) } case []byte: return string(dataT) } 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: 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.5.1/v2/metadata_test.go000066400000000000000000000201721470543206100172510ustar00rootroot00000000000000package bugsnag import ( "encoding/json" stderrors "errors" "reflect" "testing" "time" "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 } type _textMarshaller struct{} func (_textMarshaller) MarshalText() ([]byte, error) { return []byte("marshalled text"), nil } type _testStringer struct{} func (s _testStringer) String() string { return "something" } type _testError struct{} func (s _testError) Error() string { return "errorstr" } type _testStruct struct { Name *_testStringer } 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 TestMetadataAddPointer(t *testing.T) { var pointer *_testStringer md := MetaData{} md.AddStruct("emptypointer", pointer) fullPointer := &_testStringer{} md.AddStruct("fullpointer", fullPointer) if !reflect.DeepEqual(md, MetaData{ "Extra data": { "emptypointer": "", "fullpointer": "something", }, }) { t.Errorf("metadata.AddStruct didn't work: %#v", md) } } func TestMetadataAddNil(t *testing.T) { md := MetaData{} var nilMap map[string]interface{} md.AddStruct("nilmap", nilMap) var nilSlice []interface{} md.AddStruct("nilSlice", nilSlice) var nilError _testError md.AddStruct("error", nilError) var nilErrorPtr *_testError md.AddStruct("errorNilPtr", nilErrorPtr) var timeVar time.Time md.AddStruct("timeUnset", timeVar) var duration time.Duration md.AddStruct("durationUnset", duration) var marshalNilPtr *_textMarshaller md.AddStruct("marshalNilPtr", marshalNilPtr) var marshalFullPtr = &_textMarshaller{} md.AddStruct("marshalFullPtr", marshalFullPtr) var nullJsonMarshaller json.RawMessage md.AddStruct("nullJsonMarshaller", nullJsonMarshaller) var nullJsonMarshallerPtr *json.RawMessage md.AddStruct("nullJsonMarshallerPtr", nullJsonMarshallerPtr) var emptyJsonMarshaller = &json.RawMessage{} md.AddStruct("emptyJsonMarshaller", emptyJsonMarshaller) var nilBytes []byte md.AddStruct("nilBytes", nilBytes) var nilBytesPtr *[]byte md.AddStruct("nilBytesPtr", nilBytesPtr) var emptyBytes = []byte{} md.AddStruct("emptyBytes", emptyBytes) md.AddStruct("map", map[string]interface{}{ "data": _testStruct{Name: nil}, "nilmap": nilMap, "nilSlice": nilSlice, "error": nilError, "errorNilPtr": nilErrorPtr, "timeUnset": timeVar, "durationUnset": duration, "marshalNilPtr": marshalNilPtr, "marshalFullPtr": marshalFullPtr, "nullJsonMarshaller": nullJsonMarshaller, "nullJsonMarshallerPtr": nullJsonMarshallerPtr, "emptyJsonMarshaller": emptyJsonMarshaller, "nilBytes": nilBytes, "nilBytesPtr": nilBytesPtr, "emptyBytes": emptyBytes, }) if !reflect.DeepEqual(md, MetaData{ "map": { "data": map[string]interface{}{ "Name": "", }, "nilmap": map[string]interface{}{}, "nilSlice": []interface{}{}, "error": "errorstr", "errorNilPtr": "", "timeUnset": "0001-01-01T00:00:00Z", "durationUnset": "0s", "marshalFullPtr": "marshalled text", "marshalNilPtr": "", "nullJsonMarshaller": "null", "nullJsonMarshallerPtr": "", "emptyJsonMarshaller": "", "nilBytes": "", "nilBytesPtr": "", "emptyBytes": "", }, "nilmap": map[string]interface{}{}, "Extra data": { "nilSlice": []interface{}{}, "error": "errorstr", "errorNilPtr": "", "timeUnset": "0001-01-01T00:00:00Z", "durationUnset": "0s", "marshalFullPtr": "marshalled text", "marshalNilPtr": "", "nullJsonMarshaller": "null", "nullJsonMarshallerPtr": "", "emptyJsonMarshaller": "", "nilBytes": "", "nilBytesPtr": "", "emptyBytes": "", }, }) { t.Errorf("metadata.AddStruct didn't work: %#v", md) } } 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", "error": stderrors.New("some error"), "time": time.Date(2023, 12, 5, 23, 59, 59, 123456789, time.UTC), "duration": 105567462 * time.Millisecond, "text": _textMarshaller{}, "json": json.RawMessage(`{"json_property": "json_value"}`), "bytes": []byte(`lots of bytes`), "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]", "error": "some error", "time": "2023-12-05T23:59:59.123456789Z", "duration": "29h19m27.462s", "text": "marshalled text", "json": `{"json_property": "json_value"}`, "bytes": "lots of bytes", "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.5.1/v2/middleware.go000066400000000000000000000037551470543206100165570ustar00rootroot00000000000000package 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 && request != nil { event.MetaData.Update(MetaData{ "request": { "params": request.URL.Query(), }, }) } } return nil } bugsnag-go-2.5.1/v2/middleware_test.go000066400000000000000000000044101470543206100176030ustar00rootroot00000000000000package bugsnag import ( "bytes" "fmt" "log" "net/http" "reflect" "testing" "github.com/bugsnag/bugsnag-go/v2/errors" ) 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") } } func TestHttpRequestMiddleware(t *testing.T) { var req *http.Request rawData := []interface{}{req} event := &Event{RawData: rawData} config := &Configuration{} err := httpRequestMiddleware(event, config) if err != nil { t.Errorf("Should not happen") } } bugsnag-go-2.5.1/v2/notifier.go000066400000000000000000000121441470543206100162510ustar00rootroot00000000000000package bugsnag import ( "github.com/bugsnag/bugsnag-go/v2/errors" ) var publisher reportPublisher = newPublisher() // 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.5.1/v2/notifier_test.go000066400000000000000000000227321470543206100173140ustar00rootroot00000000000000package bugsnag_test import ( "fmt" "runtime" "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") }() var expected []errors.StackFrame golangVersion := runtime.Version() // TODO remove this after dropping support for Golang 1.11 // Golang version < 1.12 cannot unwrap inlined functions correctly. if strings.HasPrefix(golangVersion, "go1.11") { expected = append(expected, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4.1", File: "notifier_test.go"}, //inlined crash func errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4", File: "notifier_test.go"}, ) } else { expected = append(expected, errors.StackFrame{Name: "crash", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func4", File: "notifier_test.go"}, ) } assertStackframesMatch(t, expected) }) t.Run("bugsnag.AutoNotify", func(st *testing.T) { func() { defer func() { recover() }() defer bugsnag.AutoNotify() crash("NaN") }() var expected []errors.StackFrame golangVersion := runtime.Version() // TODO remove this after dropping support for Golang 1.11 // Golang version < 1.12 cannot unwrap inlined functions correctly. if strings.HasPrefix(golangVersion, "go1.11") { expected = append(expected, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5.1", File: "notifier_test.go"}, //inlined crash func errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5", File: "notifier_test.go"}, ) } else { expected = append(expected, errors.StackFrame{Name: "crash", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func5", File: "notifier_test.go"}, ) } assertStackframesMatch(t, expected) }) // 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") }() var expected []errors.StackFrame golangVersion := runtime.Version() // TODO remove this after dropping support for Golang 1.11 // Golang version < 1.12 cannot unwrap inlined functions correctly. if strings.HasPrefix(golangVersion, "go1.11") { expected = append(expected, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6.1", File: "notifier_test.go"}, //inlined crash func errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6", File: "notifier_test.go"}, ) } else { expected = append(expected, errors.StackFrame{Name: "crash", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func6", File: "notifier_test.go"}, ) } assertStackframesMatch(t, expected) }) t.Run("bugsnag.Recover", func(st *testing.T) { func() { defer bugsnag.Recover() crash("NaN") }() var expected []errors.StackFrame golangVersion := runtime.Version() // TODO remove this after dropping support for Golang 1.11 // Golang version < 1.12 cannot unwrap inlined functions correctly. if strings.HasPrefix(golangVersion, "go1.11") { expected = append(expected, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7.1", File: "notifier_test.go"}, //inlined crash func errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7", File: "notifier_test.go"}, ) } else { expected = append(expected, errors.StackFrame{Name: "crash", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7.1", File: "notifier_test.go"}, errors.StackFrame{Name: "TestStackframesAreSkippedCorrectly.func7", File: "notifier_test.go"}, ) } assertStackframesMatch(t, expected) }) } 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++ break } } } 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.5.1/v2/panicwrap.go000066400000000000000000000016731470543206100164230ustar00rootroot00000000000000package 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.5.1/v2/panicwrap_test.go000066400000000000000000000103131470543206100174510ustar00rootroot00000000000000package 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.5.1/v2/payload.go000066400000000000000000000072121470543206100160630ustar00rootroot00000000000000package 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.5.1/v2/payload_test.go000066400000000000000000000075101470543206100171230ustar00rootroot00000000000000package 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.5.1/v2/report.go000066400000000000000000000050001470543206100157360ustar00rootroot00000000000000package 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.5.1/v2/report_publisher.go000066400000000000000000000034451470543206100200260ustar00rootroot00000000000000package bugsnag import ( "context" "fmt" ) type reportPublisher interface { publishReport(*payload) error setMainProgramContext(context.Context) delivery() } func (defPub *defaultReportPublisher) delivery() { waitForEnd: for { select { case <-defPub.mainProgramCtx.Done(): defPub.isClosing = true break waitForEnd case p, ok := <-defPub.eventsChan: if ok { 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) } } else { p.logf("Event channel closed") return } } } // Send remaining elements from the queue close(defPub.eventsChan) for p := range defPub.eventsChan { 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) } } } type defaultReportPublisher struct { eventsChan chan *payload mainProgramCtx context.Context isClosing bool } func newPublisher() reportPublisher { defPub := defaultReportPublisher{isClosing: false, mainProgramCtx: context.TODO()} defPub.eventsChan = make(chan *payload, 100) return &defPub } func (defPub *defaultReportPublisher) setMainProgramContext(ctx context.Context) { defPub.mainProgramCtx = ctx } func (defPub *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() } if defPub.isClosing { return fmt.Errorf("main program is stopping, new events won't be sent") } select { case defPub.eventsChan <- p: default: p.logf("Events channel full. Discarding value") } return nil } bugsnag-go-2.5.1/v2/request_extractor.go000066400000000000000000000057161470543206100202240ustar00rootroot00000000000000package 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.5.1/v2/request_extractor_test.go000066400000000000000000000073461470543206100212640ustar00rootroot00000000000000package 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.5.1/v2/sessions/000077500000000000000000000000001470543206100157475ustar00rootroot00000000000000bugsnag-go-2.5.1/v2/sessions/config.go000066400000000000000000000104321470543206100175430ustar00rootroot00000000000000package 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.5.1/v2/sessions/config_test.go000066400000000000000000000051771470543206100206140ustar00rootroot00000000000000package 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.5.1/v2/sessions/integration_test.go000066400000000000000000000101031470543206100216530ustar00rootroot00000000000000package 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.5.1/v2/sessions/payload.go000066400000000000000000000044301470543206100177300ustar00rootroot00000000000000package 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.5.1/v2/sessions/publisher.go000066400000000000000000000050661470543206100203020ustar00rootroot00000000000000package 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.5.1/v2/sessions/publisher_test.go000066400000000000000000000160011470543206100213300ustar00rootroot00000000000000package 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.5.1/v2/sessions/session.go000066400000000000000000000010731470543206100177620ustar00rootroot00000000000000package 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.5.1/v2/sessions/startup.go000066400000000000000000000020561470543206100200030ustar00rootroot00000000000000package 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.5.1/v2/sessions/tracker.go000066400000000000000000000072501470543206100177350ustar00rootroot00000000000000package 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.5.1/v2/sessions/tracker_test.go000066400000000000000000000045531470543206100207770ustar00rootroot00000000000000package 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(time.Millisecond * 500) // wait for sessions to get consumed 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.5.1/v2/testutil/000077500000000000000000000000001470543206100157565ustar00rootroot00000000000000bugsnag-go-2.5.1/v2/testutil/json.go000066400000000000000000000077121470543206100172650ustar00rootroot00000000000000// 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) } }