Repository: datreeio/datree
Pull request #464 (2022-03-03 12:53:09)

feat: report unexpected cli errors


Patch:
diff --git a/bl/errorReporter/errorReporter.go b/bl/errorReporter/errorReporter.go
new file mode 100644
index 0000000..60b28f4
--- /dev/null
+++ b/bl/errorReporter/errorReporter.go
@@ -0,0 +1,76 @@
+package errorReporter
+
+import (
+	"fmt"
+	"runtime/debug"
+
+	"github.com/datreeio/datree/cmd"
+	"github.com/datreeio/datree/pkg/cliClient"
+	"github.com/datreeio/datree/pkg/deploymentConfig"
+	"github.com/datreeio/datree/pkg/localConfig"
+)
+
+func ReportCliError(panicErr interface{}) {
+	reporter := NewErrorReporter(cliClient.NewCliClient(deploymentConfig.URL), localConfig.NewLocalConfig())
+	reporter.ReportCliError(panicErr)
+}
+
+type LocalConfig interface {
+	GetLocalConfiguration() (*localConfig.ConfigContent, error)
+}
+
+type CliClient interface {
+	ReportCliError(reportCliErrorRequest cliClient.ReportCliErrorRequest) (StatusCode int, Error error)
+}
+
+type ErrorReporter struct {
+	config LocalConfig
+	client CliClient
+}
+
+func NewErrorReporter(client CliClient, localConfig LocalConfig) *ErrorReporter {
+	return &ErrorReporter{
+		client: client,
+		config: localConfig,
+	}
+}
+
+func (reporter *ErrorReporter) ReportCliError(panicErr interface{}) {
+	status, err := reporter.client.ReportCliError(cliClient.ReportCliErrorRequest{
+		Token:        reporter.getCliId(),
+		CliVersion:   cmd.CliVersion,
+		ErrorMessage: parsePanicError(panicErr),
+		StackTrace:   string(debug.Stack()),
+	})
+	if err != nil {
+		fmt.Println("failed to report unexpected error:\nstatus code:", status, "\nerror:", err.Error())
+	} else {
+		fmt.Println("unexpected error reported")
+	}
+}
+
+func (reporter *ErrorReporter) getCliId() (cliId string) {
+	defer func() {
+		if panicErr := recover(); panicErr != nil {
+			cliId = "unknown"
+		}
+	}()
+
+	config, err := reporter.config.GetLocalConfiguration()
+	if err != nil {
+		return "unknown"
+	} else {
+		return config.CliId
+	}
+}
+
+func parsePanicError(panicErr interface{}) string {
+	switch panicErr := panicErr.(type) {
+	case string:
+		return panicErr
+	case error:
+		return panicErr.Error()
+	default:
+		return fmt.Sprintf("%v", panicErr)
+	}
+}
diff --git a/go.mod b/go.mod
index 4388e36..6e7f9d6 100644
--- a/go.mod
+++ b/go.mod
@@ -20,7 +20,6 @@ require (
 	github.com/yannh/kubeconform v0.4.8
 	gopkg.in/yaml.v2 v2.4.0
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
-	sigs.k8s.io/yaml v1.2.0
 )
 
 require (
@@ -53,3 +52,5 @@ require (
 	golang.org/x/text v0.3.3 // indirect
 	gopkg.in/ini.v1 v1.51.0 // indirect
 )
+
+require sigs.k8s.io/yaml v1.2.0 // indirect
diff --git a/main.go b/main.go
index ee45b0c..1d6f0ba 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"os"
 
+	"github.com/datreeio/datree/bl/errorReporter"
 	"github.com/datreeio/datree/cmd"
 	"github.com/datreeio/datree/cmd/test"
 )
