composer: glitchtip integration

This commit is contained in:
Diaa Sami 2024-01-18 20:22:36 +01:00 committed by Ondřej Budai
parent 95b4979d88
commit c9c51613a4
63 changed files with 8857 additions and 220 deletions

View file

@ -21,6 +21,7 @@ type ComposerConfigFile struct {
SplunkHost string `env:"SPLUNK_HEC_HOST"`
SplunkPort string `env:"SPLUNK_HEC_PORT"`
SplunkToken string `env:"SPLUNK_HEC_TOKEN"`
GlitchTipDSN string `env:"GLITCHTIP_DSN"`
}
type KojiAPIConfig struct {

View file

@ -6,6 +6,7 @@ import (
"os"
"github.com/coreos/go-systemd/activation"
"github.com/getsentry/sentry-go"
slogger "github.com/osbuild/osbuild-composer/pkg/splunk_logger"
"github.com/sirupsen/logrus"
)
@ -70,6 +71,17 @@ func main() {
logrus.AddHook(hook)
}
if config.GlitchTipDSN != "" {
err = sentry.Init(sentry.ClientOptions{
Dsn: config.GlitchTipDSN,
})
if err != nil {
panic(err)
}
} else {
logrus.Warn("GLITCHTIP_DSN not configured, skipping initializing Sentry/Glitchtip")
}
stateDir, ok := os.LookupEnv("STATE_DIRECTORY")
if !ok {
logrus.Fatal("STATE_DIRECTORY is not set. Is the service file missing StateDirectory=?")

3
go.mod
View file

@ -21,6 +21,7 @@ require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/deepmap/oapi-codegen v1.8.2
github.com/getkin/kin-openapi v0.93.0
github.com/getsentry/sentry-go v0.26.0
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/go-cmp v0.6.0
@ -151,7 +152,7 @@ require (
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.18 // indirect
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect

9
go.sum
View file

@ -161,9 +161,12 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.93.0 h1:fc9z+9VADQla6bEb7V+dtZmr9Gj4qB6ZsD8c3pqEK0E=
github.com/getkin/kin-openapi v0.93.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA=
github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@ -459,8 +462,8 @@ github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU=
@ -510,6 +513,7 @@ github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqw
github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -713,7 +717,6 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=

View file

@ -12,6 +12,8 @@ import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/routers"
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@ -87,6 +89,12 @@ func (s *Server) Handler(path string) http.Handler {
e.Use(middleware.Recover())
e.Logger = common.Logger()
if sentry.CurrentHub().Client() == nil {
logrus.Warn("Sentry/Glitchtip not initialized, echo middleware was not enabled")
} else {
e.Use(sentryecho.New(sentryecho.Options{}))
}
handler := apiHandlers{
server: s,
}

View file

@ -118,6 +118,12 @@ objects:
optional: true
- name: SPLUNK_HEC_PORT
value: "${SPLUNK_HEC_PORT}"
- name: GLITCHTIP_DSN
valueFrom:
secretKeyRef:
key: dsn
name: "${GLITCHTIP_DSN_NAME}"
optional: true
ports:
- name: composer-api
protocol: TCP
@ -486,3 +492,6 @@ parameters:
- description: Splunk HTTP Event Collector port
name: SPLUNK_HEC_PORT
value: "443"
- name: GLITCHTIP_DSN_NAME
value: "composer-stage-dsn"
description: Name of the secret for connecting to sentry/glitchtip

13
vendor/github.com/getsentry/sentry-go/.codecov.yml generated vendored Normal file
View file

@ -0,0 +1,13 @@
codecov:
# across
notify:
# Do not notify until at least this number of reports have been uploaded
# from the CI pipeline. We normally have more than that number, but 6
# should be enough to get a first notification.
after_n_builds: 6
coverage:
status:
project:
default:
# Do not fail the commit status if the coverage was reduced up to this value
threshold: 0.5%

13
vendor/github.com/getsentry/sentry-go/.craft.yml generated vendored Normal file
View file

@ -0,0 +1,13 @@
minVersion: 0.35.0
changelogPolicy: simple
artifactProvider:
name: none
targets:
- name: github
tagPrefix: v
- name: github
tagPrefix: otel/v
tagOnly: true
- name: registry
sdks:
github:getsentry/sentry-go:

5
vendor/github.com/getsentry/sentry-go/.gitattributes generated vendored Normal file
View file

@ -0,0 +1,5 @@
# Tell Git to use LF for line endings on all platforms.
# Required to have correct test data on Windows.
# https://github.com/mvdan/github-actions-golang#caveats
# https://github.com/actions/checkout/issues/135#issuecomment-613361104
* text eol=lf

14
vendor/github.com/getsentry/sentry-go/.gitignore generated vendored Normal file
View file

@ -0,0 +1,14 @@
# Code coverage artifacts
coverage.txt
coverage.out
coverage.html
.coverage/
# Just my personal way of tracking stuff — Kamil
FIXME.md
TODO.md
!NOTES.md
# IDE system files
.idea
.vscode

46
vendor/github.com/getsentry/sentry-go/.golangci.yml generated vendored Normal file
View file

@ -0,0 +1,46 @@
linters:
disable-all: true
enable:
- bodyclose
- dogsled
- dupl
- errcheck
- exportloopref
- gochecknoinits
- goconst
- gocritic
- gocyclo
- godot
- gofmt
- goimports
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- prealloc
- revive
- staticcheck
- typecheck
- unconvert
- unparam
- unused
- whitespace
issues:
exclude-rules:
- path: _test\.go
linters:
- goconst
- prealloc
- path: _test\.go
text: "G306:"
linters:
- gosec
- path: errors_test\.go
linters:
- unused
- path: http/example_test\.go
linters:
- errcheck
- bodyclose

781
vendor/github.com/getsentry/sentry-go/CHANGELOG.md generated vendored Normal file
View file

@ -0,0 +1,781 @@
# Changelog
## 0.26.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.26.0.
### Breaking Changes
As previously announced, this release removes some methods from the SDK.
- `sentry.TransactionName()` use `sentry.WithTransactionName()` instead.
- `sentry.OpName()` use `sentry.WithOpName()` instead.
- `sentry.TransctionSource()` use `sentry.WithTransactionSource()` instead.
- `sentry.SpanSampled()` use `sentry.WithSpanSampled()` instead.
### Features
- Add `WithDescription` span option ([#751](https://github.com/getsentry/sentry-go/pull/751))
```go
span := sentry.StartSpan(ctx, "http.client", WithDescription("GET /api/users"))
```
- Add support for package name parsing in Go 1.20 and higher ([#730](https://github.com/getsentry/sentry-go/pull/730))
### Bug Fixes
- Apply `ClientOptions.SampleRate` only to errors & messages ([#754](https://github.com/getsentry/sentry-go/pull/754))
- Check if git is available before executing any git commands ([#737](https://github.com/getsentry/sentry-go/pull/737))
## 0.25.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.25.0.
### Breaking Changes
As previously announced, this release removes two global constants from the SDK.
- `sentry.Version` was removed. Use `sentry.SDKVersion` instead ([#727](https://github.com/getsentry/sentry-go/pull/727))
- `sentry.SDKIdentifier` was removed. Use `Client.GetSDKIdentifier()` instead ([#727](https://github.com/getsentry/sentry-go/pull/727))
### Features
- Add `ClientOptions.IgnoreTransactions`, which allows you to ignore specific transactions based on their name ([#717](https://github.com/getsentry/sentry-go/pull/717))
- Add `ClientOptions.Tags`, which allows you to set global tags that are applied to all events. You can also define tags by setting `SENTRY_TAGS_` environment variables ([#718](https://github.com/getsentry/sentry-go/pull/718))
### Bug fixes
- Fix an issue in the profiler that would cause an infinite loop if the duration of a transaction is longer than 30 seconds ([#724](https://github.com/getsentry/sentry-go/issues/724))
### Misc
- `dsn.RequestHeaders()` is not to be removed, though it is still considered deprecated and should only be used when using a custom transport that sends events to the `/store` endpoint ([#720](https://github.com/getsentry/sentry-go/pull/720))
## 0.24.1
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.1.
### Bug fixes
- Prevent a panic in `sentryotel.flushSpanProcessor()` ([(#711)](https://github.com/getsentry/sentry-go/pull/711))
- Prevent a panic when setting the SDK identifier ([#715](https://github.com/getsentry/sentry-go/pull/715))
## 0.24.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.24.0.
### Deprecations
- `sentry.Version` to be removed in 0.25.0. Use `sentry.SDKVersion` instead.
- `sentry.SDKIdentifier` to be removed in 0.25.0. Use `Client.GetSDKIdentifier()` instead.
- `dsn.RequestHeaders()` to be removed after 0.25.0, but no earlier than December 1, 2023. Requests to the `/envelope` endpoint are authenticated using the DSN in the envelope header.
### Features
- Run a single instance of the profiler instead of multiple ones for each Go routine ([#655](https://github.com/getsentry/sentry-go/pull/655))
- Use the route path as the transaction names when using the Gin integration ([#675](https://github.com/getsentry/sentry-go/pull/675))
- Set the SDK name accordingly when a framework integration is used ([#694](https://github.com/getsentry/sentry-go/pull/694))
- Read release information (VCS revision) from `debug.ReadBuildInfo` ([#704](https://github.com/getsentry/sentry-go/pull/704))
### Bug fixes
- [otel] Fix incorrect usage of `attributes.Value.AsString` ([#684](https://github.com/getsentry/sentry-go/pull/684))
- Fix trace function name parsing in profiler on go1.21+ ([#695](https://github.com/getsentry/sentry-go/pull/695))
### Misc
- Test against Go 1.21 ([#695](https://github.com/getsentry/sentry-go/pull/695))
- Make tests more robust ([#698](https://github.com/getsentry/sentry-go/pull/698), [#699](https://github.com/getsentry/sentry-go/pull/699), [#700](https://github.com/getsentry/sentry-go/pull/700), [#702](https://github.com/getsentry/sentry-go/pull/702))
## 0.23.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.23.0.
### Features
- Initial support for [Cron Monitoring](https://docs.sentry.io/product/crons/) ([#661](https://github.com/getsentry/sentry-go/pull/661))
This is how the basic usage of the feature looks like:
```go
// 🟡 Notify Sentry your job is running:
checkinId := sentry.CaptureCheckIn(
&sentry.CheckIn{
MonitorSlug: "<monitor-slug>",
Status: sentry.CheckInStatusInProgress,
},
nil,
)
// Execute your scheduled task here...
// 🟢 Notify Sentry your job has completed successfully:
sentry.CaptureCheckIn(
&sentry.CheckIn{
ID: *checkinId,
MonitorSlug: "<monitor-slug>",
Status: sentry.CheckInStatusOK,
},
nil,
)
```
A full example of using Crons Monitoring is available [here](https://github.com/getsentry/sentry-go/blob/dde4d360660838f3c2e0ced8205bc8f7a8d312d9/_examples/crons/main.go).
More documentation on configuring and using Crons [can be found here](https://docs.sentry.io/platforms/go/crons/).
- Add support for [Event Attachments](https://docs.sentry.io/platforms/go/enriching-events/attachments/) ([#670](https://github.com/getsentry/sentry-go/pull/670))
It's now possible to add file/binary payloads to Sentry events:
```go
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.AddAttachment(&Attachment{
Filename: "report.html",
ContentType: "text/html",
Payload: []byte("<h1>Look, HTML</h1>"),
})
})
```
The attachment will then be accessible on the Issue Details page.
- Add sampling decision to trace envelope header ([#666](https://github.com/getsentry/sentry-go/pull/666))
- Expose SpanFromContext function ([#672](https://github.com/getsentry/sentry-go/pull/672))
### Bug fixes
- Make `Span.Finish` a no-op when the span is already finished ([#660](https://github.com/getsentry/sentry-go/pull/660))
## 0.22.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.22.0.
This release contains initial [profiling](https://docs.sentry.io/product/profiling/) support, as well as a few bug fixes and improvements.
### Features
- Initial (alpha) support for [profiling](https://docs.sentry.io/product/profiling/) ([#626](https://github.com/getsentry/sentry-go/pull/626))
Profiling is disabled by default. To enable it, configure both `TracesSampleRate` and `ProfilesSampleRate` when initializing the SDK:
```go
err := sentry.Init(sentry.ClientOptions{
Dsn: "__DSN__",
EnableTracing: true,
TracesSampleRate: 1.0,
// The sampling rate for profiling is relative to TracesSampleRate. In this case, we'll capture profiles for 100% of transactions.
ProfilesSampleRate: 1.0,
})
```
More documentation on profiling and current limitations [can be found here](https://docs.sentry.io/platforms/go/profiling/).
- Add transactions/tracing support go the Gin integration ([#644](https://github.com/getsentry/sentry-go/pull/644))
### Bug fixes
- Always set a valid source on transactions ([#637](https://github.com/getsentry/sentry-go/pull/637))
- Clone scope.Context in more places to avoid panics on concurrent reads and writes ([#638](https://github.com/getsentry/sentry-go/pull/638))
- Fixes [#570](https://github.com/getsentry/sentry-go/issues/570)
- Fix frames recognized as not being in-app still showing as in-app ([#647](https://github.com/getsentry/sentry-go/pull/647))
## 0.21.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.21.0.
Note: this release includes one **breaking change** and some **deprecations**, which are listed below.
### Breaking Changes
**This change does not apply if you use [https://sentry.io](https://sentry.io)**
- Remove support for the `/store` endpoint ([#631](https://github.com/getsentry/sentry-go/pull/631))
- This change requires a self-hosted version of Sentry 20.6.0 or higher. If you are using a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka *on-premise*) older than 20.6.0, then you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/) your instance.
### Features
- Rename four span option functions ([#611](https://github.com/getsentry/sentry-go/pull/611), [#624](https://github.com/getsentry/sentry-go/pull/624))
- `TransctionSource` -> `WithTransactionSource`
- `SpanSampled` -> `WithSpanSampled`
- `OpName` -> `WithOpName`
- `TransactionName` -> `WithTransactionName`
- Old functions `TransctionSource`, `SpanSampled`, `OpName`, and `TransactionName` are still available but are now **deprecated** and will be removed in a future release.
- Make `client.EventFromMessage` and `client.EventFromException` methods public ([#607](https://github.com/getsentry/sentry-go/pull/607))
- Add `client.SetException` method ([#607](https://github.com/getsentry/sentry-go/pull/607))
- This allows to set or add errors to an existing `Event`.
### Bug Fixes
- Protect from panics while doing concurrent reads/writes to Span data fields ([#609](https://github.com/getsentry/sentry-go/pull/609))
- [otel] Improve detection of Sentry-related spans ([#632](https://github.com/getsentry/sentry-go/pull/632), [#636](https://github.com/getsentry/sentry-go/pull/636))
- Fixes cases when HTTP spans containing requests to Sentry were captured by Sentry ([#627](https://github.com/getsentry/sentry-go/issues/627))
### Misc
- Drop testing in (legacy) GOPATH mode ([#618](https://github.com/getsentry/sentry-go/pull/618))
- Remove outdated documentation from https://pkg.go.dev/github.com/getsentry/sentry-go ([#623](https://github.com/getsentry/sentry-go/pull/623))
## 0.20.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.20.0.
Note: this release has some **breaking changes**, which are listed below.
### Breaking Changes
- Remove the following methods: `Scope.SetTransaction()`, `Scope.Transaction()` ([#605](https://github.com/getsentry/sentry-go/pull/605))
Span.Name should be used instead to access the transaction's name.
For example, the following [`TracesSampler`](https://docs.sentry.io/platforms/go/configuration/sampling/#setting-a-sampling-function) function should be now written as follows:
**Before:**
```go
TracesSampler: func(ctx sentry.SamplingContext) float64 {
hub := sentry.GetHubFromContext(ctx.Span.Context())
if hub.Scope().Transaction() == "GET /health" {
return 0
}
return 1
},
```
**After:**
```go
TracesSampler: func(ctx sentry.SamplingContext) float64 {
if ctx.Span.Name == "GET /health" {
return 0
}
return 1
},
```
### Features
- Add `Span.SetContext()` method ([#599](https://github.com/getsentry/sentry-go/pull/599/))
- It is recommended to use it instead of `hub.Scope().SetContext` when setting or updating context on transactions.
- Add `DebugMeta` interface to `Event` and extend `Frame` structure with more fields ([#606](https://github.com/getsentry/sentry-go/pull/606))
- More about DebugMeta interface [here](https://develop.sentry.dev/sdk/event-payloads/debugmeta/).
### Bug Fixes
- [otel] Fix missing OpenTelemetry context on some events ([#599](https://github.com/getsentry/sentry-go/pull/599), [#605](https://github.com/getsentry/sentry-go/pull/605))
- Fixes ([#596](https://github.com/getsentry/sentry-go/issues/596)).
- [otel] Better handling for HTTP span attributes ([#610](https://github.com/getsentry/sentry-go/pull/610))
### Misc
- Bump minimum versions: `github.com/kataras/iris/v12` to 12.2.0, `github.com/labstack/echo/v4` to v4.10.0 ([#595](https://github.com/getsentry/sentry-go/pull/595))
- Resolves [GO-2022-1144 / CVE-2022-41717](https://deps.dev/advisory/osv/GO-2022-1144), [GO-2023-1495 / CVE-2022-41721](https://deps.dev/advisory/osv/GO-2023-1495), [GO-2022-1059 / CVE-2022-32149](https://deps.dev/advisory/osv/GO-2022-1059).
- Bump `google.golang.org/protobuf` minimum required version to 1.29.1 ([#604](https://github.com/getsentry/sentry-go/pull/604))
- This fixes a potential denial of service issue ([CVE-2023-24535](https://github.com/advisories/GHSA-hw7c-3rfg-p46j)).
- Exclude the `otel` module when building in GOPATH mode ([#615](https://github.com/getsentry/sentry-go/pull/615))
## 0.19.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.19.0.
### Features
- Add support for exception mechanism metadata ([#564](https://github.com/getsentry/sentry-go/pull/564/))
- More about exception mechanisms [here](https://develop.sentry.dev/sdk/event-payloads/exception/#exception-mechanism).
### Bug Fixes
- [otel] Use the correct "trace" context when sending a Sentry error ([#580](https://github.com/getsentry/sentry-go/pull/580/))
### Misc
- Drop support for Go 1.17, add support for Go 1.20 ([#563](https://github.com/getsentry/sentry-go/pull/563/))
- According to our policy, we're officially supporting the last three minor releases of Go.
- Switch repository license to MIT ([#583](https://github.com/getsentry/sentry-go/pull/583/))
- More about Sentry licensing [here](https://open.sentry.io/licensing/).
- Bump `golang.org/x/text` minimum required version to 0.3.8 ([#586](https://github.com/getsentry/sentry-go/pull/586))
- This fixes [CVE-2022-32149](https://github.com/advisories/GHSA-69ch-w2m2-3vjp) vulnerability.
## 0.18.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.18.0.
This release contains initial support for [OpenTelemetry](https://opentelemetry.io/) and various other bug fixes and improvements.
**Note**: This is the last release supporting Go 1.17.
### Features
- Initial support for [OpenTelemetry](https://opentelemetry.io/).
You can now send all your OpenTelemetry spans to Sentry.
Install the `otel` module
```bash
go get github.com/getsentry/sentry-go \
github.com/getsentry/sentry-go/otel
```
Configure the Sentry and OpenTelemetry SDKs
```go
import (
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"github.com/getsentry/sentry-go"
"github.com/getsentry/sentry-go/otel"
// ...
)
// Initlaize the Sentry SDK
sentry.Init(sentry.ClientOptions{
Dsn: "__DSN__",
EnableTracing: true,
TracesSampleRate: 1.0,
})
// Set up the Sentry span processor
tp := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(sentryotel.NewSentrySpanProcessor()),
// ...
)
otel.SetTracerProvider(tp)
// Set up the Sentry propagator
otel.SetTextMapPropagator(sentryotel.NewSentryPropagator())
```
You can read more about using OpenTelemetry with Sentry in our [docs](https://docs.sentry.io/platforms/go/performance/instrumentation/opentelemetry/).
### Bug Fixes
- Do not freeze the Dynamic Sampling Context when no Sentry values are present in the baggage header ([#532](https://github.com/getsentry/sentry-go/pull/532))
- Create a frozen Dynamic Sampling Context when calling `span.ToBaggage()` ([#566](https://github.com/getsentry/sentry-go/pull/566))
- Fix baggage parsing and encoding in vendored otel package ([#568](https://github.com/getsentry/sentry-go/pull/568))
### Misc
- Add `Span.SetDynamicSamplingContext()` ([#539](https://github.com/getsentry/sentry-go/pull/539/))
- Add various getters for `Dsn` ([#540](https://github.com/getsentry/sentry-go/pull/540))
- Add `SpanOption::SpanSampled` ([#546](https://github.com/getsentry/sentry-go/pull/546))
- Add `Span.SetData()` ([#542](https://github.com/getsentry/sentry-go/pull/542))
- Add `Span.IsTransaction()` ([#543](https://github.com/getsentry/sentry-go/pull/543))
- Add `Span.GetTransaction()` method ([#558](https://github.com/getsentry/sentry-go/pull/558))
## 0.17.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.17.0.
This release contains a new `BeforeSendTransaction` hook option and corrects two regressions introduced in `0.16.0`.
### Features
- Add `BeforeSendTransaction` hook to `ClientOptions` ([#517](https://github.com/getsentry/sentry-go/pull/517))
- Here's [an example](https://github.com/getsentry/sentry-go/blob/master/_examples/http/main.go#L56-L66) of how BeforeSendTransaction can be used to modify or drop transaction events.
### Bug Fixes
- Do not crash in Span.Finish() when the Client is empty [#520](https://github.com/getsentry/sentry-go/pull/520)
- Fixes [#518](https://github.com/getsentry/sentry-go/issues/518)
- Attach non-PII/non-sensitive request headers to events when `ClientOptions.SendDefaultPii` is set to `false` ([#524](https://github.com/getsentry/sentry-go/pull/524))
- Fixes [#523](https://github.com/getsentry/sentry-go/issues/523)
### Misc
- Clarify how to handle logrus.Fatalf events ([#501](https://github.com/getsentry/sentry-go/pull/501/))
- Rename the `examples` directory to `_examples` ([#521](https://github.com/getsentry/sentry-go/pull/521))
- This removes an indirect dependency to `github.com/golang-jwt/jwt`
## 0.16.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.16.0.
Due to ongoing work towards a stable API for `v1.0.0`, we sadly had to include **two breaking changes** in this release.
### Breaking Changes
- Add `EnableTracing`, a boolean option flag to enable performance monitoring (`false` by default).
- If you're using `TracesSampleRate` or `TracesSampler`, this option is **required** to enable performance monitoring.
```go
sentry.Init(sentry.ClientOptions{
EnableTracing: true,
TracesSampleRate: 1.0,
})
```
- Unify TracesSampler [#498](https://github.com/getsentry/sentry-go/pull/498)
- `TracesSampler` was changed to a callback that must return a `float64` between `0.0` and `1.0`.
For example, you can apply a sample rate of `1.0` (100%) to all `/api` transactions, and a sample rate of `0.5` (50%) to all other transactions.
You can read more about this in our [SDK docs](https://docs.sentry.io/platforms/go/configuration/filtering/#using-sampling-to-filter-transaction-events).
```go
sentry.Init(sentry.ClientOptions{
TracesSampler: sentry.TracesSampler(func(ctx sentry.SamplingContext) float64 {
hub := sentry.GetHubFromContext(ctx.Span.Context())
name := hub.Scope().Transaction()
if strings.HasPrefix(name, "GET /api") {
return 1.0
}
return 0.5
}),
}
```
### Features
- Send errors logged with [Logrus](https://github.com/sirupsen/logrus) to Sentry.
- Have a look at our [logrus examples](https://github.com/getsentry/sentry-go/blob/master/_examples/logrus/main.go) on how to use the integration.
- Add support for Dynamic Sampling [#491](https://github.com/getsentry/sentry-go/pull/491)
- You can read more about Dynamic Sampling in our [product docs](https://docs.sentry.io/product/data-management-settings/dynamic-sampling/).
- Add detailed logging about the reason transactions are being dropped.
- You can enable SDK logging via `sentry.ClientOptions.Debug: true`.
### Bug Fixes
- Do not clone the hub when calling `StartTransaction` [#505](https://github.com/getsentry/sentry-go/pull/505)
- Fixes [#502](https://github.com/getsentry/sentry-go/issues/502)
## 0.15.0
- fix: Scope values should not override Event values (#446)
- feat: Make maximum amount of spans configurable (#460)
- feat: Add a method to start a transaction (#482)
- feat: Extend User interface by adding Data, Name and Segment (#483)
- feat: Add ClientOptions.SendDefaultPII (#485)
## 0.14.0
- feat: Add function to continue from trace string (#434)
- feat: Add `max-depth` options (#428)
- *[breaking]* ref: Use a `Context` type mapping to a `map[string]interface{}` for all event contexts (#444)
- *[breaking]* ref: Replace deprecated `ioutil` pkg with `os` & `io` (#454)
- ref: Optimize `stacktrace.go` from size and speed (#467)
- ci: Test against `go1.19` and `go1.18`, drop `go1.16` and `go1.15` support (#432, #477)
- deps: Dependency update to fix CVEs (#462, #464, #477)
_NOTE:_ This version drops support for Go 1.16 and Go 1.15. The currently supported Go versions are the last 3 stable releases: 1.19, 1.18 and 1.17.
## v0.13.0
- ref: Change DSN ProjectID to be a string (#420)
- fix: When extracting PCs from stack frames, try the `PC` field (#393)
- build: Bump gin-gonic/gin from v1.4.0 to v1.7.7 (#412)
- build: Bump Go version in go.mod (#410)
- ci: Bump golangci-lint version in GH workflow (#419)
- ci: Update GraphQL config with appropriate permissions (#417)
- ci: ci: Add craft release automation (#422)
## v0.12.0
- feat: Automatic Release detection (#363, #369, #386, #400)
- fix: Do not change Hub.lastEventID for transactions (#379)
- fix: Do not clear LastEventID when events are dropped (#382)
- Updates to documentation (#366, #385)
_NOTE:_
This version drops support for Go 1.14, however no changes have been made that would make the SDK not work with Go 1.14. The currently supported Go versions are the last 3 stable releases: 1.15, 1.16 and 1.17.
There are two behavior changes related to `LastEventID`, both of which were intended to align the behavior of the Sentry Go SDK with other Sentry SDKs.
The new [automatic release detection feature](https://github.com/getsentry/sentry-go/issues/335) makes it easier to use Sentry and separate events per release without requiring extra work from users. We intend to improve this functionality in a future release by utilizing information that will be available in runtime starting with Go 1.18. The tracking issue is [#401](https://github.com/getsentry/sentry-go/issues/401).
## v0.11.0
- feat(transports): Category-based Rate Limiting ([#354](https://github.com/getsentry/sentry-go/pull/354))
- feat(transports): Report User-Agent identifying SDK ([#357](https://github.com/getsentry/sentry-go/pull/357))
- fix(scope): Include event processors in clone ([#349](https://github.com/getsentry/sentry-go/pull/349))
- Improvements to `go doc` documentation ([#344](https://github.com/getsentry/sentry-go/pull/344), [#350](https://github.com/getsentry/sentry-go/pull/350), [#351](https://github.com/getsentry/sentry-go/pull/351))
- Miscellaneous changes to our testing infrastructure with GitHub Actions
([57123a40](https://github.com/getsentry/sentry-go/commit/57123a409be55f61b1d5a6da93c176c55a399ad0), [#128](https://github.com/getsentry/sentry-go/pull/128), [#338](https://github.com/getsentry/sentry-go/pull/338), [#345](https://github.com/getsentry/sentry-go/pull/345), [#346](https://github.com/getsentry/sentry-go/pull/346), [#352](https://github.com/getsentry/sentry-go/pull/352), [#353](https://github.com/getsentry/sentry-go/pull/353), [#355](https://github.com/getsentry/sentry-go/pull/355))
_NOTE:_
This version drops support for Go 1.13. The currently supported Go versions are the last 3 stable releases: 1.14, 1.15 and 1.16.
Users of the tracing functionality (`StartSpan`, etc) should upgrade to this version to benefit from separate rate limits for errors and transactions.
There are no breaking changes and upgrading should be a smooth experience for all users.
## v0.10.0
- feat: Debug connection reuse (#323)
- fix: Send root span data as `Event.Extra` (#329)
- fix: Do not double sample transactions (#328)
- fix: Do not override trace context of transactions (#327)
- fix: Drain and close API response bodies (#322)
- ci: Run tests against Go tip (#319)
- ci: Move away from Travis in favor of GitHub Actions (#314) (#321)
## v0.9.0
- feat: Initial tracing and performance monitoring support (#285)
- doc: Revamp sentryhttp documentation (#304)
- fix: Hub.PopScope never empties the scope stack (#300)
- ref: Report Event.Timestamp in local time (#299)
- ref: Report Breadcrumb.Timestamp in local time (#299)
_NOTE:_
This version introduces support for [Sentry's Performance Monitoring](https://docs.sentry.io/platforms/go/performance/).
The new tracing capabilities are beta, and we plan to expand them on future versions. Feedback is welcome, please open new issues on GitHub.
The `sentryhttp` package got better API docs, an [updated usage example](https://github.com/getsentry/sentry-go/tree/master/_examples/http) and support for creating automatic transactions as part of Performance Monitoring.
## v0.8.0
- build: Bump required version of Iris (#296)
- fix: avoid unnecessary allocation in Client.processEvent (#293)
- doc: Remove deprecation of sentryhttp.HandleFunc (#284)
- ref: Update sentryhttp example (#283)
- doc: Improve documentation of sentryhttp package (#282)
- doc: Clarify SampleRate documentation (#279)
- fix: Remove RawStacktrace (#278)
- docs: Add example of custom HTTP transport
- ci: Test against go1.15, drop go1.12 support (#271)
_NOTE:_
This version comes with a few updates. Some examples and documentation have been
improved. We've bumped the supported version of the Iris framework to avoid
LGPL-licensed modules in the module dependency graph.
The `Exception.RawStacktrace` and `Thread.RawStacktrace` fields have been
removed to conform to Sentry's ingestion protocol, only `Exception.Stacktrace`
and `Thread.Stacktrace` should appear in user code.
## v0.7.0
- feat: Include original error when event cannot be encoded as JSON (#258)
- feat: Use Hub from request context when available (#217, #259)
- feat: Extract stack frames from golang.org/x/xerrors (#262)
- feat: Make Environment Integration preserve existing context data (#261)
- feat: Recover and RecoverWithContext with arbitrary types (#268)
- feat: Report bad usage of CaptureMessage and CaptureEvent (#269)
- feat: Send debug logging to stderr by default (#266)
- feat: Several improvements to documentation (#223, #245, #250, #265)
- feat: Example of Recover followed by panic (#241, #247)
- feat: Add Transactions and Spans (to support OpenTelemetry Sentry Exporter) (#235, #243, #254)
- fix: Set either Frame.Filename or Frame.AbsPath (#233)
- fix: Clone requestBody to new Scope (#244)
- fix: Synchronize access and mutation of Hub.lastEventID (#264)
- fix: Avoid repeated syscalls in prepareEvent (#256)
- fix: Do not allocate new RNG for every event (#256)
- fix: Remove stale replace directive in go.mod (#255)
- fix(http): Deprecate HandleFunc, remove duplication (#260)
_NOTE:_
This version comes packed with several fixes and improvements and no breaking
changes.
Notably, there is a change in how the SDK reports file names in stack traces
that should resolve any ambiguity when looking at stack traces and using the
Suspect Commits feature.
We recommend all users to upgrade.
## v0.6.1
- fix: Use NewEvent to init Event struct (#220)
_NOTE:_
A change introduced in v0.6.0 with the intent of avoiding allocations made a
pattern used in official examples break in certain circumstances (attempting
to write to a nil map).
This release reverts the change such that maps in the Event struct are always
allocated.
## v0.6.0
- feat: Read module dependencies from runtime/debug (#199)
- feat: Support chained errors using Unwrap (#206)
- feat: Report chain of errors when available (#185)
- **[breaking]** fix: Accept http.RoundTripper to customize transport (#205)
Before the SDK accepted a concrete value of type `*http.Transport` in
`ClientOptions`, now it accepts any value implementing the `http.RoundTripper`
interface. Note that `*http.Transport` implements `http.RoundTripper`, so most
code bases will continue to work unchanged.
Users of custom transport gain the ability to pass in other implementations of
`http.RoundTripper` and may be able to simplify their code bases.
- fix: Do not panic when scope event processor drops event (#192)
- **[breaking]** fix: Use time.Time for timestamps (#191)
Users of sentry-go typically do not need to manipulate timestamps manually.
For those who do, the field type changed from `int64` to `time.Time`, which
should be more convenient to use. The recommended way to get the current time
is `time.Now().UTC()`.
- fix: Report usage error including stack trace (#189)
- feat: Add Exception.ThreadID field (#183)
- ci: Test against Go 1.14, drop 1.11 (#170)
- feat: Limit reading bytes from request bodies (#168)
- **[breaking]** fix: Rename fasthttp integration package sentryhttp => sentryfasthttp
The current recommendation is to use a named import, in which case existing
code should not require any change:
```go
package main
import (
"fmt"
"github.com/getsentry/sentry-go"
sentryfasthttp "github.com/getsentry/sentry-go/fasthttp"
"github.com/valyala/fasthttp"
)
```
_NOTE:_
This version includes some new features and a few breaking changes, none of
which should pose troubles with upgrading. Most code bases should be able to
upgrade without any changes.
## v0.5.1
- fix: Ignore err.Cause() when it is nil (#160)
## v0.5.0
- fix: Synchronize access to HTTPTransport.disabledUntil (#158)
- docs: Update Flush documentation (#153)
- fix: HTTPTransport.Flush panic and data race (#140)
_NOTE:_
This version changes the implementation of the default transport, modifying the
behavior of `sentry.Flush`. The previous behavior was to wait until there were
no buffered events; new concurrent events kept `Flush` from returning. The new
behavior is to wait until the last event prior to the call to `Flush` has been
sent or the timeout; new concurrent events have no effect. The new behavior is
inline with the [Unified API
Guidelines](https://docs.sentry.io/development/sdk-dev/unified-api/).
We have updated the documentation and examples to clarify that `Flush` is meant
to be called typically only once before program termination, to wait for
in-flight events to be sent to Sentry. Calling `Flush` after every event is not
recommended, as it introduces unnecessary latency to the surrounding function.
Please verify the usage of `sentry.Flush` in your code base.
## v0.4.0
- fix(stacktrace): Correctly report package names (#127)
- fix(stacktrace): Do not rely on AbsPath of files (#123)
- build: Require github.com/ugorji/go@v1.1.7 (#110)
- fix: Correctly store last event id (#99)
- fix: Include request body in event payload (#94)
- build: Reset go.mod version to 1.11 (#109)
- fix: Eliminate data race in modules integration (#105)
- feat: Add support for path prefixes in the DSN (#102)
- feat: Add HTTPClient option (#86)
- feat: Extract correct type and value from top-most error (#85)
- feat: Check for broken pipe errors in Gin integration (#82)
- fix: Client.CaptureMessage accept nil EventModifier (#72)
## v0.3.1
- feat: Send extra information exposed by the Go runtime (#76)
- fix: Handle new lines in module integration (#65)
- fix: Make sure that cache is locked when updating for contextifyFramesIntegration
- ref: Update Iris integration and example to version 12
- misc: Remove indirect dependencies in order to move them to separate go.mod files
## v0.3.0
- feat: Retry event marshaling without contextual data if the first pass fails
- fix: Include `url.Parse` error in `DsnParseError`
- fix: Make more `Scope` methods safe for concurrency
- fix: Synchronize concurrent access to `Hub.client`
- ref: Remove mutex from `Scope` exported API
- ref: Remove mutex from `Hub` exported API
- ref: Compile regexps for `filterFrames` only once
- ref: Change `SampleRate` type to `float64`
- doc: `Scope.Clear` not safe for concurrent use
- ci: Test sentry-go with `go1.13`, drop `go1.10`
_NOTE:_
This version removes some of the internal APIs that landed publicly (namely `Hub/Scope` mutex structs) and may require (but shouldn't) some changes to your code.
It's not done through major version update, as we are still in `0.x` stage.
## v0.2.1
- fix: Run `Contextify` integration on `Threads` as well
## v0.2.0
- feat: Add `SetTransaction()` method on the `Scope`
- feat: `fasthttp` framework support with `sentryfasthttp` package
- fix: Add `RWMutex` locks to internal `Hub` and `Scope` changes
## v0.1.3
- feat: Move frames context reading into `contextifyFramesIntegration` (#28)
_NOTE:_
In case of any performance issues due to source contexts IO, you can let us know and turn off the integration in the meantime with:
```go
sentry.Init(sentry.ClientOptions{
Integrations: func(integrations []sentry.Integration) []sentry.Integration {
var filteredIntegrations []sentry.Integration
for _, integration := range integrations {
if integration.Name() == "ContextifyFrames" {
continue
}
filteredIntegrations = append(filteredIntegrations, integration)
}
return filteredIntegrations
},
})
```
## v0.1.2
- feat: Better source code location resolution and more useful inapp frames (#26)
- feat: Use `noopTransport` when no `Dsn` provided (#27)
- ref: Allow empty `Dsn` instead of returning an error (#22)
- fix: Use `NewScope` instead of literal struct inside a `scope.Clear` call (#24)
- fix: Add to `WaitGroup` before the request is put inside a buffer (#25)
## v0.1.1
- fix: Check for initialized `Client` in `AddBreadcrumbs` (#20)
- build: Bump version when releasing with Craft (#19)
## v0.1.0
- First stable release! \o/
## v0.0.1-beta.5
- feat: **[breaking]** Add `NewHTTPTransport` and `NewHTTPSyncTransport` which accepts all transport options
- feat: New `HTTPSyncTransport` that blocks after each call
- feat: New `Echo` integration
- ref: **[breaking]** Remove `BufferSize` option from `ClientOptions` and move it to `HTTPTransport` instead
- ref: Export default `HTTPTransport`
- ref: Export `net/http` integration handler
- ref: Set `Request` instantly in the package handlers, not in `recoverWithSentry` so it can be accessed later on
- ci: Add craft config
## v0.0.1-beta.4
- feat: `IgnoreErrors` client option and corresponding integration
- ref: Reworked `net/http` integration, wrote better example and complete readme
- ref: Reworked `Gin` integration, wrote better example and complete readme
- ref: Reworked `Iris` integration, wrote better example and complete readme
- ref: Reworked `Negroni` integration, wrote better example and complete readme
- ref: Reworked `Martini` integration, wrote better example and complete readme
- ref: Remove `Handle()` from frameworks handlers and return it directly from New
## v0.0.1-beta.3
- feat: `Iris` framework support with `sentryiris` package
- feat: `Gin` framework support with `sentrygin` package
- feat: `Martini` framework support with `sentrymartini` package
- feat: `Negroni` framework support with `sentrynegroni` package
- feat: Add `Hub.Clone()` for easier frameworks integration
- feat: Return `EventID` from `Recovery` methods
- feat: Add `NewScope` and `NewEvent` functions and use them in the whole codebase
- feat: Add `AddEventProcessor` to the `Client`
- fix: Operate on requests body copy instead of the original
- ref: Try to read source files from the root directory, based on the filename as well, to make it work on AWS Lambda
- ref: Remove `gocertifi` dependence and document how to provide your own certificates
- ref: **[breaking]** Remove `Decorate` and `DecorateFunc` methods in favor of `sentryhttp` package
- ref: **[breaking]** Allow for integrations to live on the client, by passing client instance in `SetupOnce` method
- ref: **[breaking]** Remove `GetIntegration` from the `Hub`
- ref: **[breaking]** Remove `GlobalEventProcessors` getter from the public API
## v0.0.1-beta.2
- feat: Add `AttachStacktrace` client option to include stacktrace for messages
- feat: Add `BufferSize` client option to configure transport buffer size
- feat: Add `SetRequest` method on a `Scope` to control `Request` context data
- feat: Add `FromHTTPRequest` for `Request` type for easier extraction
- ref: Extract `Request` information more accurately
- fix: Attach `ServerName`, `Release`, `Dist`, `Environment` options to the event
- fix: Don't log events dropped due to full transport buffer as sent
- fix: Don't panic and create an appropriate event when called `CaptureException` or `Recover` with `nil` value
## v0.0.1-beta
- Initial release

98
vendor/github.com/getsentry/sentry-go/CONTRIBUTING.md generated vendored Normal file
View file

@ -0,0 +1,98 @@
# Contributing to sentry-go
Hey, thank you if you're reading this, we welcome your contribution!
## Sending a Pull Request
Please help us save time when reviewing your PR by following this simple
process:
1. Is your PR a simple typo fix? Read no further, **click that green "Create
pull request" button**!
2. For more complex PRs that involve behavior changes or new APIs, please
consider [opening an **issue**][new-issue] describing the problem you're
trying to solve if there's not one already.
A PR is often one specific solution to a problem and sometimes talking about
the problem unfolds new possible solutions. Remember we will be responsible
for maintaining the changes later.
3. Fixing a bug and changing a behavior? Please add automated tests to prevent
future regression.
4. Practice writing good commit messages. We have [commit
guidelines][commit-guide].
5. We have [guidelines for PR submitters][pr-guide]. A short summary:
- Good PR descriptions are very helpful and most of the time they include
**why** something is done and why done in this particular way. Also list
other possible solutions that were considered and discarded.
- Be your own first reviewer. Make sure your code compiles and passes the
existing tests.
[new-issue]: https://github.com/getsentry/sentry-go/issues/new/choose
[commit-guide]: https://develop.sentry.dev/code-review/#commit-guidelines
[pr-guide]: https://develop.sentry.dev/code-review/#guidelines-for-submitters
Please also read through our [SDK Development docs](https://develop.sentry.dev/sdk/).
It contains information about SDK features, expected payloads and best practices for
contributing to Sentry SDKs.
## Community
The public-facing channels for support and development of Sentry SDKs can be found on [Discord](https://discord.gg/Ww9hbqr).
## Testing
```console
$ go test
```
### Watch mode
Use: https://github.com/cespare/reflex
```console
$ reflex -g '*.go' -d "none" -- sh -c 'printf "\n"; go test'
```
### With data race detection
```console
$ go test -race
```
### Coverage
```console
$ go test -race -coverprofile=coverage.txt -covermode=atomic && go tool cover -html coverage.txt
```
## Linting
Lint with [`golangci-lint`](https://github.com/golangci/golangci-lint):
```console
$ golangci-lint run
```
## Release
1. Update `CHANGELOG.md` with new version in `vX.X.X` format title and list of changes.
The command below can be used to get a list of changes since the last tag, with the format used in `CHANGELOG.md`:
```console
$ git log --no-merges --format=%s $(git describe --abbrev=0).. | sed 's/^/- /'
```
2. Commit with `misc: vX.X.X changelog` commit message and push to `master`.
3. Let [`craft`](https://github.com/getsentry/craft) do the rest:
```console
$ craft prepare X.X.X
$ craft publish X.X.X
```

21
vendor/github.com/getsentry/sentry-go/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Functional Software, Inc. dba Sentry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
vendor/github.com/getsentry/sentry-go/MIGRATION.md generated vendored Normal file
View file

@ -0,0 +1,3 @@
# `raven-go` to `sentry-go` Migration Guide
A [`raven-go` to `sentry-go` migration guide](https://docs.sentry.io/platforms/go/migration/) is available at the official Sentry documentation site.

83
vendor/github.com/getsentry/sentry-go/Makefile generated vendored Normal file
View file

@ -0,0 +1,83 @@
.DEFAULT_GOAL := help
MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
MKFILE_DIR := $(dir $(MKFILE_PATH))
ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
GO = go
TIMEOUT = 300
# Parse Makefile and display the help
help: ## Show help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: help
build: ## Build everything
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go build' for module: $${dir}"; \
go build ./...; \
done;
.PHONY: build
### Tests (inspired by https://github.com/open-telemetry/opentelemetry-go/blob/main/Makefile)
TEST_TARGETS := test-short test-verbose test-race
test-race: ARGS=-race
test-short: ARGS=-short
test-verbose: ARGS=-v -race
$(TEST_TARGETS): test
test: $(ALL_GO_MOD_DIRS:%=test/%) ## Run tests
test/%: DIR=$*
test/%:
@echo ">>> Running tests for module: $(DIR)"
@# We use '-count=1' to disable test caching.
(cd $(DIR) && $(GO) test -count=1 -timeout $(TIMEOUT)s $(ARGS) ./...)
.PHONY: $(TEST_TARGETS) test
# Coverage
COVERAGE_MODE = atomic
COVERAGE_PROFILE = coverage.out
COVERAGE_REPORT_DIR = .coverage
COVERAGE_REPORT_DIR_ABS = "$(MKFILE_DIR)/$(COVERAGE_REPORT_DIR)"
$(COVERAGE_REPORT_DIR):
mkdir -p $(COVERAGE_REPORT_DIR)
clean-report-dir: $(COVERAGE_REPORT_DIR)
test $(COVERAGE_REPORT_DIR) && rm -f $(COVERAGE_REPORT_DIR)/*
test-coverage: $(COVERAGE_REPORT_DIR) clean-report-dir ## Test with coverage enabled
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
echo ">>> Running tests with coverage for module: $${dir}"; \
DIR_ABS=$$(python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' $${dir}) ; \
REPORT_NAME=$$(basename $${DIR_ABS}); \
(cd "$${dir}" && \
$(GO) test -count=1 -timeout $(TIMEOUT)s -coverpkg=./... -covermode=$(COVERAGE_MODE) -coverprofile="$(COVERAGE_PROFILE)" ./... && \
cp $(COVERAGE_PROFILE) "$(COVERAGE_REPORT_DIR_ABS)/$${REPORT_NAME}_$(COVERAGE_PROFILE)" && \
$(GO) tool cover -html=$(COVERAGE_PROFILE) -o coverage.html); \
done;
.PHONY: test-coverage clean-report-dir
mod-tidy: ## Check go.mod tidiness
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go mod tidy' for module: $${dir}"; \
go mod tidy -go=1.18 -compat=1.18; \
done; \
git diff --exit-code;
.PHONY: mod-tidy
vet: ## Run "go vet"
set -e ; \
for dir in $(ALL_GO_MOD_DIRS); do \
cd "$${dir}"; \
echo ">>> Running 'go vet' for module: $${dir}"; \
go vet ./...; \
done;
.PHONY: vet
lint: ## Lint (using "golangci-lint")
golangci-lint run
.PHONY: lint
fmt: ## Format all Go files
gofmt -l -w -s .
.PHONY: fmt

105
vendor/github.com/getsentry/sentry-go/README.md generated vendored Normal file
View file

@ -0,0 +1,105 @@
<p align="center">
<a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
<picture>
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-white.png" media="(prefers-color-scheme: dark)" />
<source srcset="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" />
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" alt="Sentry" width="280">
</picture>
</a>
</p>
# Official Sentry SDK for Go
[![Build Status](https://github.com/getsentry/sentry-go/workflows/go-workflow/badge.svg)](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow)
[![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go)
[![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
[![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
[![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
`sentry-go` provides a Sentry client implementation for the Go programming
language. This is the next generation of the Go SDK for [Sentry](https://sentry.io/),
intended to replace the `raven-go` package.
> Looking for the old `raven-go` SDK documentation? See the Legacy client section [here](https://docs.sentry.io/clients/go/).
> If you want to start using `sentry-go` instead, check out the [migration guide](https://docs.sentry.io/platforms/go/migration/).
## Requirements
The only requirement is a Go compiler.
We verify this package against the 3 most recent releases of Go. Those are the
supported versions. The exact versions are defined in
[`GitHub workflow`](.github/workflows/test.yml).
In addition, we run tests against the current master branch of the Go toolchain,
though support for this configuration is best-effort.
## Installation
`sentry-go` can be installed like any other Go library through `go get`:
```console
$ go get github.com/getsentry/sentry-go@latest
```
Check out the [list of released versions](https://github.com/getsentry/sentry-go/releases).
## Configuration
To use `sentry-go`, youll need to import the `sentry-go` package and initialize
it with your DSN and other [options](https://pkg.go.dev/github.com/getsentry/sentry-go#ClientOptions).
If not specified in the SDK initialization, the
[DSN](https://docs.sentry.io/product/sentry-basics/dsn-explainer/),
[Release](https://docs.sentry.io/product/releases/) and
[Environment](https://docs.sentry.io/product/sentry-basics/environments/)
are read from the environment variables `SENTRY_DSN`, `SENTRY_RELEASE` and
`SENTRY_ENVIRONMENT`, respectively.
More on this in the [Configuration section of the official Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/configuration/).
## Usage
The SDK supports reporting errors and tracking application performance.
To get started, have a look at one of our [examples](_examples/):
- [Basic error instrumentation](_examples/basic/main.go)
- [Error and tracing for HTTP servers](_examples/http/main.go)
We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go).
For more detailed information about how to get the most out of `sentry-go`,
checkout the official documentation:
- [Sentry Go SDK documentation](https://docs.sentry.io/platforms/go/)
- Guides:
- [net/http](https://docs.sentry.io/platforms/go/guides/http/)
- [echo](https://docs.sentry.io/platforms/go/guides/echo/)
- [fasthttp](https://docs.sentry.io/platforms/go/guides/fasthttp/)
- [gin](https://docs.sentry.io/platforms/go/guides/gin/)
- [iris](https://docs.sentry.io/platforms/go/guides/iris/)
- [martini](https://docs.sentry.io/platforms/go/guides/martini/)
- [negroni](https://docs.sentry.io/platforms/go/guides/negroni/)
## Resources
- [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
- [GitHub Project](https://github.com/getsentry/sentry-go)
- [![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
- [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
- [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/)
- [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions)
- [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
- [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry)
- [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry)
## License
Licensed under
[The MIT License](https://opensource.org/licenses/mit/), see
[`LICENSE`](LICENSE).
## Community
Join Sentry's [`#go` channel on Discord](https://discord.gg/Ww9hbqr) to get
involved and help us improve the SDK!

117
vendor/github.com/getsentry/sentry-go/check_in.go generated vendored Normal file
View file

@ -0,0 +1,117 @@
package sentry
import "time"
type CheckInStatus string
const (
CheckInStatusInProgress CheckInStatus = "in_progress"
CheckInStatusOK CheckInStatus = "ok"
CheckInStatusError CheckInStatus = "error"
)
type checkInScheduleType string
const (
checkInScheduleTypeCrontab checkInScheduleType = "crontab"
checkInScheduleTypeInterval checkInScheduleType = "interval"
)
type MonitorSchedule interface {
// scheduleType is a private method that must be implemented for monitor schedule
// implementation. It should never be called. This method is made for having
// specific private implementation of MonitorSchedule interface.
scheduleType() checkInScheduleType
}
type crontabSchedule struct {
Type string `json:"type"`
Value string `json:"value"`
}
func (c crontabSchedule) scheduleType() checkInScheduleType {
return checkInScheduleTypeCrontab
}
// CrontabSchedule defines the MonitorSchedule with a cron format.
// Example: "8 * * * *".
func CrontabSchedule(scheduleString string) MonitorSchedule {
return crontabSchedule{
Type: string(checkInScheduleTypeCrontab),
Value: scheduleString,
}
}
type intervalSchedule struct {
Type string `json:"type"`
Value int64 `json:"value"`
Unit string `json:"unit"`
}
func (i intervalSchedule) scheduleType() checkInScheduleType {
return checkInScheduleTypeInterval
}
type MonitorScheduleUnit string
const (
MonitorScheduleUnitMinute MonitorScheduleUnit = "minute"
MonitorScheduleUnitHour MonitorScheduleUnit = "hour"
MonitorScheduleUnitDay MonitorScheduleUnit = "day"
MonitorScheduleUnitWeek MonitorScheduleUnit = "week"
MonitorScheduleUnitMonth MonitorScheduleUnit = "month"
MonitorScheduleUnitYear MonitorScheduleUnit = "year"
)
// IntervalSchedule defines the MonitorSchedule with an interval format.
//
// Example:
//
// IntervalSchedule(1, sentry.MonitorScheduleUnitDay)
func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule {
return intervalSchedule{
Type: string(checkInScheduleTypeInterval),
Value: value,
Unit: string(unit),
}
}
type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout
Schedule MonitorSchedule `json:"schedule,omitempty"`
// The allowed margin of minutes after the expected check-in time that
// the monitor will not be considered missed for.
CheckInMargin int64 `json:"checkin_margin,omitempty"`
// The allowed duration in minutes that the monitor may be `in_progress`
// for before being considered failed due to timeout.
MaxRuntime int64 `json:"max_runtime,omitempty"`
// A tz database string representing the timezone which the monitor's execution schedule is in.
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Timezone string `json:"timezone,omitempty"`
}
type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout
// Check-In ID (unique and client generated)
ID EventID `json:"check_in_id"`
// The distinct slug of the monitor.
MonitorSlug string `json:"monitor_slug"`
// The status of the check-in.
Status CheckInStatus `json:"status"`
// The duration of the check-in. Will only take effect if the status is ok or error.
Duration time.Duration `json:"duration,omitempty"`
}
// serializedCheckIn is used by checkInMarshalJSON method on Event struct.
// See https://develop.sentry.dev/sdk/check-ins/
type serializedCheckIn struct { //nolint: maligned
// Check-In ID (unique and client generated).
CheckInID string `json:"check_in_id"`
// The distinct slug of the monitor.
MonitorSlug string `json:"monitor_slug"`
// The status of the check-in.
Status CheckInStatus `json:"status"`
// The duration of the check-in in seconds. Will only take effect if the status is ok or error.
Duration float64 `json:"duration,omitempty"`
Release string `json:"release,omitempty"`
Environment string `json:"environment,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
}

740
vendor/github.com/getsentry/sentry-go/client.go generated vendored Normal file
View file

@ -0,0 +1,740 @@
package sentry
import (
"context"
"crypto/x509"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/getsentry/sentry-go/internal/debug"
)
// The identifier of the SDK.
const sdkIdentifier = "sentry.go"
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
const maxErrorDepth = 10
// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is
// meant to bound memory usage and prevent too large transaction events that
// would be rejected by Sentry.
const defaultMaxSpans = 1000
// hostname is the host name reported by the kernel. It is precomputed once to
// avoid syscalls when capturing events.
//
// The error is ignored because retrieving the host name is best-effort. If the
// error is non-nil, there is nothing to do other than retrying. We choose not
// to retry for now.
var hostname, _ = os.Hostname()
// lockedRand is a random number generator safe for concurrent use. Its API is
// intentionally limited and it is not meant as a full replacement for a
// rand.Rand.
type lockedRand struct {
mu sync.Mutex
r *rand.Rand
}
// Float64 returns a pseudo-random number in [0.0,1.0).
func (r *lockedRand) Float64() float64 {
r.mu.Lock()
defer r.mu.Unlock()
return r.r.Float64()
}
// rng is the internal random number generator.
//
// We do not use the global functions from math/rand because, while they are
// safe for concurrent use, any package in a build could change the seed and
// affect the generated numbers, for instance making them deterministic. On the
// other hand, the source returned from rand.NewSource is not safe for
// concurrent use, so we need to couple its use with a sync.Mutex.
var rng = &lockedRand{
// #nosec G404 -- We are fine using transparent, non-secure value here.
r: rand.New(rand.NewSource(time.Now().UnixNano())),
}
// usageError is used to report to Sentry an SDK usage error.
//
// It is not exported because it is never returned by any function or method in
// the exported API.
type usageError struct {
error
}
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
// can be enabled by either using Logger.SetOutput directly or with Debug client option.
var Logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
// EventProcessor is a function that processes an event.
// Event processors are used to change an event before it is sent to Sentry.
type EventProcessor func(event *Event, hint *EventHint) *Event
// EventModifier is the interface that wraps the ApplyToEvent method.
//
// ApplyToEvent changes an event based on external data and/or
// an event hint.
type EventModifier interface {
ApplyToEvent(event *Event, hint *EventHint) *Event
}
var globalEventProcessors []EventProcessor
// AddGlobalEventProcessor adds processor to the global list of event
// processors. Global event processors apply to all events.
//
// AddGlobalEventProcessor is deprecated. Most users will prefer to initialize
// the SDK with Init and provide a ClientOptions.BeforeSend function or use
// Scope.AddEventProcessor instead.
func AddGlobalEventProcessor(processor EventProcessor) {
globalEventProcessors = append(globalEventProcessors, processor)
}
// Integration allows for registering a functions that modify or discard captured events.
type Integration interface {
Name() string
SetupOnce(client *Client)
}
// ClientOptions that configures a SDK Client.
type ClientOptions struct {
// The DSN to use. If the DSN is not set, the client is effectively
// disabled.
Dsn string
// In debug mode, the debug information is printed to stdout to help you
// understand what sentry is doing.
Debug bool
// Configures whether SDK should generate and attach stacktraces to pure
// capture message calls.
AttachStacktrace bool
// The sample rate for event submission in the range [0.0, 1.0]. By default,
// all events are sent. Thus, as a historical special case, the sample rate
// 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the
// empty string.
SampleRate float64
// Enable performance tracing.
EnableTracing bool
// The sample rate for sampling traces in the range [0.0, 1.0].
TracesSampleRate float64
// Used to customize the sampling of traces, overrides TracesSampleRate.
TracesSampler TracesSampler
// The sample rate for profiling traces in the range [0.0, 1.0].
// This is relative to TracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
ProfilesSampleRate float64
// List of regexp strings that will be used to match against event's message
// and if applicable, caught errors type and value.
// If the match is found, then a whole event will be dropped.
IgnoreErrors []string
// List of regexp strings that will be used to match against a transaction's
// name. If a match is found, then the transaction will be dropped.
IgnoreTransactions []string
// If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
// By default, no such data is sent.
SendDefaultPII bool
// BeforeSend is called before error events are sent to Sentry.
// Use it to mutate the event or return nil to discard the event.
BeforeSend func(event *Event, hint *EventHint) *Event
// BeforeSendTransaction is called before transaction events are sent to Sentry.
// Use it to mutate the transaction or return nil to discard the transaction.
BeforeSendTransaction func(event *Event, hint *EventHint) *Event
// Before breadcrumb add callback.
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
// Integrations to be installed on the current Client, receives default
// integrations.
Integrations func([]Integration) []Integration
// io.Writer implementation that should be used with the Debug mode.
DebugWriter io.Writer
// The transport to use. Defaults to HTTPTransport.
Transport Transport
// The server name to be reported.
ServerName string
// The release to be sent with events.
//
// Some Sentry features are built around releases, and, thus, reporting
// events with a non-empty release improves the product experience. See
// https://docs.sentry.io/product/releases/.
//
// If Release is not set, the SDK will try to derive a default value
// from environment variables or the Git repository in the working
// directory.
//
// If you distribute a compiled binary, it is recommended to set the
// Release value explicitly at build time. As an example, you can use:
//
// go build -ldflags='-X main.release=VALUE'
//
// That will set the value of a predeclared variable 'release' in the
// 'main' package to 'VALUE'. Then, use that variable when initializing
// the SDK:
//
// sentry.Init(ClientOptions{Release: release})
//
// See https://golang.org/cmd/go/ and https://golang.org/cmd/link/ for
// the official documentation of -ldflags and -X, respectively.
Release string
// The dist to be sent with events.
Dist string
// The environment to be sent with events.
Environment string
// Maximum number of breadcrumbs
// when MaxBreadcrumbs is negative then ignore breadcrumbs.
MaxBreadcrumbs int
// Maximum number of spans.
//
// See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits
// applied during event ingestion. Events that exceed these limits might get dropped.
MaxSpans int
// An optional pointer to http.Client that will be used with a default
// HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy,
// HTTPSProxy and CaCerts options ignored.
HTTPClient *http.Client
// An optional pointer to http.Transport that will be used with a default
// HTTPTransport. Using your own transport will make HTTPProxy, HTTPSProxy
// and CaCerts options ignored.
HTTPTransport http.RoundTripper
// An optional HTTP proxy to use.
// This will default to the HTTP_PROXY environment variable.
HTTPProxy string
// An optional HTTPS proxy to use.
// This will default to the HTTPS_PROXY environment variable.
// HTTPS_PROXY takes precedence over HTTP_PROXY for https requests.
HTTPSProxy string
// An optional set of SSL certificates to use.
CaCerts *x509.CertPool
// MaxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
MaxErrorDepth int
// Default event tags. These are overridden by tags set on a scope.
Tags map[string]string
}
// Client is the underlying processor that is used by the main API and Hub
// instances. It must be created with NewClient.
type Client struct {
mu sync.RWMutex
options ClientOptions
dsn *Dsn
eventProcessors []EventProcessor
integrations []Integration
sdkIdentifier string
sdkVersion string
// Transport is read-only. Replacing the transport of an existing client is
// not supported, create a new client instead.
Transport Transport
}
// NewClient creates and returns an instance of Client configured using
// ClientOptions.
//
// Most users will not create clients directly. Instead, initialize the SDK with
// Init and use the package-level functions (for simple programs that run on a
// single goroutine) or hub methods (for concurrent programs, for example web
// servers).
func NewClient(options ClientOptions) (*Client, error) {
// The default error event sample rate for all SDKs is 1.0 (send all).
//
// In Go, the zero value (default) for float64 is 0.0, which means that
// constructing a client with NewClient(ClientOptions{}), or, equivalently,
// initializing the SDK with Init(ClientOptions{}) without an explicit
// SampleRate would drop all events.
//
// To retain the desired default behavior, we exceptionally flip SampleRate
// from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful
// anyway, and the same end result can be achieved in many other ways like
// not initializing the SDK, setting the DSN to the empty string or using an
// event processor that always returns nil.
//
// An alternative API could be such that default options don't need to be
// the same as Go's zero values, for example using the Functional Options
// pattern. That would either require a breaking change if we want to reuse
// the obvious NewClient name, or a new function as an alternative
// constructor.
if options.SampleRate == 0.0 {
options.SampleRate = 1.0
}
if options.Debug {
debugWriter := options.DebugWriter
if debugWriter == nil {
debugWriter = os.Stderr
}
Logger.SetOutput(debugWriter)
}
if options.Dsn == "" {
options.Dsn = os.Getenv("SENTRY_DSN")
}
if options.Release == "" {
options.Release = defaultRelease()
}
if options.Environment == "" {
options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
}
if options.MaxErrorDepth == 0 {
options.MaxErrorDepth = maxErrorDepth
}
if options.MaxSpans == 0 {
options.MaxSpans = defaultMaxSpans
}
// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
// to GODEBUG). It is not a supported feature: recognized debug options
// may change any time.
//
// The intended public is SDK developers. It is orthogonal to
// options.Debug, which is also available for SDK users.
dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",")
sort.Strings(dbg)
// dbgOpt returns true when the given debug option is enabled, for
// example SENTRYGODEBUG=someopt=1.
dbgOpt := func(opt string) bool {
s := opt + "=1"
return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s
}
if dbgOpt("httpdump") || dbgOpt("httptrace") {
options.HTTPTransport = &debug.Transport{
RoundTripper: http.DefaultTransport,
Output: os.Stderr,
Dump: dbgOpt("httpdump"),
Trace: dbgOpt("httptrace"),
}
}
var dsn *Dsn
if options.Dsn != "" {
var err error
dsn, err = NewDsn(options.Dsn)
if err != nil {
return nil, err
}
}
client := Client{
options: options,
dsn: dsn,
sdkIdentifier: sdkIdentifier,
sdkVersion: SDKVersion,
}
client.setupTransport()
client.setupIntegrations()
return &client, nil
}
func (client *Client) setupTransport() {
opts := client.options
transport := opts.Transport
if transport == nil {
if opts.Dsn == "" {
transport = new(noopTransport)
} else {
httpTransport := NewHTTPTransport()
// When tracing is enabled, use larger buffer to
// accommodate more concurrent events.
// TODO(tracing): consider using separate buffers per
// event type.
if opts.EnableTracing {
httpTransport.BufferSize = 1000
}
transport = httpTransport
}
}
transport.Configure(opts)
client.Transport = transport
}
func (client *Client) setupIntegrations() {
integrations := []Integration{
new(contextifyFramesIntegration),
new(environmentIntegration),
new(modulesIntegration),
new(ignoreErrorsIntegration),
new(ignoreTransactionsIntegration),
new(globalTagsIntegration),
}
if client.options.Integrations != nil {
integrations = client.options.Integrations(integrations)
}
for _, integration := range integrations {
if client.integrationAlreadyInstalled(integration.Name()) {
Logger.Printf("Integration %s is already installed\n", integration.Name())
continue
}
client.integrations = append(client.integrations, integration)
integration.SetupOnce(client)
Logger.Printf("Integration installed: %s\n", integration.Name())
}
sort.Slice(client.integrations, func(i, j int) bool {
return client.integrations[i].Name() < client.integrations[j].Name()
})
}
// AddEventProcessor adds an event processor to the client. It must not be
// called from concurrent goroutines. Most users will prefer to use
// ClientOptions.BeforeSend or Scope.AddEventProcessor instead.
//
// Note that typical programs have only a single client created by Init and the
// client is shared among multiple hubs, one per goroutine, such that adding an
// event processor to the client affects all hubs that share the client.
func (client *Client) AddEventProcessor(processor EventProcessor) {
client.eventProcessors = append(client.eventProcessors, processor)
}
// Options return ClientOptions for the current Client.
func (client *Client) Options() ClientOptions {
// Note: internally, consider using `client.options` instead of `client.Options()` to avoid copying the object each time.
return client.options
}
// CaptureMessage captures an arbitrary message.
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
event := client.EventFromMessage(message, LevelInfo)
return client.CaptureEvent(event, hint, scope)
}
// CaptureException captures an error.
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
event := client.EventFromException(exception, LevelError)
return client.CaptureEvent(event, hint, scope)
}
// CaptureCheckIn captures a check in.
func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID {
event := client.EventFromCheckIn(checkIn, monitorConfig)
if event != nil && event.CheckIn != nil {
client.CaptureEvent(event, nil, scope)
return &event.CheckIn.ID
}
return nil
}
// CaptureEvent captures an event on the currently active client if any.
//
// The event must already be assembled. Typically code would instead use
// the utility methods like CaptureException. The return value is the
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
return client.processEvent(event, hint, scope)
}
// Recover captures a panic.
// Returns EventID if successfully, or nil if there's no error to recover from.
func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID {
if err == nil {
err = recover()
}
// Normally we would not pass a nil Context, but RecoverWithContext doesn't
// use the Context for communicating deadline nor cancelation. All it does
// is store the Context in the EventHint and there nil means the Context is
// not available.
// nolint: staticcheck
return client.RecoverWithContext(nil, err, hint, scope)
}
// RecoverWithContext captures a panic and passes relevant context object.
// Returns EventID if successfully, or nil if there's no error to recover from.
func (client *Client) RecoverWithContext(
ctx context.Context,
err interface{},
hint *EventHint,
scope EventModifier,
) *EventID {
if err == nil {
err = recover()
}
if err == nil {
return nil
}
if ctx != nil {
if hint == nil {
hint = &EventHint{}
}
if hint.Context == nil {
hint.Context = ctx
}
}
var event *Event
switch err := err.(type) {
case error:
event = client.EventFromException(err, LevelFatal)
case string:
event = client.EventFromMessage(err, LevelFatal)
default:
event = client.EventFromMessage(fmt.Sprintf("%#v", err), LevelFatal)
}
return client.CaptureEvent(event, hint, scope)
}
// Flush waits until the underlying Transport sends any buffered events to the
// Sentry server, blocking for at most the given timeout. It returns false if
// the timeout was reached. In that case, some events may not have been sent.
//
// Flush should be called before terminating the program to avoid
// unintentionally dropping events.
//
// Do not call Flush indiscriminately after every call to CaptureEvent,
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func (client *Client) Flush(timeout time.Duration) bool {
return client.Transport.Flush(timeout)
}
// EventFromMessage creates an event from the given message string.
func (client *Client) EventFromMessage(message string, level Level) *Event {
if message == "" {
err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())}
return client.EventFromException(err, level)
}
event := NewEvent()
event.Level = level
event.Message = message
if client.options.AttachStacktrace {
event.Threads = []Thread{{
Stacktrace: NewStacktrace(),
Crashed: false,
Current: true,
}}
}
return event
}
// EventFromException creates a new Sentry event from the given `error` instance.
func (client *Client) EventFromException(exception error, level Level) *Event {
event := NewEvent()
event.Level = level
err := exception
if err == nil {
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
}
event.SetException(err, client.options.MaxErrorDepth)
return event
}
// EventFromCheckIn creates a new Sentry event from the given `check_in` instance.
func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event {
if checkIn == nil {
return nil
}
event := NewEvent()
event.Type = checkInType
var checkInID EventID
if checkIn.ID == "" {
checkInID = EventID(uuid())
} else {
checkInID = checkIn.ID
}
event.CheckIn = &CheckIn{
ID: checkInID,
MonitorSlug: checkIn.MonitorSlug,
Status: checkIn.Status,
Duration: checkIn.Duration,
}
event.MonitorConfig = monitorConfig
return event
}
func (client *Client) SetSDKIdentifier(identifier string) {
client.mu.Lock()
defer client.mu.Unlock()
client.sdkIdentifier = identifier
}
func (client *Client) GetSDKIdentifier() string {
client.mu.RLock()
defer client.mu.RUnlock()
return client.sdkIdentifier
}
// reverse reverses the slice a in place.
func reverse(a []Exception) {
for i := len(a)/2 - 1; i >= 0; i-- {
opp := len(a) - 1 - i
a[i], a[opp] = a[opp], a[i]
}
}
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
if event == nil {
err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())}
return client.CaptureException(err, hint, scope)
}
// Transactions are sampled by options.TracesSampleRate or
// options.TracesSampler when they are started. Other events
// (errors, messages) are sampled here. Does not apply to check-ins.
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
Logger.Println("Event dropped due to SampleRate hit.")
return nil
}
if event = client.prepareEvent(event, hint, scope); event == nil {
return nil
}
// Apply beforeSend* processors
if hint == nil {
hint = &EventHint{}
}
if event.Type == transactionType && client.options.BeforeSendTransaction != nil {
// Transaction events
if event = client.options.BeforeSendTransaction(event, hint); event == nil {
Logger.Println("Transaction dropped due to BeforeSendTransaction callback.")
return nil
}
} else if event.Type != transactionType && event.Type != checkInType && client.options.BeforeSend != nil {
// All other events
if event = client.options.BeforeSend(event, hint); event == nil {
Logger.Println("Event dropped due to BeforeSend callback.")
return nil
}
}
client.Transport.SendEvent(event)
return &event.EventID
}
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
if event.EventID == "" {
// TODO set EventID when the event is created, same as in other SDKs. It's necessary for profileTransaction.ID.
event.EventID = EventID(uuid())
}
if event.Timestamp.IsZero() {
event.Timestamp = time.Now()
}
if event.Level == "" {
event.Level = LevelInfo
}
if event.ServerName == "" {
event.ServerName = client.options.ServerName
if event.ServerName == "" {
event.ServerName = hostname
}
}
if event.Release == "" {
event.Release = client.options.Release
}
if event.Dist == "" {
event.Dist = client.options.Dist
}
if event.Environment == "" {
event.Environment = client.options.Environment
}
event.Platform = "go"
event.Sdk = SdkInfo{
Name: client.GetSDKIdentifier(),
Version: SDKVersion,
Integrations: client.listIntegrations(),
Packages: []SdkPackage{{
Name: "sentry-go",
Version: SDKVersion,
}},
}
if scope != nil {
event = scope.ApplyToEvent(event, hint)
if event == nil {
return nil
}
}
for _, processor := range client.eventProcessors {
id := event.EventID
event = processor(event, hint)
if event == nil {
Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
return nil
}
}
for _, processor := range globalEventProcessors {
id := event.EventID
event = processor(event, hint)
if event == nil {
Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
return nil
}
}
if event.sdkMetaData.transactionProfile != nil {
event.sdkMetaData.transactionProfile.UpdateFromEvent(event)
}
return event
}
func (client *Client) listIntegrations() []string {
integrations := make([]string, len(client.integrations))
for i, integration := range client.integrations {
integrations[i] = integration.Name()
}
return integrations
}
func (client *Client) integrationAlreadyInstalled(name string) bool {
for _, integration := range client.integrations {
if integration.Name() == name {
return true
}
}
return false
}
// sample returns true with the given probability, which must be in the range
// [0.0, 1.0].
func sample(probability float64) bool {
return rng.Float64() < probability
}

6
vendor/github.com/getsentry/sentry-go/doc.go generated vendored Normal file
View file

@ -0,0 +1,6 @@
/*
Package repository: https://github.com/getsentry/sentry-go/
For more information about Sentry and SDK features, please have a look at the official documentation site: https://docs.sentry.io/platforms/go/
*/
package sentry

234
vendor/github.com/getsentry/sentry-go/dsn.go generated vendored Normal file
View file

@ -0,0 +1,234 @@
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
}
}
// DsnParseError represents an error that occurs if a Sentry
// DSN cannot be parsed.
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 string
}
// NewDsn creates a Dsn by parsing rawURL. Most users will never call this
// function directly. It is provided for use in custom Transport
// implementations.
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 parsedURL.Path == "" || parsedURL.Path == "/" {
return nil, &DsnParseError{"empty project id"}
}
pathSegments := strings.Split(parsedURL.Path[1:], "/")
projectID := pathSegments[len(pathSegments)-1]
if projectID == "" {
return nil, &DsnParseError{"empty 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("/%s", dsn.projectID)
return url
}
// Get the scheme of the DSN.
func (dsn Dsn) GetScheme() string {
return string(dsn.scheme)
}
// Get the public key of the DSN.
func (dsn Dsn) GetPublicKey() string {
return dsn.publicKey
}
// Get the secret key of the DSN.
func (dsn Dsn) GetSecretKey() string {
return dsn.secretKey
}
// Get the host of the DSN.
func (dsn Dsn) GetHost() string {
return dsn.host
}
// Get the port of the DSN.
func (dsn Dsn) GetPort() int {
return dsn.port
}
// Get the path of the DSN.
func (dsn Dsn) GetPath() string {
return dsn.path
}
// Get the project ID of the DSN.
func (dsn Dsn) GetProjectID() string {
return dsn.projectID
}
// GetAPIURL returns the URL of the envelope endpoint of the project
// associated with the DSN.
func (dsn Dsn) GetAPIURL() *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/%s/%s/", dsn.projectID, "envelope")
parsedURL, _ := url.Parse(rawURL)
return parsedURL
}
// RequestHeaders returns all the necessary headers that have to be used in the transport when seinding events
// to the /store endpoint.
//
// Deprecated: This method shall only be used if you want to implement your own transport that sends events to
// the /store endpoint. If you're using the transport provided by the SDK, all necessary headers to authenticate
// against the /envelope endpoint are added automatically.
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(), SDKVersion, 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,
}
}
// MarshalJSON converts the Dsn struct to JSON.
func (dsn Dsn) MarshalJSON() ([]byte, error) {
return json.Marshal(dsn.String())
}
// UnmarshalJSON converts JSON data to the Dsn struct.
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
}

View file

@ -0,0 +1,123 @@
package sentry
import (
"strconv"
"strings"
"github.com/getsentry/sentry-go/internal/otel/baggage"
)
const (
sentryPrefix = "sentry-"
)
// DynamicSamplingContext holds information about the current event that can be used to make dynamic sampling decisions.
type DynamicSamplingContext struct {
Entries map[string]string
Frozen bool
}
func DynamicSamplingContextFromHeader(header []byte) (DynamicSamplingContext, error) {
bag, err := baggage.Parse(string(header))
if err != nil {
return DynamicSamplingContext{}, err
}
entries := map[string]string{}
for _, member := range bag.Members() {
// We only store baggage members if their key starts with "sentry-".
if k, v := member.Key(), member.Value(); strings.HasPrefix(k, sentryPrefix) {
entries[strings.TrimPrefix(k, sentryPrefix)] = v
}
}
return DynamicSamplingContext{
Entries: entries,
// If there's at least one Sentry value, we consider the DSC frozen
Frozen: len(entries) > 0,
}, nil
}
func DynamicSamplingContextFromTransaction(span *Span) DynamicSamplingContext {
entries := map[string]string{}
hub := hubFromContext(span.Context())
scope := hub.Scope()
client := hub.Client()
if client == nil || scope == nil {
return DynamicSamplingContext{
Entries: map[string]string{},
Frozen: false,
}
}
if traceID := span.TraceID.String(); traceID != "" {
entries["trace_id"] = traceID
}
if sampleRate := span.sampleRate; sampleRate != 0 {
entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
}
if dsn := client.dsn; dsn != nil {
if publicKey := dsn.publicKey; publicKey != "" {
entries["public_key"] = publicKey
}
}
if release := client.options.Release; release != "" {
entries["release"] = release
}
if environment := client.options.Environment; environment != "" {
entries["environment"] = environment
}
// Only include the transaction name if it's of good quality (not empty and not SourceURL)
if span.Source != "" && span.Source != SourceURL {
if span.IsTransaction() {
entries["transaction"] = span.Name
}
}
if userSegment := scope.user.Segment; userSegment != "" {
entries["user_segment"] = userSegment
}
if span.Sampled.Bool() {
entries["sampled"] = "true"
} else {
entries["sampled"] = "false"
}
return DynamicSamplingContext{
Entries: entries,
Frozen: true,
}
}
func (d DynamicSamplingContext) HasEntries() bool {
return len(d.Entries) > 0
}
func (d DynamicSamplingContext) IsFrozen() bool {
return d.Frozen
}
func (d DynamicSamplingContext) String() string {
members := []baggage.Member{}
for k, entry := range d.Entries {
member, err := baggage.NewMember(sentryPrefix+k, entry)
if err != nil {
continue
}
members = append(members, member)
}
if len(members) > 0 {
baggage, err := baggage.New(members...)
if err != nil {
return ""
}
return baggage.String()
}
return ""
}

135
vendor/github.com/getsentry/sentry-go/echo/README.md generated vendored Normal file
View file

@ -0,0 +1,135 @@
<p align="center">
<a href="https://sentry.io" target="_blank" align="center">
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" width="280">
</a>
<br />
</p>
# Official Sentry Echo Handler for Sentry-go SDK
**Godoc:** https://godoc.org/github.com/getsentry/sentry-go/echo
**Example:** https://github.com/getsentry/sentry-go/tree/master/_examples/echo
## Installation
```sh
go get github.com/getsentry/sentry-go/echo
```
```go
import (
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// To initialize Sentry's handler, you need to initialize Sentry itself beforehand
if err := sentry.Init(sentry.ClientOptions{
Dsn: "your-public-dsn",
}); err != nil {
fmt.Printf("Sentry initialization failed: %v\n", err)
}
// Then create your app
app := echo.New()
app.Use(middleware.Logger())
app.Use(middleware.Recover())
// Once it's done, you can attach the handler as one of your middleware
app.Use(sentryecho.New(sentryecho.Options{}))
// Set up routes
app.GET("/", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "Hello, World!")
})
// And run it
app.Logger.Fatal(app.Start(":3000"))
```
## Configuration
`sentryecho` accepts a struct of `Options` that allows you to configure how the handler will behave.
Currently it respects 3 options:
```go
// Repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true,
// as echo includes its own Recover middleware that handles http responses.
Repanic bool
// WaitForDelivery configures whether you want to block the request before moving forward with the response.
// Because Echo's `Recover` handler doesn't restart the application,
// it's safe to either skip this option or set it to `false`.
WaitForDelivery bool
// Timeout for the event delivery requests.
Timeout time.Duration
```
## Usage
`sentryecho` attaches an instance of `*sentry.Hub` (https://godoc.org/github.com/getsentry/sentry-go#Hub) to the `echo.Context`, which makes it available throughout the rest of the request's lifetime.
You can access it by using the `sentryecho.GetHubFromContext()` method on the context itself in any of your proceeding middleware and routes.
And it should be used instead of the global `sentry.CaptureMessage`, `sentry.CaptureException`, or any other calls, as it keeps the separation of data between the requests.
**Keep in mind that `*sentry.Hub` won't be available in middleware attached before to `sentryecho`!**
```go
app := echo.New()
app.Use(middleware.Logger())
app.Use(middleware.Recover())
app.Use(sentryecho.New(sentryecho.Options{
Repanic: true,
}))
app.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
if hub := sentryecho.GetHubFromContext(ctx); hub != nil {
hub.Scope().SetTag("someRandomTag", "maybeYouNeedIt")
}
return next(ctx)
}
})
app.GET("/", func(ctx echo.Context) error {
if hub := sentryecho.GetHubFromContext(ctx); hub != nil {
hub.WithScope(func(scope *sentry.Scope) {
scope.SetExtra("unwantedQuery", "someQueryDataMaybe")
hub.CaptureMessage("User provided unwanted query string, but we recovered just fine")
})
}
return ctx.String(http.StatusOK, "Hello, World!")
})
app.GET("/foo", func(ctx echo.Context) error {
// sentryecho handler will catch it just fine. Also, because we attached "someRandomTag"
// in the middleware before, it will be sent through as well
panic("y tho")
})
app.Logger.Fatal(app.Start(":3000"))
```
### Accessing Request in `BeforeSend` callback
```go
sentry.Init(sentry.ClientOptions{
Dsn: "your-public-dsn",
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
if hint.Context != nil {
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok {
// You have access to the original Request here
}
}
return event
},
})
```

View file

@ -0,0 +1,88 @@
package sentryecho
import (
"context"
"net/http"
"time"
"github.com/getsentry/sentry-go"
"github.com/labstack/echo/v4"
)
// The identifier of the Echo SDK.
const sdkIdentifier = "sentry.go.echo"
const valuesKey = "sentry"
type handler struct {
repanic bool
waitForDelivery bool
timeout time.Duration
}
type Options struct {
// Repanic configures whether Sentry should repanic after recovery, in most cases it should be set to true,
// as echo includes it's own Recover middleware what handles http responses.
Repanic bool
// WaitForDelivery configures whether you want to block the request before moving forward with the response.
// Because Echo's Recover handler doesn't restart the application,
// it's safe to either skip this option or set it to false.
WaitForDelivery bool
// Timeout for the event delivery requests.
Timeout time.Duration
}
// New returns a function that satisfies echo.HandlerFunc interface
// It can be used with Use() methods.
func New(options Options) echo.MiddlewareFunc {
timeout := options.Timeout
if timeout == 0 {
timeout = 2 * time.Second
}
return (&handler{
repanic: options.Repanic,
timeout: timeout,
waitForDelivery: options.WaitForDelivery,
}).handle
}
func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
hub := sentry.GetHubFromContext(ctx.Request().Context())
if hub == nil {
hub = sentry.CurrentHub().Clone()
}
if client := hub.Client(); client != nil {
client.SetSDKIdentifier(sdkIdentifier)
}
hub.Scope().SetRequest(ctx.Request())
ctx.Set(valuesKey, hub)
defer h.recoverWithSentry(hub, ctx.Request())
return next(ctx)
}
}
func (h *handler) recoverWithSentry(hub *sentry.Hub, r *http.Request) {
if err := recover(); err != nil {
eventID := hub.RecoverWithContext(
context.WithValue(r.Context(), sentry.RequestContextKey, r),
err,
)
if eventID != nil && h.waitForDelivery {
hub.Flush(h.timeout)
}
if h.repanic {
panic(err)
}
}
}
// GetHubFromContext retrieves attached *sentry.Hub instance from echo.Context.
func GetHubFromContext(ctx echo.Context) *sentry.Hub {
if hub, ok := ctx.Get(valuesKey).(*sentry.Hub); ok {
return hub
}
return nil
}

395
vendor/github.com/getsentry/sentry-go/hub.go generated vendored Normal file
View file

@ -0,0 +1,395 @@
package sentry
import (
"context"
"sync"
"time"
)
type contextKey int
// Keys used to store values in a Context. Use with Context.Value to access
// values stored by the SDK.
const (
// HubContextKey is the key used to store the current Hub.
HubContextKey = contextKey(1)
// RequestContextKey is the key used to store the current http.Request.
RequestContextKey = contextKey(2)
)
// defaultMaxBreadcrumbs is the default maximum number of breadcrumbs added to
// an event. Can be overwritten with the maxBreadcrumbs option.
const defaultMaxBreadcrumbs = 30
// maxBreadcrumbs is the absolute maximum number of breadcrumbs added to an
// event. The maxBreadcrumbs option cannot be set higher than this value.
const maxBreadcrumbs = 100
// currentHub is the initial Hub with no Client bound and an empty Scope.
var currentHub = NewHub(nil, NewScope())
// Hub is the central object that manages scopes and clients.
//
// This can be used to capture events and manage the scope.
// The default hub that is available automatically.
//
// In most situations developers do not need to interface the hub. Instead
// toplevel convenience functions are exposed that will automatically dispatch
// to global (CurrentHub) hub. In some situations this might not be
// possible in which case it might become necessary to manually work with the
// hub. This is for instance the case when working with async code.
type Hub struct {
mu sync.RWMutex
stack *stack
lastEventID EventID
}
type layer struct {
// mu protects concurrent reads and writes to client.
mu sync.RWMutex
client *Client
// scope is read-only, not protected by mu.
scope *Scope
}
// Client returns the layer's client. Safe for concurrent use.
func (l *layer) Client() *Client {
l.mu.RLock()
defer l.mu.RUnlock()
return l.client
}
// SetClient sets the layer's client. Safe for concurrent use.
func (l *layer) SetClient(c *Client) {
l.mu.Lock()
defer l.mu.Unlock()
l.client = c
}
type stack []*layer
// NewHub returns an instance of a Hub with provided Client and Scope bound.
func NewHub(client *Client, scope *Scope) *Hub {
hub := Hub{
stack: &stack{{
client: client,
scope: scope,
}},
}
return &hub
}
// CurrentHub returns an instance of previously initialized Hub stored in the global namespace.
func CurrentHub() *Hub {
return currentHub
}
// LastEventID returns the ID of the last event (error or message) captured
// through the hub and sent to the underlying transport.
//
// Transactions and events dropped by sampling or event processors do not change
// the last event ID.
//
// LastEventID is a convenience method to cover use cases in which errors are
// captured indirectly and the ID is needed. For example, it can be used as part
// of an HTTP middleware to log the ID of the last error, if any.
//
// For more flexibility, consider instead using the ClientOptions.BeforeSend
// function or event processors.
func (hub *Hub) LastEventID() EventID {
hub.mu.RLock()
defer hub.mu.RUnlock()
return hub.lastEventID
}
// stackTop returns the top layer of the hub stack. Valid hubs always have at
// least one layer, therefore stackTop always return a non-nil pointer.
func (hub *Hub) stackTop() *layer {
hub.mu.RLock()
defer hub.mu.RUnlock()
stack := hub.stack
stackLen := len(*stack)
top := (*stack)[stackLen-1]
return top
}
// Clone returns a copy of the current Hub with top-most scope and client copied over.
func (hub *Hub) Clone() *Hub {
top := hub.stackTop()
scope := top.scope
if scope != nil {
scope = scope.Clone()
}
return NewHub(top.Client(), scope)
}
// Scope returns top-level Scope of the current Hub or nil if no Scope is bound.
func (hub *Hub) Scope() *Scope {
top := hub.stackTop()
return top.scope
}
// Client returns top-level Client of the current Hub or nil if no Client is bound.
func (hub *Hub) Client() *Client {
top := hub.stackTop()
return top.Client()
}
// PushScope pushes a new scope for the current Hub and reuses previously bound Client.
func (hub *Hub) PushScope() *Scope {
top := hub.stackTop()
var scope *Scope
if top.scope != nil {
scope = top.scope.Clone()
} else {
scope = NewScope()
}
hub.mu.Lock()
defer hub.mu.Unlock()
*hub.stack = append(*hub.stack, &layer{
client: top.Client(),
scope: scope,
})
return scope
}
// PopScope drops the most recent scope.
//
// Calls to PopScope must be coordinated with PushScope. For most cases, using
// WithScope should be more convenient.
//
// Calls to PopScope that do not match previous calls to PushScope are silently
// ignored.
func (hub *Hub) PopScope() {
hub.mu.Lock()
defer hub.mu.Unlock()
stack := *hub.stack
stackLen := len(stack)
if stackLen > 1 {
// Never pop the last item off the stack, the stack should always have
// at least one item.
*hub.stack = stack[0 : stackLen-1]
}
}
// BindClient binds a new Client for the current Hub.
func (hub *Hub) BindClient(client *Client) {
top := hub.stackTop()
top.SetClient(client)
}
// WithScope runs f in an isolated temporary scope.
//
// It is useful when extra data should be sent with a single capture call, for
// instance a different level or tags.
//
// The scope passed to f starts as a clone of the current scope and can be
// freely modified without affecting the current scope.
//
// It is a shorthand for PushScope followed by PopScope.
func (hub *Hub) WithScope(f func(scope *Scope)) {
scope := hub.PushScope()
defer hub.PopScope()
f(scope)
}
// ConfigureScope runs f in the current scope.
//
// It is useful to set data that applies to all events that share the current
// scope.
//
// Modifying the scope affects all references to the current scope.
//
// See also WithScope for making isolated temporary changes.
func (hub *Hub) ConfigureScope(f func(scope *Scope)) {
scope := hub.Scope()
f(scope)
}
// CaptureEvent calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureEvent(event *Event) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil || scope == nil {
return nil
}
eventID := client.CaptureEvent(event, nil, scope)
if event.Type != transactionType && eventID != nil {
hub.mu.Lock()
hub.lastEventID = *eventID
hub.mu.Unlock()
}
return eventID
}
// CaptureMessage calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureMessage(message string) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil || scope == nil {
return nil
}
eventID := client.CaptureMessage(message, nil, scope)
if eventID != nil {
hub.mu.Lock()
hub.lastEventID = *eventID
hub.mu.Unlock()
}
return eventID
}
// CaptureException calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureException(exception error) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil || scope == nil {
return nil
}
eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope)
if eventID != nil {
hub.mu.Lock()
hub.lastEventID = *eventID
hub.mu.Unlock()
}
return eventID
}
// CaptureCheckIn calls the method of the same name on currently bound Client instance
// passing it a top-level Scope.
// Returns CheckInID if the check-in was captured successfully, or nil otherwise.
func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
client, scope := hub.Client(), hub.Scope()
if client == nil {
return nil
}
return client.CaptureCheckIn(checkIn, monitorConfig, scope)
}
// AddBreadcrumb records a new breadcrumb.
//
// The total number of breadcrumbs that can be recorded are limited by the
// configuration on the client.
func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
client := hub.Client()
// If there's no client, just store it on the scope straight away
if client == nil {
hub.Scope().AddBreadcrumb(breadcrumb, maxBreadcrumbs)
return
}
max := client.options.MaxBreadcrumbs
if max < 0 {
return
}
if client.options.BeforeBreadcrumb != nil {
if hint == nil {
hint = &BreadcrumbHint{}
}
if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
return
}
}
if max == 0 {
max = defaultMaxBreadcrumbs
} else if max > maxBreadcrumbs {
max = maxBreadcrumbs
}
hub.Scope().AddBreadcrumb(breadcrumb, max)
}
// Recover calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) Recover(err interface{}) *EventID {
if err == nil {
err = recover()
}
client, scope := hub.Client(), hub.Scope()
if client == nil || scope == nil {
return nil
}
return client.Recover(err, &EventHint{RecoveredException: err}, scope)
}
// RecoverWithContext calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID {
if err == nil {
err = recover()
}
client, scope := hub.Client(), hub.Scope()
if client == nil || scope == nil {
return nil
}
return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope)
}
// Flush waits until the underlying Transport sends any buffered events to the
// Sentry server, blocking for at most the given timeout. It returns false if
// the timeout was reached. In that case, some events may not have been sent.
//
// Flush should be called before terminating the program to avoid
// unintentionally dropping events.
//
// Do not call Flush indiscriminately after every call to CaptureEvent,
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func (hub *Hub) Flush(timeout time.Duration) bool {
client := hub.Client()
if client == nil {
return false
}
return client.Flush(timeout)
}
// HasHubOnContext checks whether Hub instance is bound to a given Context struct.
func HasHubOnContext(ctx context.Context) bool {
_, ok := ctx.Value(HubContextKey).(*Hub)
return ok
}
// GetHubFromContext tries to retrieve Hub instance from the given Context struct
// or return nil if one is not found.
func GetHubFromContext(ctx context.Context) *Hub {
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
return hub
}
return nil
}
// hubFromContext returns either a hub stored in the context or the current hub.
// The return value is guaranteed to be non-nil, unlike GetHubFromContext.
func hubFromContext(ctx context.Context) *Hub {
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
return hub
}
return currentHub
}
// SetHubOnContext stores given Hub instance on the Context struct and returns a new Context.
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
return context.WithValue(ctx, HubContextKey, hub)
}

391
vendor/github.com/getsentry/sentry-go/integrations.go generated vendored Normal file
View file

@ -0,0 +1,391 @@
package sentry
import (
"fmt"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"sync"
)
// ================================
// Modules Integration
// ================================
type modulesIntegration struct {
once sync.Once
modules map[string]string
}
func (mi *modulesIntegration) Name() string {
return "Modules"
}
func (mi *modulesIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(mi.processor)
}
func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event {
if len(event.Modules) == 0 {
mi.once.Do(func() {
info, ok := debug.ReadBuildInfo()
if !ok {
Logger.Print("The Modules integration is not available in binaries built without module support.")
return
}
mi.modules = extractModules(info)
})
}
event.Modules = mi.modules
return event
}
func extractModules(info *debug.BuildInfo) map[string]string {
modules := map[string]string{
info.Main.Path: info.Main.Version,
}
for _, dep := range info.Deps {
ver := dep.Version
if dep.Replace != nil {
ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Replace.Version)
}
modules[dep.Path] = strings.TrimSuffix(ver, " ")
}
return modules
}
// ================================
// Environment Integration
// ================================
type environmentIntegration struct{}
func (ei *environmentIntegration) Name() string {
return "Environment"
}
func (ei *environmentIntegration) SetupOnce(client *Client) {
client.AddEventProcessor(ei.processor)
}
func (ei *environmentIntegration) processor(event *Event, _ *EventHint) *Event {
// Initialize maps as necessary.
contextNames := []string{"device", "os", "runtime"}
if event.Contexts == nil {
event.Contexts = make(map[string]Context, len(contextNames))
}
for _, name := range contextNames {
if event.Contexts[name] == nil {
event.Contexts[name] = make(Context)
}
}
// Set contextual information preserving existing data. For each context, if
// the existing value is not of type map[string]interface{}, then no
// additional information is added.
if deviceContext, ok := event.Contexts["device"]; ok {
if _, ok := deviceContext["arch"]; !ok {
deviceContext["arch"] = runtime.GOARCH
}
if _, ok := deviceContext["num_cpu"]; !ok {
deviceContext["num_cpu"] = runtime.NumCPU()
}
}
if osContext, ok := event.Contexts["os"]; ok {
if _, ok := osContext["name"]; !ok {
osContext["name"] = runtime.GOOS
}
}
if runtimeContext, ok := event.Contexts["runtime"]; ok {
if _, ok := runtimeContext["name"]; !ok {
runtimeContext["name"] = "go"
}
if _, ok := runtimeContext["version"]; !ok {
runtimeContext["version"] = runtime.Version()
}
if _, ok := runtimeContext["go_numroutines"]; !ok {
runtimeContext["go_numroutines"] = runtime.NumGoroutine()
}
if _, ok := runtimeContext["go_maxprocs"]; !ok {
runtimeContext["go_maxprocs"] = runtime.GOMAXPROCS(0)
}
if _, ok := runtimeContext["go_numcgocalls"]; !ok {
runtimeContext["go_numcgocalls"] = runtime.NumCgoCall()
}
}
return event
}
// ================================
// Ignore Errors Integration
// ================================
type ignoreErrorsIntegration struct {
ignoreErrors []*regexp.Regexp
}
func (iei *ignoreErrorsIntegration) Name() string {
return "IgnoreErrors"
}
func (iei *ignoreErrorsIntegration) SetupOnce(client *Client) {
iei.ignoreErrors = transformStringsIntoRegexps(client.options.IgnoreErrors)
client.AddEventProcessor(iei.processor)
}
func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event {
suspects := getIgnoreErrorsSuspects(event)
for _, suspect := range suspects {
for _, pattern := range iei.ignoreErrors {
if pattern.Match([]byte(suspect)) {
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
}
}
}
return event
}
func transformStringsIntoRegexps(strings []string) []*regexp.Regexp {
var exprs []*regexp.Regexp
for _, s := range strings {
r, err := regexp.Compile(s)
if err == nil {
exprs = append(exprs, r)
}
}
return exprs
}
func getIgnoreErrorsSuspects(event *Event) []string {
suspects := []string{}
if event.Message != "" {
suspects = append(suspects, event.Message)
}
for _, ex := range event.Exception {
suspects = append(suspects, ex.Type, ex.Value)
}
return suspects
}
// ================================
// Ignore Transactions Integration
// ================================
type ignoreTransactionsIntegration struct {
ignoreTransactions []*regexp.Regexp
}
func (iei *ignoreTransactionsIntegration) Name() string {
return "IgnoreTransactions"
}
func (iei *ignoreTransactionsIntegration) SetupOnce(client *Client) {
iei.ignoreTransactions = transformStringsIntoRegexps(client.options.IgnoreTransactions)
client.AddEventProcessor(iei.processor)
}
func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint) *Event {
suspect := event.Transaction
if suspect == "" {
return event
}
for _, pattern := range iei.ignoreTransactions {
if pattern.Match([]byte(suspect)) {
Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern)
return nil
}
}
return event
}
// ================================
// Contextify Frames Integration
// ================================
type contextifyFramesIntegration struct {
sr sourceReader
contextLines int
cachedLocations sync.Map
}
func (cfi *contextifyFramesIntegration) Name() string {
return "ContextifyFrames"
}
func (cfi *contextifyFramesIntegration) SetupOnce(client *Client) {
cfi.sr = newSourceReader()
cfi.contextLines = 5
client.AddEventProcessor(cfi.processor)
}
func (cfi *contextifyFramesIntegration) processor(event *Event, _ *EventHint) *Event {
// Range over all exceptions
for _, ex := range event.Exception {
// If it has no stacktrace, just bail out
if ex.Stacktrace == nil {
continue
}
// If it does, it should have frames, so try to contextify them
ex.Stacktrace.Frames = cfi.contextify(ex.Stacktrace.Frames)
}
// Range over all threads
for _, th := range event.Threads {
// If it has no stacktrace, just bail out
if th.Stacktrace == nil {
continue
}
// If it does, it should have frames, so try to contextify them
th.Stacktrace.Frames = cfi.contextify(th.Stacktrace.Frames)
}
return event
}
func (cfi *contextifyFramesIntegration) contextify(frames []Frame) []Frame {
contextifiedFrames := make([]Frame, 0, len(frames))
for _, frame := range frames {
if !frame.InApp {
contextifiedFrames = append(contextifiedFrames, frame)
continue
}
var path string
if cachedPath, ok := cfi.cachedLocations.Load(frame.AbsPath); ok {
if p, ok := cachedPath.(string); ok {
path = p
}
} else {
// Optimize for happy path here
if fileExists(frame.AbsPath) {
path = frame.AbsPath
} else {
path = cfi.findNearbySourceCodeLocation(frame.AbsPath)
}
}
if path == "" {
contextifiedFrames = append(contextifiedFrames, frame)
continue
}
lines, contextLine := cfi.sr.readContextLines(path, frame.Lineno, cfi.contextLines)
contextifiedFrames = append(contextifiedFrames, cfi.addContextLinesToFrame(frame, lines, contextLine))
}
return contextifiedFrames
}
func (cfi *contextifyFramesIntegration) findNearbySourceCodeLocation(originalPath string) string {
trimmedPath := strings.TrimPrefix(originalPath, "/")
components := strings.Split(trimmedPath, "/")
for len(components) > 0 {
components = components[1:]
possibleLocation := strings.Join(components, "/")
if fileExists(possibleLocation) {
cfi.cachedLocations.Store(originalPath, possibleLocation)
return possibleLocation
}
}
cfi.cachedLocations.Store(originalPath, "")
return ""
}
func (cfi *contextifyFramesIntegration) addContextLinesToFrame(frame Frame, lines [][]byte, contextLine int) Frame {
for i, line := range lines {
switch {
case i < contextLine:
frame.PreContext = append(frame.PreContext, string(line))
case i == contextLine:
frame.ContextLine = string(line)
default:
frame.PostContext = append(frame.PostContext, string(line))
}
}
return frame
}
// ================================
// Global Tags Integration
// ================================
const envTagsPrefix = "SENTRY_TAGS_"
type globalTagsIntegration struct {
tags map[string]string
envTags map[string]string
}
func (ti *globalTagsIntegration) Name() string {
return "GlobalTags"
}
func (ti *globalTagsIntegration) SetupOnce(client *Client) {
ti.tags = make(map[string]string, len(client.options.Tags))
for k, v := range client.options.Tags {
ti.tags[k] = v
}
ti.envTags = loadEnvTags()
client.AddEventProcessor(ti.processor)
}
func (ti *globalTagsIntegration) processor(event *Event, _ *EventHint) *Event {
if len(ti.tags) == 0 && len(ti.envTags) == 0 {
return event
}
if event.Tags == nil {
event.Tags = make(map[string]string, len(ti.tags)+len(ti.envTags))
}
for k, v := range ti.tags {
if _, ok := event.Tags[k]; !ok {
event.Tags[k] = v
}
}
for k, v := range ti.envTags {
if _, ok := event.Tags[k]; !ok {
event.Tags[k] = v
}
}
return event
}
func loadEnvTags() map[string]string {
tags := map[string]string{}
for _, pair := range os.Environ() {
parts := strings.Split(pair, "=")
if !strings.HasPrefix(parts[0], envTagsPrefix) {
continue
}
tag := strings.TrimPrefix(parts[0], envTagsPrefix)
tags[tag] = parts[1]
}
return tags
}

522
vendor/github.com/getsentry/sentry-go/interfaces.go generated vendored Normal file
View file

@ -0,0 +1,522 @@
package sentry
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"reflect"
"strings"
"time"
)
// eventType is the type of an error event.
const eventType = "event"
// transactionType is the type of a transaction event.
const transactionType = "transaction"
// profileType is the type of a profile event.
// currently, profiles are always sent as part of a transaction event.
const profileType = "profile"
// checkInType is the type of a check in event.
const checkInType = "check_in"
// Level marks the severity of the event.
type Level string
// Describes the severity of the event.
const (
LevelDebug Level = "debug"
LevelInfo Level = "info"
LevelWarning Level = "warning"
LevelError Level = "error"
LevelFatal Level = "fatal"
)
func getSensitiveHeaders() map[string]bool {
return map[string]bool{
"Authorization": true,
"Cookie": true,
"X-Forwarded-For": true,
"X-Real-Ip": true,
}
}
// SdkInfo contains all metadata about about the SDK being used.
type SdkInfo struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Integrations []string `json:"integrations,omitempty"`
Packages []SdkPackage `json:"packages,omitempty"`
}
// SdkPackage describes a package that was installed.
type SdkPackage struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}
// TODO: This type could be more useful, as map of interface{} is too generic
// and requires a lot of type assertions in beforeBreadcrumb calls
// plus it could just be map[string]interface{} then.
// BreadcrumbHint contains information that can be associated with a Breadcrumb.
type BreadcrumbHint map[string]interface{}
// Breadcrumb specifies an application event that occurred before a Sentry event.
// An event may contain one or more breadcrumbs.
type Breadcrumb struct {
Type string `json:"type,omitempty"`
Category string `json:"category,omitempty"`
Message string `json:"message,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Level Level `json:"level,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// TODO: provide constants for known breadcrumb types.
// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types.
// MarshalJSON converts the Breadcrumb struct to JSON.
func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
// We want to omit time.Time zero values, otherwise the server will try to
// interpret dates too far in the past. However, encoding/json doesn't
// support the "omitempty" option for struct types. See
// https://golang.org/issues/11939.
//
// We overcome the limitation and achieve what we want by shadowing fields
// and a few type tricks.
// breadcrumb aliases Breadcrumb to allow calling json.Marshal without an
// infinite loop. It preserves all fields while none of the attached
// methods.
type breadcrumb Breadcrumb
if b.Timestamp.IsZero() {
return json.Marshal(struct {
// Embed all of the fields of Breadcrumb.
*breadcrumb
// Timestamp shadows the original Timestamp field and is meant to
// remain nil, triggering the omitempty behavior.
Timestamp json.RawMessage `json:"timestamp,omitempty"`
}{breadcrumb: (*breadcrumb)(b)})
}
return json.Marshal((*breadcrumb)(b))
}
// Attachment allows associating files with your events to aid in investigation.
// An event may contain one or more attachments.
type Attachment struct {
Filename string
ContentType string
Payload []byte
}
// User describes the user associated with an Event. If this is used, at least
// an ID or an IP address should be provided.
type User struct {
ID string `json:"id,omitempty"`
Email string `json:"email,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
Username string `json:"username,omitempty"`
Name string `json:"name,omitempty"`
Segment string `json:"segment,omitempty"`
Data map[string]string `json:"data,omitempty"`
}
func (u User) IsEmpty() bool {
if len(u.ID) > 0 {
return false
}
if len(u.Email) > 0 {
return false
}
if len(u.IPAddress) > 0 {
return false
}
if len(u.Username) > 0 {
return false
}
if len(u.Name) > 0 {
return false
}
if len(u.Segment) > 0 {
return false
}
if len(u.Data) > 0 {
return false
}
return true
}
// Request contains information on a HTTP request related to the event.
type Request struct {
URL string `json:"url,omitempty"`
Method string `json:"method,omitempty"`
Data string `json:"data,omitempty"`
QueryString string `json:"query_string,omitempty"`
Cookies string `json:"cookies,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
Env map[string]string `json:"env,omitempty"`
}
// NewRequest returns a new Sentry Request from the given http.Request.
//
// NewRequest avoids operations that depend on network access. In particular, it
// does not read r.Body.
func NewRequest(r *http.Request) *Request {
protocol := schemeHTTP
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
protocol = schemeHTTPS
}
url := fmt.Sprintf("%s://%s%s", protocol, r.Host, r.URL.Path)
var cookies string
var env map[string]string
headers := map[string]string{}
if client := CurrentHub().Client(); client != nil && client.options.SendDefaultPII {
// We read only the first Cookie header because of the specification:
// https://tools.ietf.org/html/rfc6265#section-5.4
// When the user agent generates an HTTP request, the user agent MUST NOT
// attach more than one Cookie header field.
cookies = r.Header.Get("Cookie")
for k, v := range r.Header {
headers[k] = strings.Join(v, ",")
}
if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
}
} else {
sensitiveHeaders := getSensitiveHeaders()
for k, v := range r.Header {
if _, ok := sensitiveHeaders[k]; !ok {
headers[k] = strings.Join(v, ",")
}
}
}
headers["Host"] = r.Host
return &Request{
URL: url,
Method: r.Method,
QueryString: r.URL.RawQuery,
Cookies: cookies,
Headers: headers,
Env: env,
}
}
// Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct {
Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"`
Handled *bool `json:"handled,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
}
// SetUnhandled indicates that the exception is an unhandled exception, i.e.
// from a panic.
func (m *Mechanism) SetUnhandled() {
h := false
m.Handled = &h
}
// Exception specifies an error that occurred.
type Exception struct {
Type string `json:"type,omitempty"` // used as the main issue title
Value string `json:"value,omitempty"` // used as the main issue subtitle
Module string `json:"module,omitempty"`
ThreadID string `json:"thread_id,omitempty"`
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
Mechanism *Mechanism `json:"mechanism,omitempty"`
}
// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
// but which shouldn't get send to Sentry.
type SDKMetaData struct {
dsc DynamicSamplingContext
transactionProfile *profileInfo
}
// Contains information about how the name of the transaction was determined.
type TransactionInfo struct {
Source TransactionSource `json:"source,omitempty"`
}
// The DebugMeta interface is not used in Golang apps, but may be populated
// when proxying Events from other platforms, like iOS, Android, and the
// Web. (See: https://develop.sentry.dev/sdk/event-payloads/debugmeta/ ).
type DebugMeta struct {
SdkInfo *DebugMetaSdkInfo `json:"sdk_info,omitempty"`
Images []DebugMetaImage `json:"images,omitempty"`
}
type DebugMetaSdkInfo struct {
SdkName string `json:"sdk_name,omitempty"`
VersionMajor int `json:"version_major,omitempty"`
VersionMinor int `json:"version_minor,omitempty"`
VersionPatchlevel int `json:"version_patchlevel,omitempty"`
}
type DebugMetaImage struct {
Type string `json:"type,omitempty"` // all
ImageAddr string `json:"image_addr,omitempty"` // macho,elf,pe
ImageSize int `json:"image_size,omitempty"` // macho,elf,pe
DebugID string `json:"debug_id,omitempty"` // macho,elf,pe,wasm,sourcemap
DebugFile string `json:"debug_file,omitempty"` // macho,elf,pe,wasm
CodeID string `json:"code_id,omitempty"` // macho,elf,pe,wasm
CodeFile string `json:"code_file,omitempty"` // macho,elf,pe,wasm,sourcemap
ImageVmaddr string `json:"image_vmaddr,omitempty"` // macho,elf,pe
Arch string `json:"arch,omitempty"` // macho,elf,pe
UUID string `json:"uuid,omitempty"` // proguard
}
// EventID is a hexadecimal string representing a unique uuid4 for an Event.
// An EventID must be 32 characters long, lowercase and not have any dashes.
type EventID string
type Context = map[string]interface{}
// Event is the fundamental data structure that is sent to Sentry.
type Event struct {
Breadcrumbs []*Breadcrumb `json:"breadcrumbs,omitempty"`
Contexts map[string]Context `json:"contexts,omitempty"`
Dist string `json:"dist,omitempty"`
Environment string `json:"environment,omitempty"`
EventID EventID `json:"event_id,omitempty"`
Extra map[string]interface{} `json:"extra,omitempty"`
Fingerprint []string `json:"fingerprint,omitempty"`
Level Level `json:"level,omitempty"`
Message string `json:"message,omitempty"`
Platform string `json:"platform,omitempty"`
Release string `json:"release,omitempty"`
Sdk SdkInfo `json:"sdk,omitempty"`
ServerName string `json:"server_name,omitempty"`
Threads []Thread `json:"threads,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
Timestamp time.Time `json:"timestamp"`
Transaction string `json:"transaction,omitempty"`
User User `json:"user,omitempty"`
Logger string `json:"logger,omitempty"`
Modules map[string]string `json:"modules,omitempty"`
Request *Request `json:"request,omitempty"`
Exception []Exception `json:"exception,omitempty"`
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
// The fields below are only relevant for transactions.
Type string `json:"type,omitempty"`
StartTime time.Time `json:"start_timestamp"`
Spans []*Span `json:"spans,omitempty"`
TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"`
// The fields below are only relevant for crons/check ins
CheckIn *CheckIn `json:"check_in,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
// The fields below are not part of the final JSON payload.
sdkMetaData SDKMetaData
attachments []*Attachment
}
// SetException appends the unwrapped errors to the event's exception list.
//
// maxErrorDepth is the maximum depth of the error chain we will look
// into while unwrapping the errors.
func (e *Event) SetException(exception error, maxErrorDepth int) {
err := exception
if err == nil {
return
}
for i := 0; i < maxErrorDepth && err != nil; i++ {
e.Exception = append(e.Exception, Exception{
Value: err.Error(),
Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err),
})
switch previous := err.(type) {
case interface{ Unwrap() error }:
err = previous.Unwrap()
case interface{ Cause() error }:
err = previous.Cause()
default:
err = nil
}
}
// Add a trace of the current stack to the most recent error in a chain if
// it doesn't have a stack trace yet.
// We only add to the most recent error to avoid duplication and because the
// current stack is most likely unrelated to errors deeper in the chain.
if e.Exception[0].Stacktrace == nil {
e.Exception[0].Stacktrace = NewStacktrace()
}
// event.Exception should be sorted such that the most recent error is last.
reverse(e.Exception)
}
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
// to prevent accidentally storing T when we mean *T.
// For example, the TraceContext must be stored as *TraceContext to pick up the
// MarshalJSON method (and avoid copying).
// type EventContext interface{ EventContext() }
// MarshalJSON converts the Event struct to JSON.
func (e *Event) MarshalJSON() ([]byte, error) {
// We want to omit time.Time zero values, otherwise the server will try to
// interpret dates too far in the past. However, encoding/json doesn't
// support the "omitempty" option for struct types. See
// https://golang.org/issues/11939.
//
// We overcome the limitation and achieve what we want by shadowing fields
// and a few type tricks.
if e.Type == transactionType {
return e.transactionMarshalJSON()
} else if e.Type == checkInType {
return e.checkInMarshalJSON()
}
return e.defaultMarshalJSON()
}
func (e *Event) defaultMarshalJSON() ([]byte, error) {
// event aliases Event to allow calling json.Marshal without an infinite
// loop. It preserves all fields while none of the attached methods.
type event Event
// errorEvent is like Event with shadowed fields for customizing JSON
// marshaling.
type errorEvent struct {
*event
// Timestamp shadows the original Timestamp field. It allows us to
// include the timestamp when non-zero and omit it otherwise.
Timestamp json.RawMessage `json:"timestamp,omitempty"`
// The fields below are not part of error events and only make sense to
// be sent for transactions. They shadow the respective fields in Event
// and are meant to remain nil, triggering the omitempty behavior.
Type json.RawMessage `json:"type,omitempty"`
StartTime json.RawMessage `json:"start_timestamp,omitempty"`
Spans json.RawMessage `json:"spans,omitempty"`
TransactionInfo json.RawMessage `json:"transaction_info,omitempty"`
}
x := errorEvent{event: (*event)(e)}
if !e.Timestamp.IsZero() {
b, err := e.Timestamp.MarshalJSON()
if err != nil {
return nil, err
}
x.Timestamp = b
}
return json.Marshal(x)
}
func (e *Event) transactionMarshalJSON() ([]byte, error) {
// event aliases Event to allow calling json.Marshal without an infinite
// loop. It preserves all fields while none of the attached methods.
type event Event
// transactionEvent is like Event with shadowed fields for customizing JSON
// marshaling.
type transactionEvent struct {
*event
// The fields below shadow the respective fields in Event. They allow us
// to include timestamps when non-zero and omit them otherwise.
StartTime json.RawMessage `json:"start_timestamp,omitempty"`
Timestamp json.RawMessage `json:"timestamp,omitempty"`
}
x := transactionEvent{event: (*event)(e)}
if !e.Timestamp.IsZero() {
b, err := e.Timestamp.MarshalJSON()
if err != nil {
return nil, err
}
x.Timestamp = b
}
if !e.StartTime.IsZero() {
b, err := e.StartTime.MarshalJSON()
if err != nil {
return nil, err
}
x.StartTime = b
}
return json.Marshal(x)
}
func (e *Event) checkInMarshalJSON() ([]byte, error) {
checkIn := serializedCheckIn{
CheckInID: string(e.CheckIn.ID),
MonitorSlug: e.CheckIn.MonitorSlug,
Status: e.CheckIn.Status,
Duration: e.CheckIn.Duration.Seconds(),
Release: e.Release,
Environment: e.Environment,
MonitorConfig: nil,
}
if e.MonitorConfig != nil {
checkIn.MonitorConfig = &MonitorConfig{
Schedule: e.MonitorConfig.Schedule,
CheckInMargin: e.MonitorConfig.CheckInMargin,
MaxRuntime: e.MonitorConfig.MaxRuntime,
Timezone: e.MonitorConfig.Timezone,
}
}
return json.Marshal(checkIn)
}
// NewEvent creates a new Event.
func NewEvent() *Event {
event := Event{
Contexts: make(map[string]Context),
Extra: make(map[string]interface{}),
Tags: make(map[string]string),
Modules: make(map[string]string),
}
return &event
}
// Thread specifies threads that were running at the time of an event.
type Thread struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
Crashed bool `json:"crashed,omitempty"`
Current bool `json:"current,omitempty"`
}
// EventHint contains information that can be associated with an Event.
type EventHint struct {
Data interface{}
EventID string
OriginalException error
RecoveredException interface{}
Context context.Context
Request *http.Request
Response *http.Response
}

View file

@ -0,0 +1,79 @@
package debug
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"net/http/httputil"
)
// Transport implements http.RoundTripper and can be used to wrap other HTTP
// transports for debugging, normally http.DefaultTransport.
type Transport struct {
http.RoundTripper
Output io.Writer
// Dump controls whether to dump HTTP request and responses.
Dump bool
// Trace enables usage of net/http/httptrace.
Trace bool
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
var buf bytes.Buffer
if t.Dump {
b, err := httputil.DumpRequestOut(req, true)
if err != nil {
panic(err)
}
_, err = buf.Write(ensureTrailingNewline(b))
if err != nil {
panic(err)
}
}
if t.Trace {
trace := &httptrace.ClientTrace{
DNSDone: func(di httptrace.DNSDoneInfo) {
fmt.Fprintf(&buf, "* DNS %v → %v\n", req.Host, di.Addrs)
},
GotConn: func(ci httptrace.GotConnInfo) {
fmt.Fprintf(&buf, "* Connection local=%v remote=%v", ci.Conn.LocalAddr(), ci.Conn.RemoteAddr())
if ci.Reused {
fmt.Fprint(&buf, " (reused)")
}
if ci.WasIdle {
fmt.Fprintf(&buf, " (idle %v)", ci.IdleTime)
}
fmt.Fprintln(&buf)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
}
resp, err := t.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
if t.Dump {
b, err := httputil.DumpResponse(resp, true)
if err != nil {
panic(err)
}
_, err = buf.Write(ensureTrailingNewline(b))
if err != nil {
panic(err)
}
}
_, err = io.Copy(t.Output, &buf)
if err != nil {
panic(err)
}
return resp, nil
}
func ensureTrailingNewline(b []byte) []byte {
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
return b
}

View file

@ -0,0 +1,12 @@
## Why do we have this "otel/baggage" folder?
The root sentry-go SDK (namely, the Dynamic Sampling functionality) needs an implementation of the [baggage spec](https://www.w3.org/TR/baggage/).
For that reason, we've taken the existing baggage implementation from the [opentelemetry-go](https://github.com/open-telemetry/opentelemetry-go/) repository, and fixed a few things that in our opinion were violating the specification.
These issues are:
1. Baggage string value `one%20two` should be properly parsed as "one two"
1. Baggage string value `one+two` should be parsed as "one+two"
1. Go string value "one two" should be encoded as `one%20two` (percent encoding), and NOT as `one+two` (URL query encoding).
1. Go string value "1=1" might be encoded as `1=1`, because the spec says: "Note, value MAY contain any number of the equal sign (=) characters. Parsers MUST NOT assume that the equal sign is only used to separate key and value.". `1%3D1` is also valid, but to simplify the implementation we're not doing it.
Changes were made in this PR: https://github.com/getsentry/sentry-go/pull/568

View file

@ -0,0 +1,604 @@
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/baggage/baggage.go
//
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package baggage
import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"unicode/utf8"
"github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage"
)
const (
maxMembers = 180
maxBytesPerMembers = 4096
maxBytesPerBaggageString = 8192
listDelimiter = ","
keyValueDelimiter = "="
propertyDelimiter = ";"
keyDef = `([\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+)`
valueDef = `([\x21\x23-\x2b\x2d-\x3a\x3c-\x5B\x5D-\x7e]*)`
keyValueDef = `\s*` + keyDef + `\s*` + keyValueDelimiter + `\s*` + valueDef + `\s*`
)
var (
keyRe = regexp.MustCompile(`^` + keyDef + `$`)
valueRe = regexp.MustCompile(`^` + valueDef + `$`)
propertyRe = regexp.MustCompile(`^(?:\s*` + keyDef + `\s*|` + keyValueDef + `)$`)
)
var (
errInvalidKey = errors.New("invalid key")
errInvalidValue = errors.New("invalid value")
errInvalidProperty = errors.New("invalid baggage list-member property")
errInvalidMember = errors.New("invalid baggage list-member")
errMemberNumber = errors.New("too many list-members in baggage-string")
errMemberBytes = errors.New("list-member too large")
errBaggageBytes = errors.New("baggage-string too large")
)
// Property is an additional metadata entry for a baggage list-member.
type Property struct {
key, value string
// hasValue indicates if a zero-value value means the property does not
// have a value or if it was the zero-value.
hasValue bool
// hasData indicates whether the created property contains data or not.
// Properties that do not contain data are invalid with no other check
// required.
hasData bool
}
// NewKeyProperty returns a new Property for key.
//
// If key is invalid, an error will be returned.
func NewKeyProperty(key string) (Property, error) {
if !keyRe.MatchString(key) {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
p := Property{key: key, hasData: true}
return p, nil
}
// NewKeyValueProperty returns a new Property for key with value.
//
// If key or value are invalid, an error will be returned.
func NewKeyValueProperty(key, value string) (Property, error) {
if !keyRe.MatchString(key) {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
if !valueRe.MatchString(value) {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidValue, value)
}
p := Property{
key: key,
value: value,
hasValue: true,
hasData: true,
}
return p, nil
}
func newInvalidProperty() Property {
return Property{}
}
// parseProperty attempts to decode a Property from the passed string. It
// returns an error if the input is invalid according to the W3C Baggage
// specification.
func parseProperty(property string) (Property, error) {
if property == "" {
return newInvalidProperty(), nil
}
match := propertyRe.FindStringSubmatch(property)
if len(match) != 4 {
return newInvalidProperty(), fmt.Errorf("%w: %q", errInvalidProperty, property)
}
p := Property{hasData: true}
if match[1] != "" {
p.key = match[1]
} else {
p.key = match[2]
p.value = match[3]
p.hasValue = true
}
return p, nil
}
// validate ensures p conforms to the W3C Baggage specification, returning an
// error otherwise.
func (p Property) validate() error {
errFunc := func(err error) error {
return fmt.Errorf("invalid property: %w", err)
}
if !p.hasData {
return errFunc(fmt.Errorf("%w: %q", errInvalidProperty, p))
}
if !keyRe.MatchString(p.key) {
return errFunc(fmt.Errorf("%w: %q", errInvalidKey, p.key))
}
if p.hasValue && !valueRe.MatchString(p.value) {
return errFunc(fmt.Errorf("%w: %q", errInvalidValue, p.value))
}
if !p.hasValue && p.value != "" {
return errFunc(errors.New("inconsistent value"))
}
return nil
}
// Key returns the Property key.
func (p Property) Key() string {
return p.key
}
// Value returns the Property value. Additionally, a boolean value is returned
// indicating if the returned value is the empty if the Property has a value
// that is empty or if the value is not set.
func (p Property) Value() (string, bool) {
return p.value, p.hasValue
}
// String encodes Property into a string compliant with the W3C Baggage
// specification.
func (p Property) String() string {
if p.hasValue {
return fmt.Sprintf("%s%s%v", p.key, keyValueDelimiter, p.value)
}
return p.key
}
type properties []Property
func fromInternalProperties(iProps []baggage.Property) properties {
if len(iProps) == 0 {
return nil
}
props := make(properties, len(iProps))
for i, p := range iProps {
props[i] = Property{
key: p.Key,
value: p.Value,
hasValue: p.HasValue,
}
}
return props
}
func (p properties) asInternal() []baggage.Property {
if len(p) == 0 {
return nil
}
iProps := make([]baggage.Property, len(p))
for i, prop := range p {
iProps[i] = baggage.Property{
Key: prop.key,
Value: prop.value,
HasValue: prop.hasValue,
}
}
return iProps
}
func (p properties) Copy() properties {
if len(p) == 0 {
return nil
}
props := make(properties, len(p))
copy(props, p)
return props
}
// validate ensures each Property in p conforms to the W3C Baggage
// specification, returning an error otherwise.
func (p properties) validate() error {
for _, prop := range p {
if err := prop.validate(); err != nil {
return err
}
}
return nil
}
// String encodes properties into a string compliant with the W3C Baggage
// specification.
func (p properties) String() string {
props := make([]string, len(p))
for i, prop := range p {
props[i] = prop.String()
}
return strings.Join(props, propertyDelimiter)
}
// Member is a list-member of a baggage-string as defined by the W3C Baggage
// specification.
type Member struct {
key, value string
properties properties
// hasData indicates whether the created property contains data or not.
// Properties that do not contain data are invalid with no other check
// required.
hasData bool
}
// NewMember returns a new Member from the passed arguments. The key will be
// used directly while the value will be url decoded after validation. An error
// is returned if the created Member would be invalid according to the W3C
// Baggage specification.
func NewMember(key, value string, props ...Property) (Member, error) {
m := Member{
key: key,
value: value,
properties: properties(props).Copy(),
hasData: true,
}
if err := m.validate(); err != nil {
return newInvalidMember(), err
}
//// NOTE(anton): I don't think we need to unescape here
// decodedValue, err := url.PathUnescape(value)
// if err != nil {
// return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
// }
// m.value = decodedValue
return m, nil
}
func newInvalidMember() Member {
return Member{}
}
// parseMember attempts to decode a Member from the passed string. It returns
// an error if the input is invalid according to the W3C Baggage
// specification.
func parseMember(member string) (Member, error) {
if n := len(member); n > maxBytesPerMembers {
return newInvalidMember(), fmt.Errorf("%w: %d", errMemberBytes, n)
}
var (
key, value string
props properties
)
parts := strings.SplitN(member, propertyDelimiter, 2)
switch len(parts) {
case 2:
// Parse the member properties.
for _, pStr := range strings.Split(parts[1], propertyDelimiter) {
p, err := parseProperty(pStr)
if err != nil {
return newInvalidMember(), err
}
props = append(props, p)
}
fallthrough
case 1:
// Parse the member key/value pair.
// Take into account a value can contain equal signs (=).
kv := strings.SplitN(parts[0], keyValueDelimiter, 2)
if len(kv) != 2 {
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidMember, member)
}
// "Leading and trailing whitespaces are allowed but MUST be trimmed
// when converting the header into a data structure."
key = strings.TrimSpace(kv[0])
value = strings.TrimSpace(kv[1])
var err error
if !keyRe.MatchString(key) {
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidKey, key)
}
if !valueRe.MatchString(value) {
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidValue, value)
}
decodedValue, err := url.PathUnescape(value)
if err != nil {
return newInvalidMember(), fmt.Errorf("%w: %q", err, value)
}
value = decodedValue
default:
// This should never happen unless a developer has changed the string
// splitting somehow. Panic instead of failing silently and allowing
// the bug to slip past the CI checks.
panic("failed to parse baggage member")
}
return Member{key: key, value: value, properties: props, hasData: true}, nil
}
// validate ensures m conforms to the W3C Baggage specification.
// A key is just an ASCII string, but a value must be URL encoded UTF-8,
// returning an error otherwise.
func (m Member) validate() error {
if !m.hasData {
return fmt.Errorf("%w: %q", errInvalidMember, m)
}
if !keyRe.MatchString(m.key) {
return fmt.Errorf("%w: %q", errInvalidKey, m.key)
}
//// NOTE(anton): IMO it's too early to validate the value here.
// if !valueRe.MatchString(m.value) {
// return fmt.Errorf("%w: %q", errInvalidValue, m.value)
// }
return m.properties.validate()
}
// Key returns the Member key.
func (m Member) Key() string { return m.key }
// Value returns the Member value.
func (m Member) Value() string { return m.value }
// Properties returns a copy of the Member properties.
func (m Member) Properties() []Property { return m.properties.Copy() }
// String encodes Member into a string compliant with the W3C Baggage
// specification.
func (m Member) String() string {
// A key is just an ASCII string, but a value is URL encoded UTF-8.
s := fmt.Sprintf("%s%s%s", m.key, keyValueDelimiter, percentEncodeValue(m.value))
if len(m.properties) > 0 {
s = fmt.Sprintf("%s%s%s", s, propertyDelimiter, m.properties.String())
}
return s
}
// percentEncodeValue encodes the baggage value, using percent-encoding for
// disallowed octets.
func percentEncodeValue(s string) string {
const upperhex = "0123456789ABCDEF"
var sb strings.Builder
for byteIndex, width := 0, 0; byteIndex < len(s); byteIndex += width {
runeValue, w := utf8.DecodeRuneInString(s[byteIndex:])
width = w
char := string(runeValue)
if valueRe.MatchString(char) && char != "%" {
// The character is returned as is, no need to percent-encode
sb.WriteString(char)
} else {
// We need to percent-encode each byte of the multi-octet character
for j := 0; j < width; j++ {
b := s[byteIndex+j]
sb.WriteByte('%')
// Bitwise operations are inspired by "net/url"
sb.WriteByte(upperhex[b>>4])
sb.WriteByte(upperhex[b&15])
}
}
}
return sb.String()
}
// Baggage is a list of baggage members representing the baggage-string as
// defined by the W3C Baggage specification.
type Baggage struct { //nolint:golint
list baggage.List
}
// New returns a new valid Baggage. It returns an error if it results in a
// Baggage exceeding limits set in that specification.
//
// It expects all the provided members to have already been validated.
func New(members ...Member) (Baggage, error) {
if len(members) == 0 {
return Baggage{}, nil
}
b := make(baggage.List)
for _, m := range members {
if !m.hasData {
return Baggage{}, errInvalidMember
}
// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
}
// Check member numbers after deduplication.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
}
bag := Baggage{b}
if n := len(bag.String()); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
return bag, nil
}
// Parse attempts to decode a baggage-string from the passed string. It
// returns an error if the input is invalid according to the W3C Baggage
// specification.
//
// If there are duplicate list-members contained in baggage, the last one
// defined (reading left-to-right) will be the only one kept. This diverges
// from the W3C Baggage specification which allows duplicate list-members, but
// conforms to the OpenTelemetry Baggage specification.
func Parse(bStr string) (Baggage, error) {
if bStr == "" {
return Baggage{}, nil
}
if n := len(bStr); n > maxBytesPerBaggageString {
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
}
b := make(baggage.List)
for _, memberStr := range strings.Split(bStr, listDelimiter) {
m, err := parseMember(memberStr)
if err != nil {
return Baggage{}, err
}
// OpenTelemetry resolves duplicates by last-one-wins.
b[m.key] = baggage.Item{
Value: m.value,
Properties: m.properties.asInternal(),
}
}
// OpenTelemetry does not allow for duplicate list-members, but the W3C
// specification does. Now that we have deduplicated, ensure the baggage
// does not exceed list-member limits.
if len(b) > maxMembers {
return Baggage{}, errMemberNumber
}
return Baggage{b}, nil
}
// Member returns the baggage list-member identified by key.
//
// If there is no list-member matching the passed key the returned Member will
// be a zero-value Member.
// The returned member is not validated, as we assume the validation happened
// when it was added to the Baggage.
func (b Baggage) Member(key string) Member {
v, ok := b.list[key]
if !ok {
// We do not need to worry about distinguishing between the situation
// where a zero-valued Member is included in the Baggage because a
// zero-valued Member is invalid according to the W3C Baggage
// specification (it has an empty key).
return newInvalidMember()
}
return Member{
key: key,
value: v.Value,
properties: fromInternalProperties(v.Properties),
hasData: true,
}
}
// Members returns all the baggage list-members.
// The order of the returned list-members does not have significance.
//
// The returned members are not validated, as we assume the validation happened
// when they were added to the Baggage.
func (b Baggage) Members() []Member {
if len(b.list) == 0 {
return nil
}
members := make([]Member, 0, len(b.list))
for k, v := range b.list {
members = append(members, Member{
key: k,
value: v.Value,
properties: fromInternalProperties(v.Properties),
hasData: true,
})
}
return members
}
// SetMember returns a copy the Baggage with the member included. If the
// baggage contains a Member with the same key the existing Member is
// replaced.
//
// If member is invalid according to the W3C Baggage specification, an error
// is returned with the original Baggage.
func (b Baggage) SetMember(member Member) (Baggage, error) {
if !member.hasData {
return b, errInvalidMember
}
n := len(b.list)
if _, ok := b.list[member.key]; !ok {
n++
}
list := make(baggage.List, n)
for k, v := range b.list {
// Do not copy if we are just going to overwrite.
if k == member.key {
continue
}
list[k] = v
}
list[member.key] = baggage.Item{
Value: member.value,
Properties: member.properties.asInternal(),
}
return Baggage{list: list}, nil
}
// DeleteMember returns a copy of the Baggage with the list-member identified
// by key removed.
func (b Baggage) DeleteMember(key string) Baggage {
n := len(b.list)
if _, ok := b.list[key]; ok {
n--
}
list := make(baggage.List, n)
for k, v := range b.list {
if k == key {
continue
}
list[k] = v
}
return Baggage{list: list}
}
// Len returns the number of list-members in the Baggage.
func (b Baggage) Len() int {
return len(b.list)
}
// String encodes Baggage into a string compliant with the W3C Baggage
// specification. The returned string will be invalid if the Baggage contains
// any invalid list-members.
func (b Baggage) String() string {
members := make([]string, 0, len(b.list))
for k, v := range b.list {
members = append(members, Member{
key: k,
value: v.Value,
properties: fromInternalProperties(v.Properties),
}.String())
}
return strings.Join(members, listDelimiter)
}

View file

@ -0,0 +1,45 @@
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/c21b6b6bb31a2f74edd06e262f1690f3f6ea3d5c/internal/baggage/baggage.go
//
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package baggage provides base types and functionality to store and retrieve
baggage in Go context. This package exists because the OpenTracing bridge to
OpenTelemetry needs to synchronize state whenever baggage for a context is
modified and that context contains an OpenTracing span. If it were not for
this need this package would not need to exist and the
`go.opentelemetry.io/otel/baggage` package would be the singular place where
W3C baggage is handled.
*/
package baggage
// List is the collection of baggage members. The W3C allows for duplicates,
// but OpenTelemetry does not, therefore, this is represented as a map.
type List map[string]Item
// Item is the value and metadata properties part of a list-member.
type Item struct {
Value string
Properties []Property
}
// Property is a metadata entry for a list-member.
type Property struct {
Key, Value string
// HasValue indicates if a zero-value value means the property does not
// have a value or if it was the zero-value.
HasValue bool
}

View file

@ -0,0 +1,46 @@
package ratelimit
import (
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Reference:
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-common/src/constants.rs#L116-L127
// Category classifies supported payload types that can be ingested by Sentry
// and, therefore, rate limited.
type Category string
// Known rate limit categories. As a special case, the CategoryAll applies to
// all known payload types.
const (
CategoryAll Category = ""
CategoryError Category = "error"
CategoryTransaction Category = "transaction"
)
// knownCategories is the set of currently known categories. Other categories
// are ignored for the purpose of rate-limiting.
var knownCategories = map[Category]struct{}{
CategoryAll: {},
CategoryError: {},
CategoryTransaction: {},
}
// String returns the category formatted for debugging.
func (c Category) String() string {
switch c {
case "":
return "CategoryAll"
default:
caser := cases.Title(language.English)
rv := "Category"
for _, w := range strings.Fields(string(c)) {
rv += caser.String(w)
}
return rv
}
}

View file

@ -0,0 +1,22 @@
package ratelimit
import "time"
// A Deadline is a time instant when a rate limit expires.
type Deadline time.Time
// After reports whether the deadline d is after other.
func (d Deadline) After(other Deadline) bool {
return time.Time(d).After(time.Time(other))
}
// Equal reports whether d and e represent the same deadline.
func (d Deadline) Equal(e Deadline) bool {
return time.Time(d).Equal(time.Time(e))
}
// String returns the deadline formatted for debugging.
func (d Deadline) String() string {
// Like time.Time.String, but without the monotonic clock reading.
return time.Time(d).Round(0).String()
}

View file

@ -0,0 +1,3 @@
// Package ratelimit provides tools to work with rate limits imposed by Sentry's
// data ingestion pipeline.
package ratelimit

View file

@ -0,0 +1,64 @@
package ratelimit
import (
"net/http"
"time"
)
// Map maps categories to rate limit deadlines.
//
// A rate limit is in effect for a given category if either the category's
// deadline or the deadline for the special CategoryAll has not yet expired.
//
// Use IsRateLimited to check whether a category is rate-limited.
type Map map[Category]Deadline
// IsRateLimited returns true if the category is currently rate limited.
func (m Map) IsRateLimited(c Category) bool {
return m.isRateLimited(c, time.Now())
}
func (m Map) isRateLimited(c Category, now time.Time) bool {
return m.Deadline(c).After(Deadline(now))
}
// Deadline returns the deadline when the rate limit for the given category or
// the special CategoryAll expire, whichever is furthest into the future.
func (m Map) Deadline(c Category) Deadline {
categoryDeadline := m[c]
allDeadline := m[CategoryAll]
if categoryDeadline.After(allDeadline) {
return categoryDeadline
}
return allDeadline
}
// Merge merges the other map into m.
//
// If a category appears in both maps, the deadline that is furthest into the
// future is preserved.
func (m Map) Merge(other Map) {
for c, d := range other {
if d.After(m[c]) {
m[c] = d
}
}
}
// FromResponse returns a rate limit map from an HTTP response.
func FromResponse(r *http.Response) Map {
return fromResponse(r, time.Now())
}
func fromResponse(r *http.Response, now time.Time) Map {
s := r.Header.Get("X-Sentry-Rate-Limits")
if s != "" {
return parseXSentryRateLimits(s, now)
}
if r.StatusCode == http.StatusTooManyRequests {
s := r.Header.Get("Retry-After")
deadline, _ := parseRetryAfter(s, now)
return Map{CategoryAll: deadline}
}
return Map{}
}

View file

@ -0,0 +1,76 @@
package ratelimit
import (
"errors"
"math"
"strconv"
"strings"
"time"
)
var errInvalidXSRLRetryAfter = errors.New("invalid retry-after value")
// parseXSentryRateLimits returns a RateLimits map by parsing an input string in
// the format of the X-Sentry-Rate-Limits header.
//
// Example
//
// X-Sentry-Rate-Limits: 60:transaction, 2700:default;error;security
//
// This will rate limit transactions for the next 60 seconds and errors for the
// next 2700 seconds.
//
// Limits for unknown categories are ignored.
func parseXSentryRateLimits(s string, now time.Time) Map {
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-server/src/utils/rate_limits.rs#L44-L82
m := make(Map, len(knownCategories))
for _, limit := range strings.Split(s, ",") {
limit = strings.TrimSpace(limit)
if limit == "" {
continue
}
components := strings.Split(limit, ":")
if len(components) == 0 {
continue
}
retryAfter, err := parseXSRLRetryAfter(strings.TrimSpace(components[0]), now)
if err != nil {
continue
}
categories := ""
if len(components) > 1 {
categories = components[1]
}
for _, category := range strings.Split(categories, ";") {
c := Category(strings.ToLower(strings.TrimSpace(category)))
if _, ok := knownCategories[c]; !ok {
// skip unknown categories, keep m small
continue
}
// always keep the deadline furthest into the future
if retryAfter.After(m[c]) {
m[c] = retryAfter
}
}
}
return m
}
// parseXSRLRetryAfter parses a string into a retry-after rate limit deadline.
//
// Valid input is a number, possibly signed and possibly floating-point,
// indicating the number of seconds to wait before sending another request.
// Negative values are treated as zero. Fractional values are rounded to the
// next integer.
func parseXSRLRetryAfter(s string, now time.Time) (Deadline, error) {
// https://github.com/getsentry/relay/blob/0424a2e017d193a93918053c90cdae9472d164bf/relay-quotas/src/rate_limit.rs#L88-L96
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return Deadline{}, errInvalidXSRLRetryAfter
}
d := time.Duration(math.Ceil(math.Max(f, 0.0))) * time.Second
if d < 0 {
d = 0
}
return Deadline(now.Add(d)), nil
}

View file

@ -0,0 +1,40 @@
package ratelimit
import (
"errors"
"strconv"
"time"
)
const defaultRetryAfter = 1 * time.Minute
var errInvalidRetryAfter = errors.New("invalid input")
// parseRetryAfter parses a string s as in the standard Retry-After HTTP header
// and returns a deadline until when requests are rate limited and therefore new
// requests should not be sent. The input may be either a date or a non-negative
// integer number of seconds.
//
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
//
// parseRetryAfter always returns a usable deadline, even in case of an error.
//
// This is the original rate limiting mechanism used by Sentry, superseeded by
// the X-Sentry-Rate-Limits response header.
func parseRetryAfter(s string, now time.Time) (Deadline, error) {
if s == "" {
goto invalid
}
if n, err := strconv.Atoi(s); err == nil {
if n < 0 {
goto invalid
}
d := time.Duration(n) * time.Second
return Deadline(now.Add(d)), nil
}
if date, err := time.Parse(time.RFC1123, s); err == nil {
return Deadline(date), nil
}
invalid:
return Deadline(now.Add(defaultRetryAfter)), errInvalidRetryAfter
}

View file

@ -0,0 +1,15 @@
## Benchmark results
```
goos: windows
goarch: amd64
pkg: github.com/getsentry/sentry-go/internal/trace
cpu: 12th Gen Intel(R) Core(TM) i7-12700K
BenchmarkEqualBytes-20 44323621 26.08 ns/op
BenchmarkStringEqual-20 60980257 18.27 ns/op
BenchmarkEqualPrefix-20 41369181 31.12 ns/op
BenchmarkFullParse-20 702012 1507 ns/op 1353.42 MB/s 1024 B/op 6 allocs/op
BenchmarkFramesIterator-20 1229971 969.3 ns/op 896 B/op 5 allocs/op
BenchmarkFramesReversedIterator-20 1271061 944.5 ns/op 896 B/op 5 allocs/op
BenchmarkSplitOnly-20 2250800 534.0 ns/op 3818.23 MB/s 128 B/op 1 allocs/op
```

View file

@ -0,0 +1,217 @@
package traceparser
import (
"bytes"
"strconv"
)
var blockSeparator = []byte("\n\n")
var lineSeparator = []byte("\n")
// Parses multi-stacktrace text dump produced by runtime.Stack([]byte, all=true).
// The parser prioritizes performance but requires the input to be well-formed in order to return correct data.
// See https://github.com/golang/go/blob/go1.20.4/src/runtime/mprof.go#L1191
func Parse(data []byte) TraceCollection {
var it = TraceCollection{}
if len(data) > 0 {
it.blocks = bytes.Split(data, blockSeparator)
}
return it
}
type TraceCollection struct {
blocks [][]byte
}
func (it TraceCollection) Length() int {
return len(it.blocks)
}
// Returns the stacktrace item at the given index.
func (it *TraceCollection) Item(i int) Trace {
// The first item may have a leading data separator and the last one may have a trailing one.
// Note: Trim() doesn't make a copy for single-character cutset under 0x80. It will just slice the original.
var data []byte
switch {
case i == 0:
data = bytes.TrimLeft(it.blocks[i], "\n")
case i == len(it.blocks)-1:
data = bytes.TrimRight(it.blocks[i], "\n")
default:
data = it.blocks[i]
}
var splitAt = bytes.IndexByte(data, '\n')
if splitAt < 0 {
return Trace{header: data}
}
return Trace{
header: data[:splitAt],
data: data[splitAt+1:],
}
}
// Trace represents a single stacktrace block, identified by a Goroutine ID and a sequence of Frames.
type Trace struct {
header []byte
data []byte
}
var goroutinePrefix = []byte("goroutine ")
// GoID parses the Goroutine ID from the header.
func (t *Trace) GoID() (id uint64) {
if bytes.HasPrefix(t.header, goroutinePrefix) {
var line = t.header[len(goroutinePrefix):]
var splitAt = bytes.IndexByte(line, ' ')
if splitAt >= 0 {
id, _ = strconv.ParseUint(string(line[:splitAt]), 10, 64)
}
}
return id
}
// UniqueIdentifier can be used as a map key to identify the trace.
func (t *Trace) UniqueIdentifier() []byte {
return t.data
}
func (t *Trace) Frames() FrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return FrameIterator{lines: lines, i: 0, len: len(lines)}
}
func (t *Trace) FramesReversed() ReverseFrameIterator {
var lines = bytes.Split(t.data, lineSeparator)
return ReverseFrameIterator{lines: lines, i: len(lines)}
}
const framesElided = "...additional frames elided..."
// FrameIterator iterates over stack frames.
type FrameIterator struct {
lines [][]byte
i int
len int
}
// Next returns the next frame, or nil if there are none.
func (it *FrameIterator) Next() Frame {
return Frame{it.popLine(), it.popLine()}
}
func (it *FrameIterator) popLine() []byte {
switch {
case it.i >= it.len:
return nil
case string(it.lines[it.i]) == framesElided:
it.i++
return it.popLine()
default:
it.i++
return it.lines[it.i-1]
}
}
// HasNext return true if there are values to be read.
func (it *FrameIterator) HasNext() bool {
return it.i < it.len
}
// LengthUpperBound returns the maximum number of elements this stacks may contain.
// The actual number may be lower because of elided frames. As such, the returned value
// cannot be used to iterate over the frames but may be used to reserve capacity.
func (it *FrameIterator) LengthUpperBound() int {
return it.len / 2
}
// ReverseFrameIterator iterates over stack frames in reverse order.
type ReverseFrameIterator struct {
lines [][]byte
i int
}
// Next returns the next frame, or nil if there are none.
func (it *ReverseFrameIterator) Next() Frame {
var line2 = it.popLine()
return Frame{it.popLine(), line2}
}
func (it *ReverseFrameIterator) popLine() []byte {
it.i--
switch {
case it.i < 0:
return nil
case string(it.lines[it.i]) == framesElided:
return it.popLine()
default:
return it.lines[it.i]
}
}
// HasNext return true if there are values to be read.
func (it *ReverseFrameIterator) HasNext() bool {
return it.i > 1
}
// LengthUpperBound returns the maximum number of elements this stacks may contain.
// The actual number may be lower because of elided frames. As such, the returned value
// cannot be used to iterate over the frames but may be used to reserve capacity.
func (it *ReverseFrameIterator) LengthUpperBound() int {
return len(it.lines) / 2
}
type Frame struct {
line1 []byte
line2 []byte
}
// UniqueIdentifier can be used as a map key to identify the frame.
func (f *Frame) UniqueIdentifier() []byte {
// line2 contains file path, line number and program-counter offset from the beginning of a function
// e.g. C:/Users/name/scoop/apps/go/current/src/testing/testing.go:1906 +0x63a
return f.line2
}
var createdByPrefix = []byte("created by ")
func (f *Frame) Func() []byte {
if bytes.HasPrefix(f.line1, createdByPrefix) {
// Since go1.21, the line ends with " in goroutine X", saying which goroutine created this one.
// We currently don't have use for that so just remove it.
var line = f.line1[len(createdByPrefix):]
var spaceAt = bytes.IndexByte(line, ' ')
if spaceAt < 0 {
return line
}
return line[:spaceAt]
}
var end = bytes.LastIndexByte(f.line1, '(')
if end >= 0 {
return f.line1[:end]
}
return f.line1
}
func (f *Frame) File() (path []byte, lineNumber int) {
var line = f.line2
if len(line) > 0 && line[0] == '\t' {
line = line[1:]
}
var splitAt = bytes.IndexByte(line, ' ')
if splitAt >= 0 {
line = line[:splitAt]
}
splitAt = bytes.LastIndexByte(line, ':')
if splitAt < 0 {
return line, 0
}
lineNumber, _ = strconv.Atoi(string(line[splitAt+1:]))
return line[:splitAt], lineNumber
}

View file

@ -0,0 +1,73 @@
package sentry
// Based on https://github.com/getsentry/vroom/blob/d11c26063e802d66b9a592c4010261746ca3dfa4/internal/sample/sample.go
import (
"time"
)
type (
profileDevice struct {
Architecture string `json:"architecture"`
Classification string `json:"classification"`
Locale string `json:"locale"`
Manufacturer string `json:"manufacturer"`
Model string `json:"model"`
}
profileOS struct {
BuildNumber string `json:"build_number"`
Name string `json:"name"`
Version string `json:"version"`
}
profileRuntime struct {
Name string `json:"name"`
Version string `json:"version"`
}
profileSample struct {
ElapsedSinceStartNS uint64 `json:"elapsed_since_start_ns"`
StackID int `json:"stack_id"`
ThreadID uint64 `json:"thread_id"`
}
profileThreadMetadata struct {
Name string `json:"name,omitempty"`
Priority int `json:"priority,omitempty"`
}
profileStack []int
profileTrace struct {
Frames []*Frame `json:"frames"`
Samples []profileSample `json:"samples"`
Stacks []profileStack `json:"stacks"`
ThreadMetadata map[uint64]*profileThreadMetadata `json:"thread_metadata"`
}
profileInfo struct {
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Device profileDevice `json:"device"`
Environment string `json:"environment,omitempty"`
EventID string `json:"event_id"`
OS profileOS `json:"os"`
Platform string `json:"platform"`
Release string `json:"release"`
Dist string `json:"dist"`
Runtime profileRuntime `json:"runtime"`
Timestamp time.Time `json:"timestamp"`
Trace *profileTrace `json:"profile"`
Transaction profileTransaction `json:"transaction"`
Version string `json:"version"`
}
// see https://github.com/getsentry/vroom/blob/a91e39416723ec44fc54010257020eeaf9a77cbd/internal/transaction/transaction.go
profileTransaction struct {
ActiveThreadID uint64 `json:"active_thread_id"`
DurationNS uint64 `json:"duration_ns,omitempty"`
ID EventID `json:"id"`
Name string `json:"name"`
TraceID string `json:"trace_id"`
}
)

451
vendor/github.com/getsentry/sentry-go/profiler.go generated vendored Normal file
View file

@ -0,0 +1,451 @@
package sentry
import (
"container/ring"
"strconv"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/getsentry/sentry-go/internal/traceparser"
)
// Start a profiler that collects samples continuously, with a buffer of up to 30 seconds.
// Later, you can collect a slice from this buffer, producing a Trace.
func startProfiling(startTime time.Time) profiler {
onProfilerStart()
p := newProfiler(startTime)
// Wait for the profiler to finish setting up before returning to the caller.
started := make(chan struct{})
go p.run(started)
if _, ok := <-started; ok {
return p
}
return nil
}
type profiler interface {
// GetSlice returns a slice of the profiled data between the given times.
GetSlice(startTime, endTime time.Time) *profilerResult
Stop(wait bool)
}
type profilerResult struct {
callerGoID uint64
trace *profileTrace
}
func getCurrentGoID() uint64 {
// We shouldn't panic but let's be super safe.
defer func() {
if err := recover(); err != nil {
Logger.Printf("Profiler panic in getCurrentGoID(): %v\n", err)
}
}()
// Buffer to read the stack trace into. We should be good with a small buffer because we only need the first line.
var stacksBuffer = make([]byte, 100)
var n = runtime.Stack(stacksBuffer, false)
if n > 0 {
var traces = traceparser.Parse(stacksBuffer[0:n])
if traces.Length() > 0 {
var trace = traces.Item(0)
return trace.GoID()
}
}
return 0
}
const profilerSamplingRateHz = 101 // 101 Hz; not 100 Hz because of the lockstep sampling (https://stackoverflow.com/a/45471031/1181370)
const profilerSamplingRate = time.Second / profilerSamplingRateHz
const stackBufferMaxGrowth = 512 * 1024
const stackBufferLimit = 10 * 1024 * 1024
const profilerRuntimeLimit = 30 // seconds
type profileRecorder struct {
startTime time.Time
stopSignal chan struct{}
stopped int64
mutex sync.RWMutex
testProfilerPanic int64
// Map from runtime.StackRecord.Stack0 to an index in stacks.
stackIndexes map[string]int
stacks []profileStack
newStacks []profileStack // New stacks created in the current interation.
stackKeyBuffer []byte
// Map from runtime.Frame.PC to an index in frames.
frameIndexes map[string]int
frames []*Frame
newFrames []*Frame // New frames created in the current interation.
// We keep a ring buffer of 30 seconds worth of samples, so that we can later slice it.
// Each bucket is a slice of samples all taken at the same time.
samplesBucketsHead *ring.Ring
// Buffer to read current stacks - will grow automatically up to stackBufferLimit.
stacksBuffer []byte
}
func newProfiler(startTime time.Time) *profileRecorder {
// Pre-allocate the profile trace for the currently active number of routines & 100 ms worth of samples.
// Other coefficients are just guesses of what might be a good starting point to avoid allocs on short runs.
return &profileRecorder{
startTime: startTime,
stopSignal: make(chan struct{}, 1),
stackIndexes: make(map[string]int, 32),
stacks: make([]profileStack, 0, 32),
newStacks: make([]profileStack, 0, 32),
frameIndexes: make(map[string]int, 128),
frames: make([]*Frame, 0, 128),
newFrames: make([]*Frame, 0, 128),
samplesBucketsHead: ring.New(profilerRuntimeLimit * profilerSamplingRateHz),
// A buffer of 2 KiB per goroutine stack looks like a good starting point (empirically determined).
stacksBuffer: make([]byte, runtime.NumGoroutine()*2048),
}
}
// This allows us to test whether panic during profiling are handled correctly and don't block execution.
// If the number is lower than 0, profilerGoroutine() will panic immedately.
// If the number is higher than 0, profiler.onTick() will panic when the given samples-set index is being collected.
var testProfilerPanic int64
var profilerRunning int64
func (p *profileRecorder) run(started chan struct{}) {
// Code backup for manual test debugging:
// if !atomic.CompareAndSwapInt64(&profilerRunning, 0, 1) {
// panic("Only one profiler can be running at a time")
// }
// We shouldn't panic but let's be super safe.
defer func() {
if err := recover(); err != nil {
Logger.Printf("Profiler panic in run(): %v\n", err)
}
atomic.StoreInt64(&testProfilerPanic, 0)
close(started)
p.stopSignal <- struct{}{}
atomic.StoreInt64(&p.stopped, 1)
atomic.StoreInt64(&profilerRunning, 0)
}()
p.testProfilerPanic = atomic.LoadInt64(&testProfilerPanic)
if p.testProfilerPanic < 0 {
Logger.Printf("Profiler panicking during startup because testProfilerPanic == %v\n", p.testProfilerPanic)
panic("This is an expected panic in profilerGoroutine() during tests")
}
// Collect the first sample immediately.
p.onTick()
// Periodically collect stacks, starting after profilerSamplingRate has passed.
collectTicker := profilerTickerFactory(profilerSamplingRate)
defer collectTicker.Stop()
var tickerChannel = collectTicker.TickSource()
started <- struct{}{}
for {
select {
case <-tickerChannel:
p.onTick()
collectTicker.Ticked()
case <-p.stopSignal:
return
}
}
}
func (p *profileRecorder) Stop(wait bool) {
if atomic.LoadInt64(&p.stopped) == 1 {
return
}
p.stopSignal <- struct{}{}
if wait {
<-p.stopSignal
}
}
func (p *profileRecorder) GetSlice(startTime, endTime time.Time) *profilerResult {
// Unlikely edge cases - profiler wasn't running at all or the given times are invalid in relation to each other.
if p.startTime.After(endTime) || startTime.After(endTime) {
return nil
}
var relativeStartNS = uint64(0)
if p.startTime.Before(startTime) {
relativeStartNS = uint64(startTime.Sub(p.startTime).Nanoseconds())
}
var relativeEndNS = uint64(endTime.Sub(p.startTime).Nanoseconds())
samplesCount, bucketsReversed, trace := p.getBuckets(relativeStartNS, relativeEndNS)
if samplesCount == 0 {
return nil
}
var result = &profilerResult{
callerGoID: getCurrentGoID(),
trace: trace,
}
trace.Samples = make([]profileSample, samplesCount)
trace.ThreadMetadata = make(map[uint64]*profileThreadMetadata, len(bucketsReversed[0].goIDs))
var s = samplesCount - 1
for _, bucket := range bucketsReversed {
var elapsedSinceStartNS = bucket.relativeTimeNS - relativeStartNS
for i, goID := range bucket.goIDs {
trace.Samples[s].ElapsedSinceStartNS = elapsedSinceStartNS
trace.Samples[s].ThreadID = goID
trace.Samples[s].StackID = bucket.stackIDs[i]
s--
if _, goroutineExists := trace.ThreadMetadata[goID]; !goroutineExists {
trace.ThreadMetadata[goID] = &profileThreadMetadata{
Name: "Goroutine " + strconv.FormatUint(goID, 10),
}
}
}
}
return result
}
// Collect all buckets of samples in the given time range while holding a read lock.
func (p *profileRecorder) getBuckets(relativeStartNS, relativeEndNS uint64) (samplesCount int, buckets []*profileSamplesBucket, trace *profileTrace) {
p.mutex.RLock()
defer p.mutex.RUnlock()
// sampleBucketsHead points at the last stored bucket so it's a good starting point to search backwards for the end.
var end = p.samplesBucketsHead
for end.Value != nil && end.Value.(*profileSamplesBucket).relativeTimeNS > relativeEndNS {
end = end.Prev()
}
// Edge case - no items stored before the given endTime.
if end.Value == nil {
return 0, nil, nil
}
{ // Find the first item after the given startTime.
var start = end
var prevBucket *profileSamplesBucket
samplesCount = 0
buckets = make([]*profileSamplesBucket, 0, int64((relativeEndNS-relativeStartNS)/uint64(profilerSamplingRate.Nanoseconds()))+1)
for start.Value != nil {
var bucket = start.Value.(*profileSamplesBucket)
// If this bucket's time is before the requests start time, don't collect it (and stop iterating further).
if bucket.relativeTimeNS < relativeStartNS {
break
}
// If this bucket time is greater than previous the bucket's time, we have exhausted the whole ring buffer
// before we were able to find the start time. That means the start time is not present and we must break.
// This happens if the slice duration exceeds the ring buffer capacity.
if prevBucket != nil && bucket.relativeTimeNS > prevBucket.relativeTimeNS {
break
}
samplesCount += len(bucket.goIDs)
buckets = append(buckets, bucket)
start = start.Prev()
prevBucket = bucket
}
}
// Edge case - if the period requested was too short and we haven't collected enough samples.
if len(buckets) < 2 {
return 0, nil, nil
}
trace = &profileTrace{
Frames: p.frames,
Stacks: p.stacks,
}
return samplesCount, buckets, trace
}
func (p *profileRecorder) onTick() {
elapsedNs := time.Since(p.startTime).Nanoseconds()
if p.testProfilerPanic > 0 {
Logger.Printf("Profiler testProfilerPanic == %v\n", p.testProfilerPanic)
if p.testProfilerPanic == 1 {
Logger.Println("Profiler panicking onTick()")
panic("This is an expected panic in Profiler.OnTick() during tests")
}
p.testProfilerPanic--
}
records := p.collectRecords()
p.processRecords(uint64(elapsedNs), records)
// Free up some memory if we don't need such a large buffer anymore.
if len(p.stacksBuffer) > len(records)*3 {
p.stacksBuffer = make([]byte, len(records)*3)
}
}
func (p *profileRecorder) collectRecords() []byte {
for {
// Capture stacks for all existing goroutines.
// Note: runtime.GoroutineProfile() would be better but we can't use it at the moment because
// it doesn't give us `gid` for each routine, see https://github.com/golang/go/issues/59663
n := runtime.Stack(p.stacksBuffer, true)
// If we couldn't read everything, increase the buffer and try again.
if n >= len(p.stacksBuffer) && n < stackBufferLimit {
var newSize = n * 2
if newSize > n+stackBufferMaxGrowth {
newSize = n + stackBufferMaxGrowth
}
if newSize > stackBufferLimit {
newSize = stackBufferLimit
}
p.stacksBuffer = make([]byte, newSize)
} else {
return p.stacksBuffer[0:n]
}
}
}
func (p *profileRecorder) processRecords(elapsedNs uint64, stacksBuffer []byte) {
var traces = traceparser.Parse(stacksBuffer)
var length = traces.Length()
// Shouldn't happen but let's be safe and don't store empty buckets.
if length == 0 {
return
}
var bucket = &profileSamplesBucket{
relativeTimeNS: elapsedNs,
stackIDs: make([]int, length),
goIDs: make([]uint64, length),
}
// reset buffers
p.newFrames = p.newFrames[:0]
p.newStacks = p.newStacks[:0]
for i := 0; i < length; i++ {
var stack = traces.Item(i)
bucket.stackIDs[i] = p.addStackTrace(stack)
bucket.goIDs[i] = stack.GoID()
}
p.mutex.Lock()
defer p.mutex.Unlock()
p.stacks = append(p.stacks, p.newStacks...)
p.frames = append(p.frames, p.newFrames...)
p.samplesBucketsHead = p.samplesBucketsHead.Next()
p.samplesBucketsHead.Value = bucket
}
func (p *profileRecorder) addStackTrace(capturedStack traceparser.Trace) int {
iter := capturedStack.Frames()
stack := make(profileStack, 0, iter.LengthUpperBound())
// Originally, we've used `capturedStack.UniqueIdentifier()` as a key but that was incorrect because it also
// contains function arguments and we want to group stacks by function name and file/line only.
// Instead, we need to parse frames and we use a list of their indexes as a key.
// We reuse the same buffer for each stack to avoid allocations; this is a hot spot.
var expectedBufferLen = cap(stack) * 5 // 4 bytes per frame + 1 byte for space
if cap(p.stackKeyBuffer) < expectedBufferLen {
p.stackKeyBuffer = make([]byte, 0, expectedBufferLen)
} else {
p.stackKeyBuffer = p.stackKeyBuffer[:0]
}
for iter.HasNext() {
var frame = iter.Next()
if frameIndex := p.addFrame(frame); frameIndex >= 0 {
stack = append(stack, frameIndex)
p.stackKeyBuffer = append(p.stackKeyBuffer, 0) // space
// The following code is just like binary.AppendUvarint() which isn't yet available in Go 1.18.
x := uint64(frameIndex) + 1
for x >= 0x80 {
p.stackKeyBuffer = append(p.stackKeyBuffer, byte(x)|0x80)
x >>= 7
}
p.stackKeyBuffer = append(p.stackKeyBuffer, byte(x))
}
}
stackIndex, exists := p.stackIndexes[string(p.stackKeyBuffer)]
if !exists {
stackIndex = len(p.stacks) + len(p.newStacks)
p.newStacks = append(p.newStacks, stack)
p.stackIndexes[string(p.stackKeyBuffer)] = stackIndex
}
return stackIndex
}
func (p *profileRecorder) addFrame(capturedFrame traceparser.Frame) int {
// NOTE: Don't convert to string yet, it's expensive and compiler can avoid it when
// indexing into a map (only needs a copy when adding a new key to the map).
var key = capturedFrame.UniqueIdentifier()
frameIndex, exists := p.frameIndexes[string(key)]
if !exists {
module, function := splitQualifiedFunctionName(string(capturedFrame.Func()))
file, line := capturedFrame.File()
frame := newFrame(module, function, string(file), line)
frameIndex = len(p.frames) + len(p.newFrames)
p.newFrames = append(p.newFrames, &frame)
p.frameIndexes[string(key)] = frameIndex
}
return frameIndex
}
type profileSamplesBucket struct {
relativeTimeNS uint64
stackIDs []int
goIDs []uint64
}
// A Ticker holds a channel that delivers “ticks” of a clock at intervals.
type profilerTicker interface {
// Stop turns off a ticker. After Stop, no more ticks will be sent.
Stop()
// TickSource returns a read-only channel of ticks.
TickSource() <-chan time.Time
// Ticked is called by the Profiler after a tick is processed to notify the ticker. Used for testing.
Ticked()
}
type timeTicker struct {
*time.Ticker
}
func (t *timeTicker) TickSource() <-chan time.Time {
return t.C
}
func (t *timeTicker) Ticked() {}
func profilerTickerFactoryDefault(d time.Duration) profilerTicker {
return &timeTicker{time.NewTicker(d)}
}
// We allow overriding the ticker for tests. CI is terribly flaky
// because the time.Ticker doesn't guarantee regular ticks - they may come (a lot) later than the given interval.
var profilerTickerFactory = profilerTickerFactoryDefault

View file

@ -0,0 +1,5 @@
//go:build !windows
package sentry
func onProfilerStart() {}

View file

@ -0,0 +1,24 @@
package sentry
import (
"sync"
"syscall"
)
// This works around the ticker resolution on Windows being ~15ms by default.
// See https://github.com/golang/go/issues/44343
func setTimeTickerResolution() {
var winmmDLL = syscall.NewLazyDLL("winmm.dll")
if winmmDLL != nil {
var timeBeginPeriod = winmmDLL.NewProc("timeBeginPeriod")
if timeBeginPeriod != nil {
timeBeginPeriod.Call(uintptr(1))
}
}
}
var setupTickerResolutionOnce sync.Once
func onProfilerStart() {
setupTickerResolutionOnce.Do(setTimeTickerResolution)
}

443
vendor/github.com/getsentry/sentry-go/scope.go generated vendored Normal file
View file

@ -0,0 +1,443 @@
package sentry
import (
"bytes"
"io"
"net/http"
"sync"
"time"
)
// Scope holds contextual data for the current scope.
//
// The scope is an object that can cloned efficiently and stores data that is
// locally relevant to an event. For instance the scope will hold recorded
// breadcrumbs and similar information.
//
// The scope can be interacted with in two ways. First, the scope is routinely
// updated with information by functions such as AddBreadcrumb which will modify
// the current scope. Second, the current scope can be configured through the
// ConfigureScope function or Hub method of the same name.
//
// The scope is meant to be modified but not inspected directly. When preparing
// an event for reporting, the current client adds information from the current
// scope into the event.
type Scope struct {
mu sync.RWMutex
breadcrumbs []*Breadcrumb
attachments []*Attachment
user User
tags map[string]string
contexts map[string]Context
extra map[string]interface{}
fingerprint []string
level Level
request *http.Request
// requestBody holds a reference to the original request.Body.
requestBody interface {
// Bytes returns bytes from the original body, lazily buffered as the
// original body is read.
Bytes() []byte
// Overflow returns true if the body is larger than the maximum buffer
// size.
Overflow() bool
}
eventProcessors []EventProcessor
}
// NewScope creates a new Scope.
func NewScope() *Scope {
scope := Scope{
breadcrumbs: make([]*Breadcrumb, 0),
attachments: make([]*Attachment, 0),
tags: make(map[string]string),
contexts: make(map[string]Context),
extra: make(map[string]interface{}),
fingerprint: make([]string, 0),
}
return &scope
}
// AddBreadcrumb adds new breadcrumb to the current scope
// and optionally throws the old one if limit is reached.
func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) {
if breadcrumb.Timestamp.IsZero() {
breadcrumb.Timestamp = time.Now()
}
scope.mu.Lock()
defer scope.mu.Unlock()
scope.breadcrumbs = append(scope.breadcrumbs, breadcrumb)
if len(scope.breadcrumbs) > limit {
scope.breadcrumbs = scope.breadcrumbs[1 : limit+1]
}
}
// ClearBreadcrumbs clears all breadcrumbs from the current scope.
func (scope *Scope) ClearBreadcrumbs() {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.breadcrumbs = []*Breadcrumb{}
}
// AddAttachment adds new attachment to the current scope.
func (scope *Scope) AddAttachment(attachment *Attachment) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.attachments = append(scope.attachments, attachment)
}
// ClearAttachments clears all attachments from the current scope.
func (scope *Scope) ClearAttachments() {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.attachments = []*Attachment{}
}
// SetUser sets the user for the current scope.
func (scope *Scope) SetUser(user User) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.user = user
}
// SetRequest sets the request for the current scope.
func (scope *Scope) SetRequest(r *http.Request) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.request = r
if r == nil {
return
}
// Don't buffer request body if we know it is oversized.
if r.ContentLength > maxRequestBodyBytes {
return
}
// Don't buffer if there is no body.
if r.Body == nil || r.Body == http.NoBody {
return
}
buf := &limitedBuffer{Capacity: maxRequestBodyBytes}
r.Body = readCloser{
Reader: io.TeeReader(r.Body, buf),
Closer: r.Body,
}
scope.requestBody = buf
}
// SetRequestBody sets the request body for the current scope.
//
// This method should only be called when the body bytes are already available
// in memory. Typically, the request body is buffered lazily from the
// Request.Body from SetRequest.
func (scope *Scope) SetRequestBody(b []byte) {
scope.mu.Lock()
defer scope.mu.Unlock()
capacity := maxRequestBodyBytes
overflow := false
if len(b) > capacity {
overflow = true
b = b[:capacity]
}
scope.requestBody = &limitedBuffer{
Capacity: capacity,
Buffer: *bytes.NewBuffer(b),
overflow: overflow,
}
}
// maxRequestBodyBytes is the default maximum request body size to send to
// Sentry.
const maxRequestBodyBytes = 10 * 1024
// A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity
// bytes. Any writes past the capacity are silently discarded, similar to
// io.Discard.
type limitedBuffer struct {
Capacity int
bytes.Buffer
overflow bool
}
// Write implements io.Writer.
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
// Silently ignore writes after overflow.
if b.overflow {
return len(p), nil
}
left := b.Capacity - b.Len()
if left < 0 {
left = 0
}
if len(p) > left {
b.overflow = true
p = p[:left]
}
return b.Buffer.Write(p)
}
// Overflow returns true if the limitedBuffer discarded bytes written to it.
func (b *limitedBuffer) Overflow() bool {
return b.overflow
}
// readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser.
type readCloser struct {
io.Reader
io.Closer
}
// SetTag adds a tag to the current scope.
func (scope *Scope) SetTag(key, value string) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.tags[key] = value
}
// SetTags assigns multiple tags to the current scope.
func (scope *Scope) SetTags(tags map[string]string) {
scope.mu.Lock()
defer scope.mu.Unlock()
for k, v := range tags {
scope.tags[k] = v
}
}
// RemoveTag removes a tag from the current scope.
func (scope *Scope) RemoveTag(key string) {
scope.mu.Lock()
defer scope.mu.Unlock()
delete(scope.tags, key)
}
// SetContext adds a context to the current scope.
func (scope *Scope) SetContext(key string, value Context) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.contexts[key] = value
}
// SetContexts assigns multiple contexts to the current scope.
func (scope *Scope) SetContexts(contexts map[string]Context) {
scope.mu.Lock()
defer scope.mu.Unlock()
for k, v := range contexts {
scope.contexts[k] = v
}
}
// RemoveContext removes a context from the current scope.
func (scope *Scope) RemoveContext(key string) {
scope.mu.Lock()
defer scope.mu.Unlock()
delete(scope.contexts, key)
}
// SetExtra adds an extra to the current scope.
func (scope *Scope) SetExtra(key string, value interface{}) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.extra[key] = value
}
// SetExtras assigns multiple extras to the current scope.
func (scope *Scope) SetExtras(extra map[string]interface{}) {
scope.mu.Lock()
defer scope.mu.Unlock()
for k, v := range extra {
scope.extra[k] = v
}
}
// RemoveExtra removes a extra from the current scope.
func (scope *Scope) RemoveExtra(key string) {
scope.mu.Lock()
defer scope.mu.Unlock()
delete(scope.extra, key)
}
// SetFingerprint sets new fingerprint for the current scope.
func (scope *Scope) SetFingerprint(fingerprint []string) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.fingerprint = fingerprint
}
// SetLevel sets new level for the current scope.
func (scope *Scope) SetLevel(level Level) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.level = level
}
// Clone returns a copy of the current scope with all data copied over.
func (scope *Scope) Clone() *Scope {
scope.mu.RLock()
defer scope.mu.RUnlock()
clone := NewScope()
clone.user = scope.user
clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
copy(clone.breadcrumbs, scope.breadcrumbs)
clone.attachments = make([]*Attachment, len(scope.attachments))
copy(clone.attachments, scope.attachments)
for key, value := range scope.tags {
clone.tags[key] = value
}
for key, value := range scope.contexts {
clone.contexts[key] = cloneContext(value)
}
for key, value := range scope.extra {
clone.extra[key] = value
}
clone.fingerprint = make([]string, len(scope.fingerprint))
copy(clone.fingerprint, scope.fingerprint)
clone.level = scope.level
clone.request = scope.request
clone.requestBody = scope.requestBody
clone.eventProcessors = scope.eventProcessors
return clone
}
// Clear removes the data from the current scope. Not safe for concurrent use.
func (scope *Scope) Clear() {
*scope = *NewScope()
}
// AddEventProcessor adds an event processor to the current scope.
func (scope *Scope) AddEventProcessor(processor EventProcessor) {
scope.mu.Lock()
defer scope.mu.Unlock()
scope.eventProcessors = append(scope.eventProcessors, processor)
}
// ApplyToEvent takes the data from the current scope and attaches it to the event.
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
scope.mu.RLock()
defer scope.mu.RUnlock()
if len(scope.breadcrumbs) > 0 {
event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
}
if len(scope.attachments) > 0 {
event.attachments = append(event.attachments, scope.attachments...)
}
if len(scope.tags) > 0 {
if event.Tags == nil {
event.Tags = make(map[string]string, len(scope.tags))
}
for key, value := range scope.tags {
event.Tags[key] = value
}
}
if len(scope.contexts) > 0 {
if event.Contexts == nil {
event.Contexts = make(map[string]Context)
}
for key, value := range scope.contexts {
if key == "trace" && event.Type == transactionType {
// Do not override trace context of
// transactions, otherwise it breaks the
// transaction event representation.
// For error events, the trace context is used
// to link errors and traces/spans in Sentry.
continue
}
// Ensure we are not overwriting event fields
if _, ok := event.Contexts[key]; !ok {
event.Contexts[key] = cloneContext(value)
}
}
}
if len(scope.extra) > 0 {
if event.Extra == nil {
event.Extra = make(map[string]interface{}, len(scope.extra))
}
for key, value := range scope.extra {
event.Extra[key] = value
}
}
if event.User.IsEmpty() {
event.User = scope.user
}
if len(event.Fingerprint) == 0 {
event.Fingerprint = append(event.Fingerprint, scope.fingerprint...)
}
if scope.level != "" {
event.Level = scope.level
}
if event.Request == nil && scope.request != nil {
event.Request = NewRequest(scope.request)
// NOTE: The SDK does not attempt to send partial request body data.
//
// The reason being that Sentry's ingest pipeline and UI are optimized
// to show structured data. Additionally, tooling around PII scrubbing
// relies on structured data; truncated request bodies would create
// invalid payloads that are more prone to leaking PII data.
//
// Users can still send more data along their events if they want to,
// for example using Event.Extra.
if scope.requestBody != nil && !scope.requestBody.Overflow() {
event.Request.Data = string(scope.requestBody.Bytes())
}
}
for _, processor := range scope.eventProcessors {
id := event.EventID
event = processor(event, hint)
if event == nil {
Logger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
return nil
}
}
return event
}
// cloneContext returns a new context with keys and values copied from the passed one.
//
// Note: a new Context (map) is returned, but the function does NOT do
// a proper deep copy: if some context values are pointer types (e.g. maps),
// they won't be properly copied.
func cloneContext(c Context) Context {
res := Context{}
for k, v := range c {
res[k] = v
}
return res
}

133
vendor/github.com/getsentry/sentry-go/sentry.go generated vendored Normal file
View file

@ -0,0 +1,133 @@
package sentry
import (
"context"
"time"
)
// The version of the SDK.
const SDKVersion = "0.26.0"
// apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK.
const apiVersion = "7"
// Init initializes the SDK with options. The returned error is non-nil if
// options is invalid, for instance if a malformed DSN is provided.
func Init(options ClientOptions) error {
hub := CurrentHub()
client, err := NewClient(options)
if err != nil {
return err
}
hub.BindClient(client)
return nil
}
// AddBreadcrumb records a new breadcrumb.
//
// The total number of breadcrumbs that can be recorded are limited by the
// configuration on the client.
func AddBreadcrumb(breadcrumb *Breadcrumb) {
hub := CurrentHub()
hub.AddBreadcrumb(breadcrumb, nil)
}
// CaptureMessage captures an arbitrary message.
func CaptureMessage(message string) *EventID {
hub := CurrentHub()
return hub.CaptureMessage(message)
}
// CaptureException captures an error.
func CaptureException(exception error) *EventID {
hub := CurrentHub()
return hub.CaptureException(exception)
}
// CaptureCheckIn captures a (cron) monitor check-in.
func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
hub := CurrentHub()
return hub.CaptureCheckIn(checkIn, monitorConfig)
}
// CaptureEvent captures an event on the currently active client if any.
//
// The event must already be assembled. Typically code would instead use
// the utility methods like CaptureException. The return value is the
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
func CaptureEvent(event *Event) *EventID {
hub := CurrentHub()
return hub.CaptureEvent(event)
}
// Recover captures a panic.
func Recover() *EventID {
if err := recover(); err != nil {
hub := CurrentHub()
return hub.Recover(err)
}
return nil
}
// RecoverWithContext captures a panic and passes relevant context object.
func RecoverWithContext(ctx context.Context) *EventID {
if err := recover(); err != nil {
var hub *Hub
if HasHubOnContext(ctx) {
hub = GetHubFromContext(ctx)
} else {
hub = CurrentHub()
}
return hub.RecoverWithContext(ctx, err)
}
return nil
}
// WithScope is a shorthand for CurrentHub().WithScope.
func WithScope(f func(scope *Scope)) {
hub := CurrentHub()
hub.WithScope(f)
}
// ConfigureScope is a shorthand for CurrentHub().ConfigureScope.
func ConfigureScope(f func(scope *Scope)) {
hub := CurrentHub()
hub.ConfigureScope(f)
}
// PushScope is a shorthand for CurrentHub().PushScope.
func PushScope() {
hub := CurrentHub()
hub.PushScope()
}
// PopScope is a shorthand for CurrentHub().PopScope.
func PopScope() {
hub := CurrentHub()
hub.PopScope()
}
// Flush waits until the underlying Transport sends any buffered events to the
// Sentry server, blocking for at most the given timeout. It returns false if
// the timeout was reached. In that case, some events may not have been sent.
//
// Flush should be called before terminating the program to avoid
// unintentionally dropping events.
//
// Do not call Flush indiscriminately after every call to CaptureEvent,
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func Flush(timeout time.Duration) bool {
hub := CurrentHub()
return hub.Flush(timeout)
}
// LastEventID returns an ID of last captured event.
func LastEventID() EventID {
hub := CurrentHub()
return hub.LastEventID()
}

70
vendor/github.com/getsentry/sentry-go/sourcereader.go generated vendored Normal file
View file

@ -0,0 +1,70 @@
package sentry
import (
"bytes"
"os"
"sync"
)
type sourceReader struct {
mu sync.Mutex
cache map[string][][]byte
}
func newSourceReader() sourceReader {
return sourceReader{
cache: make(map[string][][]byte),
}
}
func (sr *sourceReader) readContextLines(filename string, line, context int) ([][]byte, int) {
sr.mu.Lock()
defer sr.mu.Unlock()
lines, ok := sr.cache[filename]
if !ok {
data, err := os.ReadFile(filename)
if err != nil {
sr.cache[filename] = nil
return nil, 0
}
lines = bytes.Split(data, []byte{'\n'})
sr.cache[filename] = lines
}
return sr.calculateContextLines(lines, line, context)
}
func (sr *sourceReader) calculateContextLines(lines [][]byte, line, context int) ([][]byte, int) {
// Stacktrace lines are 1-indexed, slices are 0-indexed
line--
// contextLine points to a line that caused an issue itself, in relation to
// returned slice.
contextLine := context
if lines == nil || line >= len(lines) || line < 0 {
return nil, 0
}
if context < 0 {
context = 0
contextLine = 0
}
start := line - context
if start < 0 {
contextLine += start
start = 0
}
end := line + context + 1
if end > len(lines) {
end = len(lines)
}
return lines[start:end], contextLine
}

56
vendor/github.com/getsentry/sentry-go/span_recorder.go generated vendored Normal file
View file

@ -0,0 +1,56 @@
package sentry
import (
"sync"
)
// A spanRecorder stores a span tree that makes up a transaction. Safe for
// concurrent use. It is okay to add child spans from multiple goroutines.
type spanRecorder struct {
mu sync.Mutex
spans []*Span
overflowOnce sync.Once
}
// record stores a span. The first stored span is assumed to be the root of a
// span tree.
func (r *spanRecorder) record(s *Span) {
maxSpans := defaultMaxSpans
if client := CurrentHub().Client(); client != nil {
maxSpans = client.options.MaxSpans
}
r.mu.Lock()
defer r.mu.Unlock()
if len(r.spans) >= maxSpans {
r.overflowOnce.Do(func() {
root := r.spans[0]
Logger.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d",
root.TraceID, root.SpanID, maxSpans)
})
// TODO(tracing): mark the transaction event in some way to
// communicate that spans were dropped.
return
}
r.spans = append(r.spans, s)
}
// root returns the first recorded span. Returns nil if none have been recorded.
func (r *spanRecorder) root() *Span {
r.mu.Lock()
defer r.mu.Unlock()
if len(r.spans) == 0 {
return nil
}
return r.spans[0]
}
// children returns a list of all recorded spans, except the root. Returns nil
// if there are no children.
func (r *spanRecorder) children() []*Span {
r.mu.Lock()
defer r.mu.Unlock()
if len(r.spans) < 2 {
return nil
}
return r.spans[1:]
}

381
vendor/github.com/getsentry/sentry-go/stacktrace.go generated vendored Normal file
View file

@ -0,0 +1,381 @@
package sentry
import (
"go/build"
"reflect"
"runtime"
"strings"
)
const unknown string = "unknown"
// The module download is split into two parts: downloading the go.mod and downloading the actual code.
// If you have dependencies only needed for tests, then they will show up in your go.mod,
// and go get will download their go.mods, but it will not download their code.
// The test-only dependencies get downloaded only when you need it, such as the first time you run go test.
//
// https://github.com/golang/go/issues/26913#issuecomment-411976222
// Stacktrace holds information about the frames of the stack.
type Stacktrace struct {
Frames []Frame `json:"frames,omitempty"`
FramesOmitted []uint `json:"frames_omitted,omitempty"`
}
// NewStacktrace creates a stacktrace using runtime.Callers.
func NewStacktrace() *Stacktrace {
pcs := make([]uintptr, 100)
n := runtime.Callers(1, pcs)
if n == 0 {
return nil
}
runtimeFrames := extractFrames(pcs[:n])
frames := createFrames(runtimeFrames)
stacktrace := Stacktrace{
Frames: frames,
}
return &stacktrace
}
// TODO: Make it configurable so that anyone can provide their own implementation?
// Use of reflection allows us to not have a hard dependency on any given
// package, so we don't have to import it.
// ExtractStacktrace creates a new Stacktrace based on the given error.
func ExtractStacktrace(err error) *Stacktrace {
method := extractReflectedStacktraceMethod(err)
var pcs []uintptr
if method.IsValid() {
pcs = extractPcs(method)
} else {
pcs = extractXErrorsPC(err)
}
if len(pcs) == 0 {
return nil
}
runtimeFrames := extractFrames(pcs)
frames := createFrames(runtimeFrames)
stacktrace := Stacktrace{
Frames: frames,
}
return &stacktrace
}
func extractReflectedStacktraceMethod(err error) reflect.Value {
errValue := reflect.ValueOf(err)
// https://github.com/go-errors/errors
methodStackFrames := errValue.MethodByName("StackFrames")
if methodStackFrames.IsValid() {
return methodStackFrames
}
// https://github.com/pkg/errors
methodStackTrace := errValue.MethodByName("StackTrace")
if methodStackTrace.IsValid() {
return methodStackTrace
}
// https://github.com/pingcap/errors
methodGetStackTracer := errValue.MethodByName("GetStackTracer")
if methodGetStackTracer.IsValid() {
stacktracer := methodGetStackTracer.Call(nil)[0]
stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
if stacktracerStackTrace.IsValid() {
return stacktracerStackTrace
}
}
return reflect.Value{}
}
func extractPcs(method reflect.Value) []uintptr {
var pcs []uintptr
stacktrace := method.Call(nil)[0]
if stacktrace.Kind() != reflect.Slice {
return nil
}
for i := 0; i < stacktrace.Len(); i++ {
pc := stacktrace.Index(i)
switch pc.Kind() {
case reflect.Uintptr:
pcs = append(pcs, uintptr(pc.Uint()))
case reflect.Struct:
for _, fieldName := range []string{"ProgramCounter", "PC"} {
field := pc.FieldByName(fieldName)
if !field.IsValid() {
continue
}
if field.Kind() == reflect.Uintptr {
pcs = append(pcs, uintptr(field.Uint()))
break
}
}
}
}
return pcs
}
// extractXErrorsPC extracts program counters from error values compatible with
// the error types from golang.org/x/xerrors.
//
// It returns nil if err is not compatible with errors from that package or if
// no program counters are stored in err.
func extractXErrorsPC(err error) []uintptr {
// This implementation uses the reflect package to avoid a hard dependency
// on third-party packages.
// We don't know if err matches the expected type. For simplicity, instead
// of trying to account for all possible ways things can go wrong, some
// assumptions are made and if they are violated the code will panic. We
// recover from any panic and ignore it, returning nil.
//nolint: errcheck
defer func() { recover() }()
field := reflect.ValueOf(err).Elem().FieldByName("frame") // type Frame struct{ frames [3]uintptr }
field = field.FieldByName("frames")
field = field.Slice(1, field.Len()) // drop first pc pointing to xerrors.New
pc := make([]uintptr, field.Len())
for i := 0; i < field.Len(); i++ {
pc[i] = uintptr(field.Index(i).Uint())
}
return pc
}
// Frame represents a function call and it's metadata. Frames are associated
// with a Stacktrace.
type Frame struct {
Function string `json:"function,omitempty"`
Symbol string `json:"symbol,omitempty"`
// Module is, despite the name, the Sentry protocol equivalent of a Go
// package's import path.
Module string `json:"module,omitempty"`
Filename string `json:"filename,omitempty"`
AbsPath string `json:"abs_path,omitempty"`
Lineno int `json:"lineno,omitempty"`
Colno int `json:"colno,omitempty"`
PreContext []string `json:"pre_context,omitempty"`
ContextLine string `json:"context_line,omitempty"`
PostContext []string `json:"post_context,omitempty"`
InApp bool `json:"in_app"`
Vars map[string]interface{} `json:"vars,omitempty"`
// Package and the below are not used for Go stack trace frames. In
// other platforms it refers to a container where the Module can be
// found. For example, a Java JAR, a .NET Assembly, or a native
// dynamic library. They exists for completeness, allowing the
// construction and reporting of custom event payloads.
Package string `json:"package,omitempty"`
InstructionAddr string `json:"instruction_addr,omitempty"`
AddrMode string `json:"addr_mode,omitempty"`
SymbolAddr string `json:"symbol_addr,omitempty"`
ImageAddr string `json:"image_addr,omitempty"`
Platform string `json:"platform,omitempty"`
StackStart bool `json:"stack_start,omitempty"`
}
// NewFrame assembles a stacktrace frame out of runtime.Frame.
func NewFrame(f runtime.Frame) Frame {
function := f.Function
var pkg string
if function != "" {
pkg, function = splitQualifiedFunctionName(function)
}
return newFrame(pkg, function, f.File, f.Line)
}
// Like filepath.IsAbs() but doesn't care what platform you run this on.
// I.e. it also recognizies `/path/to/file` when run on Windows.
func isAbsPath(path string) bool {
if len(path) == 0 {
return false
}
// If the volume name starts with a double slash, this is an absolute path.
if len(path) >= 1 && (path[0] == '/' || path[0] == '\\') {
return true
}
// Windows absolute path, see https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
if len(path) >= 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') {
return true
}
return false
}
func newFrame(module string, function string, file string, line int) Frame {
frame := Frame{
Lineno: line,
Module: module,
Function: function,
}
switch {
case len(file) == 0:
frame.Filename = unknown
// Leave abspath as the empty string to be omitted when serializing event as JSON.
case isAbsPath(file):
frame.AbsPath = file
// TODO: in the general case, it is not trivial to come up with a
// "project relative" path with the data we have in run time.
// We shall not use filepath.Base because it creates ambiguous paths and
// affects the "Suspect Commits" feature.
// For now, leave relpath empty to be omitted when serializing the event
// as JSON. Improve this later.
default:
// f.File is a relative path. This may happen when the binary is built
// with the -trimpath flag.
frame.Filename = file
// Omit abspath when serializing the event as JSON.
}
setInAppFrame(&frame)
return frame
}
// splitQualifiedFunctionName splits a package path-qualified function name into
// package name and function name. Such qualified names are found in
// runtime.Frame.Function values.
func splitQualifiedFunctionName(name string) (pkg string, fun string) {
pkg = packageName(name)
if len(pkg) > 0 {
fun = name[len(pkg)+1:]
}
return
}
func extractFrames(pcs []uintptr) []runtime.Frame {
var frames = make([]runtime.Frame, 0, len(pcs))
callersFrames := runtime.CallersFrames(pcs)
for {
callerFrame, more := callersFrames.Next()
frames = append(frames, callerFrame)
if !more {
break
}
}
// TODO don't append and reverse, put in the right place from the start.
// reverse
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
return frames
}
// createFrames creates Frame objects while filtering out frames that are not
// meant to be reported to Sentry, those are frames internal to the SDK or Go.
func createFrames(frames []runtime.Frame) []Frame {
if len(frames) == 0 {
return nil
}
result := make([]Frame, 0, len(frames))
for _, frame := range frames {
function := frame.Function
var pkg string
if function != "" {
pkg, function = splitQualifiedFunctionName(function)
}
if !shouldSkipFrame(pkg) {
result = append(result, newFrame(pkg, function, frame.File, frame.Line))
}
}
return result
}
// TODO ID: why do we want to do this?
// I'm not aware of other SDKs skipping all Sentry frames, regardless of their position in the stactrace.
// For example, in the .NET SDK, only the first frames are skipped until the call to the SDK.
// As is, this will also hide any intermediate frames in the stack and make debugging issues harder.
func shouldSkipFrame(module string) bool {
// Skip Go internal frames.
if module == "runtime" || module == "testing" {
return true
}
// Skip Sentry internal frames, except for frames in _test packages (for testing).
if strings.HasPrefix(module, "github.com/getsentry/sentry-go") &&
!strings.HasSuffix(module, "_test") {
return true
}
return false
}
// On Windows, GOROOT has backslashes, but we want forward slashes.
var goRoot = strings.ReplaceAll(build.Default.GOROOT, "\\", "/")
func setInAppFrame(frame *Frame) {
if strings.HasPrefix(frame.AbsPath, goRoot) ||
strings.Contains(frame.Module, "vendor") ||
strings.Contains(frame.Module, "third_party") {
frame.InApp = false
} else {
frame.InApp = true
}
}
func callerFunctionName() string {
pcs := make([]uintptr, 1)
runtime.Callers(3, pcs)
callersFrames := runtime.CallersFrames(pcs)
callerFrame, _ := callersFrames.Next()
return baseName(callerFrame.Function)
}
// packageName returns the package part of the symbol name, or the empty string
// if there is none.
// It replicates https://golang.org/pkg/debug/gosym/#Sym.PackageName, avoiding a
// dependency on debug/gosym.
func packageName(name string) string {
if isCompilerGeneratedSymbol(name) {
return ""
}
pathend := strings.LastIndex(name, "/")
if pathend < 0 {
pathend = 0
}
if i := strings.Index(name[pathend:], "."); i != -1 {
return name[:pathend+i]
}
return ""
}
// baseName returns the symbol name without the package or receiver name.
// It replicates https://golang.org/pkg/debug/gosym/#Sym.BaseName, avoiding a
// dependency on debug/gosym.
func baseName(name string) string {
if i := strings.LastIndex(name, "."); i != -1 {
return name[i+1:]
}
return name
}

View file

@ -0,0 +1,15 @@
//go:build !go1.20
package sentry
import "strings"
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go below 1.20 a prefix of "type." and "go." is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go.") || strings.HasPrefix(name, "type.") {
return true
}
return false
}

View file

@ -0,0 +1,15 @@
//go:build go1.20
package sentry
import "strings"
func isCompilerGeneratedSymbol(name string) bool {
// In versions of Go 1.20 and above a prefix of "type:" and "go:" is a
// compiler-generated symbol that doesn't belong to any package.
// See variable reservedimports in cmd/compile/internal/gc/subr.go
if strings.HasPrefix(name, "go:") || strings.HasPrefix(name, "type:") {
return true
}
return false
}

View file

@ -0,0 +1,90 @@
package sentry
import (
"sync"
"time"
)
// Checks whether the transaction should be profiled (according to ProfilesSampleRate)
// and starts a profiler if so.
func (span *Span) sampleTransactionProfile() {
var sampleRate = span.clientOptions().ProfilesSampleRate
switch {
case sampleRate < 0.0 || sampleRate > 1.0:
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate out of range [0.0, 1.0]: %f\n", sampleRate)
case sampleRate == 0.0 || rng.Float64() >= sampleRate:
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate is: %f\n", sampleRate)
default:
startProfilerOnce.Do(startGlobalProfiler)
if globalProfiler == nil {
Logger.Println("Skipping transaction profiling: the profiler couldn't be started")
} else {
span.collectProfile = collectTransactionProfile
}
}
}
// transactionProfiler collects a profile for a given span.
type transactionProfiler func(span *Span) *profileInfo
var startProfilerOnce sync.Once
var globalProfiler profiler
func startGlobalProfiler() {
globalProfiler = startProfiling(time.Now())
}
func collectTransactionProfile(span *Span) *profileInfo {
result := globalProfiler.GetSlice(span.StartTime, span.EndTime)
if result == nil || result.trace == nil {
return nil
}
info := &profileInfo{
Version: "1",
EventID: uuid(),
// See https://github.com/getsentry/sentry-go/pull/626#discussion_r1204870340 for explanation why we use the Transaction time.
Timestamp: span.StartTime,
Trace: result.trace,
Transaction: profileTransaction{
DurationNS: uint64(span.EndTime.Sub(span.StartTime).Nanoseconds()),
Name: span.Name,
TraceID: span.TraceID.String(),
},
}
if len(info.Transaction.Name) == 0 {
// Name is required by Relay so use the operation name if the span name is empty.
info.Transaction.Name = span.Op
}
if result.callerGoID > 0 {
info.Transaction.ActiveThreadID = result.callerGoID
}
return info
}
func (info *profileInfo) UpdateFromEvent(event *Event) {
info.Environment = event.Environment
info.Platform = event.Platform
info.Release = event.Release
info.Dist = event.Dist
info.Transaction.ID = event.EventID
if runtimeContext, ok := event.Contexts["runtime"]; ok {
if value, ok := runtimeContext["name"]; !ok {
info.Runtime.Name = value.(string)
}
if value, ok := runtimeContext["version"]; !ok {
info.Runtime.Version = value.(string)
}
}
if osContext, ok := event.Contexts["os"]; ok {
if value, ok := osContext["name"]; !ok {
info.OS.Name = value.(string)
}
}
if deviceContext, ok := event.Contexts["device"]; ok {
if value, ok := deviceContext["arch"]; !ok {
info.Device.Architecture = value.(string)
}
}
}

View file

@ -0,0 +1,19 @@
package sentry
// A SamplingContext is passed to a TracesSampler to determine a sampling
// decision.
//
// TODO(tracing): possibly expand SamplingContext to include custom /
// user-provided data.
type SamplingContext struct {
Span *Span // The current span, always non-nil.
Parent *Span // The parent span, may be nil.
}
// The TracesSample type is an adapter to allow the use of ordinary
// functions as a TracesSampler.
type TracesSampler func(ctx SamplingContext) float64
func (f TracesSampler) Sample(ctx SamplingContext) float64 {
return f(ctx)
}

985
vendor/github.com/getsentry/sentry-go/tracing.go generated vendored Normal file
View file

@ -0,0 +1,985 @@
package sentry
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
const (
SentryTraceHeader = "sentry-trace"
SentryBaggageHeader = "baggage"
)
// A Span is the building block of a Sentry transaction. Spans build up a tree
// structure of timed operations. The span tree makes up a transaction event
// that is sent to Sentry when the root span is finished.
//
// Spans must be started with either StartSpan or Span.StartChild.
type Span struct { //nolint: maligned // prefer readability over optimal memory layout (see note below *)
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id"`
Name string `json:"name,omitempty"`
Op string `json:"op,omitempty"`
Description string `json:"description,omitempty"`
Status SpanStatus `json:"status,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
StartTime time.Time `json:"start_timestamp"`
EndTime time.Time `json:"timestamp"`
Data map[string]interface{} `json:"data,omitempty"`
Sampled Sampled `json:"-"`
Source TransactionSource `json:"-"`
// mu protects concurrent writes to map fields
mu sync.RWMutex
// sample rate the span was sampled with.
sampleRate float64
// ctx is the context where the span was started. Always non-nil.
ctx context.Context
// Dynamic Sampling context
dynamicSamplingContext DynamicSamplingContext
// parent refers to the immediate local parent span. A remote parent span is
// only referenced by setting ParentSpanID.
parent *Span
// recorder stores all spans in a transaction. Guaranteed to be non-nil.
recorder *spanRecorder
// span context, can only be set on transactions
contexts map[string]Context
// collectProfile is a function that collects a profile of the current transaction. May be nil.
collectProfile transactionProfiler
// a Once instance to make sure that Finish() is only called once.
finishOnce sync.Once
}
// TraceParentContext describes the context of a (remote) parent span.
//
// The context is normally extracted from a received "sentry-trace" header and
// used to initialize a new transaction.
//
// Note: the name might be not the best one. It was taken mostly to stay aligned
// with other SDKs, and it alludes to W3C "traceparent" header (https://www.w3.org/TR/trace-context/),
// which serves a similar purpose to "sentry-trace". We should eventually consider
// making this type internal-only and give it a better name.
type TraceParentContext struct {
TraceID TraceID
ParentSpanID SpanID
Sampled Sampled
}
// (*) Note on maligned:
//
// We prefer readability over optimal memory layout. If we ever decide to
// reorder fields, we can use a tool:
//
// go run honnef.co/go/tools/cmd/structlayout -json . Span | go run honnef.co/go/tools/cmd/structlayout-optimize
//
// Other structs would deserve reordering as well, for example Event.
// TODO: make Span.Tags and Span.Data opaque types (struct{unexported []slice}).
// An opaque type allows us to add methods and make it more convenient to use
// than maps, because maps require careful nil checks to use properly or rely on
// explicit initialization for every span, even when there might be no
// tags/data. For Span.Data, must gracefully handle values that cannot be
// marshaled into JSON (see transport.go:getRequestBodyFromEvent).
// StartSpan starts a new span to describe an operation. The new span will be a
// child of the last span stored in ctx, if any.
//
// One or more options can be used to modify the span properties. Typically one
// option as a function literal is enough. Combining multiple options can be
// useful to define and reuse specific properties with named functions.
//
// Caller should call the Finish method on the span to mark its end. Finishing a
// root span sends the span and all of its children, recursively, as a
// transaction to Sentry.
func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Span {
parent, hasParent := ctx.Value(spanContextKey{}).(*Span)
var span Span
span = Span{
// defaults
Op: operation,
StartTime: time.Now(),
Sampled: SampledUndefined,
ctx: context.WithValue(ctx, spanContextKey{}, &span),
parent: parent,
}
if hasParent {
span.TraceID = parent.TraceID
} else {
// Only set the Source if this is a transaction
span.Source = SourceCustom
// Implementation note:
//
// While math/rand is ~2x faster than crypto/rand (exact
// difference depends on hardware / OS), crypto/rand is probably
// fast enough and a safer choice.
//
// For reference, OpenTelemetry [1] uses crypto/rand to seed
// math/rand. AFAICT this approach does not preserve the
// properties from crypto/rand that make it suitable for
// cryptography. While it might be debatable whether those
// properties are important for us here, again, we're taking the
// safer path.
//
// See [2a] & [2b] for a discussion of some of the properties we
// obtain by using crypto/rand and [3a] & [3b] for why we avoid
// math/rand.
//
// Because the math/rand seed has only 64 bits (int64), if the
// first thing we do after seeding an RNG is to read in a random
// TraceID, there are only 2^64 possible values. Compared to
// UUID v4 that have 122 random bits, there is a much greater
// chance of collision [4a] & [4b].
//
// [1]: https://github.com/open-telemetry/opentelemetry-go/blob/958041ddf619a128/sdk/trace/trace.go#L25-L31
// [2a]: https://security.stackexchange.com/q/120352/246345
// [2b]: https://security.stackexchange.com/a/120365/246345
// [3a]: https://github.com/golang/go/issues/11871#issuecomment-126333686
// [3b]: https://github.com/golang/go/issues/11871#issuecomment-126357889
// [4a]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Collisions
// [4b]: https://www.wolframalpha.com/input/?i=sqrt%282*2%5E64*ln%281%2F%281-0.5%29%29%29
_, err := rand.Read(span.TraceID[:])
if err != nil {
panic(err)
}
}
_, err := rand.Read(span.SpanID[:])
if err != nil {
panic(err)
}
if hasParent {
span.ParentSpanID = parent.SpanID
}
// Apply options to override defaults.
for _, option := range options {
option(&span)
}
span.Sampled = span.sample()
if hasParent {
span.recorder = parent.spanRecorder()
} else {
span.recorder = &spanRecorder{}
}
span.recorder.record(&span)
hub := hubFromContext(ctx)
// Update scope so that all events include a trace context, allowing
// Sentry to correlate errors to transactions/spans.
hub.Scope().SetContext("trace", span.traceContext().Map())
// Start profiling only if it's a sampled root transaction.
if span.IsTransaction() && span.Sampled.Bool() {
span.sampleTransactionProfile()
}
return &span
}
// Finish sets the span's end time, unless already set. If the span is the root
// of a span tree, Finish sends the span tree to Sentry as a transaction.
//
// The logic is executed at most once per span, so that (incorrectly) calling it twice
// never double sends to Sentry.
func (s *Span) Finish() {
s.finishOnce.Do(s.doFinish)
}
// Context returns the context containing the span.
func (s *Span) Context() context.Context { return s.ctx }
// StartChild starts a new child span.
//
// The call span.StartChild(operation, options...) is a shortcut for
// StartSpan(span.Context(), operation, options...).
func (s *Span) StartChild(operation string, options ...SpanOption) *Span {
return StartSpan(s.Context(), operation, options...)
}
// SetTag sets a tag on the span. It is recommended to use SetTag instead of
// accessing the tags map directly as SetTag takes care of initializing the map
// when necessary.
func (s *Span) SetTag(name, value string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.Tags == nil {
s.Tags = make(map[string]string)
}
s.Tags[name] = value
}
// SetData sets a data on the span. It is recommended to use SetData instead of
// accessing the data map directly as SetData takes care of initializing the map
// when necessary.
func (s *Span) SetData(name, value string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.Data == nil {
s.Data = make(map[string]interface{})
}
s.Data[name] = value
}
// SetContext sets a context on the span. It is recommended to use SetContext instead of
// accessing the contexts map directly as SetContext takes care of initializing the map
// when necessary.
func (s *Span) SetContext(key string, value Context) {
s.mu.Lock()
defer s.mu.Unlock()
if s.contexts == nil {
s.contexts = make(map[string]Context)
}
s.contexts[key] = value
}
// IsTransaction checks if the given span is a transaction.
func (s *Span) IsTransaction() bool {
return s.parent == nil
}
// GetTransaction returns the transaction that contains this span.
//
// For transaction spans it returns itself. For spans that were created manually
// the method returns "nil".
func (s *Span) GetTransaction() *Span {
spanRecorder := s.spanRecorder()
if spanRecorder == nil {
// This probably means that the Span was created manually (not via
// StartTransaction/StartSpan or StartChild).
// Return "nil" to indicate that it's not a normal situation.
return nil
}
recorderRoot := spanRecorder.root()
if recorderRoot == nil {
// Same as above: manually created Span.
return nil
}
return recorderRoot
}
// TODO(tracing): maybe add shortcuts to get/set transaction name. Right now the
// transaction name is in the Scope, as it has existed there historically, prior
// to tracing.
//
// See Scope.Transaction() and Scope.SetTransaction().
//
// func (s *Span) TransactionName() string
// func (s *Span) SetTransactionName(name string)
// ToSentryTrace returns the seralized TraceParentContext from a transaction/span.
// Use this function to propagate the TraceParentContext to a downstream SDK,
// either as the value of the "sentry-trace" HTTP header, or as an html "sentry-trace" meta tag.
func (s *Span) ToSentryTrace() string {
// TODO(tracing): add instrumentation for outgoing HTTP requests using
// ToSentryTrace.
var b strings.Builder
fmt.Fprintf(&b, "%s-%s", s.TraceID.Hex(), s.SpanID.Hex())
switch s.Sampled {
case SampledTrue:
b.WriteString("-1")
case SampledFalse:
b.WriteString("-0")
}
return b.String()
}
// ToBaggage returns the serialized DynamicSamplingContext from a transaction.
// Use this function to propagate the DynamicSamplingContext to a downstream SDK,
// either as the value of the "baggage" HTTP header, or as an html "baggage" meta tag.
func (s *Span) ToBaggage() string {
if containingTransaction := s.GetTransaction(); containingTransaction != nil {
// In case there is currently no frozen DynamicSamplingContext attached to the transaction,
// create one from the properties of the transaction.
if !s.dynamicSamplingContext.IsFrozen() {
// This will return a frozen DynamicSamplingContext.
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(containingTransaction)
}
return containingTransaction.dynamicSamplingContext.String()
}
return ""
}
// SetDynamicSamplingContext sets the given dynamic sampling context on the
// current transaction.
func (s *Span) SetDynamicSamplingContext(dsc DynamicSamplingContext) {
if s.IsTransaction() {
s.dynamicSamplingContext = dsc
}
}
// doFinish runs the actual Span.Finish() logic.
func (s *Span) doFinish() {
if s.EndTime.IsZero() {
s.EndTime = monotonicTimeSince(s.StartTime)
}
if !s.Sampled.Bool() {
return
}
event := s.toEvent()
if event == nil {
return
}
if s.collectProfile != nil {
event.sdkMetaData.transactionProfile = s.collectProfile(s)
}
// TODO(tracing): add breadcrumbs
// (see https://github.com/getsentry/sentry-python/blob/f6f3525f8812f609/sentry_sdk/tracing.py#L372)
hub := hubFromContext(s.ctx)
hub.CaptureEvent(event)
}
// sentryTracePattern matches either
//
// TRACE_ID - SPAN_ID
// [[:xdigit:]]{32}-[[:xdigit:]]{16}
//
// or
//
// TRACE_ID - SPAN_ID - SAMPLED
// [[:xdigit:]]{32}-[[:xdigit:]]{16}-[01]
var sentryTracePattern = regexp.MustCompile(`^([[:xdigit:]]{32})-([[:xdigit:]]{16})(?:-([01]))?$`)
// updateFromSentryTrace parses a sentry-trace HTTP header (as returned by
// ToSentryTrace) and updates fields of the span. If the header cannot be
// recognized as valid, the span is left unchanged. The returned value indicates
// whether the span was updated.
func (s *Span) updateFromSentryTrace(header []byte) (updated bool) {
m := sentryTracePattern.FindSubmatch(header)
if m == nil {
// no match
return false
}
_, _ = hex.Decode(s.TraceID[:], m[1])
_, _ = hex.Decode(s.ParentSpanID[:], m[2])
if len(m[3]) != 0 {
switch m[3][0] {
case '0':
s.Sampled = SampledFalse
case '1':
s.Sampled = SampledTrue
}
}
return true
}
func (s *Span) updateFromBaggage(header []byte) {
if s.IsTransaction() {
dsc, err := DynamicSamplingContextFromHeader(header)
if err != nil {
return
}
s.dynamicSamplingContext = dsc
}
}
func (s *Span) MarshalJSON() ([]byte, error) {
// span aliases Span to allow calling json.Marshal without an infinite loop.
// It preserves all fields while none of the attached methods.
type span Span
var parentSpanID string
if s.ParentSpanID != zeroSpanID {
parentSpanID = s.ParentSpanID.String()
}
return json.Marshal(struct {
*span
ParentSpanID string `json:"parent_span_id,omitempty"`
}{
span: (*span)(s),
ParentSpanID: parentSpanID,
})
}
func (s *Span) clientOptions() *ClientOptions {
client := hubFromContext(s.ctx).Client()
if client != nil {
return &client.options
}
return &ClientOptions{}
}
func (s *Span) sample() Sampled {
clientOptions := s.clientOptions()
// https://develop.sentry.dev/sdk/performance/#sampling
// #1 tracing is not enabled.
if !clientOptions.EnableTracing {
Logger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
s.sampleRate = 0.0
return SampledFalse
}
// #2 explicit sampling decision via StartSpan/StartTransaction options.
if s.Sampled != SampledUndefined {
Logger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.Sampled)
switch s.Sampled {
case SampledTrue:
s.sampleRate = 1.0
case SampledFalse:
s.sampleRate = 0.0
}
return s.Sampled
}
// Variant for non-transaction spans: they inherit the parent decision.
// Note: non-transaction should always have a parent, but we check both
// conditions anyway -- the first for semantic meaning, the second to
// avoid a nil pointer dereference.
if !s.IsTransaction() && s.parent != nil {
return s.parent.Sampled
}
// #3 use TracesSampler from ClientOptions.
sampler := clientOptions.TracesSampler
samplingContext := SamplingContext{
Span: s,
Parent: s.parent,
}
if sampler != nil {
tracesSamplerSampleRate := sampler.Sample(samplingContext)
s.sampleRate = tracesSamplerSampleRate
if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
return SampledFalse
}
if tracesSamplerSampleRate == 0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate)
return SampledFalse
}
if rng.Float64() < tracesSamplerSampleRate {
return SampledTrue
}
Logger.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate)
return SampledFalse
}
// #4 inherit parent decision.
if s.parent != nil {
Logger.Printf("Using sampling decision from parent: %v", s.parent.Sampled)
switch s.parent.Sampled {
case SampledTrue:
s.sampleRate = 1.0
case SampledFalse:
s.sampleRate = 0.0
}
return s.parent.Sampled
}
// #5 use TracesSampleRate from ClientOptions.
sampleRate := clientOptions.TracesSampleRate
s.sampleRate = sampleRate
if sampleRate < 0.0 || sampleRate > 1.0 {
Logger.Printf("Dropping transaction: TracesSamplerRate out of range [0.0, 1.0]: %f", sampleRate)
return SampledFalse
}
if sampleRate == 0.0 {
Logger.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate)
return SampledFalse
}
if rng.Float64() < sampleRate {
return SampledTrue
}
return SampledFalse
}
func (s *Span) toEvent() *Event {
s.mu.Lock()
defer s.mu.Unlock()
if !s.IsTransaction() {
return nil // only transactions can be transformed into events
}
children := s.recorder.children()
finished := make([]*Span, 0, len(children))
for _, child := range children {
if child.EndTime.IsZero() {
Logger.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID)
continue
}
finished = append(finished, child)
}
// Create and attach a DynamicSamplingContext to the transaction.
// If the DynamicSamplingContext is not frozen at this point, we can assume being head of trace.
if !s.dynamicSamplingContext.IsFrozen() {
s.dynamicSamplingContext = DynamicSamplingContextFromTransaction(s)
}
contexts := map[string]Context{}
for k, v := range s.contexts {
contexts[k] = cloneContext(v)
}
contexts["trace"] = s.traceContext().Map()
// Make sure that the transaction source is valid
transactionSource := s.Source
if !transactionSource.isValid() {
transactionSource = SourceCustom
}
return &Event{
Type: transactionType,
Transaction: s.Name,
Contexts: contexts,
Tags: s.Tags,
Extra: s.Data,
Timestamp: s.EndTime,
StartTime: s.StartTime,
Spans: finished,
TransactionInfo: &TransactionInfo{
Source: transactionSource,
},
sdkMetaData: SDKMetaData{
dsc: s.dynamicSamplingContext,
},
}
}
func (s *Span) traceContext() *TraceContext {
return &TraceContext{
TraceID: s.TraceID,
SpanID: s.SpanID,
ParentSpanID: s.ParentSpanID,
Op: s.Op,
Description: s.Description,
Status: s.Status,
}
}
// spanRecorder stores the span tree. Guaranteed to be non-nil.
func (s *Span) spanRecorder() *spanRecorder { return s.recorder }
// ParseTraceParentContext parses a sentry-trace header and builds a TraceParentContext from the
// parsed values. If the header was parsed correctly, the second returned argument
// ("valid") will be set to true, otherwise (e.g., empty or malformed header) it will
// be false.
func ParseTraceParentContext(header []byte) (traceParentContext TraceParentContext, valid bool) {
s := Span{}
updated := s.updateFromSentryTrace(header)
if !updated {
return TraceParentContext{}, false
}
return TraceParentContext{
TraceID: s.TraceID,
ParentSpanID: s.ParentSpanID,
Sampled: s.Sampled,
}, true
}
// TraceID identifies a trace.
type TraceID [16]byte
func (id TraceID) Hex() []byte {
b := make([]byte, hex.EncodedLen(len(id)))
hex.Encode(b, id[:])
return b
}
func (id TraceID) String() string {
return string(id.Hex())
}
func (id TraceID) MarshalText() ([]byte, error) {
return id.Hex(), nil
}
// SpanID identifies a span.
type SpanID [8]byte
func (id SpanID) Hex() []byte {
b := make([]byte, hex.EncodedLen(len(id)))
hex.Encode(b, id[:])
return b
}
func (id SpanID) String() string {
return string(id.Hex())
}
func (id SpanID) MarshalText() ([]byte, error) {
return id.Hex(), nil
}
// Zero values of TraceID and SpanID used for comparisons.
var (
zeroTraceID TraceID
zeroSpanID SpanID
)
// Contains information about how the name of the transaction was determined.
type TransactionSource string
const (
SourceCustom TransactionSource = "custom"
SourceURL TransactionSource = "url"
SourceRoute TransactionSource = "route"
SourceView TransactionSource = "view"
SourceComponent TransactionSource = "component"
SourceTask TransactionSource = "task"
)
// A set of all valid transaction sources.
var allTransactionSources = map[TransactionSource]struct{}{
SourceCustom: {},
SourceURL: {},
SourceRoute: {},
SourceView: {},
SourceComponent: {},
SourceTask: {},
}
// isValid returns 'true' if the given transaction source is a valid
// source as recognized by the envelope protocol:
// https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
func (ts TransactionSource) isValid() bool {
_, found := allTransactionSources[ts]
return found
}
// SpanStatus is the status of a span.
type SpanStatus uint8
// Implementation note:
//
// In Relay (ingestion), the SpanStatus type is an enum used as
// Annotated<SpanStatus> when embedded in structs, making it effectively
// Option<SpanStatus>. It means the status is either null or one of the known
// string values.
//
// In Snuba (search), the SpanStatus is stored as an uint8 and defaulted to 2
// ("unknown") when not set. It means that Discover searches for
// `transaction.status:unknown` return both transactions/spans with status
// `null` or `"unknown"`. Searches for `transaction.status:""` return nothing.
//
// With that in mind, the Go SDK default is SpanStatusUndefined, which is
// null/omitted when serializing to JSON, but integrations may update the status
// automatically based on contextual information.
const (
SpanStatusUndefined SpanStatus = iota
SpanStatusOK
SpanStatusCanceled
SpanStatusUnknown
SpanStatusInvalidArgument
SpanStatusDeadlineExceeded
SpanStatusNotFound
SpanStatusAlreadyExists
SpanStatusPermissionDenied
SpanStatusResourceExhausted
SpanStatusFailedPrecondition
SpanStatusAborted
SpanStatusOutOfRange
SpanStatusUnimplemented
SpanStatusInternalError
SpanStatusUnavailable
SpanStatusDataLoss
SpanStatusUnauthenticated
maxSpanStatus
)
func (ss SpanStatus) String() string {
if ss >= maxSpanStatus {
return ""
}
m := [maxSpanStatus]string{
"",
"ok",
"cancelled", // [sic]
"unknown",
"invalid_argument",
"deadline_exceeded",
"not_found",
"already_exists",
"permission_denied",
"resource_exhausted",
"failed_precondition",
"aborted",
"out_of_range",
"unimplemented",
"internal_error",
"unavailable",
"data_loss",
"unauthenticated",
}
return m[ss]
}
func (ss SpanStatus) MarshalJSON() ([]byte, error) {
s := ss.String()
if s == "" {
return []byte("null"), nil
}
return json.Marshal(s)
}
// A TraceContext carries information about an ongoing trace and is meant to be
// stored in Event.Contexts (as *TraceContext).
type TraceContext struct {
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id"`
Op string `json:"op,omitempty"`
Description string `json:"description,omitempty"`
Status SpanStatus `json:"status,omitempty"`
}
func (tc *TraceContext) MarshalJSON() ([]byte, error) {
// traceContext aliases TraceContext to allow calling json.Marshal without
// an infinite loop. It preserves all fields while none of the attached
// methods.
type traceContext TraceContext
var parentSpanID string
if tc.ParentSpanID != zeroSpanID {
parentSpanID = tc.ParentSpanID.String()
}
return json.Marshal(struct {
*traceContext
ParentSpanID string `json:"parent_span_id,omitempty"`
}{
traceContext: (*traceContext)(tc),
ParentSpanID: parentSpanID,
})
}
func (tc TraceContext) Map() map[string]interface{} {
m := map[string]interface{}{
"trace_id": tc.TraceID,
"span_id": tc.SpanID,
}
if tc.ParentSpanID != [8]byte{} {
m["parent_span_id"] = tc.ParentSpanID
}
if tc.Op != "" {
m["op"] = tc.Op
}
if tc.Description != "" {
m["description"] = tc.Description
}
if tc.Status > 0 && tc.Status < maxSpanStatus {
m["status"] = tc.Status
}
return m
}
// Sampled signifies a sampling decision.
type Sampled int8
// The possible trace sampling decisions are: SampledFalse, SampledUndefined
// (default) and SampledTrue.
const (
SampledFalse Sampled = -1
SampledUndefined Sampled = 0
SampledTrue Sampled = 1
)
func (s Sampled) String() string {
switch s {
case SampledFalse:
return "SampledFalse"
case SampledUndefined:
return "SampledUndefined"
case SampledTrue:
return "SampledTrue"
default:
return fmt.Sprintf("SampledInvalid(%d)", s)
}
}
// Bool returns true if the sample decision is SampledTrue, false otherwise.
func (s Sampled) Bool() bool {
return s == SampledTrue
}
// A SpanOption is a function that can modify the properties of a span.
type SpanOption func(s *Span)
// WithTransactionName option sets the name of the current transaction.
//
// A span tree has a single transaction name, therefore using this option when
// starting a span affects the span tree as a whole, potentially overwriting a
// name set previously.
func WithTransactionName(name string) SpanOption {
return func(s *Span) {
s.Name = name
}
}
// WithDescription sets the description of a span.
func WithDescription(description string) SpanOption {
return func(s *Span) {
s.Description = description
}
}
// WithOpName sets the operation name for a given span.
func WithOpName(name string) SpanOption {
return func(s *Span) {
s.Op = name
}
}
// WithTransactionSource sets the source of the transaction name.
//
// Note: if the transaction source is not a valid source (as described
// by the spec https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations),
// it will be corrected to "custom" eventually, before the transaction is sent.
func WithTransactionSource(source TransactionSource) SpanOption {
return func(s *Span) {
s.Source = source
}
}
// WithSpanSampled updates the sampling flag for a given span.
func WithSpanSampled(sampled Sampled) SpanOption {
return func(s *Span) {
s.Sampled = sampled
}
}
// ContinueFromRequest returns a span option that updates the span to continue
// an existing trace. If it cannot detect an existing trace in the request, the
// span will be left unchanged.
//
// ContinueFromRequest is an alias for:
//
// ContinueFromHeaders(r.Header.Get(SentryTraceHeader), r.Header.Get(SentryBaggageHeader)).
func ContinueFromRequest(r *http.Request) SpanOption {
return ContinueFromHeaders(r.Header.Get(SentryTraceHeader), r.Header.Get(SentryBaggageHeader))
}
// ContinueFromHeaders returns a span option that updates the span to continue
// an existing TraceID and propagates the Dynamic Sampling context.
func ContinueFromHeaders(trace, baggage string) SpanOption {
return func(s *Span) {
if trace != "" {
s.updateFromSentryTrace([]byte(trace))
}
if baggage != "" {
s.updateFromBaggage([]byte(baggage))
}
// In case a sentry-trace header is present but there are no sentry-related
// values in the baggage, create an empty, frozen DynamicSamplingContext.
if trace != "" && !s.dynamicSamplingContext.HasEntries() {
s.dynamicSamplingContext = DynamicSamplingContext{
Frozen: true,
}
}
}
}
// ContinueFromTrace returns a span option that updates the span to continue
// an existing TraceID.
func ContinueFromTrace(trace string) SpanOption {
return func(s *Span) {
if trace == "" {
return
}
s.updateFromSentryTrace([]byte(trace))
}
}
// spanContextKey is used to store span values in contexts.
type spanContextKey struct{}
// TransactionFromContext returns the root span of the current transaction. It
// returns nil if no transaction is tracked in the context.
func TransactionFromContext(ctx context.Context) *Span {
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
return span.recorder.root()
}
return nil
}
// SpanFromContext returns the last span stored in the context, or nil if no span
// is set on the context.
func SpanFromContext(ctx context.Context) *Span {
if span, ok := ctx.Value(spanContextKey{}).(*Span); ok {
return span
}
return nil
}
// StartTransaction will create a transaction (root span) if there's no existing
// transaction in the context otherwise, it will return the existing transaction.
func StartTransaction(ctx context.Context, name string, options ...SpanOption) *Span {
currentTransaction, exists := ctx.Value(spanContextKey{}).(*Span)
if exists {
return currentTransaction
}
options = append(options, WithTransactionName(name))
return StartSpan(
ctx,
"",
options...,
)
}
// HTTPtoSpanStatus converts an HTTP status code to a SpanStatus.
func HTTPtoSpanStatus(code int) SpanStatus {
if code < http.StatusBadRequest {
return SpanStatusOK
}
if http.StatusBadRequest <= code && code < http.StatusInternalServerError {
switch code {
case http.StatusForbidden:
return SpanStatusPermissionDenied
case http.StatusNotFound:
return SpanStatusNotFound
case http.StatusTooManyRequests:
return SpanStatusResourceExhausted
case http.StatusRequestEntityTooLarge:
return SpanStatusFailedPrecondition
case http.StatusUnauthorized:
return SpanStatusUnauthenticated
case http.StatusConflict:
return SpanStatusAlreadyExists
default:
return SpanStatusInvalidArgument
}
}
if http.StatusInternalServerError <= code && code < 600 {
switch code {
case http.StatusGatewayTimeout:
return SpanStatusDeadlineExceeded
case http.StatusNotImplemented:
return SpanStatusUnimplemented
case http.StatusServiceUnavailable:
return SpanStatusUnavailable
default:
return SpanStatusInternalError
}
}
return SpanStatusUnknown
}

649
vendor/github.com/getsentry/sentry-go/transport.go generated vendored Normal file
View file

@ -0,0 +1,649 @@
package sentry
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
const defaultBufferSize = 30
const defaultTimeout = time.Second * 30
// maxDrainResponseBytes is the maximum number of bytes that transport
// implementations will read from response bodies when draining them.
//
// Sentry's ingestion API responses are typically short and the SDK doesn't need
// the contents of the response body. However, the net/http HTTP client requires
// response bodies to be fully drained (and closed) for TCP keep-alive to work.
//
// maxDrainResponseBytes strikes a balance between reading too much data (if the
// server is misbehaving) and reusing TCP connections.
const maxDrainResponseBytes = 16 << 10
// Transport is used by the Client to deliver events to remote server.
type Transport interface {
Flush(timeout time.Duration) bool
Configure(options ClientOptions)
SendEvent(event *Event)
}
func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) {
if options.HTTPSProxy != "" {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPSProxy)
}
}
if options.HTTPProxy != "" {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPProxy)
}
}
return http.ProxyFromEnvironment
}
func getTLSConfig(options ClientOptions) *tls.Config {
if options.CaCerts != nil {
// #nosec G402 -- We should be using `MinVersion: tls.VersionTLS12`,
// but we don't want to break peoples code without the major bump.
return &tls.Config{
RootCAs: options.CaCerts,
}
}
return nil
}
func getRequestBodyFromEvent(event *Event) []byte {
body, err := json.Marshal(event)
if err == nil {
return body
}
msg := fmt.Sprintf("Could not encode original event as JSON. "+
"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
"Please verify the data you attach to the scope. "+
"Error: %s", err)
// Try to serialize the event, with all the contextual data that allows for interface{} stripped.
event.Breadcrumbs = nil
event.Contexts = nil
event.Extra = map[string]interface{}{
"info": msg,
}
body, err = json.Marshal(event)
if err == nil {
Logger.Println(msg)
return body
}
// This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable
// Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry)
// Juuust in case something, somehow goes utterly wrong.
Logger.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " +
"Please notify the SDK owners with possibly broken payload.")
return nil
}
func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
// Attachment header
err := enc.Encode(struct {
Type string `json:"type"`
Length int `json:"length"`
Filename string `json:"filename"`
ContentType string `json:"content_type,omitempty"`
}{
Type: "attachment",
Length: len(attachment.Payload),
Filename: attachment.Filename,
ContentType: attachment.ContentType,
})
if err != nil {
return err
}
// Attachment payload
if _, err = b.Write(attachment.Payload); err != nil {
return err
}
// "Envelopes should be terminated with a trailing newline."
//
// [1]: https://develop.sentry.dev/sdk/envelopes/#envelopes
if _, err := b.Write([]byte("\n")); err != nil {
return err
}
return nil
}
func encodeEnvelopeItem(enc *json.Encoder, itemType string, body json.RawMessage) error {
// Item header
err := enc.Encode(struct {
Type string `json:"type"`
Length int `json:"length"`
}{
Type: itemType,
Length: len(body),
})
if err == nil {
// payload
err = enc.Encode(body)
}
return err
}
func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
// Construct the trace envelope header
var trace = map[string]string{}
if dsc := event.sdkMetaData.dsc; dsc.HasEntries() {
for k, v := range dsc.Entries {
trace[k] = v
}
}
// Envelope header
err := enc.Encode(struct {
EventID EventID `json:"event_id"`
SentAt time.Time `json:"sent_at"`
Dsn string `json:"dsn"`
Sdk map[string]string `json:"sdk"`
Trace map[string]string `json:"trace,omitempty"`
}{
EventID: event.EventID,
SentAt: sentAt,
Trace: trace,
Dsn: dsn.String(),
Sdk: map[string]string{
"name": event.Sdk.Name,
"version": event.Sdk.Version,
},
})
if err != nil {
return nil, err
}
if event.Type == transactionType || event.Type == checkInType {
err = encodeEnvelopeItem(enc, event.Type, body)
} else {
err = encodeEnvelopeItem(enc, eventType, body)
}
if err != nil {
return nil, err
}
// Attachments
for _, attachment := range event.attachments {
if err := encodeAttachment(enc, &b, attachment); err != nil {
return nil, err
}
}
// Profile data
if event.sdkMetaData.transactionProfile != nil {
body, err = json.Marshal(event.sdkMetaData.transactionProfile)
if err != nil {
return nil, err
}
err = encodeEnvelopeItem(enc, profileType, body)
if err != nil {
return nil, err
}
}
return &b, nil
}
func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
defer func() {
if r != nil {
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version))
r.Header.Set("Content-Type", "application/x-sentry-envelope")
auth := fmt.Sprintf("Sentry sentry_version=%s, "+
"sentry_client=%s/%s, sentry_key=%s", apiVersion, event.Sdk.Name, event.Sdk.Version, dsn.publicKey)
// The key sentry_secret is effectively deprecated and no longer needs to be set.
// However, since it was required in older self-hosted versions,
// it should still passed through to Sentry if set.
if dsn.secretKey != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.secretKey)
}
r.Header.Set("X-Sentry-Auth", auth)
}
}()
body := getRequestBodyFromEvent(event)
if body == nil {
return nil, errors.New("event could not be marshaled")
}
envelope, err := envelopeFromBody(event, dsn, time.Now(), body)
if err != nil {
return nil, err
}
return http.NewRequest(
http.MethodPost,
dsn.GetAPIURL().String(),
envelope,
)
}
func categoryFor(eventType string) ratelimit.Category {
switch eventType {
case "":
return ratelimit.CategoryError
case transactionType:
return ratelimit.CategoryTransaction
default:
return ratelimit.Category(eventType)
}
}
// ================================
// HTTPTransport
// ================================
// A batch groups items that are processed sequentially.
type batch struct {
items chan batchItem
started chan struct{} // closed to signal items started to be worked on
done chan struct{} // closed to signal completion of all items
}
type batchItem struct {
request *http.Request
category ratelimit.Category
}
// HTTPTransport is the default, non-blocking, implementation of Transport.
//
// Clients using this transport will enqueue requests in a buffer and return to
// the caller before any network communication has happened. Requests are sent
// to Sentry sequentially from a background goroutine.
type HTTPTransport struct {
dsn *Dsn
client *http.Client
transport http.RoundTripper
// buffer is a channel of batches. Calling Flush terminates work on the
// current in-flight items and starts a new batch for subsequent events.
buffer chan batch
start sync.Once
// Size of the transport buffer. Defaults to 30.
BufferSize int
// HTTP Client request timeout. Defaults to 30 seconds.
Timeout time.Duration
mu sync.RWMutex
limits ratelimit.Map
}
// NewHTTPTransport returns a new pre-configured instance of HTTPTransport.
func NewHTTPTransport() *HTTPTransport {
transport := HTTPTransport{
BufferSize: defaultBufferSize,
Timeout: defaultTimeout,
limits: make(ratelimit.Map),
}
return &transport
}
// Configure is called by the Client itself, providing it it's own ClientOptions.
func (t *HTTPTransport) Configure(options ClientOptions) {
dsn, err := NewDsn(options.Dsn)
if err != nil {
Logger.Printf("%v\n", err)
return
}
t.dsn = dsn
// A buffered channel with capacity 1 works like a mutex, ensuring only one
// goroutine can access the current batch at a given time. Access is
// synchronized by reading from and writing to the channel.
t.buffer = make(chan batch, 1)
t.buffer <- batch{
items: make(chan batchItem, t.BufferSize),
started: make(chan struct{}),
done: make(chan struct{}),
}
if options.HTTPTransport != nil {
t.transport = options.HTTPTransport
} else {
t.transport = &http.Transport{
Proxy: getProxyConfig(options),
TLSClientConfig: getTLSConfig(options),
}
}
if options.HTTPClient != nil {
t.client = options.HTTPClient
} else {
t.client = &http.Client{
Transport: t.transport,
Timeout: t.Timeout,
}
}
t.start.Do(func() {
go t.worker()
})
}
// SendEvent assembles a new packet out of Event and sends it to remote server.
func (t *HTTPTransport) SendEvent(event *Event) {
if t.dsn == nil {
return
}
category := categoryFor(event.Type)
if t.disabled(category) {
return
}
request, err := getRequestFromEvent(event, t.dsn)
if err != nil {
return
}
// <-t.buffer is equivalent to acquiring a lock to access the current batch.
// A few lines below, t.buffer <- b releases the lock.
//
// The lock must be held during the select block below to guarantee that
// b.items is not closed while trying to send to it. Remember that sending
// on a closed channel panics.
//
// Note that the select block takes a bounded amount of CPU time because of
// the default case that is executed if sending on b.items would block. That
// is, the event is dropped if it cannot be sent immediately to the b.items
// channel (used as a queue).
b := <-t.buffer
select {
case b.items <- batchItem{
request: request,
category: category,
}:
var eventType string
if event.Type == transactionType {
eventType = "transaction"
} else {
eventType = fmt.Sprintf("%s event", event.Level)
}
Logger.Printf(
"Sending %s [%s] to %s project: %s",
eventType,
event.EventID,
t.dsn.host,
t.dsn.projectID,
)
default:
Logger.Println("Event dropped due to transport buffer being full.")
}
t.buffer <- b
}
// Flush waits until any buffered events are sent to the Sentry server, blocking
// for at most the given timeout. It returns false if the timeout was reached.
// In that case, some events may not have been sent.
//
// Flush should be called before terminating the program to avoid
// unintentionally dropping events.
//
// Do not call Flush indiscriminately after every call to SendEvent. Instead, to
// have the SDK send events over the network synchronously, configure it to use
// the HTTPSyncTransport in the call to Init.
func (t *HTTPTransport) Flush(timeout time.Duration) bool {
toolate := time.After(timeout)
// Wait until processing the current batch has started or the timeout.
//
// We must wait until the worker has seen the current batch, because it is
// the only way b.done will be closed. If we do not wait, there is a
// possible execution flow in which b.done is never closed, and the only way
// out of Flush would be waiting for the timeout, which is undesired.
var b batch
for {
select {
case b = <-t.buffer:
select {
case <-b.started:
goto started
default:
t.buffer <- b
}
case <-toolate:
goto fail
}
}
started:
// Signal that there won't be any more items in this batch, so that the
// worker inner loop can end.
close(b.items)
// Start a new batch for subsequent events.
t.buffer <- batch{
items: make(chan batchItem, t.BufferSize),
started: make(chan struct{}),
done: make(chan struct{}),
}
// Wait until the current batch is done or the timeout.
select {
case <-b.done:
Logger.Println("Buffer flushed successfully.")
return true
case <-toolate:
goto fail
}
fail:
Logger.Println("Buffer flushing reached the timeout.")
return false
}
func (t *HTTPTransport) worker() {
for b := range t.buffer {
// Signal that processing of the current batch has started.
close(b.started)
// Return the batch to the buffer so that other goroutines can use it.
// Equivalent to releasing a lock.
t.buffer <- b
// Process all batch items.
for item := range b.items {
if t.disabled(item.category) {
continue
}
response, err := t.client.Do(item.request)
if err != nil {
Logger.Printf("There was an issue with sending an event: %v", err)
continue
}
t.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
// Drain body up to a limit and close it, allowing the
// transport to reuse TCP connections.
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
response.Body.Close()
}
// Signal that processing of the batch is done.
close(b.done)
}
}
func (t *HTTPTransport) disabled(c ratelimit.Category) bool {
t.mu.RLock()
defer t.mu.RUnlock()
disabled := t.limits.IsRateLimited(c)
if disabled {
Logger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
}
return disabled
}
// ================================
// HTTPSyncTransport
// ================================
// HTTPSyncTransport is a blocking implementation of Transport.
//
// Clients using this transport will send requests to Sentry sequentially and
// block until a response is returned.
//
// The blocking behavior is useful in a limited set of use cases. For example,
// use it when deploying code to a Function as a Service ("Serverless")
// platform, where any work happening in a background goroutine is not
// guaranteed to execute.
//
// For most cases, prefer HTTPTransport.
type HTTPSyncTransport struct {
dsn *Dsn
client *http.Client
transport http.RoundTripper
mu sync.Mutex
limits ratelimit.Map
// HTTP Client request timeout. Defaults to 30 seconds.
Timeout time.Duration
}
// NewHTTPSyncTransport returns a new pre-configured instance of HTTPSyncTransport.
func NewHTTPSyncTransport() *HTTPSyncTransport {
transport := HTTPSyncTransport{
Timeout: defaultTimeout,
limits: make(ratelimit.Map),
}
return &transport
}
// Configure is called by the Client itself, providing it it's own ClientOptions.
func (t *HTTPSyncTransport) Configure(options ClientOptions) {
dsn, err := NewDsn(options.Dsn)
if err != nil {
Logger.Printf("%v\n", err)
return
}
t.dsn = dsn
if options.HTTPTransport != nil {
t.transport = options.HTTPTransport
} else {
t.transport = &http.Transport{
Proxy: getProxyConfig(options),
TLSClientConfig: getTLSConfig(options),
}
}
if options.HTTPClient != nil {
t.client = options.HTTPClient
} else {
t.client = &http.Client{
Transport: t.transport,
Timeout: t.Timeout,
}
}
}
// SendEvent assembles a new packet out of Event and sends it to remote server.
func (t *HTTPSyncTransport) SendEvent(event *Event) {
if t.dsn == nil {
return
}
if t.disabled(categoryFor(event.Type)) {
return
}
request, err := getRequestFromEvent(event, t.dsn)
if err != nil {
return
}
var eventType string
if event.Type == transactionType {
eventType = "transaction"
} else {
eventType = fmt.Sprintf("%s event", event.Level)
}
Logger.Printf(
"Sending %s [%s] to %s project: %s",
eventType,
event.EventID,
t.dsn.host,
t.dsn.projectID,
)
response, err := t.client.Do(request)
if err != nil {
Logger.Printf("There was an issue with sending an event: %v", err)
return
}
t.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock()
// Drain body up to a limit and close it, allowing the
// transport to reuse TCP connections.
_, _ = io.CopyN(io.Discard, response.Body, maxDrainResponseBytes)
response.Body.Close()
}
// Flush is a no-op for HTTPSyncTransport. It always returns true immediately.
func (t *HTTPSyncTransport) Flush(_ time.Duration) bool {
return true
}
func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool {
t.mu.Lock()
defer t.mu.Unlock()
disabled := t.limits.IsRateLimited(c)
if disabled {
Logger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
}
return disabled
}
// ================================
// noopTransport
// ================================
// noopTransport is an implementation of Transport interface which drops all the events.
// Only used internally when an empty DSN is provided, which effectively disables the SDK.
type noopTransport struct{}
var _ Transport = noopTransport{}
func (noopTransport) Configure(ClientOptions) {
Logger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
}
func (noopTransport) SendEvent(*Event) {
Logger.Println("Event dropped due to noopTransport usage.")
}
func (noopTransport) Flush(time.Duration) bool {
return true
}

114
vendor/github.com/getsentry/sentry-go/util.go generated vendored Normal file
View file

@ -0,0 +1,114 @@
package sentry
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"runtime/debug"
"strings"
"time"
exec "golang.org/x/sys/execabs"
)
func uuid() string {
id := make([]byte, 16)
// Prefer rand.Read over rand.Reader, see https://go-review.googlesource.com/c/go/+/272326/.
_, _ = rand.Read(id)
id[6] &= 0x0F // clear version
id[6] |= 0x40 // set version to 4 (random uuid)
id[8] &= 0x3F // clear variant
id[8] |= 0x80 // set to IETF variant
return hex.EncodeToString(id)
}
func fileExists(fileName string) bool {
_, err := os.Stat(fileName)
return err == nil
}
// monotonicTimeSince replaces uses of time.Now() to take into account the
// monotonic clock reading stored in start, such that duration = end - start is
// unaffected by changes in the system wall clock.
func monotonicTimeSince(start time.Time) (end time.Time) {
return start.Add(time.Since(start))
}
// nolint: deadcode, unused
func prettyPrint(data interface{}) {
dbg, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(dbg))
}
// defaultRelease attempts to guess a default release for the currently running
// program.
func defaultRelease() (release string) {
// Return first non-empty environment variable known to hold release info, if any.
envs := []string{
"SENTRY_RELEASE",
"HEROKU_SLUG_COMMIT",
"SOURCE_VERSION",
"CODEBUILD_RESOLVED_SOURCE_VERSION",
"CIRCLE_SHA1",
"GAE_DEPLOYMENT_ID",
"GITHUB_SHA", // GitHub Actions - https://help.github.com/en/actions
"COMMIT_REF", // Netlify - https://docs.netlify.com/
"VERCEL_GIT_COMMIT_SHA", // Vercel - https://vercel.com/
"ZEIT_GITHUB_COMMIT_SHA", // Zeit (now known as Vercel)
"ZEIT_GITLAB_COMMIT_SHA",
"ZEIT_BITBUCKET_COMMIT_SHA",
}
for _, e := range envs {
if release = os.Getenv(e); release != "" {
Logger.Printf("Using release from environment variable %s: %s", e, release)
return release
}
}
if info, ok := debug.ReadBuildInfo(); ok {
buildInfoVcsRevision := revisionFromBuildInfo(info)
if len(buildInfoVcsRevision) > 0 {
return buildInfoVcsRevision
}
}
// Derive a version string from Git. Example outputs:
// v1.0.1-0-g9de4
// v2.0-8-g77df-dirty
// 4f72d7
if _, err := exec.LookPath("git"); err == nil {
cmd := exec.Command("git", "describe", "--long", "--always", "--dirty")
b, err := cmd.Output()
if err != nil {
// Either Git is not available or the current directory is not a
// Git repository.
var s strings.Builder
fmt.Fprintf(&s, "Release detection failed: %v", err)
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
fmt.Fprintf(&s, ": %s", err.Stderr)
}
Logger.Print(s.String())
} else {
release = strings.TrimSpace(string(b))
Logger.Printf("Using release from Git: %s", release)
return release
}
}
Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return ""
}
func revisionFromBuildInfo(info *debug.BuildInfo) string {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" && setting.Value != "" {
Logger.Printf("Using release from debug info: %s", setting.Value)
return setting.Value
}
}
return ""
}

View file

@ -1,3 +1,6 @@
SPDX short identifier: BSD-3-Clause
https://opensource.org/licenses/BSD-3-Clause
Copyright (c) 2014, David Kitchen <david@buro9.com>
All rights reserved.

View file

@ -1,4 +1,4 @@
# bluemonday [![Build Status](https://travis-ci.org/microcosm-cc/bluemonday.svg?branch=master)](https://travis-ci.org/microcosm-cc/bluemonday) [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge)
# bluemonday [![GoDoc](https://godoc.org/github.com/microcosm-cc/bluemonday?status.png)](https://godoc.org/github.com/microcosm-cc/bluemonday) [![Sourcegraph](https://sourcegraph.com/github.com/microcosm-cc/bluemonday/-/badge.svg)](https://sourcegraph.com/github.com/microcosm-cc/bluemonday?badge)
bluemonday is a HTML sanitizer implemented in Go. It is fast and highly configurable.

View file

@ -280,6 +280,49 @@ var (
"slategray", "slategrey", "snow", "springgreen", "steelblue", "tan",
"teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
"whitesmoke", "yellow", "yellowgreen"}
Alpha = regexp.MustCompile(`^[a-z]+$`)
Blur = regexp.MustCompile(`^blur\([0-9]+px\)$`)
BrightnessCont = regexp.MustCompile(`^(brightness|contrast)\([0-9]+\%\)$`)
Count = regexp.MustCompile(`^[0-9]+[\.]?[0-9]*$`)
CubicBezier = regexp.MustCompile(`^cubic-bezier\(([ ]*(0(.[0-9]+)?|1(.0)?),){3}[ ]*(0(.[0-9]+)?|1)\)$`)
Digits = regexp.MustCompile(`^digits [2-4]$`)
DropShadow = regexp.MustCompile(`drop-shadow\(([-]?[0-9]+px) ([-]?[0-9]+px)( [-]?[0-9]+px)?( ([-]?[0-9]+px))?`)
Font = regexp.MustCompile(`^('[a-z \-]+'|[a-z \-]+)$`)
Grayscale = regexp.MustCompile(`^grayscale\(([0-9]{1,2}|100)%\)$`)
GridTemplateAreas = regexp.MustCompile(`^['"]?[a-z ]+['"]?$`)
HexRGB = regexp.MustCompile(`^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$`)
HSL = regexp.MustCompile(`^hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)$`)
HSLA = regexp.MustCompile(`^hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)$`)
HueRotate = regexp.MustCompile(`^hue-rotate\(([12]?[0-9]{1,2}|3[0-5][0-9]|360)?\)$`)
Invert = regexp.MustCompile(`^invert\(([0-9]{1,2}|100)%\)$`)
Length = regexp.MustCompile(`^[\-]?([0-9]+|[0-9]*[\.][0-9]+)(%|cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|deg|rad|turn)?$`)
Matrix = regexp.MustCompile(`^matrix\(([ ]*[0-9]+[\.]?[0-9]*,){5}([ ]*[0-9]+[\.]?[0-9]*)\)$`)
Matrix3D = regexp.MustCompile(`^matrix3d\(([ ]*[0-9]+[\.]?[0-9]*,){15}([ ]*[0-9]+[\.]?[0-9]*)\)$`)
NegTime = regexp.MustCompile(`^[\-]?[0-9]+[\.]?[0-9]*(s|ms)?$`)
Numeric = regexp.MustCompile(`^[0-9]+$`)
NumericDecimal = regexp.MustCompile(`^[0-9\.]+$`)
Opactiy = regexp.MustCompile(`^opacity\(([0-9]{1,2}|100)%\)$`)
Perspective = regexp.MustCompile(`perspective\(`)
Position = regexp.MustCompile(`^[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]* [[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]*]*$`)
Opacity = regexp.MustCompile(`^(0[.]?[0-9]*)|(1.0)$`)
QuotedAlpha = regexp.MustCompile(`^["'][a-z]+["']$`)
Quotes = regexp.MustCompile(`^([ ]*["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'] ["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'])+$`)
Rect = regexp.MustCompile(`^rect\([0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px\)$`)
RGB = regexp.MustCompile(`^rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)$`)
RGBA = regexp.MustCompile(`^rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)$`)
Rotate = regexp.MustCompile(`^rotate(x|y|z)?\(([12]?|3[0-5][0-9]|360)\)$`)
Rotate3D = regexp.MustCompile(`^rotate3d\(([ ]?(1(\.0)?|0\.[0-9]+),){3}([12]?|3[0-5][0-9]|360)\)$`)
Saturate = regexp.MustCompile(`^saturate\([0-9]+%\)$`)
Sepia = regexp.MustCompile(`^sepia\(([0-9]{1,2}|100)%\)$`)
Skew = regexp.MustCompile(`skew(x|y)?\(`)
Span = regexp.MustCompile(`^span [0-9]+$`)
Steps = regexp.MustCompile(`^steps\([ ]*[0-9]+([ ]*,[ ]*(start|end)?)\)$`)
Time = regexp.MustCompile(`^[0-9]+[\.]?[0-9]*(s|ms)?$`)
TransitionProp = regexp.MustCompile(`^([a-zA-Z]+,[ ]?)*[a-zA-Z]+$`)
TranslateScale = regexp.MustCompile(`(translate|translate3d|translatex|translatey|translatez|scale|scale3d|scalex|scaley|scalez)\(`)
URL = regexp.MustCompile(`^url\([\"\']?((https|http)[a-z0-9\.\\/_:]+[\"\']?)\)$`)
ZIndex = regexp.MustCompile(`^[\-]?[0-9]+$`)
)
func multiSplit(value string, seps ...string) []string {
@ -388,9 +431,7 @@ func AnimationHandler(value string) bool {
}
func AnimationDelayHandler(value string) bool {
reg := regexp.MustCompile(`[\-]?[0-9]+[\.]?[0-9]*[s|ms]?`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if NegTime.MatchString(value) {
return true
}
values := []string{"initial", "inherit"}
@ -405,9 +446,7 @@ func AnimationDirectionHandler(value string) bool {
}
func AnimationDurationHandler(value string) bool {
reg := regexp.MustCompile(`[0-9]+[\.]?[0-9]*[s|ms]?`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Time.MatchString(value) {
return true
}
values := []string{"initial", "inherit"}
@ -422,9 +461,7 @@ func AnimationFillModeHandler(value string) bool {
}
func AnimationIterationCountHandler(value string) bool {
reg := regexp.MustCompile(`[0-9]+[\.]?[0-9]*`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Count.MatchString(value) {
return true
}
values := []string{"infinite", "initial", "inherit"}
@ -433,9 +470,7 @@ func AnimationIterationCountHandler(value string) bool {
}
func AnimationNameHandler(value string) bool {
reg := regexp.MustCompile(`[a-z]+`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Alpha.MatchString(value)
}
func AnimationPlayStateHandler(value string) bool {
@ -450,14 +485,10 @@ func TimingFunctionHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`cubic-bezier\(([ ]*(0(.[0-9]+)?|1(.0)?),){3}[ ]*(0(.[0-9]+)?|1)\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if CubicBezier.MatchString(value) {
return true
}
reg = regexp.MustCompile(`steps\([ ]*[0-9]+([ ]*,[ ]*(start|end)?)\)`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Steps.MatchString(value)
}
func BackfaceVisibilityHandler(value string) bool {
@ -518,9 +549,7 @@ func ImageHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`url\([\"\']?((https|http)[a-z0-9\.\\/_:]+[\"\']?)\)`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return URL.MatchString(value)
}
func BackgroundOriginHandler(value string) bool {
@ -535,12 +564,7 @@ func BackgroundPositionHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]* [[\-]*[0-9]+[cm|mm|in|px|pt|pc\%]*]*`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
return true
}
return false
return Position.MatchString(value)
}
func BackgroundRepeatHandler(value string) bool {
@ -816,31 +840,19 @@ func CaretColorHandler(value string) bool {
if in(splitVals, colorValues) {
return true
}
reg := regexp.MustCompile(`#[0-9abcdef]{6}`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if HexRGB.MatchString(value) {
return true
}
reg = regexp.MustCompile(`rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if RGB.MatchString(value) {
return true
}
reg = regexp.MustCompile(`rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if RGBA.MatchString(value) {
return true
}
reg = regexp.MustCompile(`hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)`)
if reg.FindString(value) == value && value != "" {
if HSL.MatchString(value) {
return true
}
reg = regexp.MustCompile(`hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
return true
}
return false
return HSLA.MatchString(value)
}
func ClearHandler(value string) bool {
@ -850,9 +862,7 @@ func ClearHandler(value string) bool {
}
func ClipHandler(value string) bool {
reg := regexp.MustCompile(`rect\([0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px,[ ]*[0-9]+px\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Rect.MatchString(value) {
return true
}
values := []string{"auto", "initial", "inherit"}
@ -865,38 +875,23 @@ func ColorHandler(value string) bool {
if in(splitVals, colorValues) {
return true
}
reg := regexp.MustCompile(`#[0-9abcdef]{6}`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if HexRGB.MatchString(value) {
return true
}
reg = regexp.MustCompile(`rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if RGB.MatchString(value) {
return true
}
reg = regexp.MustCompile(`rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if RGBA.MatchString(value) {
return true
}
reg = regexp.MustCompile(`hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if HSL.MatchString(value) {
return true
}
reg = regexp.MustCompile(`hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
return true
}
return false
return HSLA.MatchString(value)
}
func ColumnCountHandler(value string) bool {
reg := regexp.MustCompile(`[0-9]+`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Numeric.MatchString(value) {
return true
}
values := []string{"auto", "initial", "inherit"}
@ -1000,54 +995,35 @@ func FilterHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`blur\([0-9]+px\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Blur.MatchString(value) {
return true
}
reg = regexp.MustCompile(`(brightness|contrast)\([0-9]+\%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if BrightnessCont.MatchString(value) {
return true
}
reg = regexp.MustCompile(`drop-shadow\(([-]?[0-9]+px) ([-]?[0-9]+px)( [-]?[0-9]+px)?( ([-]?[0-9]+px))?`)
reg.Longest()
colorValue := strings.TrimSuffix(string(reg.ReplaceAll([]byte(value), []byte{})), ")")
if DropShadow.MatchString(value) {
return true
}
colorValue := strings.TrimSuffix(string(DropShadow.ReplaceAll([]byte(value), []byte{})), ")")
if ColorHandler(colorValue) {
return true
}
reg = regexp.MustCompile(`grayscale\(([0-9]{1,2}|100)%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Grayscale.MatchString(value) {
return true
}
reg = regexp.MustCompile(`hue-rotate\(([12]?[0-9]{1,2}|3[0-5][0-9]|360)?\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if HueRotate.MatchString(value) {
return true
}
reg = regexp.MustCompile(`invert\(([0-9]{1,2}|100)%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Invert.MatchString(value) {
return true
}
reg = regexp.MustCompile(`opacity\(([0-9]{1,2}|100)%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Opacity.MatchString(value) {
return true
}
reg = regexp.MustCompile(`saturate\([0-9]+%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Saturate.MatchString(value) {
return true
}
reg = regexp.MustCompile(`sepia\(([0-9]{1,2}|100)%\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
return true
}
//Not allowing URLs
return false
return Sepia.MatchString(value)
}
func FlexHandler(value string) bool {
@ -1092,9 +1068,7 @@ func FlexFlowHandler(value string) bool {
}
func FlexGrowHandler(value string) bool {
reg := regexp.MustCompile(`[0-9\.]+`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if NumericDecimal.MatchString(value) {
return true
}
splitVals := strings.Split(value, ";")
@ -1144,11 +1118,9 @@ func FontFamilyHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`('[a-z \-]+'|[a-z \-]+)`)
reg.Longest()
for _, i := range splitVals {
i = strings.TrimSpace(i)
if reg.FindString(i) != i {
if Font.FindString(i) != i {
return false
}
}
@ -1162,9 +1134,7 @@ func FontKerningHandler(value string) bool {
}
func FontLanguageOverrideHandler(value string) bool {
reg := regexp.MustCompile(`[a-z]+`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Alpha.MatchString(value)
}
func FontSizeHandler(value string) bool {
@ -1177,9 +1147,7 @@ func FontSizeHandler(value string) bool {
}
func FontSizeAdjustHandler(value string) bool {
reg := regexp.MustCompile(`[0-9]+[\.]?[0-9]*`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Count.MatchString(value) {
return true
}
values := []string{"auto", "initial", "inherit"}
@ -1298,9 +1266,7 @@ func GridColumnGapHandler(value string) bool {
}
func LengthHandler(value string) bool {
reg := regexp.MustCompile(`[\-]?[0-9]+[\.]?[0-9]*(%|cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|deg|rad|turn)?`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Length.MatchString(value)
}
func LineBreakHandler(value string) bool {
@ -1310,12 +1276,10 @@ func LineBreakHandler(value string) bool {
}
func GridAxisStartEndHandler(value string) bool {
reg := regexp.MustCompile(`[0-9]+`)
if reg.FindString(value) == value && value != "" {
if Numeric.MatchString(value) {
return true
}
reg = regexp.MustCompile(`span [0-9]+`)
if reg.FindString(value) == value && value != "" {
if Span.MatchString(value) {
return true
}
values := []string{"auto"}
@ -1366,9 +1330,7 @@ func GridTemplateAreasHandler(value string) bool {
if in([]string{value}, values) {
return true
}
reg := regexp.MustCompile(`['"]?[a-z ]+['"]?`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return GridTemplateAreas.MatchString(value)
}
func GridTemplateColumnsHandler(value string) bool {
@ -1551,9 +1513,7 @@ func ObjectPositionHandler(value string) bool {
}
func OpacityHandler(value string) bool {
reg := regexp.MustCompile("(0[.]?[0-9]*)|(1.0)")
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Opacity.MatchString(value) {
return true
}
values := []string{"initial", "inherit"}
@ -1562,9 +1522,7 @@ func OpacityHandler(value string) bool {
}
func OrderHandler(value string) bool {
reg := regexp.MustCompile("[0-9]+")
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Numeric.MatchString(value) {
return true
}
values := []string{"initial", "inherit"}
@ -1629,9 +1587,7 @@ func OverflowWrapHandler(value string) bool {
}
func OrphansHandler(value string) bool {
reg := regexp.MustCompile(`[0-9]+`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Numeric.MatchString(value)
}
func PaddingHandler(value string) bool {
@ -1716,9 +1672,7 @@ func QuotesHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`([ ]*["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'] ["'][\x{0022}\x{0027}\x{2039}\x{2039}\x{203A}\x{00AB}\x{00BB}\x{2018}\x{2019}\x{201C}-\x{201E}]["'])+`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Quotes.MatchString(value)
}
func ResizeHandler(value string) bool {
@ -1766,9 +1720,7 @@ func TextCombineUprightHandler(value string) bool {
if in(splitVals, values) {
return true
}
reg := regexp.MustCompile(`digits [2-4]`)
reg.Longest()
return reg.FindString(value) == value && value != ""
return Digits.MatchString(value)
}
func TextDecorationHandler(value string) bool {
@ -1813,9 +1765,7 @@ func TextJustifyHandler(value string) bool {
}
func TextOverflowHandler(value string) bool {
reg := regexp.MustCompile("[\"'][a-z]+[\"']")
reg.Longest()
if reg.FindString(value) == value && value != "" {
if QuotedAlpha.MatchString(value) {
return true
}
values := []string{"clip", "ellipsis", "initial", "inherit"}
@ -1868,18 +1818,13 @@ func TransformHandler(value string) bool {
if in([]string{value}, values) {
return true
}
reg := regexp.MustCompile(`matrix\(([ ]*[0-9]+[\.]?[0-9]*,){5}([ ]*[0-9]+[\.]?[0-9]*)\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Matrix.MatchString(value) {
return true
}
reg = regexp.MustCompile(`matrix3d\(([ ]*[0-9]+[\.]?[0-9]*,){15}([ ]*[0-9]+[\.]?[0-9]*)\)`)
if reg.FindString(value) == value && value != "" {
if Matrix3D.MatchString(value) {
return true
}
reg = regexp.MustCompile(`(translate|translate3d|translatex|translatey|translatez|scale|scale3d|scalex|scaley|scalez)\(`)
reg.Longest()
subValue := string(reg.ReplaceAll([]byte(value), []byte{}))
subValue := string(TranslateScale.ReplaceAll([]byte(value), []byte{}))
trimValue := strings.Split(strings.TrimSuffix(subValue, ")"), ",")
valid := true
for _, i := range trimValue {
@ -1891,19 +1836,13 @@ func TransformHandler(value string) bool {
if valid && trimValue != nil {
return true
}
reg = regexp.MustCompile(`rotate(x|y|z)?\(([12]?|3[0-5][0-9]|360)\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Rotate.MatchString(value) {
return true
}
reg = regexp.MustCompile(`rotate3d\(([ ]?(1(\.0)?|0\.[0-9]+),){3}([12]?|3[0-5][0-9]|360)\)`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Rotate3D.MatchString(value) {
return true
}
reg = regexp.MustCompile(`skew(x|y)?\(`)
reg.Longest()
subValue = string(reg.ReplaceAll([]byte(value), []byte{}))
subValue = string(Skew.ReplaceAll([]byte(value), []byte{}))
subValue = strings.TrimSuffix(subValue, ")")
trimValue = strings.Split(subValue, ",")
valid = true
@ -1916,9 +1855,7 @@ func TransformHandler(value string) bool {
if valid {
return true
}
reg = regexp.MustCompile(`perspective\(`)
reg.Longest()
subValue = string(reg.ReplaceAll([]byte(value), []byte{}))
subValue = string(Perspective.ReplaceAll([]byte(value), []byte{}))
subValue = strings.TrimSuffix(subValue, ")")
return LengthHandler(subValue)
}
@ -1973,9 +1910,7 @@ func TransitionHandler(value string) bool {
}
func TransitionDelayHandler(value string) bool {
reg := regexp.MustCompile("[0-9]+[.]?[0-9]*(s|ms)?")
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Time.MatchString(value) {
return true
}
values := []string{"initial", "inherit"}
@ -1984,9 +1919,7 @@ func TransitionDelayHandler(value string) bool {
}
func TransitionDurationHandler(value string) bool {
reg := regexp.MustCompile("[0-9]+[.]?[0-9]*(s|ms)?")
reg.Longest()
if reg.FindString(value) == value && value != "" {
if Time.MatchString(value) {
return true
}
values := []string{"initial", "inherit"}
@ -1995,9 +1928,7 @@ func TransitionDurationHandler(value string) bool {
}
func TransitionPropertyHandler(value string) bool {
reg := regexp.MustCompile("([a-zA-Z]+,[ ]?)*[a-zA-Z]+")
reg.Longest()
if reg.FindString(value) == value && value != "" {
if TransitionProp.MatchString(value) {
return true
}
values := []string{"none", "all", "initial", "inherit"}
@ -2075,9 +2006,7 @@ func WritingModeHandler(value string) bool {
}
func ZIndexHandler(value string) bool {
reg := regexp.MustCompile(`[\-]?[0-9]+`)
reg.Longest()
if reg.FindString(value) == value && value != "" {
if ZIndex.MatchString(value) {
return true
}
values := []string{"auto", "initial", "inherit"}

View file

@ -117,7 +117,7 @@ var (
// This is not exported as it's not useful by itself, and only has value
// within the AllowDataURIImages func
dataURIImagePrefix = regexp.MustCompile(
`^image/(gif|jpeg|png|webp);base64,`,
`^image/(gif|jpeg|png|svg\+xml|webp);base64,`,
)
)
@ -193,6 +193,7 @@ func (p *Policy) AllowImages() {
// http://en.wikipedia.org/wiki/Data_URI_scheme
//
// Images must have a mimetype matching:
//
// image/gif
// image/jpeg
// image/png

View file

@ -879,6 +879,7 @@ func (p *Policy) addDefaultElementsWithoutAttrs() {
p.setOfElementsAllowedWithoutAttrs["optgroup"] = struct{}{}
p.setOfElementsAllowedWithoutAttrs["option"] = struct{}{}
p.setOfElementsAllowedWithoutAttrs["p"] = struct{}{}
p.setOfElementsAllowedWithoutAttrs["picture"] = struct{}{}
p.setOfElementsAllowedWithoutAttrs["pre"] = struct{}{}
p.setOfElementsAllowedWithoutAttrs["q"] = struct{}{}
p.setOfElementsAllowedWithoutAttrs["rp"] = struct{}{}

View file

@ -322,9 +322,7 @@ func (p *Policy) sanitize(r io.Reader, w io.Writer) error {
aps = aa
}
if len(token.Attr) != 0 {
token.Attr = escapeAttributes(
p.sanitizeAttrs(token.Data, token.Attr, aps),
)
token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps)
}
if len(token.Attr) == 0 {
@ -434,7 +432,7 @@ func (p *Policy) sanitize(r io.Reader, w io.Writer) error {
}
if len(token.Attr) != 0 {
token.Attr = escapeAttributes(p.sanitizeAttrs(token.Data, token.Attr, aps))
token.Attr = p.sanitizeAttrs(token.Data, token.Attr, aps)
}
if len(token.Attr) == 0 && !p.allowNoAttrs(token.Data) {
@ -442,8 +440,8 @@ func (p *Policy) sanitize(r io.Reader, w io.Writer) error {
if _, err := buff.WriteString(" "); err != nil {
return err
}
break
}
break
}
if !skipElementContent {
if _, err := buff.WriteString(token.String()); err != nil {
@ -565,11 +563,9 @@ attrsLoop:
for _, ap := range apl {
if ap.regexp != nil {
if ap.regexp.MatchString(htmlAttr.Val) {
htmlAttr.Val = escapeAttribute(htmlAttr.Val)
cleanAttrs = append(cleanAttrs, htmlAttr)
}
} else {
htmlAttr.Val = escapeAttribute(htmlAttr.Val)
cleanAttrs = append(cleanAttrs, htmlAttr)
}
}
@ -1112,18 +1108,3 @@ func normaliseElementName(str string) string {
`"`,
)
}
func escapeAttributes(attrs []html.Attribute) []html.Attribute {
escapedAttrs := []html.Attribute{}
for _, attr := range attrs {
attr.Val = escapeAttribute(attr.Val)
escapedAttrs = append(escapedAttrs, attr)
}
return escapedAttrs
}
func escapeAttribute(val string) string {
val = strings.Replace(val, string([]rune{'\u00A0'}), `&nbsp;`, -1)
val = strings.Replace(val, `"`, `&quot;`, -1)
return val
}

13
vendor/modules.txt vendored
View file

@ -490,6 +490,15 @@ github.com/getkin/kin-openapi/openapi3filter
github.com/getkin/kin-openapi/routers
github.com/getkin/kin-openapi/routers/legacy
github.com/getkin/kin-openapi/routers/legacy/pathpattern
# github.com/getsentry/sentry-go v0.26.0
## explicit; go 1.18
github.com/getsentry/sentry-go
github.com/getsentry/sentry-go/echo
github.com/getsentry/sentry-go/internal/debug
github.com/getsentry/sentry-go/internal/otel/baggage
github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage
github.com/getsentry/sentry-go/internal/ratelimit
github.com/getsentry/sentry-go/internal/traceparser
# github.com/ghodss/yaml v1.0.0
## explicit
github.com/ghodss/yaml
@ -776,8 +785,8 @@ github.com/mattn/go-sqlite3
# github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
## explicit; go 1.19
github.com/matttproud/golang_protobuf_extensions/v2/pbutil
# github.com/microcosm-cc/bluemonday v1.0.18
## explicit; go 1.16
# github.com/microcosm-cc/bluemonday v1.0.23
## explicit; go 1.19
github.com/microcosm-cc/bluemonday
github.com/microcosm-cc/bluemonday/css
# github.com/miekg/pkcs11 v1.1.1