├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── test.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── breadcrumb.go ├── breadcrumb_test.go ├── breadcrumbs.go ├── breadcrumbs_test.go ├── client.go ├── client_test.go ├── config.go ├── contexts.go ├── contexts_test.go ├── culprit.go ├── culprit_test.go ├── dsn.go ├── dsn_test.go ├── environment.go ├── environment_test.go ├── errortype.go ├── errortype_test.go ├── eventID.go ├── eventID_test.go ├── example_test.go ├── exception.go ├── exception_go1.13_test.go ├── exception_test.go ├── extra.go ├── extra_test.go ├── fingerprint.go ├── fingerprint_test.go ├── go.mod ├── go.sum ├── http.go ├── httpTransport.go ├── httpTransport_test.go ├── http_test.go ├── httprequest.go ├── httprequest_test.go ├── logger.go ├── logger_test.go ├── message.go ├── message_test.go ├── modules.go ├── modules_go1.12.go ├── modules_go1.12_test.go ├── modules_test.go ├── options.go ├── options_test.go ├── packet.go ├── packet_test.go ├── platform.go ├── platform_test.go ├── queuedEvent.go ├── queuedEvent_test.go ├── release.go ├── release_test.go ├── sdk.go ├── sdk_test.go ├── sendQueue.go ├── sendQueue_test.go ├── sentry-go.go ├── sequentialSendQueue.go ├── sequentialSendQueue_test.go ├── servername.go ├── servername_test.go ├── severity.go ├── severity_test.go ├── stacktrace.go ├── stacktraceGen.go ├── stacktraceGen_test.go ├── stacktrace_test.go ├── tags.go ├── tags_test.go ├── timestamp.go ├── timestamp_test.go ├── transport.go ├── transport_test.go ├── unset.go ├── unset_test.go ├── user.go └── user_test.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Problem Statement 2 | *Tell us in as few words as possible what problem you are running into* 3 | > I am not seeing some of the fields I provided to `sentry.Extra()` in my Sentry UI 4 | 5 | ### Expected Behaviour 6 | *Tell us how you expected things to behave* 7 | > I expected that all of the fields I provided in `sentry.Extra()` would be sent to 8 | > Sentry and be shown in the user interface there. 9 | 10 | ### Environment 11 | *Tell us about the environment you are using* 12 | 13 | - [ ] **Go Version**: `go version go1.9.2 windows/amd64` (`go version`) 14 | - [ ] **Sentry Version**: `8.22.0` 15 | - [ ] **Updated `sentry-go.v1`** (`go get -u gopkg.in/SierraSoftworks/sentry-go.v1`) 16 | 17 | ### Reproduction Code 18 | *Give us a short snippet of code that shows where you are encountering the problem* 19 | 20 | ```go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | 27 | "gopkg.in/SierraSoftworks/sentry-go.v1" 28 | ) 29 | 30 | func main() { 31 | cl := sentry.NewClient( 32 | sentry.Release(fmt.Sprintf("#%s", os.Getenv("ISSUE_ID"))), 33 | ) 34 | 35 | cl.Capture( 36 | sentry.Extra(map[string]interface{}{ 37 | "field1": "this is visible", 38 | // ... 39 | "field51": "this is not visible", 40 | }) 41 | ) 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Introduction 2 | *Give us a short description of what your pull request does* 3 | > This PR adds support for the new `teapot` interface in Sentry 4 | > which allows [RFC 2324][] compliant devices to submit information 5 | > about themselves to Sentry. 6 | 7 | ### Contains 8 | *Tell us what your pull request includes* 9 | 10 | - [ ] :radioactive: Breaking API changes 11 | - [ ] :star: Features 12 | - [ ] :star2: New features 13 | - [ ] :beetle: Fixes for existing features 14 | - [ ] :100: Automated tests for my changes 15 | - [ ] :books: Documentation 16 | - [ ] :memo: New documentation 17 | - [ ] :bookmark_tabs: Fixes for existing documentation 18 | - [ ] :electric_plug: Plugins 19 | - [ ] :star2: New plugins 20 | - [ ] :beetle: Fixes for existing plugins 21 | - [ ] :100: Automated tests for my changes 22 | 23 | ### Description 24 | *Give us a more in-depth description of what this pull request hopes to solve* 25 | > In version 13.3.7 of Sentry support was added for [RFC 2324][] devices in the 26 | > form of the new `teapot` interface. This interface allows compatible devices 27 | > to submit information about their brew to Sentry as additional context for 28 | > their events. 29 | > 30 | > This PR adds a new `Teapot()` option provider and a builder which allows 31 | > information about a teapot to be included in a standard Sentry packet. 32 | > It also includes extensive automated tests to ensure that both the option 33 | > provider and builder work correctly, as well as validating that the serialized 34 | > payload matches that expected by the Sentry API. 35 | > 36 | > ```go 37 | > cl := sentry.NewClient() 38 | > cl.Capture( 39 | > sentry.Teapot().WithBrew("Rooibos").WithTemperature(97.5), 40 | > ) 41 | > ``` 42 | 43 | [RFC 2324]: http://tools.ietf.org/html/2324#section-6.5.14 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - directory: / 4 | package-ecosystem: gomod 5 | schedule: 6 | interval: daily -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Go 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-go@v3 10 | with: 11 | go-version: '>=1.19' 12 | - run: go test -v ./... -race -coverprofile=coverage.txt -covermode=atomic 13 | - uses: codecov/codecov-action@v2 14 | with: 15 | file: ./coverage.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Created by https://www.gitignore.io/api/go 4 | # Edit at https://www.gitignore.io/?templates=go 5 | 6 | ### Go ### 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | ### Go Patch ### 21 | /vendor/ 22 | /Godeps/ 23 | 24 | # End of https://www.gitignore.io/api/go 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to sentry-go 2 | The following is a set of guidelines for contributing to this package and any 3 | of its plugins. As guidelines rather than rules, feel free to propose changes 4 | to them if you encounter any problems. 5 | 6 | #### Table of Contents 7 | [What should I know before I get started?](#what-should-i-know-before-i-get-started) 8 | - [The Sentry Client SDK](#the-sentry-client-sdk) 9 | - [The Options Pattern](#the-options-pattern) 10 | 11 | [Versioning Policy](#versioning-policy) 12 | 13 | [API Style](#api-style) 14 | - [Provide data through a list of options](#provide-data-through-a-list-of-options) 15 | - [Use builders for complex configuration options](#use-builders-for-complex-configuration-options) 16 | 17 | [Test Coverage](#test-coverage) 18 | 19 | [Adding a new Sentry Interface](#adding-a-new-sentry-interface) 20 | - [Basic Option](#basic-option) 21 | - [Custom Serialization](#custom-serialization) 22 | - [Merging Multiple Options](#merging-multiple-options) 23 | - [Doing last-minute preparation on your option](#doing-last-minute-preparation-on-your-option) 24 | - [Omitting Options from the Packet](#omitting-options-from-the-packet) 25 | 26 | ## What should I know before I get started? 27 | 28 | ### The Sentry Client SDK 29 | Sentry provides [documentation for client developers](https://docs.sentry.io/clientdev/) 30 | on their [docs website](https://docs.sentry.io). If you are looking to add a new feature, 31 | interface or field to this library, you should start by reading the official documentation 32 | to ensure it is compatible with Sentry's API. 33 | 34 | ### The Options Pattern 35 | There is a great article on [halls-of-valhalla.org](https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/) 36 | which explains what the Options Pattern is and how it can be leveraged in Go. 37 | 38 | This library makes heavy use of the Options Pattern to enable both the building 39 | of event packets (that are then sent to Sentry) and the configuration of the library's 40 | behaviour. 41 | 42 | In addition to configuring the core library, these options can be requested later 43 | from a [`Client`](https://godoc.org/gopkg.in/SierraSoftworks/sentry-go.v0#Client) 44 | using the `GetOption(name)` method, allowing plugins to provide and use their own 45 | option types. 46 | 47 | ## Versioning Policy 48 | This package uses Semantic Versioning for its public API. We are currently on 49 | version 1 of that public API and will endeavour to avoid the need to bump that 50 | major version at all costs - unless there is absolutely not other cource of action. 51 | 52 | Due to the way that this package's API is designed, it should be easily possible 53 | for most implementation details to be changed without affecting the public API and 54 | the behaviour of the various components contained within it. 55 | 56 | If we do encounter the need to update the public API in a backwards incompatible 57 | manner, we will leverage [gopkg.in](https://gopkg.in) to provide users of the 58 | old version with consistent access to the version they depend upon as well as 59 | bumping our SemVer major version. 60 | 61 | 62 | ## API Style 63 | This library endeavours to provide a simple API where "doing the right thing" is 64 | easy and obvious. To achieve that, it both limits the number of methods available 65 | on common interfaces and pushes the notion of consistency in all interactions. 66 | 67 | Specifically, you will notice two major patterns throughout this package's API: 68 | 69 | ### Provide data through a list of options 70 | Most of this library's options are configurable or can have sensible default specified 71 | by us. As a result, most user interaction will involve customizing those defaults or 72 | providing optional data. To make this as easy as possible, we use the Options Pattern 73 | when creating clients or capturing events. 74 | 75 | ##### Example 76 | ```go 77 | cl := sentry.NewClient( 78 | sentry.DSN("..."), 79 | sentry.Release("v1.0.0"), 80 | sentry.Logger("root"), 81 | ) 82 | 83 | cl.Capture( 84 | sentry.Message("This is an example event"), 85 | sentry.Level(sentry.Info), 86 | ) 87 | ``` 88 | 89 | ##### Code 90 | ```go 91 | func MyOption() sentry.Option { 92 | return &myOption{} 93 | } 94 | 95 | type myOption struct { 96 | 97 | } 98 | 99 | func (o *myOption) Class() string { 100 | return "my.option" 101 | } 102 | ``` 103 | 104 | ### Use builders for complex configuration options 105 | As much as possible, we want to avoid forcing users to fill in 106 | complex objects unless absolutely necessary. If sensible defaults 107 | can be selected, or fields are optional, they should be configurable 108 | through a builder interface rather than being a requirement of the 109 | option constructor. 110 | 111 | This pattern allows a developer to easily discover fields they may 112 | provide and gain insight into the requirements and options available 113 | to them when using your option. 114 | 115 | ##### Example 116 | ```go 117 | sentry.DefaultBreadcrumbs(). 118 | NewDefault(nil). 119 | WithMessage("This is an example breadcrumb"). 120 | WithCategory("example"). 121 | WithLevel(sentry.Info) 122 | ``` 123 | 124 | ##### Code 125 | When implementing your builder, you should provide a builder interface 126 | whose methods all return the same builder interface. This allows your 127 | builder's methods to be easily chained together. 128 | 129 | ```go 130 | type MyOptionBuilder interface { 131 | WithStringProperty(value string) MyOptionBuilder 132 | WithIntProperty(value int) MyOptionBuilder 133 | } 134 | ``` 135 | 136 | ## Test Coverage 137 | The value of test coverage as a metric may be endlessly debated, however 138 | this library places a heavy emphasis on using it as an indicator of poor 139 | test coverage within a module. In addition to high test coverage, we strive 140 | for high assertion coverage (with over 1000 assertions in the current test 141 | suite). 142 | 143 | If you are making a pull request on this library, please ensure that you have 144 | implemented a comprehensive set of tests to verify all assumptions about its 145 | behaviour as well as to assert the behaviour of its API. This will ensure that 146 | we more easily catch breaking API changes before they are released into the 147 | wild. 148 | 149 | ### Running Tests in Development 150 | To make developing high quality tests as easy as possible, we make use of 151 | [GoConvey](http://goconvey.co/). Convey is a test framework and runner which 152 | simplifies writing complex test trees and provides an excellent interface 153 | through which the realtime status of your tests can be viewed. 154 | 155 | To use it, just do the following: 156 | 157 | ```bash 158 | $ go get github.com/smartystreets/goconvey 159 | $ $GOPATH/bin/goconvey --port 8080 160 | ``` 161 | 162 | And then open up your web browser: http://localhost:8080/ 163 | 164 | ## Adding a new Sentry Interface 165 | Sometimes you'll want to take advantage of a Sentry processor which isn't 166 | yet supported by this library. This library makes implementing your own 167 | options trivially easy, not only allowing you to add those new interfaces, 168 | but to replace the default implementations if you don't like the way they 169 | work. 170 | 171 | **WARNING** If you're using an option that doesn't implement `Omit()` and 172 | always return `true` then you need to ensure that your `Class()` name matches 173 | one of the valid [Sentry interfaces](https://docs.sentry.io/clientdev/interfaces/). 174 | Failure to do so will result in Sentry responding with an error message. 175 | 176 | #### Basic Option 177 | The following is a basic option which can be used in calls to 178 | `sentry.NewClient(...)`, `client.Capture(...)` and `client.With(...)`. 179 | It will be added to the packet under the class name `my_interface` and 180 | will be serialized as a JSON object like `{ "field": "value" }`. 181 | 182 | ```go 183 | package sentry 184 | 185 | // MyOption should create a new instance of your myOption type 186 | // and return it as an Option interface (or derivative thereof). 187 | // You should avoid directly exposing the struct and adopt the 188 | // builder pattern if there is the potential need for additional 189 | // configuration. 190 | func MyOption(field string) Option { 191 | // If an empty field is invalid, then return a nil option 192 | // and it will be ignored by the options processor. 193 | if field == "" { 194 | return nil 195 | } 196 | 197 | return &myOption{ 198 | Field: field, 199 | } 200 | } 201 | 202 | type myOption struct { 203 | Field string `json:"field"` 204 | } 205 | 206 | func (i *myOption) Class() string { 207 | return "my_interface" 208 | } 209 | ``` 210 | 211 | #### Custom Serialization 212 | If you need to serialize your option as something other than a JSON 213 | object, you simply need to implement the `MarshalJSON()` method. This 214 | also applies in situations where your object must be marshalled to 215 | a type other than itself. 216 | 217 | ```go 218 | import "encoding/json" 219 | 220 | func (i *myOption) MarshalJSON() ([]byte, error) { 221 | return json.Marshal(i.Field) 222 | } 223 | ``` 224 | 225 | #### Merging Multiple Options 226 | Sometimes you won't want to simply replace an option's value if a new 227 | instance of it is provided. In these situations, you'll want to implement 228 | the `Merge()` method which allows you to control how your option behaves 229 | when it encounters another option with the same `Class()`. 230 | 231 | ```go 232 | import "gopkg.in/SierraSoftworks/sentry-go.v1" 233 | 234 | func (i *myOption) Merge(old sentry.Option) sentry.Option { 235 | if old, ok := old.(*myOption); ok { 236 | return &myOption{ 237 | Field: fmt.Sprintf("%s,%s", old.Field, i.Field), 238 | } 239 | } 240 | 241 | // Replace by default if we don't know how to handle the old type 242 | return i 243 | } 244 | ``` 245 | 246 | #### Doing last-minute preparation on your option 247 | If your option uses a builder interface to configure its fields before 248 | being sent, then you might want to do some processing just before the 249 | option is embedded in the Packet. This is where the `Finalize()` method 250 | comes in. 251 | 252 | `Finalize()` will be called when your option is added to a packet for 253 | transmission, so you can use a chainable builder interface like 254 | `MyOption().WithField("example")`. 255 | 256 | ```go 257 | import "strings" 258 | 259 | func (i *myOption) Finalize() { 260 | i.Field = strings.TrimSpace(i.Field) 261 | } 262 | ``` 263 | 264 | #### Omitting Options from the Packet 265 | In some situations you might find that you want to not include an 266 | option in the packet after all, perhaps the user hasn't provided all 267 | the required information or you couldn't gather it automatically. 268 | 269 | The `Omit()` method allows your option to tell the packet whether or 270 | not to include it. We actually use it internally for things like the DSN 271 | which shouldn't be sent to Sentry in the packet, but which we still want 272 | to read from the options builder. 273 | 274 | ```go 275 | func (i *myOption) Omit() bool { 276 | return len(i.Field) == 0 277 | } 278 | ``` 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2018 Sierra Softworks 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sentry-go [![Build Status](https://travis-ci.org/SierraSoftworks/sentry-go.svg?branch=master)](https://travis-ci.org/SierraSoftworks/sentry-go) [![](https://godoc.org/gopkg.in/SierraSoftworks/sentry-go.v1?status.svg)](http://godoc.org/gopkg.in/SierraSoftworks/sentry-go.v1) [![codecov](https://codecov.io/gh/SierraSoftworks/sentry-go/branch/master/graph/badge.svg)](https://codecov.io/gh/SierraSoftworks/sentry-go) 2 | **A robust Sentry client for Go applications** 3 | 4 | This library is a re-imagining of how Go applications should interact 5 | with a Sentry server. It aims to offer a concise, easy to understand and 6 | easy to extend toolkit for sending events to Sentry, with a strong emphasis 7 | on being easy to use. 8 | 9 | ## Features 10 | - **A beautiful API** which makes it obvious exactly what the best way to 11 | solve a problem is. 12 | - **Comprehensive** coverage of the various objects that can be sent to Sentry 13 | so you won't be left wondering why everyone else gets to play with Breadcrumbs 14 | but you still can't... 15 | - **StackTrace Support** using the official `pkg/errors` stacktrace provider, 16 | for maximum compatibility and easy integration with other libraries. 17 | - **HTTP Context Helpers** to let you quickly expose HTTP request context as 18 | part of your errors - with optional support for sending cookies, headers and 19 | payload data. 20 | - **Extensive documentation** which makes figuring out the right way to use 21 | something as easy as possible without the need to go diving into the code. 22 | 23 | In addition to the features listed above, the library offers support for a number 24 | of more advanced use cases, including sending events to multiple different Sentry 25 | DSNs, derived client contexts, custom interface types and custom transports. 26 | 27 | ## Versions 28 | This package follows SemVer and uses [gopkg.in](https://gopkg.in) to provide access 29 | to those versions. 30 | 31 | - [sentry-go.v0](https://gopkg.in/SierraSoftworks/sentry-go.v0) - `import ("gopkg.in/SierraSoftworks/sentry-go.v0")` 32 | 33 | This version is the latest `master` branch. You should avoid depending on this version unless you 34 | are performing active development against `sentry-go`. 35 | - [**sentry-go.v1**](https://gopkg.in/SierraSoftworks/sentry-go.v1) - `import ("gopkg.in/SierraSoftworks/sentry-go.v1")` 36 | 37 | This version of `sentry-go` maintains API compatibility with the package's v1 API. If you used 38 | `sentry-go` for a project prior to 2019-09-25 then this is the version you should retain until 39 | you can update your code. It will receive bug and security fixes. 40 | 41 | - [**sentry-go.v2**](https://gopkg.in/SierraSoftworks/sentry-go.v2) - `import ("gopkg.in/SierraSoftworks/sentry-go.v2")` 42 | 43 | This version is the most recent release of `sentry-go` and will maintain API compatibility. If you 44 | are creating a project that relies on `sentry-go` then this is the version you should use. 45 | 46 | ## Examples 47 | 48 | ### Breadcrumbs and Exceptions 49 | ```go 50 | package main 51 | 52 | import ( 53 | "fmt" 54 | 55 | "gopkg.in/SierraSoftworks/sentry-go.v2" 56 | "github.com/pkg/errors" 57 | ) 58 | 59 | func main() { 60 | sentry.AddDefaultOptions( 61 | sentry.DSN("..."), // If you don't override this, it'll be fetched from $SENTRY_DSN 62 | sentry.Release("v1.0.0"), 63 | ) 64 | 65 | cl := sentry.NewClient() 66 | 67 | sentry.DefaultBreadcrumbs().NewDefault(nil).WithMessage("Application started").WithCategory("log") 68 | 69 | err := errors.New("error with a stacktrace") 70 | 71 | id := cl.Capture( 72 | sentry.Message("Example exception submission to Sentry"), 73 | sentry.ExceptionForError(err), 74 | ).Wait().EventID() 75 | fmt.Println("Sent event to Sentry: ", id) 76 | } 77 | ``` 78 | 79 | ### HTTP Request Context 80 | ```go 81 | package main 82 | 83 | import ( 84 | "net/http" 85 | "os" 86 | 87 | "gopkg.in/SierraSoftworks/sentry-go.v2" 88 | ) 89 | 90 | func main() { 91 | cl := sentry.NewClient( 92 | sentry.Release("v1.0.0"), 93 | ) 94 | 95 | http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { 96 | cl := cl.With( 97 | sentry.HTTPRequest(req).WithHeaders(), 98 | ) 99 | 100 | res.Header().Set("Content-Type", "application/json") 101 | res.WriteHeader(404) 102 | res.Write([]byte(`{"error":"Not Found","message":"We could not find the route you requested, please check your URL and try again."}`)) 103 | 104 | cl.Capture( 105 | sentry.Message("Route Not Found: [%s] %s", req.Method, req.URL.Path), 106 | sentry.Level(sentry.Warning), 107 | ) 108 | }) 109 | 110 | if err := http.ListenAndServe(":8080", nil); err != nil { 111 | cl.Capture( 112 | sentry.ExceptionForError(err), 113 | sentry.Level(sentry.Fatal), 114 | sentry.Extra(map[string]interface{}{ 115 | "port": 8080, 116 | }), 117 | ) 118 | 119 | os.Exit(1) 120 | } 121 | } 122 | ``` 123 | 124 | ## Advanced Use Cases 125 | 126 | ### Custom SendQueues 127 | The default send queue provided by this library is a serial, buffered, queue 128 | which waits for a request to complete before sending the next. This works well 129 | to limit the potential for clients DoSing your Sentry server, but might not 130 | be what you want. 131 | 132 | For situations where you'd prefer to use a different type of queue algorithm, 133 | this library allows you to change the queue implementation both globally and 134 | on a per-client basis. You may also opt to use multiple send queues spread 135 | between different clients to impose custom behaviour for different portions 136 | of your application. 137 | 138 | ```go 139 | import "gopkg.in/SierraSoftworks/sentry-go.v2" 140 | 141 | func main() { 142 | // Configure a new global send queue 143 | sentry.AddDefaultOptions( 144 | sentry.UseSendQueue(sentry.NewSequentialSendQueue(10)), 145 | ) 146 | 147 | cl := sentry.NewClient() 148 | cl.Capture(sentry.Message("Sent over the global queue")) 149 | 150 | // Create a client with its own send queue 151 | cl2 := sentry.NewClient( 152 | UseSendQueue(sentry.NewSequentialSendQueue(100)), 153 | ) 154 | cl2.Capture(sentry.Message("Sent over the client's queue")) 155 | } 156 | ``` 157 | 158 | SendQueue implementations must implement the `SendQueue` interface, which 159 | requires it to provide both the `Enqueue` and `Shutdown` methods. 160 | -------------------------------------------------------------------------------- /breadcrumb.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "time" 4 | 5 | // A Breadcrumb keeps track of an action which took place in the application 6 | // leading up to an event. 7 | type Breadcrumb interface { 8 | // WithMessage sets the message displayed for this breadcrumb 9 | WithMessage(msg string) Breadcrumb 10 | 11 | // WithCategory sets the category that this breadcrumb belongs to 12 | WithCategory(cat string) Breadcrumb 13 | 14 | // Level sets the severity level of this breadcrumb to one of the 15 | // predefined severity levels. 16 | WithLevel(s Severity) Breadcrumb 17 | 18 | // WithTimestamp overrides the timestamp of this breadcrumb with 19 | // a new one. 20 | WithTimestamp(ts time.Time) Breadcrumb 21 | } 22 | 23 | func newBreadcrumb(typename string, data map[string]interface{}) *breadcrumb { 24 | if typename == "default" { 25 | typename = "" 26 | } 27 | 28 | return &breadcrumb{ 29 | Timestamp: time.Now().UTC().Unix(), 30 | Type: typename, 31 | Data: data, 32 | } 33 | } 34 | 35 | type breadcrumb struct { 36 | Timestamp int64 `json:"timestamp"` 37 | Type string `json:"type,omitempty"` 38 | Message string `json:"message,omitempty"` 39 | Data map[string]interface{} `json:"data,omitempty"` 40 | Category string `json:"category,omitempty"` 41 | Level Severity `json:"level,omitempty"` 42 | } 43 | 44 | func (b *breadcrumb) WithMessage(msg string) Breadcrumb { 45 | b.Message = msg 46 | return b 47 | } 48 | 49 | func (b *breadcrumb) WithCategory(cat string) Breadcrumb { 50 | b.Category = cat 51 | return b 52 | } 53 | 54 | func (b *breadcrumb) WithTimestamp(ts time.Time) Breadcrumb { 55 | b.Timestamp = ts.UTC().Unix() 56 | return b 57 | } 58 | 59 | func (b *breadcrumb) WithLevel(s Severity) Breadcrumb { 60 | b.Level = s 61 | return b 62 | } 63 | -------------------------------------------------------------------------------- /breadcrumb_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func ExampleBreadcrumb() { 11 | b := DefaultBreadcrumbs().NewDefault(nil) 12 | 13 | // You can set the severity level for the breadcrumb 14 | b.WithLevel(Error) 15 | 16 | // You can configure the category that the breadcrumb belongs to 17 | b.WithCategory("auth") 18 | 19 | // You can also specify a message describing the breadcrumb 20 | b.WithMessage("User's credentials were invalid") 21 | 22 | // And if you need to change the timestamp, you can do that too 23 | b.WithTimestamp(time.Now()) 24 | 25 | // All together now! 26 | DefaultBreadcrumbs(). 27 | NewDefault(nil). 28 | WithLevel(Error). 29 | WithCategory("auth"). 30 | WithMessage("User's credentials were invalid"). 31 | WithTimestamp(time.Now()) 32 | } 33 | 34 | func TestBreadcrumb(t *testing.T) { 35 | data := map[string]interface{}{ 36 | "test": true, 37 | } 38 | 39 | t.Run("newBreadcrumb", func(t *testing.T) { 40 | b := newBreadcrumb("default", data) 41 | 42 | if assert.NotNil(t, b) { 43 | assert.Implements(t, (*Breadcrumb)(nil), b) 44 | assert.Equal(t, "", b.Type, "It should set the correct type") 45 | assert.NotEqual(t, 0, b.Timestamp, "It should set the timestamp") 46 | assert.Equal(t, data, b.Data, "It should set the correct data") 47 | } 48 | }) 49 | 50 | t.Run("WithMessage()", func(t *testing.T) { 51 | b := newBreadcrumb("default", data) 52 | 53 | if assert.NotNil(t, b) { 54 | bb := b.WithMessage("test") 55 | assert.Equal(t, b, bb, "It should return the breadcrumb for chaining") 56 | assert.Equal(t, "test", b.Message) 57 | } 58 | }) 59 | 60 | t.Run("WithCategory()", func(t *testing.T) { 61 | b := newBreadcrumb("default", data) 62 | 63 | if assert.NotNil(t, b) { 64 | bb := b.WithCategory("test") 65 | assert.Equal(t, b, bb, "It should return the breadcrumb for chaining") 66 | assert.Equal(t, "test", b.Category) 67 | } 68 | }) 69 | 70 | t.Run("WithLevel()", func(t *testing.T) { 71 | b := newBreadcrumb("default", data) 72 | 73 | if assert.NotNil(t, b) { 74 | bb := b.WithLevel(Error) 75 | assert.Equal(t, b, bb, "It should return the breadcrumb for chaining") 76 | assert.Equal(t, Error, b.Level) 77 | } 78 | }) 79 | 80 | t.Run("WithTimestamp()", func(t *testing.T) { 81 | b := newBreadcrumb("default", data) 82 | now := time.Now() 83 | 84 | if assert.NotNil(t, b) { 85 | bb := b.WithTimestamp(now) 86 | assert.Equal(t, b, bb, "It should return the breadcrumb for chaining") 87 | assert.Equal(t, now.UTC().Unix(), b.Timestamp) 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /breadcrumbs.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | ) 7 | 8 | var globalBreadcrumbs = NewBreadcrumbsList(10) 9 | 10 | func init() { 11 | AddDefaultOptions(Breadcrumbs(DefaultBreadcrumbs())) 12 | } 13 | 14 | // DefaultBreadcrumbs are registered for inclusion in situations where 15 | // you have not specified your own Breadcrumbs collection. You can use 16 | // them to keep track of global events throughout your application. 17 | func DefaultBreadcrumbs() BreadcrumbsList { 18 | return globalBreadcrumbs 19 | } 20 | 21 | // Breadcrumbs can be included in your events to help track down 22 | // the sequence of steps that resulted in a failure. 23 | func Breadcrumbs(list BreadcrumbsList) Option { 24 | if opt, ok := list.(Option); ok { 25 | return opt 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // A BreadcrumbsList is responsible for keeping track of all the 32 | // breadcrumbs that make up a sequence. It will automatically remove 33 | // old breadcrumbs as new ones are added and is both type-safe and 34 | // O(1) execution time for inserts and removals. 35 | type BreadcrumbsList interface { 36 | // Adjusts the maximum number of breadcrumbs which will be maintained 37 | // in this list. 38 | WithSize(length int) BreadcrumbsList 39 | 40 | // NewDefault creates a new breadcrumb using the `default` type. 41 | // You can provide any data you wish to include in the breadcrumb, 42 | // or nil if you do not wish to include any extra data. 43 | NewDefault(data map[string]interface{}) Breadcrumb 44 | 45 | // NewNavigation creates a new navigation breadcrumb which represents 46 | // a transition from one page to another. 47 | NewNavigation(from, to string) Breadcrumb 48 | 49 | // NewHTTPRequest creates a new HTTP request breadcrumb which 50 | // describes the results of an HTTP request. 51 | NewHTTPRequest(method, url string, statusCode int, reason string) Breadcrumb 52 | } 53 | 54 | // NewBreadcrumbsList will create a new BreadcrumbsList which can be 55 | // used to track breadcrumbs within a specific context. 56 | func NewBreadcrumbsList(size int) BreadcrumbsList { 57 | return &breadcrumbsList{ 58 | MaxLength: size, 59 | Length: 0, 60 | } 61 | } 62 | 63 | type breadcrumbsList struct { 64 | MaxLength int 65 | 66 | Head *breadcrumbListNode 67 | Tail *breadcrumbListNode 68 | Length int 69 | mutex sync.RWMutex 70 | } 71 | 72 | func (l *breadcrumbsList) Class() string { 73 | return "breadcrumbs" 74 | } 75 | 76 | func (l *breadcrumbsList) WithSize(length int) BreadcrumbsList { 77 | l.mutex.Lock() 78 | defer l.mutex.Unlock() 79 | 80 | l.MaxLength = length 81 | 82 | if length == 0 { 83 | l.Head = nil 84 | l.Tail = nil 85 | l.Length = 0 86 | } 87 | 88 | for l.Length > l.MaxLength && l.Head != nil { 89 | l.Head = l.Head.Next 90 | l.Length-- 91 | } 92 | 93 | return l 94 | } 95 | 96 | func (l *breadcrumbsList) NewDefault(data map[string]interface{}) Breadcrumb { 97 | if data == nil { 98 | data = map[string]interface{}{} 99 | } 100 | 101 | b := newBreadcrumb("default", data) 102 | l.append(b) 103 | return b 104 | } 105 | 106 | func (l *breadcrumbsList) NewNavigation(from, to string) Breadcrumb { 107 | b := newBreadcrumb("navigation", map[string]interface{}{ 108 | "from": from, 109 | "to": to, 110 | }) 111 | l.append(b) 112 | return b 113 | } 114 | 115 | func (l *breadcrumbsList) NewHTTPRequest(method, url string, statusCode int, reason string) Breadcrumb { 116 | b := newBreadcrumb("http", map[string]interface{}{ 117 | "method": method, 118 | "url": url, 119 | "status_code": statusCode, 120 | "reason": reason, 121 | }) 122 | l.append(b) 123 | return b 124 | } 125 | 126 | func (l *breadcrumbsList) MarshalJSON() ([]byte, error) { 127 | return json.Marshal(l.list()) 128 | } 129 | 130 | func (l *breadcrumbsList) append(b Breadcrumb) { 131 | l.mutex.Lock() 132 | defer l.mutex.Unlock() 133 | 134 | // If we've disabled the breadcrumbs collector, skip 135 | // any extra work. 136 | if l.MaxLength == 0 { 137 | return 138 | } 139 | 140 | n := &breadcrumbListNode{ 141 | Value: b, 142 | Next: nil, 143 | } 144 | 145 | if l.Tail != nil { 146 | l.Tail.Next = n 147 | } else { 148 | l.Head = n 149 | } 150 | l.Tail = n 151 | l.Length++ 152 | 153 | for l.Length > l.MaxLength && l.Head != nil { 154 | l.Head = l.Head.Next 155 | l.Length-- 156 | } 157 | } 158 | 159 | func (l *breadcrumbsList) list() []Breadcrumb { 160 | l.mutex.RLock() 161 | defer l.mutex.RUnlock() 162 | 163 | current := l.Head 164 | out := []Breadcrumb{} 165 | for current != nil { 166 | out = append(out, current.Value) 167 | current = current.Next 168 | } 169 | 170 | return out 171 | } 172 | 173 | type breadcrumbListNode struct { 174 | Next *breadcrumbListNode 175 | Value Breadcrumb 176 | } 177 | -------------------------------------------------------------------------------- /breadcrumbs_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleDefaultBreadcrumbs() { 10 | // We can change the maximum number of breadcrumbs to be stored 11 | DefaultBreadcrumbs().WithSize(5) 12 | 13 | DefaultBreadcrumbs().NewDefault(nil).WithMessage("This is an example") 14 | DefaultBreadcrumbs().NewDefault(map[string]interface{}{ 15 | "example": true, 16 | }).WithMessage("It should give you an idea of how you can use breadcrumbs in your app") 17 | 18 | DefaultBreadcrumbs(). 19 | NewNavigation("introduction", "navigation"). 20 | WithCategory("navigation"). 21 | WithMessage("You can use them to represent navigations from one page to another") 22 | 23 | DefaultBreadcrumbs(). 24 | NewNavigation("navigation", "http"). 25 | WithCategory("navigation"). 26 | WithMessage("Or to represent changes in the state of your application's workflows") 27 | 28 | DefaultBreadcrumbs(). 29 | NewHTTPRequest("GET", "https://example.com/api/v1/awesome", 200, "OK"). 30 | WithLevel(Debug). 31 | WithMessage("I think we can agree that they're pretty awesome") 32 | 33 | NewClient().Capture(Message("Finally, we send the event with all our breadcrumbs included")) 34 | } 35 | 36 | func ExampleBreadcrumbs() { 37 | rootClient := NewClient() 38 | DefaultBreadcrumbs().NewDefault(nil).WithMessage("Breadcrumb in the default context") 39 | 40 | breadcrumbs := NewBreadcrumbsList(10) 41 | contextClient := rootClient.With(Breadcrumbs(breadcrumbs)) 42 | breadcrumbs.NewDefault(nil).WithMessage("Breadcrumb in the private context") 43 | 44 | // Will include only the first breadcrumb 45 | rootClient.Capture( 46 | Message("Event in default context"), 47 | Logger("default"), 48 | ) 49 | 50 | // Will include only the second breadcrumb 51 | contextClient.Capture( 52 | Message("Event in private context"), 53 | Logger("private"), 54 | ) 55 | } 56 | 57 | func TestBreadcrumbs(t *testing.T) { 58 | t.Run("Options Providers", func(t *testing.T) { 59 | assert.NotNil(t, testGetOptionsProvider(t, &breadcrumbsList{}), "Breadcrumbs should be registered as a default options provider") 60 | }) 61 | 62 | t.Run("DefaultBreadcrumbs()", func(t *testing.T) { 63 | assert.NotNil(t, DefaultBreadcrumbs()) 64 | assert.Implements(t, (*BreadcrumbsList)(nil), DefaultBreadcrumbs()) 65 | }) 66 | 67 | t.Run("Breadcrumbs()", func(t *testing.T) { 68 | assert.Nil(t, Breadcrumbs(nil), "it should return a nil option if it receives a nil breadcrumbs list") 69 | 70 | l := NewBreadcrumbsList(3) 71 | assert.NotNil(t, l, "it should create a breadcrumbs list") 72 | 73 | b := Breadcrumbs(l) 74 | assert.NotNil(t, b, "it should return an option when the list is not nil") 75 | 76 | assert.Equal(t, "breadcrumbs", b.Class(), "it should use the correct option class") 77 | }) 78 | } 79 | 80 | func TestNewBreadcrumbsList(t *testing.T) { 81 | l := NewBreadcrumbsList(3) 82 | assert.NotNil(t, l, "it should return a non-nil list") 83 | 84 | assert.Implements(t, (*Option)(nil), l, "it should implement the option interface") 85 | 86 | ll, ok := l.(*breadcrumbsList) 87 | assert.True(t, ok, "it should actually be a *breadcrumbsList") 88 | 89 | assert.Equal(t, 3, ll.MaxLength, "it should have the right max length") 90 | assert.Equal(t, 0, ll.Length, "it should start with no breadcrumbs") 91 | assert.Nil(t, ll.Head, "it should have no head to start with") 92 | assert.Nil(t, ll.Tail, "it should have no tail to start with") 93 | 94 | t.Run("NewDefault(nil)", func(t *testing.T) { 95 | b := l.NewDefault(nil) 96 | assert.NotNil(t, b, "it should return a non-nil breadcrumb") 97 | assert.Implements(t, (*Breadcrumb)(nil), b, "the breadcrumb should implement the Breadcrumb interface") 98 | 99 | assert.NotNil(t, ll.Tail, "the list's tail should no longer be nil") 100 | assert.Equal(t, ll.Tail.Value, b, "the list's tail should now be the new breadcrumb") 101 | 102 | bb, ok := b.(*breadcrumb) 103 | assert.True(t, ok, "it should actually be a *breadcrumb object") 104 | assert.Equal(t, "", bb.Type, "it should use the default breadcrumb type") 105 | assert.Equal(t, map[string]interface{}{}, bb.Data, "it should use the passed breadcrumb data") 106 | }) 107 | 108 | t.Run("NewDefault(data)", func(t *testing.T) { 109 | data := map[string]interface{}{ 110 | "test": true, 111 | } 112 | 113 | b := l.NewDefault(data) 114 | assert.NotNil(t, b, "it should return a non-nil breadcrumb") 115 | assert.Implements(t, (*Breadcrumb)(nil), b, "the breadcrumb should implement the Breadcrumb interface") 116 | 117 | assert.NotNil(t, ll.Tail, "the list's tail should no longer be nil") 118 | assert.Equal(t, ll.Tail.Value, b, "the list's tail should now be the new breadcrumb") 119 | 120 | bb, ok := b.(*breadcrumb) 121 | assert.True(t, ok, "it should actually be a *breadcrumb object") 122 | assert.Equal(t, "", bb.Type, "it should use the default breadcrumb type") 123 | assert.Equal(t, data, bb.Data, "it should use the passed breadcrumb data") 124 | }) 125 | 126 | t.Run("NewNavigation()", func(t *testing.T) { 127 | b := l.NewNavigation("/from", "/to") 128 | assert.NotNil(t, b, "it should return a non-nil breadcrumb") 129 | assert.Implements(t, (*Breadcrumb)(nil), b, "the breadcrumb should implement the Breadcrumb interface") 130 | 131 | assert.NotNil(t, ll.Tail, "the list's tail should no longer be nil") 132 | assert.Equal(t, ll.Tail.Value, b, "the list's tail should now be the new breadcrumb") 133 | 134 | bb, ok := b.(*breadcrumb) 135 | assert.True(t, ok, "it should actually be a *breadcrumb object") 136 | assert.Equal(t, "navigation", bb.Type, "it should use the default breadcrumb type") 137 | assert.Equal(t, map[string]interface{}{ 138 | "from": "/from", 139 | "to": "/to", 140 | }, bb.Data, "it should use the correct breadcrumb data") 141 | }) 142 | 143 | t.Run("NewHTTPRequest()", func(t *testing.T) { 144 | b := l.NewHTTPRequest("GET", "/test", 200, "OK") 145 | assert.NotNil(t, b, "it should return a non-nil breadcrumb") 146 | assert.Implements(t, (*Breadcrumb)(nil), b, "the breadcrumb should implement the Breadcrumb interface") 147 | 148 | assert.NotNil(t, ll.Tail, "the list's tail should no longer be nil") 149 | assert.Equal(t, ll.Tail.Value, b, "the list's tail should now be the new breadcrumb") 150 | 151 | bb, ok := b.(*breadcrumb) 152 | assert.True(t, ok, "it should actually be a *breadcrumb object") 153 | assert.Equal(t, "http", bb.Type, "it should use the default breadcrumb type") 154 | assert.Equal(t, map[string]interface{}{ 155 | "method": "GET", 156 | "url": "/test", 157 | "status_code": 200, 158 | "reason": "OK", 159 | }, bb.Data, "it should use the correct breadcrumb data") 160 | }) 161 | 162 | t.Run("WithSize()", func(t *testing.T) { 163 | cl := l.WithSize(5) 164 | assert.Equal(t, l, cl, "it should return the list so that the call is chainable") 165 | assert.Equal(t, 5, ll.MaxLength, "it should update the lists's max size") 166 | 167 | var b Breadcrumb 168 | for i := 0; i < ll.MaxLength*2; i++ { 169 | b = l.NewDefault(map[string]interface{}{ 170 | "index": i, 171 | }) 172 | } 173 | 174 | assert.Equal(t, ll.MaxLength, ll.Length, "the list should cap out at its max size") 175 | 176 | l.WithSize(1) 177 | assert.Equal(t, 1, ll.Length, "the list should be resized to the new max size") 178 | 179 | assert.Equal(t, b, ll.Head.Value, "the head of the list should be the last breadcrumb which was added") 180 | assert.Equal(t, b, ll.Tail.Value, "the tail of the list should be the last breadcrumb which was added") 181 | }) 182 | 183 | t.Run("append()", func(t *testing.T) { 184 | l.WithSize(0).WithSize(3) 185 | 186 | var b Breadcrumb 187 | for i := 0; i < 10; i++ { 188 | b = l.NewDefault(map[string]interface{}{ 189 | "index": i, 190 | }) 191 | } 192 | 193 | assert.Equal(t, 3, ll.Length, "it should evict values to ensure that the length remains capped") 194 | assert.Equal(t, b, ll.Tail.Value, "it should add new breadcrumbs at the end of the list") 195 | }) 196 | 197 | t.Run("list()", func(t *testing.T) { 198 | l.WithSize(0).WithSize(3) 199 | assert.Equal(t, 0, ll.Length, "should start with an empty breadcrumbs list") 200 | 201 | ol := ll.list() 202 | assert.NotNil(t, ol, "should not return nil if the list is empty") 203 | assert.Len(t, ol, 0, "it should return an empty list") 204 | 205 | for i := 0; i < 10; i++ { 206 | l.NewDefault(map[string]interface{}{"index": i}) 207 | } 208 | assert.Equal(t, 3, ll.Length, "should now have three breadcrumbs in the list") 209 | 210 | ol = ll.list() 211 | assert.NotNil(t, ol, "should not return nil if the list is non-empty") 212 | assert.Len(t, ol, 3, "should return the maximum number of items if the list is full") 213 | 214 | for i, item := range ol { 215 | assert.IsType(t, &breadcrumb{}, item, "every list item should be a *breadcrumb") 216 | assert.Equal(t, i+7, item.(*breadcrumb).Data["index"], "the items should be in the right order") 217 | } 218 | }) 219 | 220 | t.Run("MarshalJSON()", func(t *testing.T) { 221 | l.WithSize(0).WithSize(5).NewDefault(map[string]interface{}{"test": true}) 222 | 223 | data := testOptionsSerialize(t, Breadcrumbs(l)) 224 | assert.NotNil(t, data, "should not return a nil result") 225 | assert.IsType(t, []interface{}{}, data, "should return a JSON array") 226 | }) 227 | } 228 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // A Client is responsible for letting you interact with the Sentry API. 4 | // You can create derivative clients 5 | type Client interface { 6 | // With creates a new derivative client with the provided options 7 | // set as part of its defaults. 8 | With(options ...Option) Client 9 | 10 | // GetOption allows you to retrieve a specific configuration object 11 | // by its Class name from this client. It is useful if you are interested 12 | // in using the client to configure Sentry plugins. 13 | // If an option with the given className could not be found, nil will 14 | // be returned. 15 | GetOption(className string) Option 16 | 17 | // Capture will queue an event for sending to Sentry and return a 18 | // QueuedEvent object which can be used to keep tabs on when it is 19 | // actually sent, if you are curious. 20 | Capture(options ...Option) QueuedEvent 21 | } 22 | 23 | var defaultClient = NewClient() 24 | 25 | // DefaultClient is a singleton client instance which can be used instead 26 | // of instantiating a new client manually. 27 | func DefaultClient() Client { 28 | return defaultClient 29 | } 30 | 31 | type client struct { 32 | parent *client 33 | options []Option 34 | } 35 | 36 | // NewClient will create a new client instance with the provided 37 | // default options and config. 38 | func NewClient(options ...Option) Client { 39 | return &client{ 40 | parent: nil, 41 | options: options, 42 | } 43 | } 44 | 45 | func (c *client) Capture(options ...Option) QueuedEvent { 46 | p := NewPacket().SetOptions(c.fullDefaultOptions()...).SetOptions(options...) 47 | 48 | return c.SendQueue().Enqueue(c, p) 49 | } 50 | 51 | func (c *client) With(options ...Option) Client { 52 | return &client{ 53 | parent: c, 54 | options: options, 55 | } 56 | } 57 | 58 | func (c *client) GetOption(className string) Option { 59 | var opt Option 60 | for _, o := range c.fullDefaultOptions() { 61 | if o == nil { 62 | continue 63 | } 64 | 65 | if o.Class() != className { 66 | continue 67 | } 68 | 69 | if mergeable, ok := o.(MergeableOption); ok { 70 | opt = mergeable.Merge(opt) 71 | continue 72 | } 73 | 74 | opt = o 75 | } 76 | 77 | return opt 78 | } 79 | 80 | func (c *client) DSN() string { 81 | opt := c.GetOption("sentry-go.dsn") 82 | if opt == nil { 83 | return "" 84 | } 85 | 86 | dsnOpt, ok := opt.(*dsnOption) 87 | if !ok { 88 | // Should never be the case unless someone implements a 89 | // custom dsn option we don't know how to handle 90 | return "" 91 | } 92 | 93 | return dsnOpt.dsn 94 | } 95 | 96 | func (c *client) Transport() Transport { 97 | opt := c.GetOption("sentry-go.transport") 98 | if opt == nil { 99 | // Should never be the case, we have this set as a base default 100 | return newHTTPTransport() 101 | } 102 | 103 | transOpt, ok := opt.(*transportOption) 104 | if !ok { 105 | // Should never be the case unless someone implements their own custom 106 | // transport option that we don't know how to handle. 107 | return newHTTPTransport() 108 | } 109 | 110 | return transOpt.transport 111 | } 112 | 113 | func (c *client) SendQueue() SendQueue { 114 | opt := c.GetOption("sentry-go.sendqueue") 115 | if opt == nil { 116 | // Should never be the case, we have this set as a base default 117 | return NewSequentialSendQueue(100) 118 | } 119 | 120 | sqOpt, ok := opt.(*sendQueueOption) 121 | if !ok { 122 | // Should never be the case unless someone implements their own custom 123 | // sendqueue option that we don't know how to handle. 124 | return NewSequentialSendQueue(100) 125 | } 126 | 127 | return sqOpt.queue 128 | } 129 | 130 | func (c *client) fullDefaultOptions() []Option { 131 | if c.parent == nil { 132 | rootOpts := []Option{} 133 | for _, provider := range defaultOptionProviders { 134 | opt := provider() 135 | if opt != nil { 136 | rootOpts = append(rootOpts, opt) 137 | } 138 | } 139 | 140 | return append(rootOpts, c.options...) 141 | } 142 | 143 | return append(c.parent.fullDefaultOptions(), c.options...) 144 | } 145 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func ExampleClient() { 13 | // You can create a new root client directly and configure 14 | // it by passing any options you wish 15 | cl := NewClient( 16 | DSN(""), 17 | ) 18 | 19 | // You can then create a derivative client with any context-specific 20 | // options. These are useful if you want to encapsulate context-specific 21 | // information like the HTTP request that is being handled. 22 | var r *http.Request 23 | ctxCl := cl.With( 24 | HTTPRequest(r).WithHeaders(), 25 | Logger("http"), 26 | ) 27 | 28 | // You can then use the client to capture an event and send it to Sentry 29 | err := fmt.Errorf("an error occurred") 30 | ctxCl.Capture( 31 | ExceptionForError(err), 32 | ) 33 | } 34 | 35 | func ExampleDefaultClient() { 36 | DefaultClient().Capture( 37 | Message("This is an example message"), 38 | ) 39 | } 40 | 41 | func TestDefaultClient(t *testing.T) { 42 | cl := DefaultClient() 43 | assert.NotNil(t, cl, "it should not return nil") 44 | assert.Implements(t, (*Client)(nil), cl, "it should implement the Client interface") 45 | assert.Equal(t, defaultClient, cl, "it should return the global defaultClient") 46 | } 47 | 48 | func TestNewClient(t *testing.T) { 49 | opt := &testOption{} 50 | cl := NewClient(opt) 51 | 52 | if assert.NotNil(t, cl, "it should not return nil") { 53 | assert.Implements(t, (*Client)(nil), cl, "it should implement the Client interface") 54 | 55 | cll, ok := cl.(*client) 56 | assert.True(t, ok, "it should actually return a *client") 57 | 58 | assert.Nil(t, cll.parent, "it should set the parent of the client to nil") 59 | assert.Equal(t, []Option{opt}, cll.options, "it should set the client's options correctly") 60 | 61 | t.Run("Capture()", func(t *testing.T) { 62 | tr := testNewTestTransport() 63 | assert.NotNil(t, tr, "the test transport should not be nil") 64 | 65 | cl := NewClient(UseTransport(tr)) 66 | assert.NotNil(t, cl, "the client should not be nil") 67 | 68 | e := cl.Capture(Message("test")) 69 | assert.NotNil(t, e, "the event handle should not be nil") 70 | 71 | ei, ok := e.(QueuedEventInternal) 72 | assert.True(t, ok, "the event handle should be convertible to an internal queued event") 73 | 74 | select { 75 | case p := <-tr.ch: 76 | assert.Equal(t, ei.Packet(), p, "the packet should match the internal event's packet") 77 | 78 | pi, ok := p.(*packet) 79 | assert.True(t, ok, "the packet should actually be a *packet") 80 | assert.Contains(t, *pi, Message("test").Class(), "the packet should contain the message") 81 | assert.Equal(t, Message("test"), (*pi)[Message("test").Class()], "the message should be serialized under its key") 82 | case <-time.After(100 * time.Millisecond): 83 | t.Error("the event was not dispatched within the timeout of 100ms") 84 | } 85 | }) 86 | 87 | t.Run("With()", func(t *testing.T) { 88 | opt := &testOption{} 89 | 90 | ctxCl := cl.With(opt) 91 | assert.NotNil(t, ctxCl, "the new client should not be nil") 92 | assert.NotEqual(t, cl, ctxCl, "the new client should not be the same as the old client") 93 | 94 | cll, ok := ctxCl.(*client) 95 | assert.True(t, ok, "the new client should actually be a *client") 96 | assert.Equal(t, cl, cll.parent, "the new client should have its parent configured to be the original client") 97 | assert.Equal(t, []Option{opt}, cll.options, "the new client should have the right list of options") 98 | }) 99 | 100 | t.Run("GetOption()", func(t *testing.T) { 101 | assert.Nil(t, NewClient().GetOption("unknown-option-class"), "it should return nil for an unrecognized option") 102 | assert.Nil(t, NewClient(nil).GetOption("unknown-option-class"), "it should ignore nil options") 103 | 104 | opt := &testOption{} 105 | assert.Equal(t, opt, NewClient(opt).GetOption("test"), "it should return an option if it is present") 106 | assert.Equal(t, opt, NewClient(&testOption{}, opt).GetOption("test"), "it should return the most recent non-mergeable option") 107 | 108 | assert.Equal(t, &testMergeableOption{3}, NewClient(&testMergeableOption{1}, &testMergeableOption{2}).GetOption("test"), "it should merge options when they support it") 109 | }) 110 | 111 | t.Run("fullDefaultOptions()", func(t *testing.T) { 112 | opts := cll.fullDefaultOptions() 113 | assert.NotNil(t, opts, "the full options list should not be nil") 114 | assert.NotEmpty(t, opts, "the full options list should not be empty") 115 | 116 | assert.Contains(t, opts, opt, "it should include the options passed to the client") 117 | 118 | i := 0 119 | for _, provider := range defaultOptionProviders { 120 | opt := provider() 121 | if opt == nil { 122 | continue 123 | } 124 | 125 | if i >= len(opts) { 126 | t.Error("there are fewer options than there are providers which return option values") 127 | break 128 | } 129 | 130 | assert.IsType(t, opt, opts[i], "Expected opts[%d] to have type %s but got %s instead", i, opt.Class(), opts[i].Class()) 131 | 132 | i++ 133 | } 134 | 135 | opt1 := &testMergeableOption{data: 1} 136 | opt2 := &testMergeableOption{data: 2} 137 | cl := NewClient(opt1) 138 | assert.NotNil(t, cl, "the client should not be nil") 139 | 140 | dcl := cl.With(opt2) 141 | assert.NotNil(t, dcl, "the derived client should not be nil") 142 | 143 | cll, ok := dcl.(*client) 144 | assert.True(t, ok, "the derived client should actually be a *client") 145 | 146 | opts = cll.fullDefaultOptions() 147 | assert.Contains(t, opts, opt1, "the parent's options should be present in the list") 148 | assert.Contains(t, opts, opt2, "the derive client's options should be present in the list") 149 | }) 150 | } 151 | } 152 | 153 | func TestClientConfigInterface(t *testing.T) { 154 | t.Run("DSN()", func (t *testing.T) { 155 | t.Run("with no default DSN", func(t *testing.T) { 156 | oldDefaultOptionProviders := defaultOptionProviders 157 | defer func() { 158 | defaultOptionProviders = oldDefaultOptionProviders 159 | }() 160 | 161 | defaultOptionProviders = []func() Option{} 162 | 163 | cl := NewClient() 164 | assert.NotNil(t, cl, "the client should not be nil") 165 | 166 | cfg, ok := cl.(Config) 167 | assert.True(t, ok, "the client should implement the Config interface") 168 | assert.Equal(t, "", cfg.DSN(), "the client should return an empty DSN") 169 | }) 170 | 171 | t.Run("with a custom DSN option implementation", func(t *testing.T) { 172 | cl := NewClient(&testCustomClassOption{"sentry-go.dsn"}) 173 | assert.NotNil(t, cl, "the client should not be nil") 174 | 175 | cfg, ok := cl.(Config) 176 | assert.True(t, ok, "the client should implement the Config interface") 177 | assert.Equal(t, "", cfg.DSN(), "the client should return an empty DSN") 178 | }) 179 | 180 | t.Run("with no custom DSN", func( t *testing.T) { 181 | cl := NewClient() 182 | assert.NotNil(t, cl, "the client should not be nil") 183 | 184 | cfg, ok := cl.(Config) 185 | assert.True(t, ok, "the client should implement the Config interface") 186 | assert.Equal(t, "", cfg.DSN(), "the client should return an empty DSN") 187 | }) 188 | 189 | t.Run("with a custom DSN", func( t *testing.T) { 190 | cl := NewClient(DSN("test")) 191 | assert.NotNil(t, cl, "the client should not be nil") 192 | 193 | cfg, ok := cl.(Config) 194 | assert.True(t, ok, "the client should implement the Config interface") 195 | assert.Equal(t, "test", cfg.DSN(), "the client should return the DSN") 196 | }) 197 | 198 | t.Run("with a custom DSN on the parent", func( t *testing.T) { 199 | cl := NewClient(DSN("test")) 200 | assert.NotNil(t, cl, "the client should not be nil") 201 | 202 | dcl := cl.With() 203 | assert.NotNil(t, dcl, "the derived client should not be nil") 204 | 205 | cfg, ok := dcl.(Config) 206 | assert.True(t, ok, "the derived client should implement the Config interface") 207 | assert.Equal(t, "test", cfg.DSN(), "the derived client should return the DSN") 208 | }) 209 | }) 210 | 211 | t.Run("SendQueue()", func(t *testing.T) { 212 | t.Run("with no default send queue", func(t *testing.T) { 213 | oldDefaultOptionProviders := defaultOptionProviders 214 | defer func() { 215 | defaultOptionProviders = oldDefaultOptionProviders 216 | }() 217 | 218 | defaultOptionProviders = []func() Option{} 219 | 220 | cl := NewClient() 221 | assert.NotNil(t, cl, "the client should not be nil") 222 | 223 | cfg, ok := cl.(Config) 224 | assert.True(t, ok, "the client should implement the Config interface") 225 | assert.NotNil(t, cfg.SendQueue(), "the client should not have a nil send queue") 226 | assert.IsType(t, NewSequentialSendQueue(0), cfg.SendQueue(), "the client should default to the sequential send queue") 227 | }) 228 | 229 | t.Run("with a custom send queue option implementation", func(t *testing.T) { 230 | cl := NewClient(&testCustomClassOption{"sentry-go.sendqueue"}) 231 | assert.NotNil(t, cl, "the client should not be nil") 232 | 233 | cfg, ok := cl.(Config) 234 | assert.True(t, ok, "the client should implement the Config interface") 235 | assert.NotNil(t, cfg.SendQueue(), "the client should not have a nil send queue") 236 | assert.IsType(t, NewSequentialSendQueue(0), cfg.SendQueue(), "the client should default to the sequential send queue") 237 | }) 238 | 239 | t.Run("with no custom send queue", func( t *testing.T) { 240 | cl := NewClient() 241 | assert.NotNil(t, cl, "the client should not be nil") 242 | 243 | cfg, ok := cl.(Config) 244 | assert.True(t, ok, "the client should implement the Config interface") 245 | assert.NotNil(t, cfg.SendQueue(), "the client should not have a nil send queue") 246 | assert.IsType(t, NewSequentialSendQueue(0), cfg.SendQueue(), "the client should default to the sequential send queue") 247 | assert.Equal(t, DefaultClient().GetOption("sentry-go.sendqueue").(*sendQueueOption).queue, cfg.SendQueue(), "the client should use the global default send queue") 248 | }) 249 | 250 | t.Run("with a custom send queue", func( t *testing.T) { 251 | q := NewSequentialSendQueue(0) 252 | cl := NewClient(UseSendQueue(q)) 253 | assert.NotNil(t, cl, "the client should not be nil") 254 | 255 | cfg, ok := cl.(Config) 256 | assert.True(t, ok, "the client should implement the Config interface") 257 | assert.NotNil(t, cfg.SendQueue(), "the client should not have a nil send queue") 258 | assert.Equal(t, q, cfg.SendQueue(), "the client should use the configured send queue") 259 | }) 260 | 261 | t.Run("with a custom send queue on the parent", func( t *testing.T) { 262 | q := NewSequentialSendQueue(0) 263 | cl := NewClient(UseSendQueue(q)) 264 | assert.NotNil(t, cl, "the client should not be nil") 265 | 266 | dcl := cl.With() 267 | assert.NotNil(t, dcl, "the derived client should not be nil") 268 | 269 | cfg, ok := dcl.(Config) 270 | assert.True(t, ok, "the derived client should implement the Config interface") 271 | assert.NotNil(t, cfg.SendQueue(), "the client should not have a nil send queue") 272 | assert.Equal(t, q, cfg.SendQueue(), "the client should use the configured send queue") 273 | }) 274 | }) 275 | 276 | t.Run("Transport()", func(t *testing.T) { 277 | t.Run("with no default transports", func(t *testing.T) { 278 | oldDefaultOptionProviders := defaultOptionProviders 279 | defer func() { 280 | defaultOptionProviders = oldDefaultOptionProviders 281 | }() 282 | 283 | defaultOptionProviders = []func() Option{} 284 | 285 | cl := NewClient() 286 | assert.NotNil(t, cl, "the client should not be nil") 287 | 288 | cfg, ok := cl.(Config) 289 | assert.True(t, ok, "the client should implement the Config interface") 290 | assert.NotNil(t, cfg.Transport(), "the client should not have a nil transport") 291 | assert.IsType(t, newHTTPTransport(), cfg.Transport(), "the client should default to the HTTP transport") 292 | }) 293 | 294 | t.Run("with a custom transport option implementation", func(t *testing.T) { 295 | cl := NewClient(&testCustomClassOption{"sentry-go.transport"}) 296 | assert.NotNil(t, cl, "the client should not be nil") 297 | 298 | cfg, ok := cl.(Config) 299 | assert.True(t, ok, "the client should implement the Config interface") 300 | assert.NotNil(t, cfg.Transport(), "the client should not have a nil transport") 301 | assert.IsType(t, newHTTPTransport(), cfg.Transport(), "the client should default to the HTTP transport") 302 | }) 303 | 304 | t.Run("with no custom transport", func( t *testing.T) { 305 | cl := NewClient() 306 | assert.NotNil(t, cl, "the client should not be nil") 307 | 308 | cfg, ok := cl.(Config) 309 | assert.True(t, ok, "the client should implement the Config interface") 310 | assert.NotNil(t, cfg.Transport(), "the client should not have a nil transport") 311 | assert.IsType(t, newHTTPTransport(), cfg.Transport(), "the client should default to the HTTP transport") 312 | assert.Equal(t, DefaultClient().GetOption("sentry-go.transport").(*transportOption).transport, cfg.Transport(), "the client should use the global default transport") 313 | }) 314 | 315 | t.Run("with a custom transport", func( t *testing.T) { 316 | tr := newHTTPTransport() 317 | cl := NewClient(UseTransport(tr)) 318 | assert.NotNil(t, cl, "the client should not be nil") 319 | 320 | cfg, ok := cl.(Config) 321 | assert.True(t, ok, "the client should implement the Config interface") 322 | assert.NotNil(t, cfg.Transport(), "the client should not have a nil transport") 323 | assert.Equal(t, tr, cfg.Transport(), "the client should use the configured transport") 324 | }) 325 | 326 | t.Run("with a custom transport on the parent", func( t *testing.T) { 327 | tr := newHTTPTransport() 328 | cl := NewClient(UseTransport(tr)) 329 | assert.NotNil(t, cl, "the client should not be nil") 330 | 331 | dcl := cl.With() 332 | assert.NotNil(t, dcl, "the derived client should not be nil") 333 | 334 | cfg, ok := dcl.(Config) 335 | assert.True(t, ok, "the derived client should implement the Config interface") 336 | assert.NotNil(t, cfg.Transport(), "the client should not have a nil transport") 337 | assert.Equal(t, tr, cfg.Transport(), "the client should use the configured transport") 338 | }) 339 | }) 340 | } 341 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // A Config allows you to control how events are sent to Sentry. 4 | // It is usually populated through the standard build pipeline 5 | // through the DSN() and UseTransport() options. 6 | type Config interface { 7 | DSN() string 8 | Transport() Transport 9 | SendQueue() SendQueue 10 | } 11 | -------------------------------------------------------------------------------- /contexts.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/matishsiao/goInfo" 9 | ) 10 | 11 | func init() { 12 | gi := goInfo.GetInfo() 13 | 14 | AddDefaultOptions( 15 | RuntimeContext("go", strings.TrimPrefix(runtime.Version(), "go")), 16 | OSContext(&OSContextInfo{ 17 | Name: gi.GoOS, 18 | Version: gi.OS, 19 | KernelVersion: gi.Core, 20 | }), 21 | DeviceContext(&DeviceContextInfo{ 22 | Architecture: gi.Platform, 23 | Family: gi.Kernel, 24 | Model: "Unknown", 25 | }), 26 | ) 27 | } 28 | 29 | // OSContextInfo describes the operating system that your application 30 | // is running on. 31 | type OSContextInfo struct { 32 | Type string `json:"type,omitempty"` 33 | Name string `json:"name"` 34 | Version string `json:"version,omitempty"` 35 | Build string `json:"build,omitempty"` 36 | KernelVersion string `json:"kernel_version,omitempty"` 37 | Rooted bool `json:"rooted,omitempty"` 38 | } 39 | 40 | // OSContext allows you to set the context describing 41 | // the operating system that your application is running on. 42 | func OSContext(info *OSContextInfo) Option { 43 | return Context("os", info) 44 | } 45 | 46 | // DeviceContextInfo describes the device that your application 47 | // is running on. 48 | type DeviceContextInfo struct { 49 | Type string `json:"type,omitempty"` 50 | Name string `json:"name"` 51 | Family string `json:"family,omitempty"` 52 | Model string `json:"model,omitempty"` 53 | ModelID string `json:"model_id,omitempty"` 54 | Architecture string `json:"arch,omitempty"` 55 | BatteryLevel int `json:"battery_level,omitempty"` 56 | Orientation string `json:"orientation,omitempty"` 57 | } 58 | 59 | // DeviceContext allows you to set the context describing the 60 | // device that your application is being executed on. 61 | func DeviceContext(info *DeviceContextInfo) Option { 62 | return Context("device", info) 63 | } 64 | 65 | // RuntimeContext allows you to set the information 66 | // pertaining to the runtime that your program is 67 | // executing on. 68 | func RuntimeContext(name, version string) Option { 69 | return Context("runtime", map[string]string{ 70 | "name": name, 71 | "version": version, 72 | }) 73 | } 74 | 75 | // Context allows you to manually set a context entry 76 | // by providing its key and the data to accompany it. 77 | // This is a low-level method and you should be familiar 78 | // with the correct usage of contexts before using it. 79 | // https://docs.sentry.io/clientdev/interfaces/contexts/ 80 | func Context(key string, data interface{}) Option { 81 | return &contextOption{ 82 | contexts: map[string]interface{}{ 83 | key: data, 84 | }, 85 | } 86 | } 87 | 88 | type contextOption struct { 89 | contexts map[string]interface{} 90 | } 91 | 92 | func (o *contextOption) Class() string { 93 | return "contexts" 94 | } 95 | 96 | func (o *contextOption) MarshalJSON() ([]byte, error) { 97 | return json.Marshal(o.contexts) 98 | } 99 | 100 | func (o *contextOption) Merge(old Option) Option { 101 | if old, ok := old.(*contextOption); ok { 102 | ctx := map[string]interface{}{} 103 | for k, v := range old.contexts { 104 | ctx[k] = v 105 | } 106 | 107 | for k, v := range o.contexts { 108 | ctx[k] = v 109 | } 110 | 111 | return &contextOption{ctx} 112 | } 113 | 114 | return o 115 | } 116 | -------------------------------------------------------------------------------- /contexts_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func ExampleRuntimeContext() { 11 | cl := NewClient( 12 | // You can configure this when creating your client 13 | RuntimeContext("go", runtime.Version()), 14 | ) 15 | 16 | cl.Capture( 17 | // Or when sending an event 18 | RuntimeContext("go", runtime.Version()), 19 | ) 20 | } 21 | 22 | func ExampleOSContext() { 23 | osInfo := OSContextInfo{ 24 | Version: "CentOS 7.3", 25 | Build: "centos7.3.1611", 26 | KernelVersion: "3.10.0-514", 27 | Rooted: false, 28 | } 29 | 30 | cl := NewClient( 31 | // You can provide this when creating your client 32 | OSContext(&osInfo), 33 | ) 34 | 35 | cl.Capture( 36 | // Or when you send an event 37 | OSContext(&osInfo), 38 | ) 39 | } 40 | 41 | func ExampleDeviceContext() { 42 | deviceInfo := DeviceContextInfo{ 43 | Architecture: "arm", 44 | BatteryLevel: 100, 45 | Family: "Samsung Galaxy", 46 | Model: "Samsung Galaxy S8", 47 | ModelID: "SM-G95550", 48 | Name: "Samsung Galaxy S8", 49 | Orientation: "portrait", 50 | } 51 | 52 | cl := NewClient( 53 | // You can provide this when creating your client 54 | DeviceContext(&deviceInfo), 55 | ) 56 | 57 | cl.Capture( 58 | // Or when you send an event 59 | DeviceContext(&deviceInfo), 60 | ) 61 | } 62 | 63 | func TestRuntimeContext(t *testing.T) { 64 | c := RuntimeContext("go", runtime.Version()) 65 | 66 | assert.NotNil(t, c, "it should not return a nil option") 67 | assert.IsType(t, Context("runtime", nil), c, "it should return the same thing as a Context()") 68 | 69 | cc, ok := c.(*contextOption) 70 | assert.True(t, ok, "it should actually return a *contextOption") 71 | if assert.Contains(t, cc.contexts, "runtime", "it should specify a runtime context") { 72 | assert.Equal(t, map[string]string{ 73 | "name": "go", 74 | "version": runtime.Version(), 75 | }, cc.contexts["runtime"], "it should specify the correct context values") 76 | } 77 | } 78 | 79 | func TestOSContext(t *testing.T) { 80 | osInfo := OSContextInfo{ 81 | Version: "CentOS 7.3", 82 | Build: "centos7.3.1611", 83 | KernelVersion: "3.10.0-514", 84 | Rooted: false, 85 | } 86 | 87 | c := OSContext(&osInfo) 88 | 89 | assert.NotNil(t, c, "it should not return a nil option") 90 | assert.IsType(t, Context("os", nil), c, "it should return the same thing as a Context()") 91 | 92 | cc, ok := c.(*contextOption) 93 | assert.True(t, ok, "it should actually return a *contextOption") 94 | if assert.Contains(t, cc.contexts, "os", "it should specify an os context") { 95 | assert.Equal(t, &osInfo, cc.contexts["os"], "it should specify the correct context values") 96 | } 97 | } 98 | 99 | func TestDeviceContext(t *testing.T) { 100 | deviceInfo := DeviceContextInfo{ 101 | Architecture: "arm", 102 | BatteryLevel: 100, 103 | Family: "Samsung Galaxy", 104 | Model: "Samsung Galaxy S8", 105 | ModelID: "SM-G95550", 106 | Name: "Samsung Galaxy S8", 107 | Orientation: "portrait", 108 | } 109 | 110 | c := DeviceContext(&deviceInfo) 111 | 112 | assert.NotNil(t, c, "it should not return a nil option") 113 | assert.IsType(t, Context("device", nil), c, "it should return the same thing as a Context()") 114 | 115 | cc, ok := c.(*contextOption) 116 | assert.True(t, ok, "it should actually return a *contextOption") 117 | if assert.Contains(t, cc.contexts, "device", "it should specify an os context") { 118 | assert.Equal(t, &deviceInfo, cc.contexts["device"], "it should specify the correct context values") 119 | } 120 | } 121 | 122 | func TestContext(t *testing.T) { 123 | c := Context("test", "data") 124 | assert.NotNil(t, c, "it should not return a nil option") 125 | assert.IsType(t, &contextOption{}, c, "it should actually return a *contextOption") 126 | 127 | cc := c.(*contextOption) 128 | assert.Contains(t, cc.contexts, "test", "it should set the 'test' context") 129 | assert.Equal(t, "data", cc.contexts["test"], "it should set the context data correctly") 130 | } 131 | 132 | func TestContextOption(t *testing.T) { 133 | c := Context("test", "data") 134 | assert.NotNil(t, c, "it should not return a nil option") 135 | 136 | assert.IsType(t, &contextOption{}, c, "it should actually return a *contextOption") 137 | cc := c.(*contextOption) 138 | 139 | assert.Equal(t, "contexts", c.Class(), "it should use the right option class") 140 | assert.Implements(t, (*MergeableOption)(nil), c, "it should implement the MergeableOption interface") 141 | 142 | t.Run("Merge()", func(t *testing.T) { 143 | t.Run("Unknown Type", func(t *testing.T) { 144 | out := cc.Merge(&testOption{}) 145 | assert.Equal(t, c, out, "it should overwrite the original value") 146 | }) 147 | 148 | t.Run("Existing Context", func(t *testing.T) { 149 | old := Context("test", "oldData") 150 | out := cc.Merge(old) 151 | 152 | assert.NotNil(t, out, "it should not return a nil result") 153 | assert.IsType(t, &contextOption{}, out, "it should return a new *contextOption") 154 | 155 | oo := out.(*contextOption) 156 | assert.Equal(t, map[string]interface{}{ 157 | "test": "data", 158 | }, oo.contexts) 159 | }) 160 | 161 | t.Run("Existing Different Context", func(t *testing.T) { 162 | old := Context("old", "oldData") 163 | out := cc.Merge(old) 164 | 165 | assert.NotNil(t, out, "it should not return a nil result") 166 | assert.IsType(t, &contextOption{}, out, "it should return a new *contextOption") 167 | 168 | oo := out.(*contextOption) 169 | assert.Equal(t, map[string]interface{}{ 170 | "test": "data", 171 | "old": "oldData", 172 | }, oo.contexts) 173 | }) 174 | }) 175 | 176 | t.Run("MarshalJSON()", func(t *testing.T) { 177 | c := Context("test", "data") 178 | assert.Equal(t, map[string]interface{}{ "test": "data" }, testOptionsSerialize(t, c)) 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /culprit.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | // Culprit allows you to specify the name of the transaction (or culprit) 6 | // which casued this event. 7 | // For example, in a web app, this might be the route name: `/welcome/` 8 | func Culprit(culprit string) Option { 9 | return &culpritOption{culprit} 10 | } 11 | 12 | type culpritOption struct { 13 | culprit string 14 | } 15 | 16 | func (o *culpritOption) Class() string { 17 | return "culprit" 18 | } 19 | 20 | func (o *culpritOption) MarshalJSON() ([]byte, error) { 21 | return json.Marshal(o.culprit) 22 | } 23 | -------------------------------------------------------------------------------- /culprit_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleCulprit() { 10 | cl := NewClient( 11 | // You can set this when creating your client 12 | Culprit("example"), 13 | ) 14 | 15 | cl.Capture( 16 | // Or you can set it when sending an event 17 | Culprit("example"), 18 | ) 19 | } 20 | 21 | func TestCulprit(t *testing.T) { 22 | c := Culprit("test") 23 | assert.Implements(t, (*Option)(nil), c, "it should implement the Option interface") 24 | assert.Equal(t, "culprit", c.Class(), "it should use the correct option class") 25 | assert.Equal(t, "test", testOptionsSerialize(t, c), "it should serialize to the value which was passed in the constructor") 26 | } 27 | -------------------------------------------------------------------------------- /dsn.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "path" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func init() { 14 | AddDefaultOptions(DSN(os.Getenv("SENTRY_DSN"))) 15 | } 16 | 17 | // DSN lets you specify the unique Sentry DSN used to submit events for 18 | // your application. Specifying an empty DSN will disable the client. 19 | func DSN(dsn string) Option { 20 | return &dsnOption{dsn} 21 | } 22 | 23 | type dsnOption struct { 24 | dsn string 25 | } 26 | 27 | func (o *dsnOption) Class() string { 28 | return "sentry-go.dsn" 29 | } 30 | 31 | func (o *dsnOption) Omit() bool { 32 | return true 33 | } 34 | 35 | const ( 36 | // ErrBadURL is returned when a DSN cannot be parsed due to 37 | // formatting errors in its URL 38 | ErrBadURL = ErrType("sentry: bad DSN URL") 39 | 40 | // ErrMissingPublicKey is returned when a DSN does not have 41 | // a valid public key contained within its URL 42 | ErrMissingPublicKey = ErrType("sentry: missing public key") 43 | 44 | // ErrMissingPrivateKey is returned when a DSN does not have 45 | // a valid private key contained within its URL 46 | // [DEPRECATED] error is never thrown since Sentry 9 has deprecated the secret key requirement 47 | ErrMissingPrivateKey = ErrType("sentry: missing private key") 48 | 49 | // ErrMissingProjectID is returned when a DSN does not have a valid 50 | // project ID contained within its URL 51 | ErrMissingProjectID = ErrType("sentry: missing project ID") 52 | ) 53 | 54 | type dsn struct { 55 | URL string 56 | PublicKey string 57 | PrivateKey string 58 | ProjectID string 59 | } 60 | 61 | func newDSN(url string) (*dsn, error) { 62 | d := &dsn{} 63 | if err := d.Parse(url); err != nil { 64 | return nil, err 65 | } 66 | 67 | return d, nil 68 | } 69 | 70 | func (d *dsn) AuthHeader() string { 71 | if d.PublicKey == "" { 72 | return "" 73 | } 74 | 75 | if d.PrivateKey == "" { 76 | return fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s", d.PublicKey) 77 | } 78 | 79 | return fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", d.PublicKey, d.PrivateKey) 80 | } 81 | 82 | func (d *dsn) Parse(dsn string) error { 83 | if dsn == "" { 84 | return nil 85 | } 86 | 87 | uri, err := url.Parse(dsn) 88 | if err != nil { 89 | return errors.Wrap(err, ErrBadURL.Error()) 90 | } 91 | 92 | if uri.User == nil { 93 | return errors.Wrap(fmt.Errorf("missing URL user"), ErrMissingPublicKey.Error()) 94 | } 95 | 96 | d.PublicKey = uri.User.Username() 97 | 98 | privateKey, ok := uri.User.Password() 99 | if ok { 100 | d.PrivateKey = privateKey 101 | } 102 | 103 | uri.User = nil 104 | 105 | if idx := strings.LastIndex(uri.Path, "/"); idx != -1 { 106 | d.ProjectID = uri.Path[idx+1:] 107 | uri.Path = fmt.Sprintf("%s/", path.Join(uri.Path[:idx+1], "api", d.ProjectID, "store")) 108 | } 109 | 110 | if d.ProjectID == "" { 111 | return errors.Wrap(fmt.Errorf("missing Project ID"), ErrMissingProjectID.Error()) 112 | } 113 | 114 | d.URL = uri.String() 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /dsn_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleDSN() { 10 | cl := NewClient( 11 | // You can configure the DSN when creating a client 12 | DSN("https://key:pass@example.com/sentry/1"), 13 | ) 14 | 15 | cl.Capture( 16 | // You can also configure the DSN when sending an event 17 | DSN(""), 18 | Message("This won't be sent"), 19 | ) 20 | } 21 | 22 | func TestDSN(t *testing.T) { 23 | t.Run("Parse()", func (t *testing.T) { 24 | cases := []struct { 25 | Name string 26 | URL string 27 | Error error 28 | }{ 29 | {"With a valid URL", "https://u:p@example.com/sentry/1", nil}, 30 | {"With a badly formatted URL", ":", ErrBadURL}, 31 | {"Without a public key", "https://example.com/sentry/1", ErrMissingPublicKey}, 32 | {"Without a private key", "https://u@example.com/sentry/1", nil}, 33 | {"Without a project ID", "https://u:p@example.com", ErrMissingProjectID}, 34 | } 35 | 36 | for _, tc := range cases { 37 | tc := tc 38 | t.Run(tc.Name, func(t *testing.T) { 39 | d := &dsn{} 40 | err := d.Parse(tc.URL) 41 | 42 | if tc.Error == nil { 43 | assert.Nil(t, err, "it should not return an error") 44 | } else { 45 | assert.NotNil(t, err, "it should return an error") 46 | assert.Regexp(t, "^" + tc.Error.Error(), err.Error(), "it should return the right error") 47 | } 48 | }) 49 | } 50 | }) 51 | 52 | t.Run("AuthHeader()", func(t *testing.T) { 53 | assert.Equal(t, "", (&dsn{PrivateKey: "secret"}).AuthHeader(), "should return no auth header if no public key is provided") 54 | assert.Equal(t, "Sentry sentry_version=4, sentry_key=key", (&dsn{PublicKey: "key"}).AuthHeader(), "should return an auth header with just the public key if no private key is provided") 55 | assert.Equal(t, "Sentry sentry_version=4, sentry_key=key, sentry_secret=secret", (&dsn{PublicKey: "key", PrivateKey: "secret"}).AuthHeader(), "should return a full auth header both the public and private key are provided") 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | func init() { 9 | AddDefaultOptionProvider(func() Option { 10 | if env := os.Getenv("ENV"); env != "" { 11 | return Environment(env) 12 | } 13 | 14 | if env := os.Getenv("ENVIRONMENT"); env != "" { 15 | return Environment(env) 16 | } 17 | 18 | return nil 19 | }) 20 | } 21 | 22 | // Environment allows you to configure the name of the environment 23 | // you pass to Sentry with your event. This would usually be something 24 | // like "production" or "staging". 25 | func Environment(env string) Option { 26 | return &environmentOption{env} 27 | } 28 | 29 | type environmentOption struct { 30 | env string 31 | } 32 | 33 | func (o *environmentOption) Class() string { 34 | return "environment" 35 | } 36 | 37 | func (o *environmentOption) MarshalJSON() ([]byte, error) { 38 | return json.Marshal(o.env) 39 | } 40 | -------------------------------------------------------------------------------- /environment_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func ExampleEnvironment() { 11 | cl := NewClient( 12 | // You can configure your environment at the client level 13 | Environment("development"), 14 | ) 15 | 16 | cl.Capture( 17 | // ...or at the event level 18 | Environment("prod"), 19 | ) 20 | } 21 | 22 | func TestEnvironment(t *testing.T) { 23 | o := Environment("testing") 24 | assert.NotNil(t, o, "it should not return a nil option") 25 | assert.Implements(t, (*Option)(nil), o, "it should implement the option interface") 26 | assert.Equal(t, "environment", o.Class(), "it should use the correct option class") 27 | 28 | t.Run("No Environment", func(t *testing.T) { 29 | os.Unsetenv("ENV") 30 | os.Unsetenv("ENVIRONMENT") 31 | 32 | opt := testGetOptionsProvider(t, &environmentOption{}) 33 | assert.Nil(t, opt, "it should not be registered as a default option provider") 34 | }) 35 | 36 | t.Run("$ENV=...", func(t *testing.T){ 37 | os.Setenv("ENV", "testing") 38 | defer os.Unsetenv("ENV") 39 | 40 | opt := testGetOptionsProvider(t, &environmentOption{}) 41 | assert.NotNil(t, opt, "it should be registered with the default option providers") 42 | assert.IsType(t, &environmentOption{}, opt, "it should actually be an *environmentOption") 43 | 44 | oo := opt.(*environmentOption) 45 | assert.Equal(t, "testing", oo.env, "it should set the environment to the same value as the $ENV variable") 46 | }) 47 | 48 | t.Run("$ENVIRONMENT=...", func(t *testing.T){ 49 | os.Setenv("ENVIRONMENT", "testing") 50 | defer os.Unsetenv("ENVIRONMENT") 51 | 52 | opt := testGetOptionsProvider(t, &environmentOption{}) 53 | assert.NotNil(t, opt, "it should be registered with the default option providers") 54 | assert.IsType(t, &environmentOption{}, opt, "it should actually be an *environmentOption") 55 | 56 | oo := opt.(*environmentOption) 57 | assert.Equal(t, "testing", oo.env, "it should set the environment to the same value as the $ENVIRONMENT variable") 58 | }) 59 | 60 | t.Run("MarshalJSON()", func(t *testing.T) { 61 | s := testOptionsSerialize(t, o) 62 | assert.Equal(t, "testing", s, "it should serialize to the name of the environment") 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /errortype.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "strings" 4 | 5 | // ErrType represents an error which may contain hierarchical error information. 6 | type ErrType string 7 | 8 | // IsInstance will tell you whether a given error is an instance 9 | // of this ErrType 10 | func (e ErrType) IsInstance(err error) bool { 11 | return strings.Contains(err.Error(), string(e)) 12 | } 13 | 14 | // Unwrap will unwrap this error and return the underlying error which caused 15 | // it to be triggered. 16 | func (e ErrType) Unwrap() error { 17 | return nil 18 | } 19 | 20 | // Error gets the error message for this ErrType 21 | func (e ErrType) Error() string { 22 | return string(e) 23 | } 24 | -------------------------------------------------------------------------------- /errortype_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestErrType(t *testing.T) { 11 | const errType = ErrType("sentry: this is a test error") 12 | assert.True(t, errType.IsInstance(errType), "it should be an instance of itself") 13 | assert.True(t, errType.IsInstance(errors.New(errType.Error())), "errors with the same message should be an instance of this error") 14 | assert.EqualError(t, errType, "sentry: this is a test error", "it should report the correct error message") 15 | 16 | type UnwrappableError interface { 17 | Unwrap() error 18 | } 19 | 20 | if assert.Implements(t, (*UnwrappableError)(nil), errType, "it should implement the Unwrap() method") { 21 | var err error 22 | err = errType 23 | 24 | assert.Nil(t, err.(UnwrappableError).Unwrap(), "unwrapping the error should return nil") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /eventID.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "encoding/json" 7 | "io" 8 | ) 9 | 10 | func init() { 11 | AddDefaultOptionProvider(func() Option { 12 | id, err := NewEventID() 13 | if err != nil { 14 | return nil 15 | } 16 | 17 | return EventID(id) 18 | }) 19 | } 20 | 21 | // NewEventID attempts to generate a new random UUIDv4 event 22 | // ID which can be passed to the EventID() option. 23 | func NewEventID() (string, error) { 24 | id := make([]byte, 16) 25 | _, err := io.ReadFull(rand.Reader, id) 26 | if err != nil { 27 | return "", err 28 | } 29 | id[6] &= 0x0F // clear version 30 | id[6] |= 0x40 // set version to 4 (random uuid) 31 | id[8] &= 0x3F // clear variant 32 | id[8] |= 0x80 // set to IETF variant 33 | return hex.EncodeToString(id), nil 34 | } 35 | 36 | // EventID is an option which controls the UUID used to represent 37 | // an event. The ID should be exactly 32 hexadecimal characters long 38 | // and include no dashes. 39 | // If an invalid ID is passed to this option, it will return nil and 40 | // be ignored by the packet builder. 41 | func EventID(id string) Option { 42 | if len(id) != 32 { 43 | return nil 44 | } 45 | 46 | for _, r := range id { 47 | if r <= 'f' && r >= 'a' { 48 | continue 49 | } 50 | 51 | if r <= '9' && r >= '0' { 52 | continue 53 | } 54 | 55 | return nil 56 | } 57 | 58 | return &eventIDOption{id} 59 | } 60 | 61 | func (p packet) getEventID() string { 62 | if idOpt, ok := p["event_id"]; ok { 63 | if id, ok := idOpt.(*eventIDOption); ok { 64 | return id.ID 65 | } 66 | } 67 | 68 | return "" 69 | } 70 | 71 | type eventIDOption struct { 72 | ID string 73 | } 74 | 75 | func (o *eventIDOption) Class() string { 76 | return "event_id" 77 | } 78 | 79 | func (o *eventIDOption) MarshalJSON() ([]byte, error) { 80 | return json.Marshal(o.ID) 81 | } 82 | -------------------------------------------------------------------------------- /eventID_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func ExampleEventID() { 11 | id, err := NewEventID() 12 | if err != nil { 13 | log.Fatalln(err) 14 | } 15 | 16 | cl := NewClient() 17 | 18 | ctxCl := cl.With( 19 | // You could set the event ID for a context specific 20 | // client if you wanted (but you probably shouldn't). 21 | EventID(id), 22 | ) 23 | 24 | ctxCl.Capture( 25 | // The best place to set it is when you are ready to send 26 | // an event to Sentry. 27 | EventID(id), 28 | ) 29 | } 30 | 31 | func TestEventID(t *testing.T) { 32 | id, err := NewEventID() 33 | assert.Nil(t, err, "creating an event ID shouldn't return an error") 34 | assert.Regexp(t, "^[0-9a-f]{32}$", id, "the event ID should be 32 characters long and only alphanumeric characters") 35 | 36 | t.Run("EventID()", func(t *testing.T) { 37 | assert.Nil(t, EventID("invalid"), "it should return nil if the ID has the wrong length") 38 | assert.Nil(t, EventID("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"), "it should return nil if the ID contains invalid characters") 39 | 40 | o := EventID(id) 41 | assert.NotNil(t, o, "it should return a non-nil option if the ID is valid") 42 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 43 | 44 | assert.Equal(t, "event_id", o.Class(), "it should use the correct option class") 45 | 46 | t.Run("MarshalJSON()", func(t *testing.T) { 47 | assert.Equal(t, id, testOptionsSerialize(t, EventID(id)), "it should serialize to the ID") 48 | }) 49 | }) 50 | 51 | t.Run("Packet Extensions", func(t *testing.T) { 52 | t.Run("getEventID()", func(t *testing.T) { 53 | p := NewPacket() 54 | assert.NotNil(t, p, "the packet should not be nil") 55 | 56 | pp, ok := p.(*packet) 57 | assert.True(t, ok, "the packet should actually be a *packet") 58 | assert.Equal(t, "", pp.getEventID(), "it should return an empty event ID if there is no EventID option") 59 | 60 | p = NewPacket().SetOptions(EventID(id)) 61 | assert.NotNil(t, p, "the packet should not be nil") 62 | 63 | pp, ok = p.(*packet) 64 | assert.True(t, ok, "the packet should actually be a *packet") 65 | assert.Equal(t, id, pp.getEventID(), "it should return the event ID") 66 | }) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func Example() { 11 | cl := NewClient( 12 | // Your DSN is fetched from the $SENTRY_DSN environment 13 | // variable automatically. But you can override it if you 14 | // prefer... 15 | DSN("https://key:secret@example.com/sentry/1"), 16 | Release("v1.0.0"), 17 | 18 | // Your environment is fetched from $ENV/$ENVIRONMENT automatically, 19 | // but you can override it here if you prefer. 20 | Environment("example"), 21 | 22 | Logger("example"), 23 | ) 24 | 25 | err := errors.New("something went wrong") 26 | 27 | // The HTTP request that was being handled when this error occurred 28 | var req *http.Request 29 | 30 | e := cl.Capture( 31 | Culprit("GET /api/v1/explode"), 32 | ExceptionForError(err), 33 | HTTPRequest(req).WithHeaders().WithCookies(), 34 | ) 35 | 36 | if err := e.Error(); err != nil { 37 | fmt.Printf("Failed to send event: %s", err.Error()) 38 | } else { 39 | fmt.Printf("Sent event (id: %s)\n", e.EventID()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /exception.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | ) 7 | 8 | // NewExceptionInfo creates a new ExceptionInfo object which can 9 | // then be populated with information about an exception which 10 | // occurred before being passed to the Exception() method for 11 | // submission to Sentry. 12 | func NewExceptionInfo() *ExceptionInfo { 13 | ex := &ExceptionInfo{ 14 | Type: "unknown", 15 | Value: "An unknown error has occurred", 16 | StackTrace: StackTrace(), 17 | } 18 | 19 | return ex 20 | } 21 | 22 | // An ExceptionInfo describes the details of an exception that occurred within 23 | // your application. 24 | type ExceptionInfo struct { 25 | Type string `json:"type"` 26 | Value string `json:"value"` 27 | Module string `json:"module,omitempty"` 28 | ThreadID string `json:"thread_id,omitempty"` 29 | Mechanism string `json:"mechanism,omitempty"` 30 | StackTrace StackTraceOption `json:"stacktrace,omitempty"` 31 | } 32 | 33 | // ForError updates an ExceptionInfo object with information sourced 34 | // from an error. 35 | func (e *ExceptionInfo) ForError(err error) *ExceptionInfo { 36 | e.Type = reflect.TypeOf(err).String() 37 | e.Value = err.Error() 38 | 39 | if e.StackTrace == nil { 40 | e.StackTrace = StackTrace().ForError(err) 41 | } else { 42 | e.StackTrace.ForError(err) 43 | } 44 | 45 | if m := errorMsgPattern.FindStringSubmatch(err.Error()); m != nil { 46 | e.Module = m[1] 47 | e.Type = m[2] 48 | } 49 | 50 | return e 51 | } 52 | 53 | // ExceptionForError allows you to include the details of an error which 54 | // occurred within your application as part of the event you send to Sentry. 55 | func ExceptionForError(err error) Option { 56 | if err == nil { 57 | return nil 58 | } 59 | 60 | exceptions := []*ExceptionInfo{} 61 | 62 | for err != nil { 63 | exceptions = append([]*ExceptionInfo{NewExceptionInfo().ForError(err)}, exceptions...) 64 | 65 | switch e := err.(type) { 66 | case interface { 67 | Cause() error 68 | }: 69 | err = e.Cause() 70 | case interface { 71 | Unwrap() error 72 | }: 73 | err = e.Unwrap() 74 | default: 75 | err = nil 76 | } 77 | } 78 | 79 | return &exceptionOption{ 80 | Exceptions: exceptions, 81 | } 82 | } 83 | 84 | // Exception allows you to include the details of an exception which occurred 85 | // within your application as part of the event you send to Sentry. 86 | func Exception(info *ExceptionInfo) Option { 87 | return &exceptionOption{ 88 | Exceptions: []*ExceptionInfo{info}, 89 | } 90 | } 91 | 92 | var errorMsgPattern = regexp.MustCompile(`\A(?:(\w+): )?([^:]+)`) 93 | 94 | type exceptionOption struct { 95 | Exceptions []*ExceptionInfo `json:"values"` 96 | } 97 | 98 | func (o *exceptionOption) Class() string { 99 | return "exception" 100 | } 101 | 102 | func (o *exceptionOption) Merge(old Option) Option { 103 | if old, ok := old.(*exceptionOption); ok { 104 | return &exceptionOption{ 105 | Exceptions: append(old.Exceptions, o.Exceptions...), 106 | } 107 | } 108 | 109 | return o 110 | } 111 | 112 | func (o *exceptionOption) Finalize() { 113 | for _, ex := range o.Exceptions { 114 | if ex.StackTrace != nil { 115 | if finalize, ok := ex.StackTrace.(FinalizeableOption); ok { 116 | finalize.Finalize() 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /exception_go1.13_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package sentry 4 | 5 | import ( 6 | "testing" 7 | "fmt" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestExceptionForErrorWrappingGo113(t *testing.T) { 13 | t.Run("fmt.Errorf()", func(t *testing.T) { 14 | err := fmt.Errorf("root cause") 15 | err = fmt.Errorf("cause 1: %w", err) 16 | err = fmt.Errorf("cause 2: %w", err) 17 | err = fmt.Errorf("example error: %w", err) 18 | 19 | e := ExceptionForError(err) 20 | assert.NotNil(t, e, "it should return a non-nil option") 21 | 22 | exx, ok := e.(*exceptionOption) 23 | assert.True(t, ok, "the option should actually be a *exceptionOption") 24 | 25 | assert.Len(t, exx.Exceptions, 4) 26 | assert.Equal(t, "root cause", exx.Exceptions[0].Value) 27 | }) 28 | } -------------------------------------------------------------------------------- /exception_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestException(t *testing.T) { 12 | ex := NewExceptionInfo() 13 | assert.NotNil(t, ex, "the exception info should not be nil") 14 | 15 | e := Exception(ex) 16 | assert.NotNil(t, e, "the exception option should not be nil") 17 | assert.Implements(t, (*Option)(nil), e, "it should implement the Option interface") 18 | assert.Equal(t, "exception", e.Class(), "it should use the correct option class") 19 | 20 | exx, ok := e.(*exceptionOption) 21 | assert.True(t, ok, "the exception option should actually be an *exceptionOption") 22 | 23 | t.Run("Merge()", func(t *testing.T) { 24 | assert.Implements(t, (*MergeableOption)(nil), e, "it should implement the MergeableOption interface") 25 | 26 | ex2 := NewExceptionInfo() 27 | e2 := Exception(ex2) 28 | 29 | mergeable, ok := e2.(MergeableOption) 30 | assert.True(t, ok, "the exception option should be mergeable") 31 | 32 | e3 := mergeable.Merge(e) 33 | assert.NotNil(t, e3, "the resulting merged exception option should not be nil") 34 | assert.IsType(t, e, e3, "the resulting merged exception option should be the same type as the original option") 35 | 36 | exx, ok := e3.(*exceptionOption) 37 | assert.True(t, ok, "the resulting merged exception option should actually be a *exceptionOption") 38 | 39 | if assert.Len(t, exx.Exceptions, 2, "it should contain both exceptions") { 40 | assert.Equal(t, ex, exx.Exceptions[0], "the first exception should be the first exception we found") 41 | assert.Equal(t, ex2, exx.Exceptions[1], "the second exception should be the second exception we found") 42 | } 43 | 44 | e3 = mergeable.Merge(&testOption{}) 45 | assert.Equal(t, e2, e3, "if the other option is not an exception option then it should be replaced") 46 | }) 47 | 48 | t.Run("Finalize()", func(t *testing.T) { 49 | assert.Implements(t, (*FinalizeableOption)(nil), e, "it should implement the FinalizeableOption interface") 50 | 51 | assert.Len(t, exx.Exceptions, 1, "one exception should be registered") 52 | 53 | st := exx.Exceptions[0].StackTrace 54 | assert.NotNil(t, st, "the exception shoudl have a stacktrace") 55 | st.WithInternalPrefixes("github.com/SierraSoftworks/sentry-go") 56 | 57 | sti, ok := st.(*stackTraceOption) 58 | assert.True(t, ok, "the stacktrace should actually be a *stackTraceOption") 59 | assert.NotEmpty(t, sti.Frames, "the stacktrace should include stack frames") 60 | 61 | hasInternal := false 62 | for _, frame := range sti.Frames { 63 | if frame.InApp { 64 | hasInternal = true 65 | } 66 | } 67 | assert.False(t, hasInternal, "the internal stack frames should not have been processed yet") 68 | 69 | exx.Finalize() 70 | 71 | hasInternal = false 72 | for _, frame := range sti.Frames { 73 | if frame.InApp { 74 | hasInternal = true 75 | } 76 | } 77 | assert.True(t, hasInternal, "the internal stack frames should have been identified now") 78 | }) 79 | 80 | t.Run("MarshalJSON()", func(t *testing.T) { 81 | serialized := testOptionsSerialize(t, Exception(&ExceptionInfo{ 82 | Type: "TestException", 83 | Value: "This is a test", 84 | })) 85 | 86 | assert.Equal(t, map[string]interface{}{ 87 | "values": []interface{}{ 88 | map[string]interface{}{ 89 | "type": "TestException", 90 | "value": "This is a test", 91 | }, 92 | }, 93 | }, serialized) 94 | }) 95 | } 96 | 97 | func TestExceptionForError(t *testing.T) { 98 | assert.Nil(t, ExceptionForError(nil), "it should return nil if the error is nil") 99 | 100 | err := fmt.Errorf("example error") 101 | e := ExceptionForError(err) 102 | assert.NotNil(t, e, "it should return a non-nil option") 103 | assert.Implements(t, (*Option)(nil), e, "it should implement the Option interface") 104 | 105 | t.Run("github.com/pkg/errors", func(t *testing.T) { 106 | err := errors.New("root cause") 107 | err = errors.Wrap(err, "cause 1") 108 | err = errors.Wrap(err, "cause 2") 109 | err = errors.Wrap(err, "example error") 110 | 111 | e := ExceptionForError(err) 112 | assert.NotNil(t, e, "it should return a non-nil option") 113 | 114 | exx, ok := e.(*exceptionOption) 115 | assert.True(t, ok, "the option should actually be a *exceptionOption") 116 | 117 | // errors.Wrap adds two entries to the cause heirarchy 118 | // 1 - withMessage{} 119 | // 2 - withStack{} 120 | assert.Len(t, exx.Exceptions, 1 + (3*2)) 121 | assert.Equal(t, "root cause", exx.Exceptions[0].Value) 122 | }) 123 | } 124 | 125 | func TestExceptionInfo(t *testing.T) { 126 | t.Run("NewExceptionInfo()", func (t *testing.T) { 127 | ex := NewExceptionInfo() 128 | assert.NotNil(t, ex, "it should not return nil") 129 | assert.Equal(t, "unknown", ex.Type, "it should report an 'unknown' type by default") 130 | assert.Equal(t, "An unknown error has occurred", ex.Value, "it should report a default error message") 131 | assert.NotNil(t, ex.StackTrace, "it should contain a stack trace") 132 | }) 133 | 134 | t.Run("ForError()", func(t *testing.T) { 135 | ex := NewExceptionInfo() 136 | assert.NotNil(t, ex, "it should not return nil") 137 | 138 | assert.Equal(t, ex, ex.ForError(fmt.Errorf("example error")), "it should return the same exception info object for chaining") 139 | assert.Equal(t, "example error", ex.Type, "it should load the type from the error") 140 | assert.Equal(t, "example error", ex.Value, "it should load the message from the error") 141 | assert.Equal(t, "", ex.Module, "it should load the module from the error") 142 | 143 | t.Run("with no stacktrace", func(t *testing.T) { 144 | ex := &ExceptionInfo{} 145 | ex.ForError(fmt.Errorf("example error")) 146 | assert.NotNil(t, ex.StackTrace, "it should use the location of the current call as the stack trace") 147 | }) 148 | 149 | t.Run("with a fmt.Errorf() error", func(t *testing.T) { 150 | assert.NotNil(t, ex.StackTrace, "it should use the location of the current call as the stack trace") 151 | }) 152 | 153 | t.Run("with a github.com/pkg/errors error", func(t *testing.T) { 154 | ex.ForError(errors.New("example error")) 155 | assert.NotNil(t, ex.StackTrace, "it should use the location of the error as the stack trace") 156 | }) 157 | 158 | t.Run("with a structured error message", func(t *testing.T) { 159 | ex.ForError(fmt.Errorf("test: example error")) 160 | assert.Equal(t, "test: example error", ex.Value, "it should load the message from the error") 161 | assert.Equal(t, "example error", ex.Type, "it should load the type from the error") 162 | assert.Equal(t, "test", ex.Module, "it should load the module from the error") 163 | }) 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /extra.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | // Extra allows you to provide additional arbitrary metadata with your 6 | // event. This data is not searchable, but can be invaluable in identifying 7 | // the cause of a problem. 8 | func Extra(extra map[string]interface{}) Option { 9 | if extra == nil { 10 | return nil 11 | } 12 | 13 | return &extraOption{extra} 14 | } 15 | 16 | type extraOption struct { 17 | extra map[string]interface{} 18 | } 19 | 20 | func (o *extraOption) Class() string { 21 | return "extra" 22 | } 23 | 24 | func (o *extraOption) Merge(old Option) Option { 25 | if old, ok := old.(*extraOption); ok { 26 | extra := make(map[string]interface{}, len(o.extra)) 27 | for k, v := range old.extra { 28 | extra[k] = v 29 | } 30 | 31 | for k, v := range o.extra { 32 | extra[k] = v 33 | } 34 | 35 | return &extraOption{extra} 36 | } 37 | 38 | return o 39 | } 40 | 41 | func (o *extraOption) MarshalJSON() ([]byte, error) { 42 | return json.Marshal(o.extra) 43 | } 44 | -------------------------------------------------------------------------------- /extra_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleExtra() { 10 | cl := NewClient( 11 | // You can define extra fields when you create your client 12 | Extra(map[string]interface{}{ 13 | "redis": map[string]interface{}{ 14 | "host": "redis", 15 | "port": 6379, 16 | }, 17 | }), 18 | ) 19 | 20 | cl.Capture( 21 | // You can also define extra info when you send the event 22 | // The extra object will be shallowly merged automatically, 23 | // so this would send both `redis` and `cache`. 24 | Extra(map[string]interface{}{ 25 | "cache": map[string]interface{}{ 26 | "key": "user.127.profile", 27 | "hit": false, 28 | }, 29 | }), 30 | ) 31 | } 32 | 33 | func TestExtra(t *testing.T) { 34 | data := map[string]interface{}{ 35 | "redis": map[string]interface{}{ 36 | "host": "redis", 37 | "port": 6379, 38 | }, 39 | } 40 | 41 | assert.Nil(t, Extra(nil), "it should return nil if the data is nil") 42 | 43 | o := Extra(data) 44 | assert.NotNil(t, o, "it should return a non-nil option when the data is not nil") 45 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 46 | assert.Equal(t, "extra", o.Class(), "it should use the right option class") 47 | 48 | if assert.Implements(t, (*MergeableOption)(nil), o, "it should implement the MergeableOption interface") { 49 | om := o.(MergeableOption) 50 | 51 | t.Run("Merge()", func(t *testing.T) { 52 | data2 := map[string]interface{}{ 53 | "cache": map[string]interface{}{ 54 | "key": "user.127.profile", 55 | "hit": false, 56 | }, 57 | } 58 | 59 | o2 := Extra(data2) 60 | 61 | assert.Equal(t, o, om.Merge(&testOption{}), "it should replace the old option if it is not recognized") 62 | 63 | oo := om.Merge(o2) 64 | assert.NotNil(t, oo, "it should return a non-nil merge result") 65 | assert.NotEqual(t, o, oo, "it should not re-purpose the first option") 66 | assert.NotEqual(t, o2, oo, "it should not re-purpose the second option") 67 | 68 | eo, ok := oo.(*extraOption) 69 | assert.True(t, ok, "it should actually be an *extraOption") 70 | 71 | assert.Contains(t, eo.extra, "redis", "it should contain the key from the first option") 72 | assert.Contains(t, eo.extra, "cache", "it should contain the key from the second option") 73 | 74 | data2 = map[string]interface{}{ 75 | "redis": map[string]interface{}{ 76 | "host": "redis-dev", 77 | "port": 6379, 78 | }, 79 | } 80 | 81 | o2 = Extra(data2) 82 | assert.NotNil(t, o2, "it should not be nil") 83 | oo = om.Merge(o2) 84 | assert.NotNil(t, oo, "it should return a non-nil merge result") 85 | 86 | eo, ok = oo.(*extraOption) 87 | assert.True(t, ok, "it should actually be an *extraOption") 88 | 89 | assert.Contains(t, eo.extra, "redis", "it should contain the key") 90 | assert.Equal(t, data, eo.extra, "it should use the new option's data") 91 | }) 92 | } 93 | 94 | t.Run("MarshalJSON()", func(t *testing.T) { 95 | data := map[string]interface{}{ 96 | "redis": map[string]interface{}{ 97 | "host": "redis", 98 | // Float mode required since we aren't deserializing into an int 99 | "port": 6379., 100 | }, 101 | } 102 | 103 | serialized := testOptionsSerialize(t, Extra(data)) 104 | assert.Equal(t, data, serialized, "it should serialize to the data") 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /fingerprint.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | // Fingerprint is used to configure the array of strings used to deduplicate 6 | // events when they are processed by Sentry. 7 | // You may use the special value "{{ default }}" to extend the default behaviour 8 | // if you wish. 9 | // https://docs.sentry.io/learn/rollups/#custom-grouping 10 | func Fingerprint(keys ...string) Option { 11 | return &fingerprintOption{keys} 12 | } 13 | 14 | type fingerprintOption struct { 15 | keys []string 16 | } 17 | 18 | func (o *fingerprintOption) Class() string { 19 | return "fingerprint" 20 | } 21 | 22 | func (o *fingerprintOption) MarshalJSON() ([]byte, error) { 23 | return json.Marshal(o.keys) 24 | } 25 | -------------------------------------------------------------------------------- /fingerprint_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleFingerprint() { 10 | cl := NewClient() 11 | 12 | cl.Capture( 13 | // You can specify a fingerprint that extends the default behaviour 14 | Fingerprint("{{ default }}", "http://example.com/my.url"), 15 | 16 | // Or you can define your own 17 | Fingerprint("myrpc", "POST", "/foo.bar"), 18 | ) 19 | } 20 | 21 | func TestFingerprint(t *testing.T) { 22 | o := Fingerprint("test") 23 | assert.NotNil(t, o, "it should not return a nil option") 24 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 25 | assert.Equal(t, "fingerprint", o.Class(), "it should use the right option class") 26 | 27 | t.Run("MarshalJSON()", func (t *testing.T) { 28 | assert.Equal(t, []interface{}{"test"}, testOptionsSerialize(t, o), "it should serialize as a list of fingerprint keys") 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/SierraSoftworks/sentry-go/v2 2 | 3 | require ( 4 | github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448 5 | github.com/matishsiao/goInfo v0.0.0-20170803142006-617e6440957e 6 | github.com/pkg/errors v0.9.1 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | go 1.13 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448 h1:8tNk6SPXzLDnATTrWoI5Bgw9s/x4uf0kmBpk21NZgI4= 2 | github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/matishsiao/goInfo v0.0.0-20170803142006-617e6440957e h1:Y+GY+bv5vf1gssphFsGiq6R8qdHxnpDZvYljFnXfhD8= 7 | github.com/matishsiao/goInfo v0.0.0-20170803142006-617e6440957e/go.mod h1:yLZrFIhv+Z20hxHvcZpEyKVQp9HMsOJkXAxx7yDqtvg= 8 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 9 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 14 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 15 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 16 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 17 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 18 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 19 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 25 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // HTTPRequestInfo is a low-level interface which describes 4 | // the details of an HTTP request made to a web server. 5 | // If you are using the net/http library, the HTTPRequest() 6 | // option will populate this information for you automatically. 7 | type HTTPRequestInfo struct { 8 | URL string `json:"url"` 9 | Method string `json:"method"` 10 | Query string `json:"query_string,omitempty"` 11 | 12 | // These fields are optional 13 | Cookies string `json:"cookies,omitempty"` 14 | Headers map[string]string `json:"headers,omitempty"` 15 | Env map[string]string `json:"env,omitempty"` 16 | Data interface{} `json:"data,omitempty"` 17 | } 18 | 19 | // Class is used to meet the Option interface constraints by 20 | // providing the name of the API field that this data will be 21 | // submitted in. 22 | func (o *HTTPRequestInfo) Class() string { 23 | return "request" 24 | } 25 | 26 | // HTTP creates a new HTTP interface with the raw data provided 27 | // by a user. It is useful in situations where you are not leveraging 28 | // Go's underlying net/http library or wish to have direct control over 29 | // the values sent to Sentry. 30 | // For all other purposes, the HTTPRequest() option is a more useful 31 | // replacement. 32 | func HTTP(h *HTTPRequestInfo) Option { 33 | return h 34 | } 35 | -------------------------------------------------------------------------------- /httpTransport.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "net/http" 14 | 15 | "github.com/certifi/gocertifi" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | const ( 20 | // ErrMissingRootTLSCerts is used when this library cannot load the required 21 | // RootCA certificates needed for its HTTPS transport. 22 | ErrMissingRootTLSCerts = ErrType("sentry: Failed to load root TLS certificates") 23 | ) 24 | 25 | type httpTransport struct { 26 | client *http.Client 27 | } 28 | 29 | func newHTTPTransport() Transport { 30 | t := &httpTransport{ 31 | client: http.DefaultClient, 32 | } 33 | 34 | rootCAs, err := gocertifi.CACerts() 35 | if err != nil { 36 | 37 | log.Println(ErrMissingRootTLSCerts.Error()) 38 | return t 39 | } 40 | 41 | t.client = &http.Client{ 42 | Transport: &http.Transport{ 43 | Proxy: http.ProxyFromEnvironment, 44 | TLSClientConfig: &tls.Config{RootCAs: rootCAs}, 45 | }, 46 | } 47 | 48 | return t 49 | } 50 | 51 | func (t *httpTransport) Send(dsn string, packet Packet) error { 52 | if dsn == "" { 53 | return nil 54 | } 55 | 56 | url, authHeader, err := t.parseDSN(dsn) 57 | if err != nil { 58 | return errors.Wrap(err, "failed to parse DSN") 59 | } 60 | 61 | body, contentType, err := t.serializePacket(packet) 62 | if err != nil { 63 | return errors.Wrap(err, "failed to serialize packet") 64 | } 65 | 66 | req, err := http.NewRequest("POST", url, body) 67 | if err != nil { 68 | return errors.Wrap(err, "failed to create new request") 69 | } 70 | 71 | req.Header.Set("X-Sentry-Auth", authHeader) 72 | req.Header.Set("Content-Type", contentType) 73 | req.Header.Set("User-Agent", fmt.Sprintf("sentry-go %s (Sierra Softworks; github.com/SierraSoftworks/sentry-go)", version)) 74 | 75 | res, err := t.client.Do(req) 76 | if err != nil { 77 | return errors.Wrap(err, "failed to submit request") 78 | } 79 | 80 | io.Copy(ioutil.Discard, res.Body) 81 | res.Body.Close() 82 | 83 | if res.StatusCode != 200 { 84 | return errors.Errorf("got http status %d, expected 200", res.StatusCode) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (t *httpTransport) parseDSN(dsn string) (url, authHeader string, err error) { 91 | d, err := newDSN(dsn) 92 | if err != nil { 93 | return "", "", err 94 | } 95 | 96 | return d.URL, d.AuthHeader(), nil 97 | } 98 | 99 | func (t *httpTransport) serializePacket(packet Packet) (io.Reader, string, error) { 100 | buf := bytes.NewBuffer([]byte{}) 101 | if err := json.NewEncoder(buf).Encode(packet); err != nil { 102 | return nil, "", errors.Wrap(err, "failed to encode JSON payload data") 103 | } 104 | 105 | if buf.Len() < 1000 { 106 | return buf, "application/json; charset=utf8", nil 107 | } 108 | 109 | cbuf := bytes.NewBuffer([]byte{}) 110 | b64 := base64.NewEncoder(base64.StdEncoding, cbuf) 111 | deflate, err := zlib.NewWriterLevel(b64, zlib.BestCompression) 112 | if err != nil { 113 | return nil, "", errors.Wrap(err, "failed to configure zlib deflate") 114 | } 115 | 116 | if _, err := io.Copy(deflate, buf); err != nil { 117 | return nil, "", errors.Wrap(err, "failed to deflate message") 118 | } 119 | 120 | deflate.Close() 121 | b64.Close() 122 | 123 | return cbuf, "application/octet-stream", nil 124 | } 125 | -------------------------------------------------------------------------------- /httpTransport_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "compress/zlib" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestHTTPTransport(t *testing.T) { 21 | deserializePacket := func(t *testing.T, dataType string, data io.Reader) interface{} { 22 | var out interface{} 23 | 24 | if strings.Contains(dataType, "application/json") { 25 | require.Nil(t, json.NewDecoder(data).Decode(&out), "there should be no problems deserializing the packet") 26 | } else if strings.Contains(dataType, "application/octet-stream") { 27 | b64 := base64.NewDecoder(base64.StdEncoding, data) 28 | deflate, err := zlib.NewReader(b64) 29 | defer deflate.Close() 30 | 31 | require.Nil(t, err, "there should be no errors creating the zlib deflator") 32 | require.Nil(t, json.NewDecoder(deflate).Decode(&out), "there should be no problems deserializing the packet") 33 | } else { 34 | t.Fatalf("unknown datatype for packet: %s", dataType) 35 | } 36 | 37 | return out 38 | } 39 | 40 | longMessage := func(minLength int) Option { 41 | msg := " " 42 | for len(msg) < 1000 { 43 | msg = fmt.Sprintf("%s%s", msg, msg) 44 | } 45 | 46 | return Message(msg) 47 | } 48 | 49 | tr := newHTTPTransport() 50 | require.NotNil(t, tr, "the transport should not be nil") 51 | 52 | ht, ok := tr.(*httpTransport) 53 | require.True(t, ok, "it should actually be a *httpTransport") 54 | 55 | t.Run("Send()", func(t *testing.T) { 56 | p := NewPacket() 57 | require.NotNil(t, p, "the packet should not be nil") 58 | 59 | received := false 60 | statusCode := 200 61 | 62 | mux := http.NewServeMux() 63 | require.NotNil(t, mux, "the http mux should not be nil") 64 | mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { 65 | received = true 66 | res.WriteHeader(statusCode) 67 | res.Write([]byte("No data")) 68 | 69 | assert.Equal(t, "POST", req.Method, "the request should use HTTP POST") 70 | assert.Equal(t, "/api/1/store/", req.RequestURI, "the request should use the right API endpoint") 71 | 72 | assert.Contains(t, []string{ 73 | "Sentry sentry_version=4, sentry_key=key, sentry_secret=secret", 74 | "Sentry sentry_version=4, sentry_key=key", 75 | }, req.Header.Get("X-Sentry-Auth"), "it should use the right auth header") 76 | 77 | expectedData := testSerializePacket(t, p) 78 | 79 | data := deserializePacket(t, req.Header.Get("Content-Type"), req.Body) 80 | 81 | assert.Equal(t, expectedData, data, "the data should match what we expected") 82 | }) 83 | 84 | ts := httptest.NewServer(mux) 85 | defer ts.Close() 86 | 87 | makeDSN := func(publicKey, privateKey string) string { 88 | uri, err := url.Parse(ts.URL) 89 | require.Nil(t, err, "we should not fail to parse the URI") 90 | 91 | if publicKey != "" { 92 | uri.User = url.UserPassword(publicKey, privateKey) 93 | } 94 | 95 | uri.Path = "/1" 96 | 97 | return uri.String() 98 | } 99 | 100 | cases := []struct { 101 | Name string 102 | Packet Packet 103 | DSN string 104 | StatusCode int 105 | Error error 106 | Received bool 107 | }{ 108 | {"Short Packet", NewPacket(), makeDSN("key", "secret"), 200, nil, true}, 109 | {"Long Packet", NewPacket().SetOptions(longMessage(10000)), makeDSN("key", "secret"), 200, nil, true}, 110 | {"No DSN", NewPacket(), "", 200, nil, false}, 111 | {"Invalid DSN URL", NewPacket(), ":", 400, ErrBadURL, false}, 112 | {"Missing Public Key", NewPacket(), makeDSN("", ""), 401, ErrMissingPublicKey, false}, 113 | {"Invalid Server", NewPacket(), "https://key:secret@invalid_domain.not_a_tld/sentry/1", 404, ErrType("failed to submit request"), false}, 114 | {"Missing Private Key with Required Key", NewPacket(), makeDSN("key", ""), 401, fmt.Errorf("got http status 401, expected 200"), true}, 115 | {"Missing Private Key", NewPacket(), makeDSN("key", ""), 200, nil, true}, 116 | } 117 | 118 | for _, tc := range cases { 119 | tc := tc 120 | 121 | t.Run(tc.Name, func(t *testing.T) { 122 | received = false 123 | statusCode = tc.StatusCode 124 | p = tc.Packet 125 | 126 | err := tr.Send(tc.DSN, tc.Packet) 127 | if tc.Error == nil { 128 | assert.Nil(t, err, "it should not fail to send the packet") 129 | } else if errType, ok := tc.Error.(ErrType); ok { 130 | assert.True(t, errType.IsInstance(err), "it should return the right error") 131 | } else { 132 | assert.EqualError(t, err, tc.Error.Error(), "it should return the right error") 133 | } 134 | 135 | if tc.Received { 136 | assert.True(t, received, "the server should have received the packet") 137 | } else { 138 | assert.False(t, received, "the server should not have received the packet") 139 | } 140 | }) 141 | } 142 | }) 143 | 144 | t.Run("serializePacket()", func(t *testing.T) { 145 | cases := []struct { 146 | Name string 147 | Packet Packet 148 | DataType string 149 | }{ 150 | {"Short Packet", NewPacket().SetOptions(Message("short packet")), "application/json; charset=utf8"}, 151 | {"Long Packet", NewPacket().SetOptions(longMessage(10000)), "application/octet-stream"}, 152 | } 153 | 154 | for _, tc := range cases { 155 | tc := tc 156 | t.Run(tc.Name, func(t *testing.T) { 157 | data, dataType, err := ht.serializePacket(tc.Packet) 158 | assert.Nil(t, err, "there should be no error serializing the packet") 159 | assert.Equal(t, tc.DataType, dataType, "the request datatype should be %s", tc.DataType) 160 | assert.NotNil(t, data, "the request data should not be nil") 161 | 162 | assert.Equal(t, testSerializePacket(t, tc.Packet), deserializePacket(t, dataType, data), "the serialized packet should match what we expected") 163 | }) 164 | } 165 | }) 166 | 167 | t.Run("parseDSN()", func(t *testing.T) { 168 | cases := []struct { 169 | Name string 170 | DSN string 171 | URL string 172 | AuthHeader string 173 | Error error 174 | }{ 175 | {"Empty DSN", "", "", "", nil}, 176 | {"Invalid DSN", "@", "", "", fmt.Errorf("sentry: missing public key: missing URL user")}, 177 | {"Full DSN", "https://user:pass@example.com/sentry/1", "https://example.com/sentry/api/1/store/", "Sentry sentry_version=4, sentry_key=user, sentry_secret=pass", nil}, 178 | } 179 | 180 | for _, tc := range cases { 181 | tc := tc 182 | t.Run(tc.Name, func(t *testing.T) { 183 | url, authHeader, err := ht.parseDSN(tc.DSN) 184 | if tc.Error != nil { 185 | assert.EqualError(t, err, tc.Error.Error(), "there should be an error with the right message") 186 | } else { 187 | assert.Nil(t, err, "there should be no error") 188 | } 189 | assert.Equal(t, tc.URL, url, "the parsed URL should be correct") 190 | assert.Equal(t, tc.AuthHeader, authHeader, "the parsed auth header should be correct") 191 | }) 192 | } 193 | }) 194 | 195 | // If you set $SENTRY_DSN you can send events to a live Sentry instance 196 | // to confirm that this library functions correctly. 197 | if liveTestDSN := os.Getenv("SENTRY_DSN"); liveTestDSN != "" { 198 | t.Run("Live Test", func(t *testing.T) { 199 | p := NewPacket().SetOptions( 200 | Message("Ran Live Test"), 201 | Release(version), 202 | Level(Debug), 203 | ) 204 | 205 | assert.Nil(t, tr.Send(liveTestDSN, p), "it should not fail to send the packet") 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleHTTP() { 10 | // You can manually populate all this request info in situations 11 | // where you aren't using `net/http` as your HTTP server (or don't 12 | // have access to the http.Request object). 13 | // In all other situations, you're better off using `HTTPRequest(r)` 14 | // and saving yourself the effort of building this up manually. 15 | ri := &HTTPRequestInfo{ 16 | URL: "http://example.com/my.url", 17 | Method: "GET", 18 | Query: "q=test", 19 | 20 | Cookies: "testing=1", 21 | Headers: map[string]string{ 22 | "Host": "example.com", 23 | }, 24 | Env: map[string]string{ 25 | "REMOTE_ADDR": "127.0.0.1", 26 | "REMOTE_PORT": "18204", 27 | }, 28 | Data: map[string]interface{}{ 29 | "awesome": true, 30 | }, 31 | } 32 | 33 | cl := NewClient() 34 | 35 | ctxCl := cl.With( 36 | // You can provide the HTTP request context in a context-specific 37 | // derived client 38 | HTTP(ri), 39 | ) 40 | 41 | ctxCl.Capture( 42 | // Or you can provide it when sending an event 43 | HTTP(ri), 44 | ) 45 | } 46 | 47 | func TestHTTP(t *testing.T) { 48 | r := &HTTPRequestInfo{ 49 | URL: "http://example.com/my.url", 50 | Method: "GET", 51 | Query: "q=test", 52 | 53 | Cookies: "testing=1", 54 | } 55 | 56 | assert.Equal(t, "request", r.Class(), "request info should use the correct option class") 57 | 58 | assert.Nil(t, HTTP(nil), "it should return nil if it receives a nil request") 59 | 60 | o := HTTP(r) 61 | assert.NotNil(t, o, "it should not return a nil option") 62 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 63 | 64 | assert.Equal(t, "request", o.Class(), "it should use the correct option class") 65 | 66 | t.Run("MarshalJSON()", func(t *testing.T) { 67 | assert.Equal(t, map[string]interface{}{ 68 | "url": "http://example.com/my.url", 69 | "method": "GET", 70 | "query_string": "q=test", 71 | "cookies": "testing=1", 72 | }, testOptionsSerialize(t, o), "it should serialize the request info correctly") 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /httprequest.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // An HTTPRequestOption describes an HTTP request's data 13 | type HTTPRequestOption interface { 14 | Option 15 | WithCookies() HTTPRequestOption 16 | WithHeaders() HTTPRequestOption 17 | WithEnv() HTTPRequestOption 18 | WithData(data interface{}) HTTPRequestOption 19 | Sanitize(fields ...string) HTTPRequestOption 20 | } 21 | 22 | // HTTPRequest passes the request context from a net/http 23 | // request object to Sentry. It exposes a number of options 24 | // to control what information is exposed and how it is 25 | // sanitized. 26 | func HTTPRequest(req *http.Request) HTTPRequestOption { 27 | return &httpRequestOption{ 28 | request: req, 29 | sanitize: []string{ 30 | "password", 31 | "passwd", 32 | "passphrase", 33 | "secret", 34 | }, 35 | } 36 | } 37 | 38 | const sanitizationString = "********" 39 | 40 | type httpRequestOption struct { 41 | request *http.Request 42 | withCookies bool 43 | withHeaders bool 44 | withEnv bool 45 | data interface{} 46 | sanitize []string 47 | } 48 | 49 | func (h *httpRequestOption) Class() string { 50 | return "request" 51 | } 52 | 53 | func (h *httpRequestOption) WithCookies() HTTPRequestOption { 54 | h.withCookies = true 55 | return h 56 | } 57 | 58 | func (h *httpRequestOption) WithHeaders() HTTPRequestOption { 59 | h.withHeaders = true 60 | return h 61 | } 62 | 63 | func (h *httpRequestOption) WithEnv() HTTPRequestOption { 64 | h.withEnv = true 65 | return h 66 | } 67 | 68 | func (h *httpRequestOption) WithData(data interface{}) HTTPRequestOption { 69 | h.data = data 70 | return h 71 | } 72 | 73 | func (h *httpRequestOption) Sanitize(fields ...string) HTTPRequestOption { 74 | h.sanitize = append(h.sanitize, fields...) 75 | return h 76 | } 77 | 78 | func (h *httpRequestOption) Omit() bool { 79 | return h.request == nil 80 | } 81 | 82 | func (h *httpRequestOption) MarshalJSON() ([]byte, error) { 83 | return json.Marshal(h.buildData()) 84 | } 85 | 86 | func (h *httpRequestOption) buildData() *HTTPRequestInfo { 87 | proto := "http" 88 | if h.request.TLS != nil || h.request.Header.Get("X-Forwarded-Proto") == "https" { 89 | proto = "https" 90 | } 91 | p := &HTTPRequestInfo{ 92 | Method: h.request.Method, 93 | Query: sanitizeQuery(h.request.URL.Query(), h.sanitize).Encode(), 94 | URL: proto + "://" + h.request.Host + h.request.URL.Path, 95 | Headers: make(map[string]string, 0), 96 | Env: make(map[string]string, 0), 97 | Data: h.data, 98 | } 99 | 100 | if h.withCookies { 101 | p.Cookies = h.request.Header.Get("Cookie") 102 | } 103 | 104 | if h.withEnv { 105 | p.Env = make(map[string]string, 0) 106 | 107 | if addr, port, err := net.SplitHostPort(h.request.RemoteAddr); err == nil { 108 | p.Env["REMOTE_ADDR"] = addr 109 | p.Env["REMOTE_PORT"] = port 110 | } 111 | 112 | for _, env := range os.Environ() { 113 | ps := strings.SplitN(env, "=", 2) 114 | k := ps[0] 115 | v := "" 116 | if len(ps) > 1 { 117 | v = ps[1] 118 | } 119 | 120 | if shouldSanitize(k, h.sanitize) { 121 | v = sanitizationString 122 | } 123 | 124 | p.Env[k] = v 125 | } 126 | } 127 | 128 | if h.withHeaders { 129 | p.Headers = make(map[string]string, len(h.request.Header)) 130 | for k, v := range h.request.Header { 131 | p.Headers[k] = strings.Join(v, ",") 132 | 133 | if shouldSanitize(k, h.sanitize) { 134 | p.Headers[k] = sanitizationString 135 | } 136 | } 137 | } 138 | return p 139 | } 140 | 141 | func shouldSanitize(s string, fields []string) bool { 142 | for _, keyword := range fields { 143 | if strings.Contains(strings.ToLower(s), strings.ToLower(keyword)) { 144 | return true 145 | } 146 | } 147 | 148 | return false 149 | } 150 | 151 | func sanitizeQuery(query url.Values, fields []string) url.Values { 152 | for field := range query { 153 | for _, keyword := range fields { 154 | if strings.Contains(strings.ToLower(field), strings.ToLower(keyword)) { 155 | query[field] = []string{sanitizationString} 156 | break 157 | } 158 | } 159 | } 160 | return query 161 | } 162 | -------------------------------------------------------------------------------- /httprequest_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func ExampleHTTPRequest() { 14 | cl := NewClient( 15 | Release("v1.0.0"), 16 | ) 17 | 18 | // Add your 404 handler to the default mux 19 | http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { 20 | cl := cl.With( 21 | // Set the HTTP request context for your request's client 22 | HTTPRequest(req).WithHeaders(), 23 | ) 24 | 25 | res.Header().Set("Content-Type", "application/json") 26 | res.WriteHeader(404) 27 | res.Write([]byte(`{"error":"Not Found","message":"We could not find the route you requested, please check your URL and try again."}`)) 28 | 29 | // Capture the problem using your request's client 30 | cl.Capture( 31 | Message("Route Not Found: [%s] %s", req.Method, req.URL.Path), 32 | Level(Warning), 33 | ) 34 | }) 35 | } 36 | 37 | func TestHTTPRequest(t *testing.T) { 38 | r, err := http.NewRequest("GET", "https://example.com/test?testing=1&password=test", nil) 39 | assert.Nil(t, err, "should be able to create an HTTP request object") 40 | 41 | r.RemoteAddr = "127.0.0.1:12835" 42 | r.Header.Set("Host", "example.com") 43 | r.Header.Set("X-Forwarded-Proto", "https") 44 | r.Header.Set("Cookie", "testing=1") 45 | r.Header.Set("X-Testing", "1") 46 | r.Header.Set("X-API-Key", "secret") 47 | 48 | assert.NotNil(t, HTTPRequest(nil), "it should not return nil if no request is provided") 49 | 50 | o := HTTPRequest(r) 51 | assert.NotNil(t, o, "should not return a nil option") 52 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 53 | assert.Equal(t, "request", o.Class(), "it should use the right option class") 54 | 55 | if assert.Implements(t, (*OmitableOption)(nil), o, "it should implement the OmitableOption interface") { 56 | assert.False(t, o.(OmitableOption).Omit(), "it should return false if there is a request") 57 | assert.True(t, HTTPRequest(nil).(OmitableOption).Omit(), "it should return true if there is no request") 58 | } 59 | 60 | tm := "GET" 61 | tu := "https://example.com/test" 62 | tq := url.Values{ 63 | "testing": {"1"}, 64 | "password": {sanitizationString}, 65 | } 66 | var td interface{} = nil 67 | th := map[string]string{} 68 | te := map[string]string{} 69 | tc := "" 70 | 71 | cases := []struct { 72 | Name string 73 | Opt Option 74 | Setup func() 75 | }{ 76 | {"Default", HTTPRequest(r), func() {}}, 77 | {"Default.Sanitize()", HTTPRequest(r).Sanitize("testing"), func() { 78 | tq = url.Values{ 79 | "testing": {sanitizationString}, 80 | "password": {sanitizationString}, 81 | } 82 | }}, 83 | {"WithCookies()", HTTPRequest(r).WithCookies(), func() { 84 | tc = "testing=1" 85 | }}, 86 | {"WithHeaders()", HTTPRequest(r).WithHeaders(), func() { 87 | th = map[string]string{ 88 | "Host": "example.com", 89 | "Cookie": "testing=1", 90 | "X-Testing": "1", 91 | "X-Forwarded-Proto": "https", 92 | "X-Api-Key": "secret", 93 | } 94 | }}, 95 | {"WithHeaders().Sanitize()", HTTPRequest(r).WithHeaders().Sanitize("key"), func() { 96 | th = map[string]string{ 97 | "Host": "example.com", 98 | "Cookie": "testing=1", 99 | "X-Testing": "1", 100 | "X-Forwarded-Proto": "https", 101 | "X-Api-Key": sanitizationString, 102 | } 103 | }}, 104 | {"WithEnv()", HTTPRequest(r).WithEnv(), func() { 105 | te = map[string]string{ 106 | "REMOTE_ADDR": "127.0.0.1", 107 | "REMOTE_PORT": "12835", 108 | } 109 | }}, 110 | {"WithEnv().Sanitize()", HTTPRequest(r).WithEnv().Sanitize("secret"), func() { 111 | os.Setenv("SECRET", "secret") 112 | 113 | te = map[string]string{ 114 | "REMOTE_ADDR": "127.0.0.1", 115 | "REMOTE_PORT": "12835", 116 | "SECRET": "********", 117 | } 118 | }}, 119 | {"WithData()", HTTPRequest(r).WithData("testing"), func() { 120 | td = "testing" 121 | }}, 122 | } 123 | 124 | for _, testCase := range cases { 125 | testCase := testCase 126 | t.Run(testCase.Name, func(t *testing.T) { 127 | tq = url.Values{ 128 | "testing": {"1"}, 129 | "password": {sanitizationString}, 130 | } 131 | td = nil 132 | th = map[string]string{} 133 | te = map[string]string{} 134 | tc = "" 135 | 136 | testCase.Setup() 137 | 138 | hr, ok := testCase.Opt.(*httpRequestOption) 139 | assert.True(t, ok, "the option should actually be a *httpRequestOption") 140 | 141 | d := hr.buildData() 142 | assert.NotNil(t, d, "the built data should not be nil") 143 | 144 | assert.Equal(t, tm, d.Method, "the method should be correct") 145 | assert.Equal(t, tu, d.URL, "the url should be correct") 146 | assert.Equal(t, tq.Encode(), d.Query, "the query should be correct") 147 | assert.Equal(t, td, d.Data, "the data should be correct") 148 | assert.Equal(t, th, d.Headers, "the headers should be correct") 149 | 150 | for k, v := range te { 151 | if assert.Contains(t, d.Env, k, "the environment should include the %s entry", k) { 152 | assert.Equal(t, v, d.Env[k], "the value of the %s environment variable should be correct", k) 153 | } 154 | } 155 | 156 | assert.Equal(t, tc, d.Cookies, "the cookies should be correct") 157 | }) 158 | } 159 | 160 | t.Run("MarshalJSON()", func(t *testing.T) { 161 | o := HTTPRequest(r).WithHeaders().WithCookies().WithData("test").Sanitize("key", "secret") 162 | 163 | require.NotNil(t, o, "the option should not be nil") 164 | assert.Equal(t, map[string]interface{}{ 165 | "cookies": "testing=1", 166 | "data": "test", 167 | "headers": map[string]interface{}{ 168 | "Cookie": "testing=1", 169 | "Host": "example.com", 170 | "X-Api-Key": "********", 171 | "X-Forwarded-Proto": "https", 172 | "X-Testing": "1", 173 | }, 174 | "method": "GET", 175 | "query_string": "password=%2A%2A%2A%2A%2A%2A%2A%2A&testing=1", 176 | "url": "https://example.com/test", 177 | }, testOptionsSerialize(t, o), "the request option should be serialized correctly") 178 | }) 179 | } 180 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func init() { 8 | AddDefaultOptions(Logger("root")) 9 | } 10 | 11 | // Logger allows you to configure the hostname reported to Sentry 12 | // with an event. 13 | func Logger(logger string) Option { 14 | return &loggerOption{logger} 15 | } 16 | 17 | type loggerOption struct { 18 | logger string 19 | } 20 | 21 | func (o *loggerOption) Class() string { 22 | return "logger" 23 | } 24 | 25 | func (o *loggerOption) MarshalJSON() ([]byte, error) { 26 | return json.Marshal(o.logger) 27 | } 28 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleLogger() { 10 | cl := NewClient( 11 | // You can set the logger when you create your client 12 | Logger("root"), 13 | ) 14 | 15 | cl.Capture( 16 | // You can also specify it when sending an event 17 | Logger("http"), 18 | ) 19 | } 20 | 21 | func TestLogger(t *testing.T) { 22 | assert.NotNil(t, testGetOptionsProvider(t, Logger("")), "it should be registered as a default option") 23 | 24 | o := Logger("test") 25 | assert.NotNil(t, o, "should not return a nil option") 26 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 27 | assert.Equal(t, "logger", o.Class(), "it should use the right option class") 28 | 29 | t.Run("MarshalJSON()", func(t *testing.T) { 30 | assert.Equal(t, "test", testOptionsSerialize(t, o), "it should serialize to a string") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "fmt" 4 | 5 | type messageOption struct { 6 | Message string `json:"message"` 7 | Params []interface{} `json:"params,omitempty"` 8 | Formatted string `json:"formatted,omitempty"` 9 | } 10 | 11 | func (m *messageOption) Class() string { 12 | return "sentry.interfaces.Message" 13 | } 14 | 15 | // Message generates a new message entry for Sentry, optionally 16 | // using a format string with standard fmt.Sprintf params. 17 | func Message(format string, params ...interface{}) Option { 18 | if len(params) == 0 { 19 | return &messageOption{ 20 | Message: format, 21 | } 22 | } 23 | 24 | return &messageOption{ 25 | Message: format, 26 | Params: params, 27 | Formatted: fmt.Sprintf(format, params...), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleMessage() { 10 | cl := NewClient() 11 | 12 | cl.Capture( 13 | // You can either use just a simple message 14 | Message("this is a simple message"), 15 | ) 16 | 17 | cl.Capture( 18 | // Or you can provide formatting entries as you would with 19 | // fmt.Sprintf() calls. 20 | Message("this is a %s message (%d/7 would use again)", "formatted", 5), 21 | ) 22 | } 23 | 24 | func TestMessage(t *testing.T) { 25 | o := Message("test") 26 | assert.NotNil(t, o, "should not return a nil option") 27 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 28 | assert.Equal(t, "sentry.interfaces.Message", o.Class(), "it should use the right option class") 29 | 30 | t.Run("MarshalJSON()", func(t *testing.T) { 31 | assert.Equal(t, map[string]interface{}{"message":"test"}, testOptionsSerialize(t, o), "it should serialize to an object") 32 | }) 33 | 34 | t.Run("parameters", func(t *testing.T) { 35 | o := Message("this is a %s", "test") 36 | assert.NotNil(t, o, "should not return a nil option") 37 | 38 | mi, ok := o.(*messageOption) 39 | assert.True(t, ok, "it should actually be a *messageOption") 40 | assert.Equal(t, "this is a %s", mi.Message, "it should use the right message") 41 | assert.Equal(t, []interface{}{"test"}, mi.Params, "it should have the correct parameters") 42 | assert.Equal(t, "this is a test", mi.Formatted, "it should format the message when requested") 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /modules.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | // Modules allows you to specify the versions of various modules 6 | // used by your application. 7 | func Modules(moduleVersions map[string]string) Option { 8 | if moduleVersions == nil { 9 | return nil 10 | } 11 | 12 | return &modulesOption{moduleVersions} 13 | } 14 | 15 | type modulesOption struct { 16 | moduleVersions map[string]string 17 | } 18 | 19 | func (o *modulesOption) Class() string { 20 | return "modules" 21 | } 22 | 23 | func (o *modulesOption) Merge(old Option) Option { 24 | if old, ok := old.(*modulesOption); ok { 25 | moduleVersions := make(map[string]string, len(old.moduleVersions)) 26 | for k, v := range old.moduleVersions { 27 | moduleVersions[k] = v 28 | } 29 | 30 | for k, v := range o.moduleVersions { 31 | moduleVersions[k] = v 32 | } 33 | 34 | return &modulesOption{moduleVersions} 35 | } 36 | 37 | return o 38 | } 39 | 40 | func (o *modulesOption) MarshalJSON() ([]byte, error) { 41 | return json.Marshal(o.moduleVersions) 42 | } 43 | -------------------------------------------------------------------------------- /modules_go1.12.go: -------------------------------------------------------------------------------- 1 | // +build go1.12 2 | 3 | package sentry 4 | 5 | import ( 6 | "runtime/debug" 7 | ) 8 | 9 | func init() { 10 | info, ok := debug.ReadBuildInfo() 11 | if ok { 12 | mods := map[string]string{} 13 | for _, mod := range info.Deps { 14 | mods[mod.Path] = mod.Version 15 | } 16 | 17 | AddDefaultOptions(Modules(mods)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /modules_go1.12_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.12 2 | 3 | package sentry 4 | 5 | import ( 6 | "runtime/debug" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestModulesWithGomod(t *testing.T) { 13 | _, ok := debug.ReadBuildInfo() 14 | if ok { 15 | assert.NotNil(t, testGetOptionsProvider(t, Modules(map[string]string{"test": "correct"})), "it should be registered as a default provider") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /modules_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func ExampleModules() { 11 | cl := NewClient( 12 | // You can specify module versions when creating your 13 | // client 14 | Modules(map[string]string{ 15 | "redis": "v1", 16 | "mgo": "v2", 17 | }), 18 | ) 19 | 20 | cl.Capture( 21 | // And override or expand on them when sending an event 22 | Modules(map[string]string{ 23 | "redis": "v2", 24 | "sentry-go": "v1", 25 | }), 26 | ) 27 | } 28 | 29 | func TestModules(t *testing.T) { 30 | assert.Nil(t, Modules(nil), "it should return nil if the data provided is nil") 31 | 32 | data := map[string]string{ 33 | "redis": "1.0.0", 34 | } 35 | 36 | o := Modules(data) 37 | require.NotNil(t, o, "it should not return nil if the data is non-nil") 38 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 39 | assert.Equal(t, "modules", o.Class(), "it should use the right option class") 40 | 41 | if assert.Implements(t, (*MergeableOption)(nil), o, "it should implement the MergeableOption interface") { 42 | t.Run("Merge()", func(t *testing.T) { 43 | om := o.(MergeableOption) 44 | 45 | assert.Equal(t, o, om.Merge(&testOption{}), "it should replace the old option if it is not recognized") 46 | 47 | t.Run("different entries", func(t *testing.T) { 48 | data2 := map[string]string{ 49 | "pgsql": "5.4.0", 50 | } 51 | o2 := Modules(data2) 52 | require.NotNil(t, o2, "the second module option should not be nil") 53 | 54 | oo := om.Merge(o2) 55 | require.NotNil(t, oo, "it should not return nil when it merges") 56 | 57 | ooi, ok := oo.(*modulesOption) 58 | require.True(t, ok, "it should actually be a *modulesOption") 59 | 60 | if assert.Contains(t, ooi.moduleVersions, "redis", "it should contain the first key") { 61 | assert.Equal(t, data["redis"], ooi.moduleVersions["redis"], "it should have the right value for the first key") 62 | } 63 | 64 | if assert.Contains(t, ooi.moduleVersions, "pgsql", "it should contain the second key") { 65 | assert.Equal(t, data2["pgsql"], ooi.moduleVersions["pgsql"], "it should have the right value for the second key") 66 | } 67 | }) 68 | 69 | t.Run("existing entries", func(t *testing.T) { 70 | data2 := map[string]string{ 71 | "redis": "0.8.0", 72 | } 73 | o2 := Modules(data2) 74 | require.NotNil(t, o2, "the second module option should not be nil") 75 | 76 | oo := om.Merge(o2) 77 | require.NotNil(t, oo, "it should not return nil when it merges") 78 | 79 | ooi, ok := oo.(*modulesOption) 80 | require.True(t, ok, "it should actually be a *modulesOption") 81 | 82 | if assert.Contains(t, ooi.moduleVersions, "redis", "it should contain the first key") { 83 | assert.Equal(t, data["redis"], ooi.moduleVersions["redis"], "it should have the right value for the first key") 84 | } 85 | }) 86 | }) 87 | } 88 | 89 | t.Run("MarshalJSON()", func(t *testing.T) { 90 | assert.Equal(t, map[string]interface{}{ 91 | "redis": "1.0.0", 92 | }, testOptionsSerialize(t, o)) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // An Option represents an object which can be written to the Sentry packet 4 | // as a field with a given class name. Options may implement additional 5 | // interfaces to control how their values are rendered or to offer the 6 | // ability to merge multiple instances together. 7 | type Option interface { 8 | Class() string 9 | } 10 | 11 | // An OmitableOption can opt to have itself left out of the packet by 12 | // making an addition-time determination in its Omit() function. 13 | // This is a useful tool for excluding empty fields automatically. 14 | type OmitableOption interface { 15 | Omit() bool 16 | } 17 | 18 | // The MergeableOption interface gives options the ability to merge themselves 19 | // with other instances posessing the same class name. 20 | // Sometimes it makes sense to offer the ability to merge multiple options 21 | // of the same type together before they are rendered. This interface gives 22 | // options the ability to define how that merging should occur. 23 | type MergeableOption interface { 24 | Merge(old Option) Option 25 | } 26 | 27 | // A FinalizeableOption exposes a Finalize() method which is called by the 28 | // Packet builder before its value is used. This gives the option the opportunity 29 | // to perform any last-minute formatting and configuration. 30 | type FinalizeableOption interface { 31 | Finalize() 32 | } 33 | 34 | // An AdvancedOption has the ability to directly manipulate the packet 35 | // prior to it being sent. It should be used with caution as you may 36 | // cause the packet to become invalid. 37 | type AdvancedOption interface { 38 | Apply(packet map[string]Option) 39 | } 40 | 41 | // These defaultOptionProviders are used to populate a packet before it is 42 | // configured by user provided options. Due to the need to generate some 43 | // options dynamically, these are exposed as callbacks. 44 | var defaultOptionProviders = []func() Option{} 45 | 46 | // AddDefaultOptions allows you to configure options which will be globally 47 | // set on all top-level clients by default. You can override these options 48 | // later by specifying replacements in each client or event's options list. 49 | func AddDefaultOptions(options ...Option) { 50 | for _, opt := range options { 51 | if opt == nil { 52 | continue 53 | } 54 | 55 | o := opt 56 | AddDefaultOptionProvider(func() Option { 57 | return o 58 | }) 59 | } 60 | } 61 | 62 | // AddDefaultOptionProvider allows you to register a new default option which will 63 | // be globally set on all top-level clients. You can override this option 64 | // later by specifying a replacement in each client or event's options list. 65 | func AddDefaultOptionProvider(provider func() Option) { 66 | defaultOptionProviders = append(defaultOptionProviders, provider) 67 | } 68 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "os" 7 | "reflect" 8 | 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func ExampleAddDefaultOptions() { 15 | // You can add default options to all of your root Sentry 16 | // clients like this. 17 | AddDefaultOptions( 18 | Release("v1.0.0"), 19 | DSN("..."), 20 | ) 21 | } 22 | 23 | func ExampleAddDefaultOptionProvider() { 24 | // You can also register options providers which will dynamically 25 | // generate options for each new event that is sent 26 | AddDefaultOptionProvider(func() Option { 27 | if dsn := os.Getenv("SENTRY_DSN"); dsn != "" { 28 | return DSN(dsn) 29 | } 30 | 31 | return nil 32 | }) 33 | } 34 | 35 | func TestOptions(t *testing.T) { 36 | oldOptionsProviders := defaultOptionProviders 37 | defer func() { 38 | defaultOptionProviders = oldOptionsProviders 39 | }() 40 | 41 | id, err := NewEventID() 42 | assert.Nil(t, err, "there should be no errors creating the ID") 43 | 44 | t.Run("AddDefaultOptionProvider()", func(t *testing.T) { 45 | defaultOptionProviders = []func() Option{} 46 | 47 | provider := func() Option { 48 | return EventID(id) 49 | } 50 | 51 | AddDefaultOptionProvider(provider) 52 | assert.Len(t, defaultOptionProviders, 1, "the provider should now be present in the default options providers list") 53 | 54 | for _, provider := range defaultOptionProviders { 55 | assert.Equal(t, EventID(id), provider(), "the provider should return the right option") 56 | } 57 | }) 58 | 59 | t.Run("AddDefaultOptions()", func(t *testing.T) { 60 | defaultOptionProviders = []func() Option{} 61 | 62 | AddDefaultOptions(EventID(id), nil, EventID(id)) 63 | assert.Len(t, defaultOptionProviders, 2, "the provider should now be present in the default options providers list") 64 | 65 | for _, provider := range defaultOptionProviders { 66 | assert.Equal(t, EventID(id), provider(), "the provider should return the right option") 67 | } 68 | }) 69 | } 70 | 71 | type testOption struct { 72 | } 73 | 74 | func (o *testOption) Class() string { 75 | return "test" 76 | } 77 | 78 | type testCustomClassOption struct { 79 | class string 80 | } 81 | 82 | func (o *testCustomClassOption) Class() string { 83 | return o.class 84 | } 85 | 86 | type testOmitableOption struct { 87 | omit bool 88 | } 89 | 90 | func (o *testOmitableOption) Class() string { 91 | return "test" 92 | } 93 | 94 | func (o *testOmitableOption) Omit() bool { 95 | return o.omit 96 | } 97 | 98 | type testFinalizeableOption struct { 99 | finalized bool 100 | } 101 | 102 | func (o *testFinalizeableOption) Class() string { 103 | return "test" 104 | } 105 | 106 | func (o *testFinalizeableOption) Finalize() { 107 | o.finalized = true 108 | } 109 | 110 | type testMergeableOption struct { 111 | data int 112 | } 113 | 114 | func (o *testMergeableOption) Class() string { 115 | return "test" 116 | } 117 | 118 | func (o *testMergeableOption) Merge(other Option) Option { 119 | if oo, ok := other.(*testMergeableOption); ok { 120 | return &testMergeableOption{ 121 | data: o.data + oo.data, 122 | } 123 | } 124 | 125 | return o 126 | } 127 | 128 | type testAdvancedOption struct { 129 | data map[string]Option 130 | } 131 | 132 | func (o *testAdvancedOption) Class() string { 133 | return "test" 134 | } 135 | 136 | func (o *testAdvancedOption) Apply(packet map[string]Option) { 137 | for k, v := range o.data { 138 | packet[k] = v 139 | } 140 | } 141 | 142 | type testSerializableOption struct { 143 | data string 144 | } 145 | 146 | func (o *testSerializableOption) Class() string { 147 | return "test" 148 | } 149 | 150 | func (o *testSerializableOption) MarshalJSON() ([]byte, error) { 151 | return json.Marshal(o.data) 152 | } 153 | 154 | func testGetOptionsProvider(t *testing.T, sameType Option) Option { 155 | st := reflect.TypeOf(sameType) 156 | assert.NotNil(t, st, "getting the reflection type should not fail") 157 | 158 | for _, provider := range defaultOptionProviders { 159 | opt := provider() 160 | if reflect.TypeOf(opt) == st { 161 | return opt 162 | } 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func testOptionsSerialize(t *testing.T, opt Option) interface{} { 169 | if opt == nil { 170 | return nil 171 | } 172 | 173 | var data interface{} 174 | buf := bytes.NewBuffer([]byte{}) 175 | assert.Nil(t, json.NewEncoder(buf).Encode(opt), "no error should occur when serializing to JSON") 176 | assert.Nil(t, json.NewDecoder(buf).Decode(&data), "no error should occur when deserializing from JSON") 177 | return data 178 | } 179 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // A Packet is a JSON serializable object that will be sent to 4 | // the Sentry server to describe an event. It provides convenience 5 | // methods for setting options and handling the various types of 6 | // option that can be added. 7 | type Packet interface { 8 | // SetOptions will set all non-nil options provided, intelligently 9 | // merging values when supported by an option, or replacing existing 10 | // values if not. 11 | SetOptions(options ...Option) Packet 12 | 13 | // Clone will create a copy of this packet which can then be modified 14 | // independently. In most cases it is a better idea to create a new 15 | // client with the options you wish to override, however there are 16 | // situations where this is a cleaner solution. 17 | Clone() Packet 18 | } 19 | 20 | type packet map[string]Option 21 | 22 | // NewPacket creates a new packet which will be sent to the Sentry 23 | // server after its various options have been set. 24 | // You will not usually need to create a Packet yourself, instead 25 | // you should use your `Client`'s `Capture()` method. 26 | func NewPacket() Packet { 27 | return &packet{} 28 | } 29 | 30 | func (p packet) Clone() Packet { 31 | np := packet{} 32 | for k, v := range p { 33 | np[k] = v 34 | } 35 | 36 | return &np 37 | } 38 | 39 | func (p packet) SetOptions(options ...Option) Packet { 40 | for _, opt := range options { 41 | p.setOption(opt) 42 | } 43 | 44 | return &p 45 | } 46 | 47 | func (p packet) setOption(option Option) { 48 | if option == nil { 49 | return 50 | } 51 | 52 | // If the option implements Omit(), check to see whether 53 | // it has elected to be omitted. 54 | if omittable, ok := option.(OmitableOption); ok { 55 | if omittable.Omit() { 56 | return 57 | } 58 | } 59 | 60 | // If the option implements Finalize(), call it to give the 61 | // option the chance to prepare itself properly 62 | if finalizable, ok := option.(FinalizeableOption); ok { 63 | finalizable.Finalize() 64 | } 65 | 66 | if advanced, ok := option.(AdvancedOption); ok { 67 | advanced.Apply(p) 68 | } else if existing, ok := p[option.Class()]; ok { 69 | if mergable, ok := option.(MergeableOption); ok { 70 | p[option.Class()] = mergable.Merge(existing) 71 | } else { 72 | p[option.Class()] = option 73 | } 74 | } else { 75 | p[option.Class()] = option 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func ExamplePacket() { 12 | // Create a new packet object which can be sent to 13 | // Sentry by one of the transports or send queues. 14 | p := NewPacket().SetOptions( 15 | DSN(""), 16 | Message("Custom packet creation"), 17 | ) 18 | 19 | // Create a clone of this packet if you want to use 20 | // it as a template 21 | p.Clone().SetOptions( 22 | Message("Overridden message which doesn't affect the original"), 23 | ) 24 | } 25 | 26 | func TestPacket(t *testing.T) { 27 | p := NewPacket() 28 | assert.NotNil(t, p, "should return a non-nil packet") 29 | assert.Implements(t, (*Packet)(nil), p, "it should implement the Packet interface") 30 | 31 | t.Run("SetOptions()", func(t *testing.T) { 32 | assert.Equal(t, p, p.SetOptions(), "it should return the packet to support chaining") 33 | 34 | assert.Equal(t, p, p.SetOptions(nil), "it should ignore nil options") 35 | 36 | opt := &testOption{} 37 | assert.Equal(t, p.SetOptions(opt), p.Clone().SetOptions(nil, opt), "it should ignore nil options when other options are provided") 38 | 39 | pp, ok := p.(*packet) 40 | assert.True(t, ok, "it should actually be a *packet") 41 | 42 | p.SetOptions(&testOption{}) 43 | assert.Contains(t, *pp, "test", "it should contain the option field") 44 | assert.Equal(t, &testOption{}, (*pp)["test"], "it should have the right value for the option field") 45 | 46 | t.Run("Option Replacement", func(t *testing.T) { 47 | opt1 := &testOption{} 48 | opt2 := &testOption{} 49 | 50 | p.SetOptions(opt1) 51 | assert.Same(t, opt1, (*pp)["test"], "the first option should be set in the packet") 52 | 53 | p.SetOptions(opt2) 54 | assert.Same(t, opt2, (*pp)["test"], "the first option should be replaced by the second") 55 | }) 56 | 57 | t.Run("Omit()", func(t *testing.T) { 58 | p.SetOptions(&testOmitableOption{ 59 | omit: true, 60 | }) 61 | assert.NotEqual(t, &testOmitableOption{omit: true}, (*pp)["test"], "it should omit changes if Omit() returns true") 62 | 63 | p.SetOptions(&testOmitableOption{ 64 | omit: false, 65 | }) 66 | assert.Equal(t, &testOmitableOption{omit: false}, (*pp)["test"], "it should not omit changes if Omit() returns false") 67 | }) 68 | 69 | t.Run("Finalize()", func(t *testing.T) { 70 | opt := &testFinalizeableOption{} 71 | assert.False(t, opt.finalized, "the option should initially not be finalized") 72 | 73 | p.SetOptions(opt) 74 | assert.True(t, opt.finalized, "the option should now be finalized") 75 | assert.Equal(t, opt, (*pp)["test"], "the option should be stored in the packet") 76 | }) 77 | 78 | t.Run("Merge()", func(t *testing.T) { 79 | opt1 := &testMergeableOption{data: 1} 80 | opt2 := &testMergeableOption{data: 2} 81 | 82 | p.SetOptions(opt1) 83 | assert.Same(t, opt1, (*pp)["test"], "the packet should initially contain the first option") 84 | 85 | p.SetOptions(opt2) 86 | assert.Equal(t, &testMergeableOption{data: 3}, (*pp)["test"], "the packet should then contain the merged option") 87 | assert.Equal(t, 1, opt1.data, "the first option's data shouldn't be modified") 88 | assert.Equal(t, 2, opt2.data, "the second option's data shouldn't be modified") 89 | }) 90 | 91 | t.Run("Apply()", func(t *testing.T) { 92 | opt := &testAdvancedOption{ 93 | data: map[string]Option{ 94 | "tested": Context("value", true), 95 | }, 96 | } 97 | 98 | p.SetOptions(opt) 99 | assert.Contains(t, (*pp), "tested", "it should have run the Apply() method") 100 | assert.Equal(t, Context("value", true), (*pp)["tested"], "it should have stored the correct value") 101 | }) 102 | }) 103 | 104 | t.Run("Clone()", func(t *testing.T) { 105 | assert.False(t, p == p.Clone(), "it should clone to a new packet") 106 | assert.Equal(t, p, p.Clone(), "it should clone to an equivalent packet") 107 | 108 | p := NewPacket().SetOptions(DSN(""), Message("Test")) 109 | assert.Equal(t, p, p.Clone(), "the clone should copy any options across") 110 | }) 111 | 112 | t.Run("MarshalJSON()", func(t *testing.T) { 113 | p := NewPacket() 114 | p.SetOptions(&testOption{}) 115 | 116 | assert.Equal(t, map[string]interface{}{ 117 | "test": map[string]interface{}{}, 118 | }, testSerializePacket(t, p)) 119 | 120 | p.SetOptions(&testSerializableOption{data: "testing"}) 121 | assert.Equal(t, map[string]interface{}{ 122 | "test": "testing", 123 | }, testSerializePacket(t, p)) 124 | }) 125 | } 126 | 127 | func testSerializePacket(t *testing.T, p Packet) interface{} { 128 | buf := bytes.NewBuffer([]byte{}) 129 | assert.Nil(t, json.NewEncoder(buf).Encode(p), "it should not encounter any errors serializing the packet") 130 | 131 | var data interface{} 132 | assert.Nil(t, json.NewDecoder(buf).Decode(&data), "it should not encounter any errors deserializing the packet") 133 | 134 | return data 135 | } 136 | -------------------------------------------------------------------------------- /platform.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func init() { 8 | AddDefaultOptions(Platform("go")) 9 | } 10 | 11 | // Platform allows you to configure the platform reported to Sentry. This is used 12 | // to customizae portions of the user interface. 13 | func Platform(platform string) Option { 14 | return &platformOption{platform} 15 | } 16 | 17 | type platformOption struct { 18 | platform string 19 | } 20 | 21 | func (o *platformOption) Class() string { 22 | return "platform" 23 | } 24 | 25 | func (o *platformOption) MarshalJSON() ([]byte, error) { 26 | return json.Marshal(o.platform) 27 | } 28 | -------------------------------------------------------------------------------- /platform_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExamplePlatform() { 10 | cl := NewClient( 11 | // You can set the platform at a client level 12 | Platform("go"), 13 | ) 14 | 15 | cl.Capture( 16 | // Or override it when sending the event 17 | Platform("go"), 18 | ) 19 | } 20 | 21 | func TestPlatform(t *testing.T) { 22 | assert.NotNil(t, testGetOptionsProvider(t, Platform("go")), "it should be registered as a default option") 23 | 24 | o := Platform("go") 25 | assert.NotNil(t, o, "should not return a nil option") 26 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 27 | assert.Equal(t, "platform", o.Class(), "it should use the right option class") 28 | 29 | t.Run("MarshalJSON()", func(t *testing.T) { 30 | assert.Equal(t, "go", testOptionsSerialize(t, o), "it should serialize to a string") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /queuedEvent.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // A QueuedEvent allows you to track the status of sending 4 | // an event to Sentry. 5 | type QueuedEvent interface { 6 | EventID() string 7 | Wait() QueuedEvent 8 | WaitChannel() <-chan error 9 | Error() error 10 | } 11 | 12 | // QueuedEventInternal is an interface used by SendQueue 13 | // implementations to "complete" a queued event once it has 14 | // either been sent to Sentry, or sending failed with an error. 15 | type QueuedEventInternal interface { 16 | QueuedEvent 17 | Packet() Packet 18 | Config() Config 19 | Complete(err error) 20 | } 21 | 22 | // NewQueuedEvent is used by SendQueue implementations to expose 23 | // information about the events that they are sending to Sentry. 24 | func NewQueuedEvent(cfg Config, packet Packet) QueuedEvent { 25 | e := &queuedEvent{ 26 | cfg: cfg, 27 | packet: packet, 28 | wait: make(chan struct{}), 29 | } 30 | 31 | return e 32 | } 33 | 34 | type queuedEvent struct { 35 | cfg Config 36 | packet Packet 37 | err error 38 | 39 | wait chan struct{} 40 | } 41 | 42 | func (e *queuedEvent) EventID() string { 43 | if packet, ok := e.packet.(*packet); ok { 44 | return packet.getEventID() 45 | } 46 | 47 | return "" 48 | } 49 | 50 | func (e *queuedEvent) Wait() QueuedEvent { 51 | _, _ = <-e.wait 52 | 53 | return e 54 | } 55 | 56 | func (e *queuedEvent) WaitChannel() <-chan error { 57 | ch := make(chan error) 58 | 59 | go func() { 60 | _, _ = <-e.wait 61 | 62 | if e.err != nil { 63 | ch <- e.err 64 | } 65 | close(ch) 66 | }() 67 | 68 | return ch 69 | } 70 | 71 | func (e *queuedEvent) Error() error { 72 | return e.Wait().(*queuedEvent).err 73 | } 74 | 75 | func (e *queuedEvent) Packet() Packet { 76 | return e.packet 77 | } 78 | 79 | func (e *queuedEvent) Config() Config { 80 | return e.cfg 81 | } 82 | 83 | func (e *queuedEvent) Complete(err error) { 84 | select { 85 | case _, _ = <-e.wait: 86 | default: 87 | e.err = err 88 | close(e.wait) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /queuedEvent_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func ExampleQueuedEvent() { 13 | cl := NewClient() 14 | 15 | e := cl.Capture( 16 | Message("Example Event"), 17 | ) 18 | 19 | // You can then wait on the event to be sent 20 | e.Wait() 21 | 22 | // Or you can use the WaitChannel if you want support for timeouts 23 | select { 24 | case err := <-e.WaitChannel(): 25 | if err != nil { 26 | fmt.Println("failed to send event: ", err) 27 | } else { 28 | // You can also get the EventID for this event 29 | fmt.Println("sent event: ", e.EventID()) 30 | } 31 | case <-time.After(time.Second): 32 | fmt.Println("timed out waiting for send") 33 | } 34 | } 35 | 36 | func ExampleQueuedEventInternal() { 37 | // If you're implementing your own send queue, you will want to use 38 | // the QueuedEventInternal to control when events are finished and 39 | // to access the packet and config related to them. 40 | 41 | cl := NewClient() 42 | e := cl.Capture() 43 | 44 | if ei, ok := e.(QueuedEventInternal); ok { 45 | // Get the packet for the event 46 | p := ei.Packet() 47 | 48 | // Get the config for the event 49 | cfg := ei.Config() 50 | 51 | // Use the configured transport to send the packet 52 | err := cfg.Transport().Send(cfg.DSN(), p) 53 | 54 | // Complete the event (with the error, if not nil) 55 | ei.Complete(err) 56 | } 57 | } 58 | 59 | func TestQueuedEvent(t *testing.T) { 60 | id, err := NewEventID() 61 | require.Nil(t, err, "there should be no errors creating an event ID") 62 | 63 | cl := NewClient(DSN("")) 64 | require.NotNil(t, cl, "the client should not be nil") 65 | 66 | cfg, ok := cl.(Config) 67 | require.True(t, ok, "the client should implement the Config interface") 68 | 69 | p := NewPacket().SetOptions(EventID(id)) 70 | require.NotNil(t, p, "the packet should not be nil") 71 | 72 | t.Run("NewQueuedEvent()", func(t *testing.T) { 73 | e := NewQueuedEvent(cfg, p) 74 | require.NotNil(t, e, "the event should not be nil") 75 | assert.Implements(t, (*QueuedEvent)(nil), e, "it should implement the QueuedEvent interface") 76 | 77 | ei, ok := e.(*queuedEvent) 78 | require.True(t, ok, "it should actually be a *queuedEvent") 79 | assert.Same(t, cfg, ei.cfg, "it should use the same config provider") 80 | assert.Same(t, p, ei.packet, "it should use the same packet") 81 | 82 | t.Run("EventID()", func(t *testing.T) { 83 | assert.Equal(t, id, e.EventID(), "it should have the right event ID") 84 | 85 | assert.Empty(t, NewQueuedEvent(cfg, nil).EventID(), "it should have an empty EventID for an invalid packet") 86 | }) 87 | 88 | t.Run("Wait()", func(t *testing.T) { 89 | cases := []struct { 90 | Name string 91 | Waiter func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) 92 | PreWaiterStart func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) 93 | PostWaiterStart func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) 94 | }{ 95 | { 96 | Name: "SuccessSlow", 97 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 98 | ch <- struct{}{} 99 | ei.Wait() 100 | assert.Nil(t, ei.Error(), "there should have been no error raised") 101 | }, 102 | PostWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 103 | <-ch 104 | ei.Complete(nil) 105 | }, 106 | }, 107 | { 108 | Name: "SuccessFast", 109 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 110 | ei.Wait() 111 | assert.Nil(t, ei.Error(), "there should have been no error raised") 112 | }, 113 | PreWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 114 | ei.Complete(nil) 115 | }, 116 | }, 117 | { 118 | Name: "FailSlow", 119 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 120 | ch <- struct{}{} 121 | ei.Wait() 122 | assert.EqualError(t, ei.Error(), "test error", "there should have been an error raised") 123 | }, 124 | PostWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 125 | <-ch 126 | ei.Complete(fmt.Errorf("test error")) 127 | }, 128 | }, 129 | { 130 | Name: "FailFast", 131 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 132 | ei.Wait() 133 | assert.EqualError(t, ei.Error(), "test error", "there should have been an error raised") 134 | }, 135 | PreWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 136 | ei.Complete(fmt.Errorf("test error")) 137 | }, 138 | }, 139 | } 140 | 141 | for _, tc := range cases { 142 | tc := tc 143 | t.Run(tc.Name, func(t *testing.T) { 144 | e := NewQueuedEvent(cfg, p) 145 | require.NotNil(t, e, "the event should not be nil") 146 | 147 | require.Implements(t, (*QueuedEventInternal)(nil), e, "it should implement the QueuedEventInternal interface") 148 | ei := e.(QueuedEventInternal) 149 | 150 | ch := make(chan struct{}) 151 | defer close(ch) 152 | 153 | if tc.PreWaiterStart != nil { 154 | tc.PreWaiterStart(t, ch, ei) 155 | } 156 | 157 | go func() { 158 | if tc.Waiter != nil { 159 | tc.Waiter(t, ch, ei) 160 | } 161 | 162 | ch <- struct{}{} 163 | }() 164 | 165 | if tc.PostWaiterStart != nil { 166 | tc.PostWaiterStart(t, ch, ei) 167 | } 168 | 169 | select { 170 | case <-ch: 171 | case <-time.After(100 * time.Millisecond): 172 | t.Error("timed out after 100ms with no response") 173 | } 174 | }) 175 | } 176 | }) 177 | 178 | t.Run("WaitChannel()", func(t *testing.T) { 179 | cases := []struct { 180 | Name string 181 | Complete func(ei QueuedEventInternal) 182 | Error error 183 | }{ 184 | {"SucceedFast", func(ei QueuedEventInternal) { ei.Complete(nil) }, nil}, 185 | {"SucceedSlow", func(ei QueuedEventInternal) { go func() { ei.Complete(nil) }() }, nil}, 186 | {"FailFast", func(ei QueuedEventInternal) { ei.Complete(fmt.Errorf("test error")) }, fmt.Errorf("test error")}, 187 | {"FailSlow", func(ei QueuedEventInternal) { go func() { ei.Complete(fmt.Errorf("test error")) }() }, fmt.Errorf("test error")}, 188 | } 189 | 190 | for _, tc := range cases { 191 | tc := tc 192 | t.Run(tc.Name, func(t *testing.T) { 193 | e := NewQueuedEvent(cfg, p) 194 | require.NotNil(t, e, "the event should not be nil") 195 | 196 | require.Implements(t, (*QueuedEventInternal)(nil), e, "it should implement the QueuedEventInternal interface") 197 | ei := e.(QueuedEventInternal) 198 | 199 | tc.Complete(ei) 200 | 201 | select { 202 | case err := <-e.WaitChannel(): 203 | if tc.Error != nil { 204 | assert.EqualError(t, err, tc.Error.Error(), "the right error should have been raised") 205 | } else { 206 | assert.NoError(t, err, "no error should have been raised") 207 | } 208 | case <-time.After(100 * time.Millisecond): 209 | t.Error("timeout after 100ms") 210 | } 211 | }) 212 | } 213 | }) 214 | 215 | t.Run("Error()", func(t *testing.T) { 216 | cases := []struct { 217 | Name string 218 | Waiter func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) 219 | PreWaiterStart func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) 220 | PostWaiterStart func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) 221 | }{ 222 | { 223 | Name: "SuccessSlow", 224 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 225 | ch <- struct{}{} 226 | assert.Nil(t, ei.Error(), "there should have been no error raised") 227 | }, 228 | PostWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 229 | <-ch 230 | ei.Complete(nil) 231 | }, 232 | }, 233 | { 234 | Name: "SuccessFast", 235 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 236 | assert.Nil(t, ei.Error(), "there should have been no error raised") 237 | }, 238 | PreWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 239 | ei.Complete(nil) 240 | }, 241 | }, 242 | { 243 | Name: "FailSlow", 244 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 245 | ch <- struct{}{} 246 | assert.EqualError(t, ei.Error(), "test error", "there should have been an error raised") 247 | }, 248 | PostWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 249 | <-ch 250 | ei.Complete(fmt.Errorf("test error")) 251 | }, 252 | }, 253 | { 254 | Name: "FailFast", 255 | Waiter: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 256 | assert.EqualError(t, ei.Error(), "test error", "there should have been an error raised") 257 | }, 258 | PreWaiterStart: func(t *testing.T, ch chan struct{}, ei QueuedEventInternal) { 259 | ei.Complete(fmt.Errorf("test error")) 260 | }, 261 | }, 262 | } 263 | 264 | for _, tc := range cases { 265 | tc := tc 266 | t.Run(tc.Name, func(t *testing.T) { 267 | e := NewQueuedEvent(cfg, p) 268 | require.NotNil(t, e, "the event should not be nil") 269 | 270 | require.Implements(t, (*QueuedEventInternal)(nil), e, "it should implement the QueuedEventInternal interface") 271 | ei := e.(QueuedEventInternal) 272 | 273 | ch := make(chan struct{}) 274 | defer close(ch) 275 | 276 | if tc.PreWaiterStart != nil { 277 | tc.PreWaiterStart(t, ch, ei) 278 | } 279 | 280 | go func() { 281 | if tc.Waiter != nil { 282 | tc.Waiter(t, ch, ei) 283 | } 284 | 285 | ch <- struct{}{} 286 | }() 287 | 288 | if tc.PostWaiterStart != nil { 289 | tc.PostWaiterStart(t, ch, ei) 290 | } 291 | 292 | select { 293 | case <-ch: 294 | case <-time.After(100 * time.Millisecond): 295 | t.Error("timed out after 100ms with no response") 296 | } 297 | }) 298 | } 299 | }) 300 | }) 301 | 302 | t.Run("Complete()", func(t *testing.T) { 303 | e := NewQueuedEvent(cfg, p) 304 | require.NotNil(t, e, "the event should not be nil") 305 | 306 | require.Implements(t, (*QueuedEventInternal)(nil), e, "it should implement the QueuedEventInternal interface") 307 | ei := e.(QueuedEventInternal) 308 | 309 | ei.Complete(fmt.Errorf("test error")) 310 | assert.EqualError(t, e.Error(), "test error", "it should set the error correctly") 311 | 312 | ei.Complete(nil) 313 | assert.NotNil(t, e.Error(), "it shouldn't modify the status of the event after it has been set") 314 | }) 315 | } 316 | -------------------------------------------------------------------------------- /release.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // Release allows you to configure the application release version 8 | // reported to Sentry with an event. 9 | func Release(version string) Option { 10 | return &releaseOption{version} 11 | } 12 | 13 | type releaseOption struct { 14 | version string 15 | } 16 | 17 | func (o *releaseOption) Class() string { 18 | return "release" 19 | } 20 | 21 | func (o *releaseOption) MarshalJSON() ([]byte, error) { 22 | return json.Marshal(o.version) 23 | } 24 | -------------------------------------------------------------------------------- /release_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleRelease() { 10 | cl := NewClient( 11 | // You can set the release when you create a client 12 | Release("v1.0.0"), 13 | ) 14 | 15 | cl.Capture( 16 | // You can also set it when you send an event 17 | Release("v1.0.0-dev"), 18 | ) 19 | } 20 | 21 | func TestRelease(t *testing.T) { 22 | o := Release("test") 23 | assert.NotNil(t, o, "should not return a nil option") 24 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 25 | assert.Equal(t, "release", o.Class(), "it should use the right option class") 26 | 27 | t.Run("MarshalJSON()", func(t *testing.T) { 28 | assert.Equal(t, "test", testOptionsSerialize(t, o), "it should serialize to a string") 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /sdk.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | func init() { 4 | AddDefaultOptions(&sdkOption{ 5 | Name: "SierraSoftworks/sentry-go", 6 | Version: version, 7 | Integrations: []string{}, 8 | }) 9 | } 10 | 11 | type sdkOption struct { 12 | Name string `json:"name"` 13 | Version string `json:"version"` 14 | Integrations []string `json:"integrations"` 15 | } 16 | 17 | func (o *sdkOption) Class() string { 18 | return "sdk" 19 | } 20 | -------------------------------------------------------------------------------- /sdk_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSDKOption(t *testing.T) { 10 | o := testGetOptionsProvider(t, &sdkOption{}) 11 | assert.NotNil(t, o, "it should be registered as a default option") 12 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 13 | assert.Equal(t, "sdk", o.Class(), "it should use the right option class") 14 | 15 | t.Run("MarshalJSON()", func(t *testing.T) { 16 | assert.Equal(t, map[string]interface{}{ 17 | "integrations": []interface{}{}, 18 | "name": "SierraSoftworks/sentry-go", 19 | "version": version, 20 | }, testOptionsSerialize(t, o), "it should serialize to a string") 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /sendQueue.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // A SendQueue is used by the Sentry client to coordinate the transmission 4 | // of events. Custom queues can be used to control parallelism and circuit 5 | // breaking as necessary. 6 | type SendQueue interface { 7 | // Enqueue is called by clients wishing to send an event to Sentry. 8 | // It is provided with a Config and Packet and is expected to return 9 | // a QueuedEvent compatible object which an application can use to 10 | // access information about whether the event was sent successfully 11 | // or not. 12 | Enqueue(conf Config, packet Packet) QueuedEvent 13 | 14 | // Shutdown is called by a client that wishes to stop the flow of 15 | // events through a SendQueue. 16 | Shutdown(wait bool) 17 | } 18 | 19 | const ( 20 | // ErrSendQueueFull is used when an attempt to enqueue a 21 | // new event fails as a result of no buffer space being available. 22 | ErrSendQueueFull = ErrType("sentry: send queue was full") 23 | 24 | // ErrSendQueueShutdown is used when an attempt to enqueue 25 | // a new event fails as a result of the queue having been shutdown 26 | // already. 27 | ErrSendQueueShutdown = ErrType("sentry: send queue was shutdown") 28 | ) 29 | 30 | func init() { 31 | AddDefaultOptions(UseSendQueue(NewSequentialSendQueue(100))) 32 | } 33 | 34 | // UseSendQueue allows you to specify the send queue that will be used 35 | // by a client. 36 | func UseSendQueue(queue SendQueue) Option { 37 | if queue == nil { 38 | return nil 39 | } 40 | 41 | return &sendQueueOption{queue} 42 | } 43 | 44 | type sendQueueOption struct { 45 | queue SendQueue 46 | } 47 | 48 | func (o *sendQueueOption) Class() string { 49 | return "sentry-go.sendqueue" 50 | } 51 | 52 | func (o *sendQueueOption) Omit() bool { 53 | return true 54 | } 55 | -------------------------------------------------------------------------------- /sendQueue_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleUseSendQueue() { 10 | cl := NewClient( 11 | // You can override the send queue on your root client 12 | // All of its derived clients will inherit this queue 13 | UseSendQueue(NewSequentialSendQueue(10)), 14 | ) 15 | 16 | cl.With( 17 | // Or you can override it on a derived client 18 | UseSendQueue(NewSequentialSendQueue(10)), 19 | ) 20 | } 21 | 22 | func TestSendQueue(t *testing.T) { 23 | assert.Nil(t, UseSendQueue(nil), "it should return nil if no transport is provided") 24 | 25 | q := NewSequentialSendQueue(0) 26 | o := UseSendQueue(q) 27 | assert.NotNil(t, o, "should not return a nil option") 28 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 29 | assert.Equal(t, "sentry-go.sendqueue", o.Class(), "it should use the right option class") 30 | 31 | if assert.Implements(t, (*Option)(nil), o, "it should implement the OmitableOption interface") { 32 | oo := o.(OmitableOption) 33 | assert.True(t, oo.Omit(), "it should always return true for calls to Omit()") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sentry-go.go: -------------------------------------------------------------------------------- 1 | // Package sentry gives you the ability to send events to a Sentry 2 | // server. It provides a clean API with comprehensive support for 3 | // Sentry's various interfaces and an easy to remember syntax. 4 | package sentry 5 | 6 | var version = "1.0.0" 7 | -------------------------------------------------------------------------------- /sequentialSendQueue.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // NewSequentialSendQueue creates a new sequential send queue instance with 10 | // a given buffer size which can be used as a replacement for the default 11 | // send queue. 12 | func NewSequentialSendQueue(buffer int) SendQueue { 13 | b := make(chan QueuedEventInternal, buffer) 14 | q := &sequentialSendQueue{ 15 | buffer: b, 16 | shutdownCh: make(chan struct{}), 17 | } 18 | 19 | q.wait.Add(1) 20 | go q.worker(b) 21 | return q 22 | } 23 | 24 | type sequentialSendQueue struct { 25 | buffer chan<- QueuedEventInternal 26 | shutdown bool 27 | shutdownCh chan struct{} 28 | 29 | wait sync.WaitGroup 30 | } 31 | 32 | func (q *sequentialSendQueue) Enqueue(cfg Config, packet Packet) QueuedEvent { 33 | e := NewQueuedEvent(cfg, packet) 34 | ei := e.(QueuedEventInternal) 35 | 36 | if q.shutdown { 37 | err := errors.New("sequential send queue: shutdown") 38 | ei.Complete(errors.Wrap(err, ErrSendQueueShutdown.Error())) 39 | return e 40 | } 41 | 42 | select { 43 | case q.buffer <- ei: 44 | default: 45 | if e, ok := e.(QueuedEventInternal); ok { 46 | err := errors.New("sequential send queue: buffer full") 47 | e.Complete(errors.Wrap(err, ErrSendQueueFull.Error())) 48 | } 49 | } 50 | 51 | return e 52 | } 53 | 54 | func (q *sequentialSendQueue) Shutdown(wait bool) { 55 | if q.shutdown { 56 | return 57 | } 58 | 59 | q.shutdownCh <- struct{}{} 60 | q.shutdown = true 61 | if wait { 62 | q.wait.Wait() 63 | } 64 | } 65 | 66 | func (q *sequentialSendQueue) worker(buffer <-chan QueuedEventInternal) { 67 | defer q.wait.Done() 68 | 69 | for { 70 | select { 71 | case <-q.shutdownCh: 72 | return 73 | case e, ok := <-buffer: 74 | if !ok { 75 | return 76 | } 77 | 78 | cfg := e.Config() 79 | t := cfg.Transport() 80 | if t == nil { 81 | e.Complete(errors.New("no transport configured")) 82 | continue 83 | } 84 | 85 | err := t.Send(cfg.DSN(), e.Packet()) 86 | e.Complete(err) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /sequentialSendQueue_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSequentialSendQueue(t *testing.T) { 12 | q := NewSequentialSendQueue(10) 13 | require.NotNil(t, q, "the queue should not be nil") 14 | assert.Implements(t, (*SendQueue)(nil), q, "it should implement the SendQueue interface") 15 | defer q.Shutdown(true) 16 | 17 | require.IsType(t, &sequentialSendQueue{}, q, "it should actually be a *sequentialSendQueue") 18 | 19 | t.Run("Send()", func(t *testing.T) { 20 | dsn := "http://user:pass@example.com/sentry/1" 21 | transport := testNewTestTransport() 22 | require.NotNil(t, transport, "the transport should not be nil") 23 | 24 | cl := NewClient(DSN(dsn), UseTransport(transport)) 25 | require.NotNil(t, cl, "the client should not be nil") 26 | 27 | cfg, ok := cl.(Config) 28 | require.True(t, ok, "the client should implement the Config interface") 29 | 30 | p := NewPacket() 31 | require.NotNil(t, p, "the packet should not be nil") 32 | 33 | t.Run("Normal", func(t *testing.T) { 34 | q := NewSequentialSendQueue(10) 35 | require.NotNil(t, q, "the queue should not be nil") 36 | defer q.Shutdown(true) 37 | 38 | e := q.Enqueue(cfg, p) 39 | require.NotNil(t, e, "the event should not be nil") 40 | 41 | select { 42 | case pp := <-transport.ch: 43 | assert.Equal(t, p, pp, "the packet which was sent should match the packet which was enqueued") 44 | case <-time.After(100 * time.Millisecond): 45 | t.Fatal("timed out waiting for send") 46 | } 47 | 48 | select { 49 | case err, ok := <-e.WaitChannel(): 50 | assert.False(t, ok, "the channel should have been closed") 51 | assert.NoError(t, err, "there should have been no error sending the event") 52 | case <-time.After(100 * time.Millisecond): 53 | t.Fatal("timed out waiting for event completion") 54 | } 55 | }) 56 | 57 | t.Run("QueueFull", func(t *testing.T) { 58 | q := NewSequentialSendQueue(0) 59 | require.NotNil(t, q, "the queue should not be nil") 60 | defer q.Shutdown(true) 61 | 62 | // Give the queue time to start 63 | time.Sleep(1 * time.Millisecond) 64 | 65 | e1 := q.Enqueue(cfg, p) 66 | require.NotNil(t, e1, "the event should not be nil") 67 | 68 | e2 := q.Enqueue(cfg, p) 69 | require.NotNil(t, e2, "the event should not be nil") 70 | 71 | select { 72 | case pp := <-transport.ch: 73 | assert.Equal(t, p, pp, "the packet which was sent should match the packet which was enqueued") 74 | assert.Nil(t, e1.Error(), "") 75 | case err, ok := <-e1.WaitChannel(): 76 | assert.False(t, ok, "the channel should have been closed") 77 | assert.NoError(t, err, "there should have been no error sending the event") 78 | case <-time.After(100 * time.Millisecond): 79 | t.Fatal("timed out waiting for send") 80 | } 81 | 82 | select { 83 | case <-transport.ch: 84 | t.Error("the transport should never have received the event for sending") 85 | case err, ok := <-e2.WaitChannel(): 86 | assert.True(t, ok, "the channel should not have been closed prematurely") 87 | assert.True(t, ErrSendQueueFull.IsInstance(err), "the error should be of type ErrSendQueueFull") 88 | case <-time.After(100 * time.Millisecond): 89 | t.Fatal("timed out waiting for event completion") 90 | } 91 | }) 92 | 93 | t.Run("Shutdown", func(t *testing.T) { 94 | q := NewSequentialSendQueue(10) 95 | require.NotNil(t, q, "the queue should not be nil") 96 | 97 | // Shutdown the queue 98 | q.Shutdown(true) 99 | 100 | e := q.Enqueue(cfg, p) 101 | require.NotNil(t, e, "the event should not be nil") 102 | 103 | select { 104 | case <-transport.ch: 105 | t.Error("the transport should never have received the event for sending") 106 | case err, ok := <-e.WaitChannel(): 107 | assert.True(t, ok, "the channel should not have been closed prematurely") 108 | assert.True(t, ErrSendQueueShutdown.IsInstance(err), "the error should be of type ErrSendQueueShutdown") 109 | case <-time.After(100 * time.Millisecond): 110 | t.Fatal("timed out waiting for event completion") 111 | } 112 | }) 113 | }) 114 | 115 | t.Run("Shutdown()", func(t *testing.T) { 116 | q := NewSequentialSendQueue(10) 117 | 118 | // It should be safe to call this repeatedly 119 | q.Shutdown(true) 120 | q.Shutdown(true) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /servername.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | func init() { 9 | hostname, err := os.Hostname() 10 | if err != nil { 11 | return 12 | } 13 | 14 | AddDefaultOptions(ServerName(hostname)) 15 | } 16 | 17 | // ServerName allows you to configure the hostname reported to Sentry 18 | // with an event. 19 | func ServerName(hostname string) Option { 20 | return &serverNameOption{hostname} 21 | } 22 | 23 | type serverNameOption struct { 24 | hostname string 25 | } 26 | 27 | func (o *serverNameOption) Class() string { 28 | return "server_name" 29 | } 30 | 31 | func (o *serverNameOption) MarshalJSON() ([]byte, error) { 32 | return json.Marshal(o.hostname) 33 | } 34 | -------------------------------------------------------------------------------- /servername_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleServerName() { 10 | cl := NewClient( 11 | // You can set the logger when you create your client 12 | ServerName("web01"), 13 | ) 14 | 15 | cl.Capture( 16 | // You can also specify it when sending an event 17 | ServerName("web01.prod"), 18 | ) 19 | } 20 | 21 | func TestServerName(t *testing.T) { 22 | assert.NotNil(t, testGetOptionsProvider(t, ServerName("")), "it should be registered as a default option") 23 | 24 | o := ServerName("test") 25 | assert.NotNil(t, o, "should not return a nil option") 26 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 27 | assert.Equal(t, "server_name", o.Class(), "it should use the right option class") 28 | 29 | t.Run("MarshalJSON()", func(t *testing.T) { 30 | assert.Equal(t, "test", testOptionsSerialize(t, o), "it should serialize to a string") 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /severity.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | func init() { 6 | // Configure the default severity level as Error 7 | AddDefaultOptions(Level(Error)) 8 | } 9 | 10 | // Level is used to set the severity level of an event before it 11 | // is sent to Sentry 12 | func Level(severity Severity) Option { 13 | return &levelOption{severity} 14 | } 15 | 16 | type levelOption struct { 17 | severity Severity 18 | } 19 | 20 | func (o *levelOption) Class() string { 21 | return "level" 22 | } 23 | 24 | func (o *levelOption) MarshalJSON() ([]byte, error) { 25 | return json.Marshal(o.severity) 26 | } 27 | 28 | // Severity represents a Sentry event severity (ranging from debug to fatal) 29 | type Severity string 30 | 31 | const ( 32 | // Fatal represents exceptions which result in the application exiting fatally 33 | Fatal = Severity("fatal") 34 | 35 | // Error represents exceptions which break the expected application flow 36 | Error = Severity("error") 37 | 38 | // Warning represents events which are abnormal but do not prevent the application 39 | // from operating correctly 40 | Warning = Severity("warning") 41 | 42 | // Info is used to expose information about events which occur during normal 43 | // operation of the application 44 | Info = Severity("info") 45 | 46 | // Debug is used to expose verbose information about events which occur during 47 | // normal operation of the application 48 | Debug = Severity("debug") 49 | ) 50 | -------------------------------------------------------------------------------- /severity_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleLevel() { 10 | cl := NewClient( 11 | // You can set the severity level when you create your client 12 | Level(Debug), 13 | ) 14 | 15 | cl.Capture( 16 | // You can also specify it when sending an event 17 | Level(Error), 18 | ) 19 | } 20 | 21 | func TestSeverity(t *testing.T) { 22 | assert.NotNil(t, testGetOptionsProvider(t, Level(Info)), "it should be registered as a default option") 23 | 24 | o := Level(Error) 25 | assert.NotNil(t, o, "should not return a nil option") 26 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 27 | assert.Equal(t, "level", o.Class(), "it should use the right option class") 28 | 29 | t.Run("MarshalJSON()", func(t *testing.T) { 30 | assert.Equal(t, "error", testOptionsSerialize(t, o), "it should serialize to a string") 31 | }) 32 | 33 | assert.EqualValues(t, Fatal, "fatal", "fatal should use the correct name") 34 | assert.EqualValues(t, Error, "error", "fatal should use the correct name") 35 | assert.EqualValues(t, Warning, "warning", "fatal should use the correct name") 36 | assert.EqualValues(t, Info, "info", "fatal should use the correct name") 37 | assert.EqualValues(t, Debug, "debug", "fatal should use the correct name") 38 | } 39 | -------------------------------------------------------------------------------- /stacktrace.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | var defaultInternalPrefixes = []string{"main"} 4 | 5 | // AddInternalPrefixes allows you to easily add packages which will be considered 6 | // "internal" in your stack traces. 7 | func AddInternalPrefixes(prefixes ...string) { 8 | defaultInternalPrefixes = append(defaultInternalPrefixes, prefixes...) 9 | } 10 | 11 | // StackTraceOption wraps a stacktrace and gives you tools for selecting 12 | // where it is sourced from or what is classified as an internal module. 13 | type StackTraceOption interface { 14 | Option 15 | ForError(err error) StackTraceOption 16 | WithInternalPrefixes(prefixes ...string) StackTraceOption 17 | } 18 | 19 | // StackTrace allows you to add a StackTrace to the event you submit to Sentry, 20 | // allowing you to quickly determine where in your code the event was generated. 21 | func StackTrace() StackTraceOption { 22 | return &stackTraceOption{ 23 | Frames: getStacktraceFrames(0), 24 | Omitted: []int{}, 25 | 26 | internalPrefixes: defaultInternalPrefixes, 27 | } 28 | } 29 | 30 | type stackTraceOption struct { 31 | Frames stackTraceFrames `json:"frames"` 32 | Omitted []int `json:"frames_omitted,omitempty"` 33 | 34 | internalPrefixes []string 35 | } 36 | 37 | func (o *stackTraceOption) Class() string { 38 | return "stacktrace" 39 | } 40 | 41 | func (o *stackTraceOption) ForError(err error) StackTraceOption { 42 | newFrames := getStacktraceFramesForError(err) 43 | if newFrames.Len() > 0 { 44 | o.Frames = newFrames 45 | } 46 | 47 | return o 48 | } 49 | 50 | func (o *stackTraceOption) WithInternalPrefixes(prefixes ...string) StackTraceOption { 51 | o.internalPrefixes = append(o.internalPrefixes, prefixes...) 52 | return o 53 | } 54 | 55 | func (o *stackTraceOption) Finalize() { 56 | for _, frame := range o.Frames { 57 | frame.ClassifyInternal(o.internalPrefixes) 58 | } 59 | } 60 | 61 | // stackTraceFrame describes the StackTrace for a given 62 | // exception or thread. 63 | type stackTraceFrame struct { 64 | // At least one of the following must be present 65 | Filename string `json:"filename,omitempty"` 66 | Function string `json:"function,omitempty"` 67 | Module string `json:"module,omitempty"` 68 | 69 | // These fields are optional 70 | Line int `json:"lineno,omitempty"` 71 | Column int `json:"colno,omitempty"` 72 | AbsoluteFilename string `json:"abs_path,omitempty"` 73 | PreContext []string `json:"pre_context,omitempty"` 74 | Context string `json:"context_line,omitempty"` 75 | PostContext []string `json:"post_context,omitempty"` 76 | InApp bool `json:"in_app"` 77 | Variables map[string]interface{} `json:"vars,omitempty"` 78 | Package string `json:"package,omitempty"` 79 | Platform string `json:"platform,omitempty"` 80 | ImageAddress string `json:"image_addr,omitempty"` 81 | SymbolAddress string `json:"symbol_addr,omitempty"` 82 | InstructionOffset int `json:"instruction_offset,omitempty"` 83 | } 84 | -------------------------------------------------------------------------------- /stacktraceGen.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type stackTracer interface { 12 | StackTrace() errors.StackTrace 13 | } 14 | 15 | type stackTraceFrames []*stackTraceFrame 16 | 17 | func (c stackTraceFrames) Len() int { return len(c) } 18 | func (c stackTraceFrames) Swap(i, j int) { c[j], c[i] = c[i], c[j] } 19 | func (c stackTraceFrames) Reverse() { 20 | for i, j := 0, c.Len()-1; i < j; i, j = i+1, j-1 { 21 | c.Swap(i, j) 22 | } 23 | } 24 | 25 | func getStacktraceFramesForError(err error) stackTraceFrames { 26 | if err, ok := err.(stackTracer); ok { 27 | frames := stackTraceFrames{} 28 | for _, f := range err.StackTrace() { 29 | pc := uintptr(f) - 1 30 | frame := getStacktraceFrame(pc) 31 | if frame != nil { 32 | frames = append(frames, frame) 33 | } 34 | } 35 | 36 | frames.Reverse() 37 | return frames 38 | } 39 | 40 | return stackTraceFrames{} 41 | } 42 | 43 | func getStacktraceFrames(skip int) stackTraceFrames { 44 | pcs := make([]uintptr, 30) 45 | if c := runtime.Callers(skip+2, pcs); c > 0 { 46 | frames := stackTraceFrames{} 47 | for _, pc := range pcs { 48 | frame := getStacktraceFrame(pc) 49 | if frame != nil { 50 | frames = append(frames, frame) 51 | } 52 | } 53 | 54 | frames.Reverse() 55 | return frames 56 | } 57 | 58 | return stackTraceFrames{} 59 | } 60 | 61 | func getStacktraceFrame(pc uintptr) *stackTraceFrame { 62 | frame := &stackTraceFrame{} 63 | 64 | if fn := runtime.FuncForPC(pc); fn != nil { 65 | frame.AbsoluteFilename, frame.Line = fn.FileLine(pc) 66 | frame.Package, frame.Module, frame.Function = formatFuncName(fn.Name()) 67 | frame.Filename = shortFilename(frame.AbsoluteFilename, frame.Package) 68 | } else { 69 | frame.AbsoluteFilename = "unknown" 70 | frame.Filename = "unknown" 71 | } 72 | 73 | return frame 74 | } 75 | 76 | func (f *stackTraceFrame) ClassifyInternal(internalPrefixes []string) { 77 | if f.Module == "main" { 78 | f.InApp = true 79 | return 80 | } 81 | 82 | for _, prefix := range internalPrefixes { 83 | if strings.HasPrefix(f.Package, prefix) && !strings.Contains(f.Package, "vendor") { 84 | f.InApp = true 85 | return 86 | } 87 | } 88 | } 89 | 90 | // formatFuncName converts a stack frame function name, which is commonly of the form 91 | // 'github.com/SierraSoftworks/sentry-go/v2.TestStackTraceGenerator.func3', into a well-formed 92 | // package, module, and function name. 93 | // For the above example, this will result in the following values being emitted: 94 | // - Package: github.com/SierraSoftworks/sentry-go/v2 95 | // - Module: SierraSoftworks/sentry-go/v2 96 | // - Function: TestStackTraceGenerator.func3 97 | func formatFuncName(fnName string) (pack, module, name string) { 98 | name = fnName 99 | pack = "" 100 | module = "" 101 | 102 | name = strings.Replace(name, "·", ".", -1) 103 | 104 | packageParts := strings.Split(name, "/") 105 | codeParts := strings.Split(packageParts[len(packageParts)-1], ".") 106 | 107 | pack = strings.Join(append(packageParts[:len(packageParts)-1], codeParts[0]), "/") 108 | name = strings.Join(codeParts[1:], ".") 109 | 110 | if len(packageParts) > 2 { 111 | module = strings.Join(append(packageParts[2:len(packageParts)-1], codeParts[0]), "/") 112 | } else { 113 | module = codeParts[0] 114 | } 115 | 116 | return 117 | } 118 | 119 | func shortFilename(absFile, pkg string) string { 120 | if pkg == "" { 121 | return absFile 122 | } 123 | 124 | if idx := strings.Index(absFile, fmt.Sprintf("%s/", pkg)); idx != -1 { 125 | return absFile[idx:] 126 | } 127 | 128 | return absFile 129 | } 130 | -------------------------------------------------------------------------------- /stacktraceGen_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestStackTraceGenerator(t *testing.T) { 14 | t.Run("getStacktraceFramesForError()", func(t *testing.T) { 15 | t.Run("StackTraceableError", func(t *testing.T) { 16 | err := errors.New("test error") 17 | frames := getStacktraceFramesForError(err) 18 | if assert.NotEmpty(t, frames, "there should be frames from the error") { 19 | assert.Equal(t, "TestStackTraceGenerator.func1.1", frames[frames.Len()-1].Function, "it should have the right function name as the top-most frame") 20 | } 21 | }) 22 | 23 | t.Run("error", func(t *testing.T) { 24 | err := fmt.Errorf("test error") 25 | frames := getStacktraceFramesForError(err) 26 | assert.Empty(t, frames, "there should be no frames from a normal error") 27 | }) 28 | }) 29 | 30 | t.Run("getStacktraceFrames()", func(t *testing.T) { 31 | t.Run("Skip", func(t *testing.T) { 32 | frames := getStacktraceFrames(999999999) 33 | assert.Empty(t, frames, "with an extreme skip, there should be no frames") 34 | }) 35 | 36 | t.Run("Current Function", func(t *testing.T) { 37 | frames := getStacktraceFrames(0) 38 | if assert.NotEmpty(t, frames, "there should be frames from the current function") { 39 | assert.Equal(t, "TestStackTraceGenerator.func2.2", frames[frames.Len()-1].Function, "it should have the right function name as the top-most frame") 40 | } 41 | }) 42 | }) 43 | 44 | t.Run("getStackTraceFrame()", func(t *testing.T) { 45 | pc, file, line, ok := runtime.Caller(0) 46 | require.True(t, ok, "we should be able to get the current caller") 47 | 48 | frame := getStacktraceFrame(pc) 49 | require.NotNil(t, frame, "the frame should not be nil") 50 | 51 | assert.Equal(t, file, frame.AbsoluteFilename, "the filename for the frame should match the caller") 52 | assert.Equal(t, line, frame.Line, "the line from the frame should match the caller") 53 | 54 | assert.Regexp(t, ".*/sentry-go/stacktraceGen_test.go$", frame.Filename, "it should have the correct filename") 55 | assert.Equal(t, "TestStackTraceGenerator.func3", frame.Function, "it should have the correct function name") 56 | assert.Equal(t, "sentry-go/v2", frame.Module, "it should have the correct module name") 57 | assert.Equal(t, "github.com/SierraSoftworks/sentry-go/v2", frame.Package, "it should have the correct package name") 58 | }) 59 | 60 | t.Run("stackTraceFrame.ClassifyInternal()", func(t *testing.T) { 61 | frames := getStacktraceFrames(0) 62 | require.Greater(t, frames.Len(), 3, "the number of frames should be more than 3") 63 | 64 | for i, frame := range frames { 65 | assert.False(t, frame.InApp, "all frames should initially be marked as external (frame index = %d)", i) 66 | frame.ClassifyInternal([]string{"github.com/SierraSoftworks/sentry-go"}) 67 | } 68 | 69 | assert.True(t, frames[frames.Len()-1].InApp, "the top-most frame should be marked as internal (this function)") 70 | assert.False(t, frames[0].InApp, "the bottom-most frame should be marked as external (the test harness main method)") 71 | }) 72 | 73 | t.Run("formatFuncName()", func(t *testing.T) { 74 | cases := []struct { 75 | Name string 76 | 77 | FullName string 78 | Package string 79 | Module string 80 | FunctionName string 81 | }{ 82 | {"Full Name", "github.com/SierraSoftworks/sentry-go.Context", "github.com/SierraSoftworks/sentry-go", "sentry-go", "Context"}, 83 | {"Full Name (v2)", "github.com/SierraSoftworks/sentry-go/v2.Context", "github.com/SierraSoftworks/sentry-go/v2", "sentry-go/v2", "Context"}, 84 | {"Struct Function Name", "github.com/SierraSoftworks/sentry-go.packet.Clone", "github.com/SierraSoftworks/sentry-go", "sentry-go", "packet.Clone"}, 85 | {"Struct Function Name (v2)", "github.com/SierraSoftworks/sentry-go/v2.packet.Clone", "github.com/SierraSoftworks/sentry-go/v2", "sentry-go/v2", "packet.Clone"}, 86 | {"No Package", "sentry-go.Context", "sentry-go", "sentry-go", "Context"}, 87 | } 88 | 89 | for _, tc := range cases { 90 | tc := tc 91 | t.Run(tc.Name, func(t *testing.T) { 92 | pack, module, name := formatFuncName(tc.FullName) 93 | assert.Equal(t, tc.Package, pack, "the package name should be correct") 94 | assert.Equal(t, tc.Module, module, "the module name should be correct") 95 | assert.Equal(t, tc.FunctionName, name, "the function name should be correct") 96 | }) 97 | } 98 | }) 99 | 100 | t.Run("shortFilename()", func(t *testing.T) { 101 | t.Run("GOPATH", func(t *testing.T) { 102 | GOPATH := "/go/src" 103 | pkg := "github.com/SierraSoftworks/sentry-go" 104 | file := "stacktraceGen_test.go" 105 | filename := fmt.Sprintf("%s/%s/%s", GOPATH, pkg, file) 106 | 107 | assert.Equal(t, filename, shortFilename(filename, ""), "should use the original filename if no package is provided") 108 | assert.Equal(t, filename, shortFilename(filename, "bitblob.com/bender"), "should use the original filename if the package name doesn't match the path") 109 | assert.Equal(t, fmt.Sprintf("%s/%s", pkg, file), shortFilename(filename, pkg), "should use the $pkg/$file if the package is provided") 110 | }) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /stacktrace_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func ExampleAddInternalPrefixes() { 12 | // This adds the provided prefixes to your list of internal 13 | // package prefixes used to tag stacktrace frames as in-app. 14 | AddInternalPrefixes("github.com/SierraSoftworks/sentry-go") 15 | } 16 | 17 | func ExampleStackTrace() { 18 | cl := NewClient() 19 | 20 | cl.Capture( 21 | // You can specify that a StackTrace should be included when 22 | // sending your event to Sentry 23 | StackTrace(). 24 | // You can also gather the stacktrace frames from a specific 25 | // error if it is created using `pkg/errors` 26 | ForError(errors.New("example error")). 27 | // And you can mark frames as "internal" by specifying the 28 | // internal frame prefixes here. 29 | WithInternalPrefixes( 30 | "github.com/SierraSoftworks/sentry-go", 31 | ), 32 | ) 33 | } 34 | 35 | func TestAddInternalPrefixes(t *testing.T) { 36 | assert.Contains(t, defaultInternalPrefixes, "main") 37 | AddInternalPrefixes("github.com/SierraSoftworks/sentry-go") 38 | assert.Contains(t, defaultInternalPrefixes, "github.com/SierraSoftworks/sentry-go") 39 | } 40 | 41 | func TestStackTrace(t *testing.T) { 42 | o := StackTrace() 43 | require.NotNil(t, o, "it should return a non-nil option") 44 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 45 | assert.Equal(t, "stacktrace", o.Class(), "it should use the right option class") 46 | 47 | sti, ok := o.(*stackTraceOption) 48 | require.True(t, ok, "it should actually be a *stackTraceOption") 49 | 50 | assert.NotEmpty(t, sti.Frames, "it should start off with your current stack frames") 51 | originalFrames := sti.Frames 52 | 53 | err := errors.New("example error") 54 | assert.Same(t, o, o.ForError(err), "it should reuse the same instance when adding error information") 55 | assert.NotEmpty(t, sti.Frames, "it should have loaded frame information from the error") 56 | assert.NotEqual(t, originalFrames, sti.Frames, "the frames should not be the original ones it started with") 57 | 58 | assert.Equal(t, defaultInternalPrefixes, sti.internalPrefixes, "it should start out with the default internal prefixes") 59 | 60 | o.WithInternalPrefixes("github.com/SierraSoftworks") 61 | assert.Contains(t, sti.internalPrefixes, "github.com/SierraSoftworks", "it should allow you to add new internal prefixes") 62 | 63 | if assert.Implements(t, (*FinalizeableOption)(nil), o, "it should implement the FinalizeableOption interface") { 64 | for i, frame := range sti.Frames { 65 | assert.False(t, frame.InApp, "all frames should initially be marked as external (frame index=%d)", i) 66 | } 67 | 68 | sti.Finalize() 69 | 70 | if assert.NotEmpty(t, sti.Frames, "the frames list should not be empty") { 71 | assert.True(t, sti.Frames[len(sti.Frames)-1].InApp, "the final frame should be marked as internal") 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | // Tags allow you to add additional tagging information to events which 6 | // makes it possible to easily group and query similar events. 7 | func Tags(tags map[string]string) Option { 8 | if tags == nil { 9 | return nil 10 | } 11 | 12 | return &tagsOption{tags} 13 | } 14 | 15 | type tagsOption struct { 16 | tags map[string]string 17 | } 18 | 19 | func (o *tagsOption) Class() string { 20 | return "tags" 21 | } 22 | 23 | func (o *tagsOption) Merge(old Option) Option { 24 | if old, ok := old.(*tagsOption); ok { 25 | tags := make(map[string]string, len(old.tags)) 26 | for k, v := range old.tags { 27 | tags[k] = v 28 | } 29 | 30 | for k, v := range o.tags { 31 | tags[k] = v 32 | } 33 | 34 | return &tagsOption{tags} 35 | } 36 | 37 | return o 38 | } 39 | 40 | func (o *tagsOption) MarshalJSON() ([]byte, error) { 41 | return json.Marshal(o.tags) 42 | } 43 | -------------------------------------------------------------------------------- /tags_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func ExampleTags() { 11 | cl := NewClient( 12 | // You can specify tags when creating your client 13 | Tags(map[string]string{ 14 | "redis": "v1", 15 | "mgo": "v2", 16 | }), 17 | ) 18 | 19 | cl.Capture( 20 | // And override or expand on them when sending an event 21 | Tags(map[string]string{ 22 | "redis": "v2", 23 | "sentry-go": "v1", 24 | }), 25 | ) 26 | } 27 | 28 | func TestTags(t *testing.T) { 29 | assert.Nil(t, Tags(nil), "it should return nil if the data provided is nil") 30 | 31 | data := map[string]string{ 32 | "redis": "1.0.0", 33 | } 34 | 35 | o := Tags(data) 36 | require.NotNil(t, o, "it should not return nil if the data is non-nil") 37 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 38 | assert.Equal(t, "tags", o.Class(), "it should use the right option class") 39 | 40 | if assert.Implements(t, (*MergeableOption)(nil), o, "it should implement the MergeableOption interface") { 41 | t.Run("Merge()", func(t *testing.T) { 42 | om := o.(MergeableOption) 43 | 44 | assert.Equal(t, o, om.Merge(&testOption{}), "it should replace the old option if it is not recognized") 45 | 46 | t.Run("different entries", func(t *testing.T) { 47 | data2 := map[string]string{ 48 | "pgsql": "5.4.0", 49 | } 50 | o2 := Tags(data2) 51 | require.NotNil(t, o2, "the second module option should not be nil") 52 | 53 | oo := om.Merge(o2) 54 | require.NotNil(t, oo, "it should not return nil when it merges") 55 | 56 | ooi, ok := oo.(*tagsOption) 57 | require.True(t, ok, "it should actually be a *tagsOption") 58 | 59 | if assert.Contains(t, ooi.tags, "redis", "it should contain the first key") { 60 | assert.Equal(t, data["redis"], ooi.tags["redis"], "it should have the right value for the first key") 61 | } 62 | 63 | if assert.Contains(t, ooi.tags, "pgsql", "it should contain the second key") { 64 | assert.Equal(t, data2["pgsql"], ooi.tags["pgsql"], "it should have the right value for the second key") 65 | } 66 | }) 67 | 68 | t.Run("existing entries", func(t *testing.T) { 69 | data2 := map[string]string{ 70 | "redis": "0.8.0", 71 | } 72 | o2 := Tags(data2) 73 | require.NotNil(t, o2, "the second module option should not be nil") 74 | 75 | oo := om.Merge(o2) 76 | require.NotNil(t, oo, "it should not return nil when it merges") 77 | 78 | ooi, ok := oo.(*tagsOption) 79 | require.True(t, ok, "it should actually be a *modulesOption") 80 | 81 | if assert.Contains(t, ooi.tags, "redis", "it should contain the first key") { 82 | assert.Equal(t, data["redis"], ooi.tags["redis"], "it should have the right value for the first key") 83 | } 84 | }) 85 | }) 86 | } 87 | 88 | t.Run("MarshalJSON()", func(t *testing.T) { 89 | assert.Equal(t, map[string]interface{}{ 90 | "redis": "1.0.0", 91 | }, testOptionsSerialize(t, o)) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /timestamp.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | AddDefaultOptionProvider(func() Option { 10 | return Timestamp(time.Now().UTC()) 11 | }) 12 | } 13 | 14 | // Timestamp allows you to provide a custom timestamp for an event 15 | // that is sent to Sentry. 16 | func Timestamp(timestamp time.Time) Option { 17 | return ×tampOption{timestamp} 18 | } 19 | 20 | type timestampOption struct { 21 | timestamp time.Time 22 | } 23 | 24 | func (o *timestampOption) Class() string { 25 | return "timestamp" 26 | } 27 | 28 | func (o *timestampOption) MarshalJSON() ([]byte, error) { 29 | return json.Marshal(o.timestamp.UTC().Format("2006-01-02T15:04:05")) 30 | } 31 | -------------------------------------------------------------------------------- /timestamp_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func ExampleTimestamp() { 11 | cl := NewClient() 12 | 13 | cl.Capture( 14 | // You can specify the timestamp when sending an event to Sentry 15 | Timestamp(time.Now()), 16 | ) 17 | } 18 | 19 | func TestTimestamp(t *testing.T) { 20 | assert.NotNil(t, testGetOptionsProvider(t, Timestamp(time.Now())), "it should be registered as a default option") 21 | 22 | now := time.Now() 23 | o := Timestamp(now) 24 | assert.NotNil(t, o, "should not return a nil option") 25 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 26 | assert.Equal(t, "timestamp", o.Class(), "it should use the right option class") 27 | 28 | t.Run("MarshalJSON()", func(t *testing.T) { 29 | assert.Equal(t, now.UTC().Format("2006-01-02T15:04:05"), testOptionsSerialize(t, o), "it should serialize to a string") 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // Transport is the interface that any network transport must implement 4 | // if it wishes to be used to send Sentry events 5 | type Transport interface { 6 | Send(dsn string, packet Packet) error 7 | } 8 | 9 | // UseTransport allows you to control which transport is used to 10 | // send events for a specific client or packet. 11 | func UseTransport(transport Transport) Option { 12 | if transport == nil { 13 | return nil 14 | } 15 | 16 | return &transportOption{transport} 17 | } 18 | 19 | func init() { 20 | AddDefaultOptions(UseTransport(newHTTPTransport())) 21 | } 22 | 23 | type transportOption struct { 24 | transport Transport 25 | } 26 | 27 | func (o *transportOption) Class() string { 28 | return "sentry-go.transport" 29 | } 30 | 31 | func (o *transportOption) Omit() bool { 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /transport_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleUseTransport() { 10 | var myTransport Transport 11 | 12 | cl := NewClient( 13 | // You can configure the transport to be used on a client level 14 | UseTransport(myTransport), 15 | ) 16 | 17 | cl.Capture( 18 | // Or for a specific event when it is sent 19 | UseTransport(myTransport), 20 | ) 21 | } 22 | 23 | func TestTransport(t *testing.T) { 24 | assert.Nil(t, UseTransport(nil), "it should return nil if no transport is provided") 25 | 26 | tr := newHTTPTransport() 27 | o := UseTransport(tr) 28 | assert.NotNil(t, o, "should not return a nil option") 29 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 30 | assert.Equal(t, "sentry-go.transport", o.Class(), "it should use the right option class") 31 | 32 | if assert.Implements(t, (*Option)(nil), o, "it should implement the OmitableOption interface") { 33 | oo := o.(OmitableOption) 34 | assert.True(t, oo.Omit(), "it should always return true for calls to Omit()") 35 | } 36 | } 37 | 38 | func testNewTestTransport() *testTransport { 39 | return &testTransport{ 40 | ch: make(chan Packet), 41 | } 42 | } 43 | 44 | type testTransport struct { 45 | ch chan Packet 46 | err error 47 | } 48 | 49 | func (t *testTransport) Send(dsn string, packet Packet) error { 50 | t.ch <- packet 51 | return t.err 52 | } 53 | -------------------------------------------------------------------------------- /unset.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | // Unset will unset a field on the packet prior to it being sent. 4 | func Unset(field string) Option { 5 | return &unsetOption{ 6 | className: field, 7 | } 8 | } 9 | 10 | type unsetOption struct { 11 | className string 12 | } 13 | 14 | func (o *unsetOption) Class() string { 15 | return o.className 16 | } 17 | 18 | func (o *unsetOption) MarshalJSON() ([]byte, error) { 19 | return []byte("null"), nil 20 | } 21 | 22 | func (o *unsetOption) Apply(packet map[string]Option) { 23 | delete(packet, o.className) 24 | } 25 | -------------------------------------------------------------------------------- /unset_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func ExampleUnset() { 10 | cl := NewClient( 11 | // You can remove specific default fields from your final packet if you do 12 | // not wish to send them. 13 | Unset("runtime"), 14 | ) 15 | 16 | cl.Capture( 17 | // You can also remove things that you may have added later 18 | Unset("message"), 19 | ) 20 | } 21 | 22 | func TestUnset(t *testing.T) { 23 | o := Unset("runtime") 24 | assert.Equal(t, o.Class(), "runtime", "it should use the correct class name") 25 | 26 | o = Unset("device") 27 | assert.Equal(t, o.Class(), "device", "it should use the correct class name") 28 | 29 | t.Run("MarshalJSON()", func(t *testing.T) { 30 | assert.Equal(t, nil, testOptionsSerialize(t, o), "it should serialize to nil") 31 | }) 32 | 33 | if assert.Implements(t, (*AdvancedOption)(nil), o, "it should implement the AdvancedOption interface") { 34 | p := map[string]Option{ 35 | "level": Level(Error), 36 | "release": Release("1.0.0"), 37 | } 38 | 39 | Unset("level").(AdvancedOption).Apply(p) 40 | assert.NotContains(t, "level", p, "it should remove the property from the packet") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import "encoding/json" 4 | 5 | // UserInfo provides the fields that may be specified to describe 6 | // a unique user of your application. You should specify at least 7 | // an `ID` or `IPAddress`. 8 | type UserInfo struct { 9 | ID string 10 | Email string 11 | IPAddress string 12 | Username string 13 | Extra map[string]string 14 | } 15 | 16 | // User allows you to include the details of a user that was interacting 17 | // with your application when the error occurred. 18 | func User(user *UserInfo) Option { 19 | if user == nil { 20 | return nil 21 | } 22 | 23 | o := &userOption{ 24 | fields: map[string]string{}, 25 | } 26 | 27 | for k, v := range user.Extra { 28 | o.fields[k] = v 29 | } 30 | 31 | if user.ID != "" { 32 | o.fields["id"] = user.ID 33 | } 34 | 35 | if user.Username != "" { 36 | o.fields["username"] = user.Username 37 | } 38 | 39 | if user.IPAddress != "" { 40 | o.fields["ip_address"] = user.IPAddress 41 | } 42 | 43 | if user.Email != "" { 44 | o.fields["email"] = user.Email 45 | } 46 | 47 | return o 48 | } 49 | 50 | type userOption struct { 51 | fields map[string]string 52 | } 53 | 54 | func (o *userOption) Class() string { 55 | return "user" 56 | } 57 | 58 | func (o *userOption) MarshalJSON() ([]byte, error) { 59 | return json.Marshal(o.fields) 60 | } 61 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func ExampleUser() { 11 | user := UserInfo{ 12 | ID: "17ba08f7cc89a912bf812918", 13 | Email: "test@example.com", 14 | Username: "Test User", 15 | IPAddress: "127.0.0.1", 16 | Extra: map[string]string{ 17 | "role": "Tester", 18 | }, 19 | } 20 | 21 | cl := NewClient( 22 | // You can specify your user when you create your client 23 | User(&user), 24 | ) 25 | 26 | cl.Capture( 27 | // Or when you send an event to Sentry 28 | User(&user), 29 | ) 30 | } 31 | 32 | func TestUser(t *testing.T) { 33 | assert.Nil(t, User(nil), "it should return nil if the user details are nil") 34 | 35 | user := UserInfo{ 36 | ID: "17ba08f7cc89a912bf812918", 37 | Email: "test@example.com", 38 | Username: "Test User", 39 | IPAddress: "127.0.0.1", 40 | Extra: map[string]string{ 41 | "role": "Tester", 42 | }, 43 | } 44 | 45 | fields := map[string]interface{}{ 46 | "id": "17ba08f7cc89a912bf812918", 47 | "email": "test@example.com", 48 | "username": "Test User", 49 | "ip_address": "127.0.0.1", 50 | "role": "Tester", 51 | } 52 | 53 | o := User(&user) 54 | require.NotNil(t, o, "should not return a nil option") 55 | assert.Implements(t, (*Option)(nil), o, "it should implement the Option interface") 56 | assert.Equal(t, "user", o.Class(), "it should use the right option class") 57 | 58 | t.Run("MarshalJSON()", func(t *testing.T) { 59 | assert.Equal(t, fields, testOptionsSerialize(t, o), "it should serialize to the right fields") 60 | }) 61 | } 62 | --------------------------------------------------------------------------------