@@ -12,6 +13,14 @@ const DEFAULT_ERR_EXIT_CODE = 1
 const VIOLATIONS_FOUND_EXIT_CODE = 2
 
 func main() {
+	// global error handling
+	defer func() {
+		if panicErr := recover(); panicErr != nil {
+			errorReporter.ReportCliError(panicErr)
+			panic(panicErr)
+		}
+	}()
+
 	if err := cmd.Execute(); err != nil {
 		if errors.Is(err, test.ViolationsFoundError) {
 			os.Exit(VIOLATIONS_FOUND_EXIT_CODE)
diff --git a/pkg/cliClient/reportCliError.go b/pkg/cliClient/reportCliError.go
new file mode 100644
index 0000000..1fcb0f3
--- /dev/null
+++ b/pkg/cliClient/reportCliError.go
@@ -0,0 +1,23 @@
+package cliClient
+
+import (
+	"net/http"
+)
+
+type ReportCliErrorRequest struct {
+	Token        string `json:"token"`
+	CliVersion   string `json:"cliVersion"`
+	ErrorMessage string `json:"errorMessage"`
+	StackTrace   string `json:"stackTrace"`
+}
+
+func (c *CliClient) ReportCliError(reportCliErrorRequest ReportCliErrorRequest) (StatusCode int, Error error) {
+	headers := map[string]string{}
+	res, err := c.httpClient.Request(
+		http.MethodPost,
+		"/cli/public/report-cli-error",
+		reportCliErrorRequest,
+		headers,
+	)
+	return res.StatusCode, err
+}


Test patch:
diff --git a/bl/errorReporter/errorReporter_test.go b/bl/errorReporter/errorReporter_test.go
new file mode 100644
index 0000000..add5411
--- /dev/null
+++ b/bl/errorReporter/errorReporter_test.go
@@ -0,0 +1,101 @@
+package errorReporter
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/datreeio/datree/cmd"
+	"github.com/datreeio/datree/pkg/cliClient"
+	"github.com/datreeio/datree/pkg/localConfig"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/stretchr/testify/mock"
+)
+
+// client mock
+type mockCliClient struct {
+	mock.Mock
+}
+
+func (m *mockCliClient) ReportCliError(reportCliErrorRequest cliClient.ReportCliErrorRequest) (StatusCode int, Error error) {
+	m.Called(reportCliErrorRequest)
+	return 201, nil
+}
+
+// config mock
+type mockConfig struct {
+	mock.Mock
+}
+
+func (lc *mockConfig) GetLocalConfiguration() (*localConfig.ConfigContent, error) {
+	lc.Called()
+	return &localConfig.ConfigContent{
+		CliId:         "2qRg9jzJGcA73ftqEcXuBp",
+		SchemaVersion: "111",
+	}, nil
+}
+
+// test type
+type ErrorReporterTestCaseArgs struct {
+	panicErr interface{}
+}
+type ErrorReporterTestCase struct {
+	name     string
+	args     *ErrorReporterTestCaseArgs
+	expected cliClient.ReportCliErrorRequest
+}
+
+func TestErrorReporter(t *testing.T) {
+	tests := []*ErrorReporterTestCase{
+		reportErrorWithError(),
+		reportErrorWithStringError(),
+	}
+	mockedCliClient := &mockCliClient{}
+	mockedConfig := &mockConfig{}
+	mockedCliClient.On("ReportCliError", mock.Anything).Return(nil)
+	mockedConfig.On("GetLocalConfiguration").Return(nil)
+	errorReporter := &ErrorReporter{
+		client: mockedCliClient,
+		config: mockedConfig,
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errorReporter.ReportCliError(tt.args.panicErr)
+			reportCliErrorCalledArgs := (mockedCliClient.Calls[0].Arguments.Get(0)).(cliClient.ReportCliErrorRequest)
+			assert.Equal(t, tt.expected.ErrorMessage, reportCliErrorCalledArgs.ErrorMessage)
+			assert.Equal(t, tt.expected.Token, reportCliErrorCalledArgs.Token)
+			assert.Equal(t, tt.expected.CliVersion, reportCliErrorCalledArgs.CliVersion)
+		})
+	}
+}
+
+func reportErrorWithError() *ErrorReporterTestCase {
+	return &ErrorReporterTestCase{
+		name: "should call cli client with correct args, when error is of type error",
+		args: &ErrorReporterTestCaseArgs{
+			panicErr: errors.New("this is the error message"),
+		},
+		expected: cliClient.ReportCliErrorRequest{
+			Token:        "2qRg9jzJGcA73ftqEcXuBp",
+			CliVersion:   cmd.CliVersion,
+			ErrorMessage: "this is the error message",
+			StackTrace:   mock.Anything,
+		},
+	}
+}
+
+func reportErrorWithStringError() *ErrorReporterTestCase {
+	return &ErrorReporterTestCase{
+		name: "should call cli client with correct args, when error is string",
+		args: &ErrorReporterTestCaseArgs{
+			panicErr: "this is the error message",
+		},
+		expected: cliClient.ReportCliErrorRequest{
+			Token:        "2qRg9jzJGcA73ftqEcXuBp",
+			CliVersion:   cmd.CliVersion,
+			ErrorMessage: "this is the error message",
+			StackTrace:   mock.Anything,
+		},
+	}
+}