diff --git a/go.mod b/go.mod
index a2270ec..9247bbb 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/go-coldbrew/errors
go 1.25.0
require (
- github.com/getsentry/raven-go v0.2.0
+ github.com/getsentry/sentry-go v0.43.0
github.com/go-coldbrew/log v0.2.5
github.com/go-coldbrew/options v0.2.4
github.com/google/uuid v1.6.0
@@ -14,13 +14,12 @@ require (
)
require (
- github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.39.1 // indirect
- github.com/pkg/errors v0.9.1 // indirect
golang.org/x/sys v0.42.0 // indirect
+ golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
diff --git a/go.sum b/go.sum
index e672d14..d61af1e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,17 @@
-github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
-github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
-github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
+github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4=
+github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/go-coldbrew/log v0.2.5 h1:GEa7m6fe2GF7u6LxDT2TUDNS1BzR5kGPU1D8eLIzl0I=
github.com/go-coldbrew/log v0.2.5/go.mod h1:Lrlc9y4H4tykAiekqwFgkufULf4Parh2KnnmbiDmeX4=
github.com/go-coldbrew/options v0.2.4 h1:aGcjQWhXjibRRN1XVc3mzz2IAKxNlVC4+xyhrGnSRKg=
github.com/go-coldbrew/options v0.2.4/go.mod h1:RstwV0WeRJyUN2/P7M0l67LTsLeUfCXkaLU2LrXRx7M=
+github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
@@ -47,6 +47,8 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -59,6 +61,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
github.com/stvp/rollbar v0.5.1 h1:qvyWbd0RNL5V27MBumqCXlcU7ohmHeEtKX+Czc8oeuw=
github.com/stvp/rollbar v0.5.1/go.mod h1:/fyFC854GgkbHRz/rSsiYc6h84o0G5hxBezoQqRK7Ho=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
diff --git a/notifier/README.md b/notifier/README.md
index 7bfbe8f..6f90625 100755
--- a/notifier/README.md
+++ b/notifier/README.md
@@ -39,16 +39,16 @@ import "github.com/go-coldbrew/errors/notifier"
-## func [Close]()
+## func [Close]()
```go
func Close()
```
-Close closes the airbrake notifier and flushes the error queue. You should call Close before app shutdown. Close doesn't call os.Exit.
+Close closes the airbrake notifier and flushes pending Sentry events. Sentry events are flushed with a 2 second timeout. You should call Close before app shutdown. Close doesn't call os.Exit.
-## func [GetTraceHeaderName]()
+## func [GetTraceHeaderName]()
```go
func GetTraceHeaderName() string
@@ -57,7 +57,7 @@ func GetTraceHeaderName() string
GetTraceHeaderName gets the header name for trace id default is x\-trace\-id
-## func [GetTraceId]()
+## func [GetTraceId]()
```go
func GetTraceId(ctx context.Context) string
@@ -66,7 +66,7 @@ func GetTraceId(ctx context.Context) string
GetTraceId fetches traceID from context if no trace id is found then it will return empty string
-## func [InitAirbrake]()
+## func [InitAirbrake]()
```go
func InitAirbrake(projectID int64, projectKey string)
@@ -75,7 +75,7 @@ func InitAirbrake(projectID int64, projectKey string)
InitAirbrake inits airbrake configuration projectID: airbrake project id projectKey: airbrake project key
-## func [InitRollbar]()
+## func [InitRollbar]()
```go
func InitRollbar(token, env string)
@@ -84,7 +84,7 @@ func InitRollbar(token, env string)
InitRollbar inits rollbar configuration token: rollbar token env: rollbar environment
-## func [InitSentry]()
+## func [InitSentry]()
```go
func InitSentry(dsn string)
@@ -93,7 +93,7 @@ func InitSentry(dsn string)
InitSentry inits sentry configuration dsn: sentry dsn
-## func [Notify]()
+## func [Notify]()
```go
func Notify(err error, rawData ...interface{}) error
@@ -102,7 +102,7 @@ func Notify(err error, rawData ...interface{}) error
Notify notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata
-## func [NotifyAsync]()
+## func [NotifyAsync]()
```go
func NotifyAsync(err error, rawData ...interface{}) error
@@ -111,7 +111,7 @@ func NotifyAsync(err error, rawData ...interface{}) error
NotifyAsync sends an error notification asynchronously with bounded concurrency. If the async notification pool is full, the notification is dropped to prevent goroutine explosion under sustained error bursts. Returns the original error for convenience.
-## func [NotifyOnPanic]()
+## func [NotifyOnPanic]()
```go
func NotifyOnPanic(rawData ...interface{})
@@ -120,7 +120,7 @@ func NotifyOnPanic(rawData ...interface{})
NotifyOnPanic notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata this function should be called in defer example: defer NotifyOnPanic\(ctx, "some data"\) example: defer NotifyOnPanic\(ctx, "some data", Tags\{"tag1": "value1"\}\)
-## func [NotifyWithExclude]()
+## func [NotifyWithExclude]()
```go
func NotifyWithExclude(err error, rawData ...interface{}) error
@@ -129,7 +129,7 @@ func NotifyWithExclude(err error, rawData ...interface{}) error
NotifyWithExclude notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata
-## func [NotifyWithLevel]()
+## func [NotifyWithLevel]()
```go
func NotifyWithLevel(err error, level string, rawData ...interface{}) error
@@ -138,7 +138,7 @@ func NotifyWithLevel(err error, level string, rawData ...interface{}) error
NotifyWithLevel notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify level: error level rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata
-## func [NotifyWithLevelAndSkip]()
+## func [NotifyWithLevelAndSkip]()
```go
func NotifyWithLevelAndSkip(err error, skip int, level string, rawData ...interface{}) error
@@ -147,7 +147,7 @@ func NotifyWithLevelAndSkip(err error, skip int, level string, rawData ...interf
NotifyWithLevelAndSkip notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored err: error to notify skip: skip stack frames when notify error level: error level rawData: extra data to notify with error \(can be context.Context, Tags, or any other data\) when rawData is context.Context, it will used to get extra data from loggers.FromContext\(ctx\) and tags from metadata
-## func [SetEnvironment]()
+## func [SetEnvironment]()
```go
func SetEnvironment(env string)
@@ -156,7 +156,7 @@ func SetEnvironment(env string)
SetEnvironment sets the environment. The environment is used to distinguish errors occurring in different
-## func [SetHostname]()
+## func [SetHostname]()
```go
func SetHostname(name string)
@@ -165,7 +165,7 @@ func SetHostname(name string)
SetHostname sets the hostname of the server. The hostname is used to identify the server that logged an error.
-## func [SetMaxAsyncNotifications]()
+## func [SetMaxAsyncNotifications]()
```go
func SetMaxAsyncNotifications(n int)
@@ -174,7 +174,7 @@ func SetMaxAsyncNotifications(n int)
SetMaxAsyncNotifications sets the maximum number of concurrent async notification goroutines. When the limit is reached, new async notifications are dropped to prevent goroutine explosion under sustained error bursts. Default is 1000. Can only be called once; subsequent calls are no\-ops.
-## func [SetRelease]()
+## func [SetRelease]()
```go
func SetRelease(rel string)
@@ -183,7 +183,7 @@ func SetRelease(rel string)
SetRelease sets the release tag. The release tag is used to group errors together by release.
-## func [SetServerRoot]()
+## func [SetServerRoot]()
```go
func SetServerRoot(path string)
@@ -192,7 +192,7 @@ func SetServerRoot(path string)
SetServerRoot sets the root directory of the project. The root directory is used to trim prefixes from filenames in stack traces.
-## func [SetTraceHeaderName]()
+## func [SetTraceHeaderName]()
```go
func SetTraceHeaderName(name string)
@@ -201,7 +201,7 @@ func SetTraceHeaderName(name string)
SetTraceHeaderName sets the header name for trace id default is x\-trace\-id
-## func [SetTraceId]()
+## func [SetTraceId]()
```go
func SetTraceId(ctx context.Context) context.Context
@@ -210,7 +210,7 @@ func SetTraceId(ctx context.Context) context.Context
SetTraceId updates the traceID based on context values if no trace id is found then it will create one and update the context You should use the context returned by this function instead of the one passed
-## func [UpdateTraceId]()
+## func [UpdateTraceId]()
```go
func UpdateTraceId(ctx context.Context, traceID string) context.Context
@@ -219,7 +219,7 @@ func UpdateTraceId(ctx context.Context, traceID string) context.Context
UpdateTraceId force updates the traced id to provided id if no trace id is found then it will create one and update the context You should use the context returned by this function instead of the one passed
-## type [Tags]()
+## type [Tags]()
diff --git a/notifier/notifier.go b/notifier/notifier.go
index a014ebe..826b514 100644
--- a/notifier/notifier.go
+++ b/notifier/notifier.go
@@ -8,8 +8,9 @@ import (
"strconv"
"strings"
"sync"
+ "time"
- raven "github.com/getsentry/raven-go"
+ "github.com/getsentry/sentry-go"
"github.com/go-coldbrew/errors"
"github.com/go-coldbrew/log"
"github.com/go-coldbrew/log/loggers"
@@ -22,12 +23,14 @@ import (
)
var (
- airbrake *gobrake.Notifier
- rollbarInited bool
- sentryInited bool
- serverRoot string
- hostname string
- traceHeader string = "x-trace-id"
+ airbrake *gobrake.Notifier
+ rollbarInited bool
+ sentryInited bool
+ sentryEnvironment string
+ sentryRelease string
+ serverRoot string
+ hostname string
+ traceHeader string = "x-trace-id"
// asyncSem is a semaphore that bounds the number of concurrent async
// notification goroutines. When full, new notifications are dropped
@@ -121,8 +124,12 @@ func InitRollbar(token, env string) {
// dsn: sentry dsn
func InitSentry(dsn string) {
sentryInited = false
- if err := raven.SetDSN(dsn); err != nil {
- log.Error(context.Background(), "msg", "failed to set sentry DSN", "err", err)
+ if err := sentry.Init(sentry.ClientOptions{
+ Dsn: dsn,
+ Environment: sentryEnvironment,
+ Release: sentryRelease,
+ }); err != nil {
+ log.Error(context.Background(), "msg", "failed to init sentry", "err", err)
return
}
sentryInited = true
@@ -152,32 +159,47 @@ func convToRollbar(in []errors.StackFrame) rollbar.Stack {
return out
}
-func convToSentry(in errors.ErrorExt) *raven.Stacktrace {
- out := new(raven.Stacktrace)
+func convToSentry(in errors.ErrorExt) *sentry.Stacktrace {
pcs := in.Callers()
- frames := make([]*raven.StacktraceFrame, 0)
+ frames := make([]sentry.Frame, 0, len(pcs))
callersFrames := runtime.CallersFrames(pcs)
for {
fr, more := callersFrames.Next()
if fr.Func != nil {
- frame := raven.NewStacktraceFrame(fr.PC, fr.Function, fr.File, fr.Line, 3, []string{})
- if frame != nil {
- frame.InApp = true
- frames = append(frames, frame)
+ module := fr.Function
+ function := fr.Function
+ if idx := strings.LastIndex(fr.Function, "/"); idx != -1 {
+ // Split "github.com/pkg.Func" into module and function
+ rest := fr.Function[idx+1:]
+ if dotIdx := strings.Index(rest, "."); dotIdx != -1 {
+ module = fr.Function[:idx+1+dotIdx]
+ function = rest[dotIdx+1:]
+ }
+ } else if idx := strings.Index(fr.Function, "."); idx != -1 {
+ module = fr.Function[:idx]
+ function = fr.Function[idx+1:]
}
+ frames = append(frames, sentry.Frame{
+ Function: function,
+ Module: module,
+ Filename: fr.File,
+ AbsPath: fr.File,
+ Lineno: fr.Line,
+ InApp: true,
+ })
}
if !more {
break
}
}
+ // Reverse: sentry expects oldest frame first (bottom of stack)
for i := len(frames)/2 - 1; i >= 0; i-- {
opp := len(frames) - 1 - i
frames[i], frames[opp] = frames[opp], frames[i]
}
- out.Frames = frames
- return out
+ return &sentry.Stacktrace{Frames: frames}
}
// parseRawData parses raw data to extra data and tags
@@ -223,6 +245,47 @@ func NotifyWithLevel(err error, level string, rawData ...interface{}) error {
return NotifyWithLevelAndSkip(err, 2, level, rawData...)
}
+func buildSentryEvent(err errors.ErrorExt, level string, extra map[string]interface{}, tagData []map[string]string) *sentry.Event {
+ var sentryLevel sentry.Level
+ switch level {
+ case "critical":
+ sentryLevel = sentry.LevelFatal
+ case "warning":
+ sentryLevel = sentry.LevelWarning
+ default:
+ sentryLevel = sentry.LevelError
+ }
+
+ event := &sentry.Event{
+ Message: err.Error(),
+ Level: sentryLevel,
+ Environment: sentryEnvironment,
+ Release: sentryRelease,
+ Extra: extra,
+ Exception: []sentry.Exception{
+ {
+ Type: reflect.TypeOf(err).String(),
+ Value: err.Error(),
+ Stacktrace: convToSentry(err),
+ },
+ },
+ }
+
+ if len(tagData) > 0 {
+ tags := make(map[string]string)
+ for _, t := range tagData {
+ for k, v := range t {
+ tags[k] = v
+ }
+ }
+ if len(tags) > 0 {
+ event.Tags = tags
+ }
+ }
+
+ return event
+}
+
// NotifyWithLevelAndSkip notifies error to airbrake, rollbar and sentry if they are inited and error is not ignored
// err: error to notify
// skip: skip stack frames when notify error
@@ -319,24 +382,8 @@ func doNotify(err error, skip int, level string, rawData ...interface{}) error {
}
if sentryInited {
- var defLevel raven.Severity
- switch level {
- case "critical":
- defLevel = raven.FATAL
- case "warning":
- defLevel = raven.WARNING
- default:
- defLevel = raven.ERROR
- }
- ravenExp := raven.NewException(errWithStack, convToSentry(errWithStack))
- packet := raven.NewPacketWithExtra(errWithStack.Error(), parsedData, ravenExp)
-
- for _, tags := range tagData {
- packet.AddTags(tags)
- }
-
- packet.Level = defLevel
- raven.Capture(packet, nil)
+ event := buildSentryEvent(errWithStack, level, parsedData, tagData)
+ sentry.CaptureEvent(event)
}
log.GetLogger().Log(ctx, loggers.ErrorLevel, skip+1, "err", errWithStack, "stack", errWithStack.StackFrame())
@@ -405,27 +452,24 @@ func NotifyOnPanic(rawData ...interface{}) {
rollbar.ErrorWithStack(rollbar.CRIT, e, convToRollbar(e.StackFrame()), &rollbar.Field{Name: "panic", Data: r})
}
if sentryInited {
- ravenExp := raven.NewException(e, convToSentry(e))
- packet := raven.NewPacketWithExtra(e.Error(), parsedData, ravenExp)
-
- for _, tags := range tagData {
- packet.AddTags(tags)
- }
-
- packet.Level = raven.FATAL
- raven.Capture(packet, nil)
+ event := buildSentryEvent(e, "critical", parsedData, tagData)
+ sentry.CaptureEvent(event)
}
panic(e)
}
}
-// Close closes the airbrake notifier and flushes the error queue.
+// Close closes the airbrake notifier and flushes pending Sentry events.
+// Sentry events are flushed with a 2 second timeout.
// You should call Close before app shutdown.
// Close doesn't call os.Exit.
func Close() {
if airbrake != nil {
airbrake.Close()
}
+ if sentryInited {
+ sentry.Flush(2 * time.Second)
+ }
}
// SetEnvironment sets the environment.
@@ -438,13 +482,13 @@ func SetEnvironment(env string) {
})
}
rollbar.Environment = env
- raven.SetEnvironment(env)
+ sentryEnvironment = env
}
// SetRelease sets the release tag.
// The release tag is used to group errors together by release.
func SetRelease(rel string) {
- raven.SetRelease(rel)
+ sentryRelease = rel
}
// SetTraceId updates the traceID based on context values
diff --git a/notifier/notifier_sentry_test.go b/notifier/notifier_sentry_test.go
new file mode 100644
index 0000000..41d1af2
--- /dev/null
+++ b/notifier/notifier_sentry_test.go
@@ -0,0 +1,241 @@
+package notifier
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/getsentry/sentry-go"
+ "github.com/go-coldbrew/errors"
+)
+
+// capturedEvents collects sentry events via BeforeSend for test assertions.
+type capturedEvents struct {
+ mu sync.Mutex
+ events []*sentry.Event
+}
+
+func (c *capturedEvents) add(event *sentry.Event) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.events = append(c.events, event)
+}
+
+func (c *capturedEvents) last() *sentry.Event {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if len(c.events) == 0 {
+ return nil
+ }
+ return c.events[len(c.events)-1]
+}
+
+func (c *capturedEvents) reset() {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.events = nil
+}
+
+// initTestSentry initializes sentry with a BeforeSend hook that captures events
+// instead of sending them over the network.
+func initTestSentry(t *testing.T) *capturedEvents {
+ t.Helper()
+ captured := &capturedEvents{}
+ err := sentry.Init(sentry.ClientOptions{
+ Dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
+ Transport: &sentry.HTTPSyncTransport{},
+ BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+ captured.add(event)
+ return nil // don't actually send
+ },
+ })
+ if err != nil {
+ t.Fatalf("failed to init sentry for test: %v", err)
+ }
+ sentryInited = true
+ t.Cleanup(func() {
+ sentryInited = false
+ sentryEnvironment = ""
+ sentryRelease = ""
+ })
+ return captured
+}
+
+func TestInitSentry_ValidDSN(t *testing.T) {
+ sentryInited = false
+ t.Cleanup(func() { sentryInited = false })
+ InitSentry("https://examplePublicKey@o0.ingest.sentry.io/0")
+ if !sentryInited {
+ t.Error("expected sentryInited to be true after valid DSN")
+ }
+}
+
+func TestInitSentry_InvalidDSN(t *testing.T) {
+ sentryInited = false
+ InitSentry("not-a-valid-dsn")
+ if sentryInited {
+ t.Error("expected sentryInited to be false after invalid DSN")
+ }
+}
+
+func TestConvToSentry(t *testing.T) {
+ err := errors.New("test error")
+ errExt := err.(errors.ErrorExt)
+
+ st := convToSentry(errExt)
+ if st == nil {
+ t.Fatal("expected non-nil stacktrace")
+ }
+ if len(st.Frames) == 0 {
+ t.Fatal("expected at least one frame")
+ }
+
+ // Oldest frame should be first (bottom of stack)
+ // The last frame should be closest to this test function
+ lastFrame := st.Frames[len(st.Frames)-1]
+ if lastFrame.Function == "" {
+ t.Error("expected non-empty Function on last frame")
+ }
+ if lastFrame.Filename == "" {
+ t.Error("expected non-empty Filename on last frame")
+ }
+ if lastFrame.Lineno == 0 {
+ t.Error("expected non-zero Lineno on last frame")
+ }
+ if !lastFrame.InApp {
+ t.Error("expected InApp to be true")
+ }
+}
+
+func TestSentryLevelMapping(t *testing.T) {
+ captured := initTestSentry(t)
+
+ tests := []struct {
+ level string
+ expected sentry.Level
+ }{
+ {"critical", sentry.LevelFatal},
+ {"warning", sentry.LevelWarning},
+ {"error", sentry.LevelError},
+ {"", sentry.LevelError},
+ }
+
+ for _, tc := range tests {
+ captured.reset()
+ NotifyWithLevel(errors.New("test"), tc.level)
+ event := captured.last()
+ if event == nil {
+ t.Fatalf("expected captured event for level %q", tc.level)
+ }
+ if event.Level != tc.expected {
+ t.Errorf("level %q: got %v, want %v", tc.level, event.Level, tc.expected)
+ }
+ }
+}
+
+func TestSentryTags(t *testing.T) {
+ captured := initTestSentry(t)
+
+ tags := Tags{"method": "TestService.Get", "duration": "100ms"}
+ Notify(errors.New("test error"), tags)
+
+ event := captured.last()
+ if event == nil {
+ t.Fatal("expected captured event")
+ }
+ if event.Tags["method"] != "TestService.Get" {
+ t.Errorf("expected tag method=TestService.Get, got %v", event.Tags["method"])
+ }
+ if event.Tags["duration"] != "100ms" {
+ t.Errorf("expected tag duration=100ms, got %v", event.Tags["duration"])
+ }
+}
+
+func TestSentryEnvironmentRelease(t *testing.T) {
+ captured := initTestSentry(t)
+
+ SetEnvironment("staging")
+ SetRelease("v1.2.3")
+
+ Notify(errors.New("test error"))
+
+ event := captured.last()
+ if event == nil {
+ t.Fatal("expected captured event")
+ }
+ if event.Environment != "staging" {
+ t.Errorf("expected environment=staging, got %v", event.Environment)
+ }
+ if event.Release != "v1.2.3" {
+ t.Errorf("expected release=v1.2.3, got %v", event.Release)
+ }
+}
+
+func TestSentryExtra(t *testing.T) {
+ captured := initTestSentry(t)
+
+ Notify(errors.New("test error"), "some extra data")
+
+ event := captured.last()
+ if event == nil {
+ t.Fatal("expected captured event")
+ }
+ if len(event.Extra) == 0 {
+ t.Error("expected non-empty Extra on event")
+ }
+}
+
+func TestNotifyOnPanicSentry(t *testing.T) {
+ captured := initTestSentry(t)
+
+ func() {
+ defer func() {
+ // NotifyOnPanic re-panics, so we catch it here
+ recover()
+ }()
+ defer NotifyOnPanic()
+ panic("test panic")
+ }()
+
+ event := captured.last()
+ if event == nil {
+ t.Fatal("expected captured event from panic")
+ }
+ if event.Level != sentry.LevelFatal {
+ t.Errorf("expected LevelFatal for panic, got %v", event.Level)
+ }
+}
+
+func TestCloseSentry(t *testing.T) {
+ initTestSentry(t)
+ // Close should not panic
+ Close()
+}
+
+func TestBuildSentryEvent(t *testing.T) {
+ err := errors.New("build event test")
+ errExt := err.(errors.ErrorExt)
+
+ extra := map[string]interface{}{"key": "value"}
+ tagData := []map[string]string{{"tag1": "val1"}}
+
+ event := buildSentryEvent(errExt, "warning", extra, tagData)
+
+ if event.Level != sentry.LevelWarning {
+ t.Errorf("expected LevelWarning, got %v", event.Level)
+ }
+ if event.Message != "build event test" {
+ t.Errorf("expected message 'build event test', got %v", event.Message)
+ }
+ if event.Extra["key"] != "value" {
+ t.Errorf("expected extra key=value, got %v", event.Extra["key"])
+ }
+ if event.Tags["tag1"] != "val1" {
+ t.Errorf("expected tag tag1=val1, got %v", event.Tags["tag1"])
+ }
+ if len(event.Exception) != 1 {
+ t.Fatalf("expected 1 exception, got %d", len(event.Exception))
+ }
+ if event.Exception[0].Stacktrace == nil {
+ t.Error("expected non-nil stacktrace in exception")
+ }
+}