From 0bdfcd30a1b47da68029bbba70ebf2ca8536b5cc Mon Sep 17 00:00:00 2001 From: Justin Pye Date: Thu, 15 Jan 2026 11:12:43 +1200 Subject: [PATCH 1/2] feat: add UntrackedError type to exclude user environment errors from Sentry Introduce UntrackedError for errors caused by the user's environment (permissions, disk space, docker not running, etc.) that should be displayed to the user but not sent to error tracking since they are outside abctl's control. This is distinct from abctl.Error which provides user-friendly help messages for specific errors. UntrackedError is specifically about whether an error should be reported to Sentry, not about displaying help text. Addresses Sentry issues ABCTL-A and ABCTL-37 which account for over 20,000 events combined from user environment issues (missing files, permission denied errors) that are not actionable bugs. --- internal/abctl/error.go | 20 ++++++++++++++++++++ main.go | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/abctl/error.go b/internal/abctl/error.go index bee2103a..643f1815 100644 --- a/internal/abctl/error.go +++ b/internal/abctl/error.go @@ -1,6 +1,26 @@ package abctl var _ error = (*Error)(nil) +var _ error = (*UntrackedError)(nil) + +// UntrackedError wraps errors caused by the user's environment (permissions, +// disk space, docker not running, etc.) that should be displayed to the user +// but not sent to error tracking since they are outside abctl's control. +type UntrackedError struct { + err error +} + +func (e UntrackedError) Error() string { + return e.err.Error() +} + +func (e UntrackedError) Unwrap() error { + return e.err +} + +func NewUntrackedError(err error) *UntrackedError { + return &UntrackedError{err: err} +} // Error adds a user-friendly help message to specific errors. type Error struct { diff --git a/main.go b/main.go index d8c335d5..bf3bf4c7 100644 --- a/main.go +++ b/main.go @@ -79,7 +79,11 @@ func handleErr(ctx context.Context, err error) int { return 0 } - trace.CaptureError(ctx, err) + // Only send to error tracking if not an untracked error + var untrackedErr *abctl.UntrackedError + if !errors.As(err, &untrackedErr) { + trace.CaptureError(ctx, err) + } pterm.Error.Println(err) From 588c0c1ea5695bba2666badf41de7d0d6af00a89 Mon Sep 17 00:00:00 2001 From: Justin Pye Date: Thu, 15 Jan 2026 11:15:04 +1200 Subject: [PATCH 2/2] feat: wrap user environment errors with UntrackedError Wrap the following errors to exclude them from Sentry: - ErrDocker: docker not running or inaccessible - ErrPort: port already in use - ErrAirbyteDir: airbyte directory inaccessible - ErrIpAddressForHostFlag: invalid host flag (IP address) - ErrInvalidHostFlag: invalid host flag format - cannot read PG_VERSION file (excluding not found) --- internal/cmd/local/check.go | 20 ++++++++++++++------ internal/cmd/local/local.go | 4 +++- internal/docker/docker.go | 4 +++- internal/service/manager.go | 2 +- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/internal/cmd/local/check.go b/internal/cmd/local/check.go index e92d4413..3e81460f 100644 --- a/internal/cmd/local/check.go +++ b/internal/cmd/local/check.go @@ -35,14 +35,18 @@ func dockerInstalled(ctx context.Context, telClient telemetry.Client) (docker.Ve if dockerClient == nil { if dockerClient, err = docker.New(ctx); err != nil { pterm.Error.Println("Unable to create Docker client") - return docker.Version{}, fmt.Errorf("%w: unable to create client: %w", abctl.ErrDocker, err) + return docker.Version{}, abctl.NewUntrackedError( + fmt.Errorf("%w: unable to create client: %w", abctl.ErrDocker, err), + ) } } version, err := dockerClient.Version(ctx) if err != nil { pterm.Error.Println("Unable to communicate with the Docker daemon") - return docker.Version{}, fmt.Errorf("%w: %w", abctl.ErrDocker, err) + return docker.Version{}, abctl.NewUntrackedError( + fmt.Errorf("%w: %w", abctl.ErrDocker, err), + ) } span.SetAttributes( @@ -94,10 +98,14 @@ func portAvailable(ctx context.Context, port int) error { lc := &net.ListenConfig{} listener, err := lc.Listen(ctx, "tcp", fmt.Sprintf("localhost:%d", port)) if isErrorAddressAlreadyInUse(err) { - return fmt.Errorf("%w: port %d is already in use", abctl.ErrPort, port) + return abctl.NewUntrackedError( + fmt.Errorf("%w: port %d is already in use", abctl.ErrPort, port), + ) } if err != nil { - return fmt.Errorf("%w: unable to determine if port '%d' is available: %w", abctl.ErrPort, port, err) + return abctl.NewUntrackedError( + fmt.Errorf("%w: unable to determine if port '%d' is available: %w", abctl.ErrPort, port, err), + ) } // if we're able to bind to the port (and then release it), it should be available defer func() { @@ -193,10 +201,10 @@ func (e InvalidPortError) Error() string { func validateHostFlag(host string) error { if ip := net.ParseIP(host); ip != nil { - return abctl.ErrIpAddressForHostFlag + return abctl.NewUntrackedError(abctl.ErrIpAddressForHostFlag) } if !regexp.MustCompile(`^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$`).MatchString(host) { - return abctl.ErrInvalidHostFlag + return abctl.NewUntrackedError(abctl.ErrInvalidHostFlag) } return nil } diff --git a/internal/cmd/local/local.go b/internal/cmd/local/local.go index e8c8e7c1..28c94358 100644 --- a/internal/cmd/local/local.go +++ b/internal/cmd/local/local.go @@ -27,7 +27,9 @@ func (c *Cmd) BeforeApply() error { } if err := checkAirbyteDir(); err != nil { - return fmt.Errorf("%w: %w", abctl.ErrAirbyteDir, err) + return abctl.NewUntrackedError( + fmt.Errorf("%w: %w", abctl.ErrAirbyteDir, err), + ) } return nil diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 4244c852..0b2fbb14 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -148,7 +148,9 @@ func newWithOptions(ctx context.Context, newPing newPing, goos string) (*Docker, } } - return nil, fmt.Errorf("%w: unable to create docker client", abctl.ErrDocker) + return nil, abctl.NewUntrackedError( + fmt.Errorf("%w: unable to create docker client", abctl.ErrDocker), + ) } // createAndPing attempts to create a docker client and ping it to ensure we can communicate diff --git a/internal/service/manager.go b/internal/service/manager.go index 36c0cc11..d58d5d4f 100644 --- a/internal/service/manager.go +++ b/internal/service/manager.go @@ -241,7 +241,7 @@ func EnablePsql17() (bool, error) { pgVersion, err := pgData.Version() if err != nil && !errors.Is(err, fs.ErrNotExist) { - return false, fmt.Errorf("failed to determine if any previous psql version exists: %w", err) + return false, abctl.NewUntrackedError(fmt.Errorf("failed to determine if any previous psql version exists: %w", err)) } if pgVersion == "" || pgVersion == "17" {