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