The following is a set of guidelines for contributing to this package and any of its plugins. As guidelines rather than rules, feel free to propose changes to them if you encounter any problems.
What should I know before I get started?
- Basic Option
- Custom Serialization
- Merging Multiple Options
- Doing last-minute preparation on your option
- Omitting Options from the Packet
Sentry provides documentation for client developers on their docs website. If you are looking to add a new feature, interface or field to this library, you should start by reading the official documentation to ensure it is compatible with Sentry's API.
There is a great article on halls-of-valhalla.org which explains what the Options Pattern is and how it can be leveraged in Go.
This library makes heavy use of the Options Pattern to enable both the building of event packets (that are then sent to Sentry) and the configuration of the library's behaviour.
In addition to configuring the core library, these options can be requested later
from a Client
using the GetOption(name)
method, allowing plugins to provide and use their own
option types.
This package uses Semantic Versioning for its public API. We are currently on version 1 of that public API and will endeavour to avoid the need to bump that major version at all costs - unless there is absolutely not other cource of action.
Due to the way that this package's API is designed, it should be easily possible for most implementation details to be changed without affecting the public API and the behaviour of the various components contained within it.
If we do encounter the need to update the public API in a backwards incompatible manner, we will leverage gopkg.in to provide users of the old version with consistent access to the version they depend upon as well as bumping our SemVer major version.
This library endeavours to provide a simple API where "doing the right thing" is easy and obvious. To achieve that, it both limits the number of methods available on common interfaces and pushes the notion of consistency in all interactions.
Specifically, you will notice two major patterns throughout this package's API:
Most of this library's options are configurable or can have sensible default specified by us. As a result, most user interaction will involve customizing those defaults or providing optional data. To make this as easy as possible, we use the Options Pattern when creating clients or capturing events.
cl := sentry.NewClient(
sentry.DSN("..."),
sentry.Release("v1.0.0"),
sentry.Logger("root"),
)
cl.Capture(
sentry.Message("This is an example event"),
sentry.Level(sentry.Info),
)
func MyOption() sentry.Option {
return &myOption{}
}
type myOption struct {
}
func (o *myOption) Class() string {
return "my.option"
}
As much as possible, we want to avoid forcing users to fill in complex objects unless absolutely necessary. If sensible defaults can be selected, or fields are optional, they should be configurable through a builder interface rather than being a requirement of the option constructor.
This pattern allows a developer to easily discover fields they may provide and gain insight into the requirements and options available to them when using your option.
sentry.DefaultBreadcrumbs().
NewDefault(nil).
WithMessage("This is an example breadcrumb").
WithCategory("example").
WithLevel(sentry.Info)
When implementing your builder, you should provide a builder interface whose methods all return the same builder interface. This allows your builder's methods to be easily chained together.
type MyOptionBuilder interface {
WithStringProperty(value string) MyOptionBuilder
WithIntProperty(value int) MyOptionBuilder
}
The value of test coverage as a metric may be endlessly debated, however this library places a heavy emphasis on using it as an indicator of poor test coverage within a module. In addition to high test coverage, we strive for high assertion coverage (with over 1000 assertions in the current test suite).
If you are making a pull request on this library, please ensure that you have implemented a comprehensive set of tests to verify all assumptions about its behaviour as well as to assert the behaviour of its API. This will ensure that we more easily catch breaking API changes before they are released into the wild.
To make developing high quality tests as easy as possible, we make use of GoConvey. Convey is a test framework and runner which simplifies writing complex test trees and provides an excellent interface through which the realtime status of your tests can be viewed.
To use it, just do the following:
$ go get github.com/smartystreets/goconvey
$ $GOPATH/bin/goconvey --port 8080
And then open up your web browser: http://localhost:8080/
Sometimes you'll want to take advantage of a Sentry processor which isn't yet supported by this library. This library makes implementing your own options trivially easy, not only allowing you to add those new interfaces, but to replace the default implementations if you don't like the way they work.
WARNING If you're using an option that doesn't implement Omit()
and
always return true
then you need to ensure that your Class()
name matches
one of the valid Sentry interfaces.
Failure to do so will result in Sentry responding with an error message.
The following is a basic option which can be used in calls to
sentry.NewClient(...)
, client.Capture(...)
and client.With(...)
.
It will be added to the packet under the class name my_interface
and
will be serialized as a JSON object like { "field": "value" }
.
package sentry
// MyOption should create a new instance of your myOption type
// and return it as an Option interface (or derivative thereof).
// You should avoid directly exposing the struct and adopt the
// builder pattern if there is the potential need for additional
// configuration.
func MyOption(field string) Option {
// If an empty field is invalid, then return a nil option
// and it will be ignored by the options processor.
if field == "" {
return nil
}
return &myOption{
Field: field,
}
}
type myOption struct {
Field string `json:"field"`
}
func (i *myOption) Class() string {
return "my_interface"
}
If you need to serialize your option as something other than a JSON
object, you simply need to implement the MarshalJSON()
method. This
also applies in situations where your object must be marshalled to
a type other than itself.
import "encoding/json"
func (i *myOption) MarshalJSON() ([]byte, error) {
return json.Marshal(i.Field)
}
Sometimes you won't want to simply replace an option's value if a new
instance of it is provided. In these situations, you'll want to implement
the Merge()
method which allows you to control how your option behaves
when it encounters another option with the same Class()
.
import "gopkg.in/SierraSoftworks/sentry-go.v1"
func (i *myOption) Merge(old sentry.Option) sentry.Option {
if old, ok := old.(*myOption); ok {
return &myOption{
Field: fmt.Sprintf("%s,%s", old.Field, i.Field),
}
}
// Replace by default if we don't know how to handle the old type
return i
}
If your option uses a builder interface to configure its fields before
being sent, then you might want to do some processing just before the
option is embedded in the Packet. This is where the Finalize()
method
comes in.
Finalize()
will be called when your option is added to a packet for
transmission, so you can use a chainable builder interface like
MyOption().WithField("example")
.
import "strings"
func (i *myOption) Finalize() {
i.Field = strings.TrimSpace(i.Field)
}
In some situations you might find that you want to not include an option in the packet after all, perhaps the user hasn't provided all the required information or you couldn't gather it automatically.
The Omit()
method allows your option to tell the packet whether or
not to include it. We actually use it internally for things like the DSN
which shouldn't be sent to Sentry in the packet, but which we still want
to read from the options builder.
func (i *myOption) Omit() bool {
return len(i.Field) == 0
}