refactor(sentry): decouple agent import via string-match, rename predicate
Some checks failed
CI / Test (push) Successful in 2m46s
CI / Build (push) Successful in 1m35s
CI / Build-1 (push) Successful in 1m59s
CI / Build-2 (push) Successful in 1m35s
CI / Build-3 (push) Successful in 1m35s
CI / Build-4 (push) Successful in 1m33s
CI / Build-5 (push) Successful in 1m39s
CI / Lint (push) Failing after 2m33s
CI / Coverage (push) Successful in 2m56s
CI / Vet (push) Successful in 2m7s
Some checks failed
CI / Test (push) Successful in 2m46s
CI / Build (push) Successful in 1m35s
CI / Build-1 (push) Successful in 1m59s
CI / Build-2 (push) Successful in 1m35s
CI / Build-3 (push) Successful in 1m35s
CI / Build-4 (push) Successful in 1m33s
CI / Build-5 (push) Successful in 1m39s
CI / Lint (push) Failing after 2m33s
CI / Coverage (push) Successful in 2m56s
CI / Vet (push) Successful in 2m7s
This commit is contained in:
parent
9fe796f195
commit
9135332777
5 changed files with 36 additions and 23 deletions
|
|
@ -11,9 +11,11 @@ import (
|
||||||
"github.com/torrentclaw/unarr/internal/config"
|
"github.com/torrentclaw/unarr/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrDaemonNotRunning is returned by callers that need a running daemon but
|
// ErrDaemonNotRunning is returned when no daemon state file exists on disk.
|
||||||
// find no state file on disk. Sentinel so user-facing commands (stop/reload)
|
// Callers may wrap it with %w; downstream code uses errors.Is to detect it.
|
||||||
// can wrap it and Sentry can filter it out as a non-bug.
|
// NOTE: the message text is matched by the sentry package (string-match, to
|
||||||
|
// avoid an import cycle). Keep the prefix "daemon does not appear to be
|
||||||
|
// running" stable, or update sentry.daemonNotRunningMarker accordingly.
|
||||||
var ErrDaemonNotRunning = errors.New("daemon does not appear to be running (state file not found)")
|
var ErrDaemonNotRunning = errors.New("daemon does not appear to be running (state file not found)")
|
||||||
|
|
||||||
// DaemonState is written to disk every heartbeat for external tools to read.
|
// DaemonState is written to disk every heartbeat for external tools to read.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
@ -264,7 +265,10 @@ func runDaemonReload() error {
|
||||||
func stopDaemonByPID() error {
|
func stopDaemonByPID() error {
|
||||||
state, err := agent.LoadState()
|
state, err := agent.LoadState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if errors.Is(err, agent.ErrDaemonNotRunning) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read daemon state: %w", err)
|
||||||
}
|
}
|
||||||
return killPID(state.PID)
|
return killPID(state.PID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -45,7 +46,10 @@ func startReloadWatcher(rc *ReloadableConfig) {
|
||||||
func sendReloadSignal() error {
|
func sendReloadSignal() error {
|
||||||
state, err := agent.LoadState()
|
state, err := agent.LoadState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if errors.Is(err, agent.ErrDaemonNotRunning) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read daemon state: %w", err)
|
||||||
}
|
}
|
||||||
p, err := os.FindProcess(state.PID)
|
p, err := os.FindProcess(state.PID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ import (
|
||||||
|
|
||||||
gosentry "github.com/getsentry/sentry-go"
|
gosentry "github.com/getsentry/sentry-go"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/torrentclaw/unarr/internal/agent"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// dsn is injected at build time via ldflags. If empty, Sentry is disabled.
|
// dsn is injected at build time via ldflags. If empty, Sentry is disabled.
|
||||||
|
|
@ -48,11 +46,16 @@ func Close() {
|
||||||
gosentry.Flush(flushTimeout)
|
gosentry.Flush(flushTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// daemonNotRunningMarker matches the message of agent.ErrDaemonNotRunning
|
||||||
|
// without importing the agent package — avoids a sentry → agent dependency
|
||||||
|
// that would risk a cycle if agent ever needed to report errors itself.
|
||||||
|
const daemonNotRunningMarker = "daemon does not appear to be running"
|
||||||
|
|
||||||
// CaptureError sends a non-fatal error to Sentry with optional command context.
|
// CaptureError sends a non-fatal error to Sentry with optional command context.
|
||||||
// User-input errors (unknown flag/command, bad value) are skipped — they are
|
// Expected non-bug errors (bad CLI input, daemon not running) are skipped to
|
||||||
// not bugs, just noise.
|
// keep the issue feed signal-heavy.
|
||||||
func CaptureError(err error, command string) {
|
func CaptureError(err error, command string) {
|
||||||
if err == nil || isUserInputError(err) {
|
if err == nil || shouldSkipSentry(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,10 +67,7 @@ func CaptureError(err error, command string) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func isUserInputError(err error) bool {
|
func shouldSkipSentry(err error) bool {
|
||||||
if errors.Is(err, agent.ErrDaemonNotRunning) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
var notExist *pflag.NotExistError
|
var notExist *pflag.NotExistError
|
||||||
var valueReq *pflag.ValueRequiredError
|
var valueReq *pflag.ValueRequiredError
|
||||||
var invalidVal *pflag.InvalidValueError
|
var invalidVal *pflag.InvalidValueError
|
||||||
|
|
@ -78,7 +78,8 @@ func isUserInputError(err error) bool {
|
||||||
}
|
}
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
return strings.HasPrefix(msg, "unknown command ") ||
|
return strings.HasPrefix(msg, "unknown command ") ||
|
||||||
strings.HasPrefix(msg, "required flag(s)")
|
strings.HasPrefix(msg, "required flag(s)") ||
|
||||||
|
strings.Contains(msg, daemonNotRunningMarker)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecoverPanic captures a panic and re-panics after reporting.
|
// RecoverPanic captures a panic and re-panics after reporting.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package sentry
|
package sentry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/torrentclaw/unarr/internal/agent"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnvironment(t *testing.T) {
|
func TestEnvironment(t *testing.T) {
|
||||||
|
|
@ -51,12 +50,15 @@ func TestSetUser(t *testing.T) {
|
||||||
SetUser("agent-123")
|
SetUser("agent-123")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsUserInputErrorDaemonNotRunning(t *testing.T) {
|
func TestShouldSkipSentryDaemonNotRunning(t *testing.T) {
|
||||||
if !isUserInputError(agent.ErrDaemonNotRunning) {
|
// String must stay in sync with agent.ErrDaemonNotRunning. If that sentinel
|
||||||
t.Error("ErrDaemonNotRunning should be treated as user-input error")
|
// is reworded, this test fails loudly so the marker can be updated.
|
||||||
|
err := errors.New("daemon does not appear to be running (state file not found)")
|
||||||
|
if !shouldSkipSentry(err) {
|
||||||
|
t.Error("ErrDaemonNotRunning message should be skipped")
|
||||||
}
|
}
|
||||||
wrapped := fmt.Errorf("stop daemon: %w", agent.ErrDaemonNotRunning)
|
wrapped := fmt.Errorf("read daemon state: %w", err)
|
||||||
if !isUserInputError(wrapped) {
|
if !shouldSkipSentry(wrapped) {
|
||||||
t.Error("wrapped ErrDaemonNotRunning should be treated as user-input error")
|
t.Error("wrapped ErrDaemonNotRunning message should be skipped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue