forked from vikunja/vikunja
konrad
d02d413c5e
Use sentry echo integration to send errors Only capture errors not already handled by echo Add sentry panic handler Add sentry library Add sentry init Add sentry config Co-authored-by: kolaente <k@knt.li> Reviewed-on: vikunja/api#591
189 lines
4.0 KiB
Go
189 lines
4.0 KiB
Go
package sentry
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type scheme string
|
|
|
|
const (
|
|
schemeHTTP scheme = "http"
|
|
schemeHTTPS scheme = "https"
|
|
)
|
|
|
|
func (scheme scheme) defaultPort() int {
|
|
switch scheme {
|
|
case schemeHTTPS:
|
|
return 443
|
|
case schemeHTTP:
|
|
return 80
|
|
default:
|
|
return 80
|
|
}
|
|
}
|
|
|
|
type DsnParseError struct {
|
|
Message string
|
|
}
|
|
|
|
func (e DsnParseError) Error() string {
|
|
return "[Sentry] DsnParseError: " + e.Message
|
|
}
|
|
|
|
// Dsn is used as the remote address source to client transport.
|
|
type Dsn struct {
|
|
scheme scheme
|
|
publicKey string
|
|
secretKey string
|
|
host string
|
|
port int
|
|
path string
|
|
projectID int
|
|
}
|
|
|
|
// NewDsn creates an instance of `Dsn` by parsing provided url in a `string` format.
|
|
// If Dsn is not set the client is effectively disabled.
|
|
func NewDsn(rawURL string) (*Dsn, error) {
|
|
// Parse
|
|
parsedURL, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return nil, &DsnParseError{fmt.Sprintf("invalid url: %v", err)}
|
|
}
|
|
|
|
// Scheme
|
|
var scheme scheme
|
|
switch parsedURL.Scheme {
|
|
case "http":
|
|
scheme = schemeHTTP
|
|
case "https":
|
|
scheme = schemeHTTPS
|
|
default:
|
|
return nil, &DsnParseError{"invalid scheme"}
|
|
}
|
|
|
|
// PublicKey
|
|
publicKey := parsedURL.User.Username()
|
|
if publicKey == "" {
|
|
return nil, &DsnParseError{"empty username"}
|
|
}
|
|
|
|
// SecretKey
|
|
var secretKey string
|
|
if parsedSecretKey, ok := parsedURL.User.Password(); ok {
|
|
secretKey = parsedSecretKey
|
|
}
|
|
|
|
// Host
|
|
host := parsedURL.Hostname()
|
|
if host == "" {
|
|
return nil, &DsnParseError{"empty host"}
|
|
}
|
|
|
|
// Port
|
|
var port int
|
|
if parsedURL.Port() != "" {
|
|
parsedPort, err := strconv.Atoi(parsedURL.Port())
|
|
if err != nil {
|
|
return nil, &DsnParseError{"invalid port"}
|
|
}
|
|
port = parsedPort
|
|
} else {
|
|
port = scheme.defaultPort()
|
|
}
|
|
|
|
// ProjectID
|
|
if len(parsedURL.Path) == 0 || parsedURL.Path == "/" {
|
|
return nil, &DsnParseError{"empty project id"}
|
|
}
|
|
pathSegments := strings.Split(parsedURL.Path[1:], "/")
|
|
projectID, err := strconv.Atoi(pathSegments[len(pathSegments)-1])
|
|
if err != nil {
|
|
return nil, &DsnParseError{"invalid project id"}
|
|
}
|
|
|
|
// Path
|
|
var path string
|
|
if len(pathSegments) > 1 {
|
|
path = "/" + strings.Join(pathSegments[0:len(pathSegments)-1], "/")
|
|
}
|
|
|
|
return &Dsn{
|
|
scheme: scheme,
|
|
publicKey: publicKey,
|
|
secretKey: secretKey,
|
|
host: host,
|
|
port: port,
|
|
path: path,
|
|
projectID: projectID,
|
|
}, nil
|
|
}
|
|
|
|
// String formats Dsn struct into a valid string url
|
|
func (dsn Dsn) String() string {
|
|
var url string
|
|
url += fmt.Sprintf("%s://%s", dsn.scheme, dsn.publicKey)
|
|
if dsn.secretKey != "" {
|
|
url += fmt.Sprintf(":%s", dsn.secretKey)
|
|
}
|
|
url += fmt.Sprintf("@%s", dsn.host)
|
|
if dsn.port != dsn.scheme.defaultPort() {
|
|
url += fmt.Sprintf(":%d", dsn.port)
|
|
}
|
|
if dsn.path != "" {
|
|
url += dsn.path
|
|
}
|
|
url += fmt.Sprintf("/%d", dsn.projectID)
|
|
return url
|
|
}
|
|
|
|
// StoreAPIURL returns assembled url to be used in the transport.
|
|
// It points to configures Sentry instance.
|
|
func (dsn Dsn) StoreAPIURL() *url.URL {
|
|
var rawURL string
|
|
rawURL += fmt.Sprintf("%s://%s", dsn.scheme, dsn.host)
|
|
if dsn.port != dsn.scheme.defaultPort() {
|
|
rawURL += fmt.Sprintf(":%d", dsn.port)
|
|
}
|
|
if dsn.path != "" {
|
|
rawURL += dsn.path
|
|
}
|
|
rawURL += fmt.Sprintf("/api/%d/store/", dsn.projectID)
|
|
parsedURL, _ := url.Parse(rawURL)
|
|
return parsedURL
|
|
}
|
|
|
|
// RequestHeaders returns all the necessary headers that have to be used in the transport.
|
|
func (dsn Dsn) RequestHeaders() map[string]string {
|
|
auth := fmt.Sprintf("Sentry sentry_version=%s, sentry_timestamp=%d, "+
|
|
"sentry_client=sentry.go/%s, sentry_key=%s", apiVersion, time.Now().Unix(), Version, dsn.publicKey)
|
|
|
|
if dsn.secretKey != "" {
|
|
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
|
|
}
|
|
|
|
return map[string]string{
|
|
"Content-Type": "application/json",
|
|
"X-Sentry-Auth": auth,
|
|
}
|
|
}
|
|
|
|
func (dsn Dsn) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(dsn.String())
|
|
}
|
|
|
|
func (dsn *Dsn) UnmarshalJSON(data []byte) error {
|
|
var str string
|
|
_ = json.Unmarshal(data, &str)
|
|
newDsn, err := NewDsn(str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*dsn = *newDsn
|
|
return nil
|
|
}
|