From 7fd26395edaf077487badea471b5cfe579cf0856 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 22 Nov 2021 16:23:47 +0800 Subject: [PATCH] fix: typo change: do not add console output by default feat: add GA for docs refactor: move builtin to internal refactor: relocate sentry sdk feat: report events with ga change: use http client session fix: report GA events change: sentry --- .github/workflows/unittest.yml | 2 +- boomer.go | 12 ++- docs/CHANGELOG.md | 4 + docs/README.md | 2 +- docs/cmd/hrp.md | 2 +- docs/cmd/hrp_boom.md | 2 +- docs/cmd/hrp_har2case.md | 2 +- docs/cmd/hrp_run.md | 2 +- go.mod | 1 + har2case/core.go | 19 +++++ hrp/cmd/boom.go | 12 +-- hrp/cmd/har2case.go | 2 - hrp/cmd/root.go | 3 +- hrp/cmd/run.go | 2 - hrp/main.go | 6 +- {builtin => internal/builtin}/assertion.go | 0 .../builtin}/assertion_test.go | 0 {builtin => internal/builtin}/function.go | 0 internal/ga/client.go | 79 +++++++++++++++++++ internal/ga/client_test.go | 28 +++++++ internal/ga/events.go | 67 ++++++++++++++++ internal/ga/init.go | 15 ++++ internal/sentry/init.go | 27 +++++++ internal/version/init.go | 3 + log.go | 15 ---- mkdocs.yml | 3 + parser.go | 2 +- response.go | 2 +- runner.go | 11 +++ version.go | 3 - 30 files changed, 286 insertions(+), 42 deletions(-) rename {builtin => internal/builtin}/assertion.go (100%) rename {builtin => internal/builtin}/assertion_test.go (100%) rename {builtin => internal/builtin}/function.go (100%) create mode 100644 internal/ga/client.go create mode 100644 internal/ga/client_test.go create mode 100644 internal/ga/events.go create mode 100644 internal/ga/init.go create mode 100644 internal/sentry/init.go create mode 100644 internal/version/init.go delete mode 100644 version.go diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 053881a8..3342cb3e 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -1,4 +1,4 @@ -name: Run unittest with coverage +name: Run unittests on: push: diff --git a/boomer.go b/boomer.go index a2ba7105..8ffabd3d 100644 --- a/boomer.go +++ b/boomer.go @@ -4,6 +4,8 @@ import ( "time" "github.com/myzhan/boomer" + + "github.com/httprunner/hrp/internal/ga" ) func NewStandaloneBoomer(spawnCount int, spawnRate float64) *Boomer { @@ -11,7 +13,6 @@ func NewStandaloneBoomer(spawnCount int, spawnRate float64) *Boomer { Boomer: boomer.NewStandaloneBoomer(spawnCount, spawnRate), debug: false, } - b.AddOutput(boomer.NewConsoleOutput()) return b } @@ -26,6 +27,15 @@ func (b *Boomer) SetDebug(debug bool) *Boomer { } func (b *Boomer) Run(testcases ...ITestCase) { + event := ga.EventTracking{ + Category: "RunLoadTests", + Action: "hrp boom", + } + // report start event + go ga.SendEvent(event) + // report execution timing event + defer ga.SendEvent(event.StartTiming("execution")) + var taskSlice []*boomer.Task for _, iTestCase := range testcases { testcase, err := iTestCase.ToTestCase() diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fce1a6cb..0fcf980c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History +## v0.2.1 (2021-11-26) + +- feat: report events with Google Analytics + ## v0.2.0 (2021-11-19) - feat: deploy mkdocs to github pages when PR merged diff --git a/docs/README.md b/docs/README.md index 5cb87935..abb48c79 100644 --- a/docs/README.md +++ b/docs/README.md @@ -172,7 +172,7 @@ import ( ) func TestCaseDemo(t *testing.T) { - testcase := &hrp.TestCase{ + demoTestCase := &hrp.TestCase{ Config: hrp.TConfig{ Name: "demo with complex mechanisms", BaseURL: "https://postman-echo.com", diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index d844cb56..897a216b 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -22,4 +22,4 @@ Copyright 2021 debugtalk * [hrp har2case](hrp_har2case.md) - Convert HAR to json/yaml testcase files * [hrp run](hrp_run.md) - run API test -###### Auto generated by spf13/cobra on 19-Nov-2021 +###### Auto generated by spf13/cobra on 26-Nov-2021 diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index c02e15d1..54beeed1 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -37,4 +37,4 @@ hrp boom [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 19-Nov-2021 +###### Auto generated by spf13/cobra on 26-Nov-2021 diff --git a/docs/cmd/hrp_har2case.md b/docs/cmd/hrp_har2case.md index d8de39c7..9c584e20 100644 --- a/docs/cmd/hrp_har2case.md +++ b/docs/cmd/hrp_har2case.md @@ -23,4 +23,4 @@ hrp har2case harPath... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 19-Nov-2021 +###### Auto generated by spf13/cobra on 26-Nov-2021 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index dee9f374..66391dc2 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -30,4 +30,4 @@ hrp run path... [flags] * [hrp](hrp.md) - One-stop solution for HTTP(S) testing. -###### Auto generated by spf13/cobra on 19-Nov-2021 +###### Auto generated by spf13/cobra on 26-Nov-2021 diff --git a/go.mod b/go.mod index ebcd476b..009a266d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/getsentry/sentry-go v0.11.0 github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.3.0 github.com/jinzhu/copier v0.3.2 github.com/jmespath/go-jmespath v0.4.0 github.com/maja42/goval v1.2.1 diff --git a/har2case/core.go b/har2case/core.go index 5d983b39..04c1a312 100644 --- a/har2case/core.go +++ b/har2case/core.go @@ -14,6 +14,7 @@ import ( "github.com/pkg/errors" "github.com/httprunner/hrp" + "github.com/httprunner/hrp/internal/ga" ) var log = hrp.GetLogger() @@ -41,6 +42,15 @@ func (h *HAR) SetOutputDir(dir string) { } func (h *HAR) GenJSON() (jsonPath string, err error) { + event := ga.EventTracking{ + Category: "har2case", + Action: "hrp har2case --to-json", + } + // report start event + go ga.SendEvent(event) + // report running timing event + defer ga.SendEvent(event.StartTiming("execution")) + tCase, err := h.makeTestCase() if err != nil { return "", err @@ -51,6 +61,15 @@ func (h *HAR) GenJSON() (jsonPath string, err error) { } func (h *HAR) GenYAML() (yamlPath string, err error) { + event := ga.EventTracking{ + Category: "har2case", + Action: "hrp har2case --to-yaml", + } + // report start event + go ga.SendEvent(event) + // report running timing event + defer ga.SendEvent(event.StartTiming("execution")) + tCase, err := h.makeTestCase() if err != nil { return "", err diff --git a/hrp/cmd/boom.go b/hrp/cmd/boom.go index 7ca49faa..7cf4b443 100644 --- a/hrp/cmd/boom.go +++ b/hrp/cmd/boom.go @@ -3,7 +3,7 @@ package cmd import ( "time" - "github.com/getsentry/sentry-go" + "github.com/myzhan/boomer" "github.com/spf13/cobra" "github.com/httprunner/hrp" @@ -19,15 +19,15 @@ var boomCmd = &cobra.Command{ $ hrp boom examples/ # run testcases in specified folder`, Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { - sentry.CaptureMessage("start to run load test") var paths []hrp.ITestCase for _, arg := range args { paths = append(paths, &hrp.TestCasePath{Path: arg}) } - boomer := hrp.NewStandaloneBoomer(spawnCount, spawnRate) - boomer.EnableCPUProfile(cpuProfile, cpuProfileDuration) - boomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration) - boomer.Run(paths...) + hrpBoomer := hrp.NewStandaloneBoomer(spawnCount, spawnRate) + hrpBoomer.AddOutput(boomer.NewConsoleOutput()) + hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration) + hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration) + hrpBoomer.Run(paths...) }, } diff --git a/hrp/cmd/har2case.go b/hrp/cmd/har2case.go index 92eff177..749ff4e4 100644 --- a/hrp/cmd/har2case.go +++ b/hrp/cmd/har2case.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/getsentry/sentry-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -15,7 +14,6 @@ var har2caseCmd = &cobra.Command{ Long: `Convert HAR to json/yaml testcase files`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - sentry.CaptureMessage("start to convert HAR to testcases") var outputFiles []string for _, arg := range args { var outputPath string diff --git a/hrp/cmd/root.go b/hrp/cmd/root.go index 6785c2c6..d73c699d 100644 --- a/hrp/cmd/root.go +++ b/hrp/cmd/root.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/httprunner/hrp" + "github.com/httprunner/hrp/internal/version" ) // RootCmd represents the base command when called without any subcommands @@ -24,7 +25,7 @@ Copyright 2021 debugtalk`, } hrp.SetLogLevel(logLevel) }, - Version: hrp.VERSION, + Version: version.VERSION, } var ( diff --git a/hrp/cmd/run.go b/hrp/cmd/run.go index 9647cd55..6cf20870 100644 --- a/hrp/cmd/run.go +++ b/hrp/cmd/run.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/getsentry/sentry-go" "github.com/spf13/cobra" "github.com/httprunner/hrp" @@ -17,7 +16,6 @@ var runCmd = &cobra.Command{ $ hrp run examples/ # run testcases in specified folder`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - sentry.CaptureMessage("start to run API tests") var paths []hrp.ITestCase for _, arg := range args { paths = append(paths, &hrp.TestCasePath{Path: arg}) diff --git a/hrp/main.go b/hrp/main.go index 1c6e4fa2..c3c613f1 100644 --- a/hrp/main.go +++ b/hrp/main.go @@ -1,15 +1,13 @@ package main import ( - "time" - - "github.com/getsentry/sentry-go" "github.com/httprunner/hrp/hrp/cmd" + "github.com/httprunner/hrp/internal/sentry" ) func main() { // Flush buffered events before the program terminates. - defer sentry.Flush(2 * time.Second) + defer sentry.Flush() cmd.Execute() } diff --git a/builtin/assertion.go b/internal/builtin/assertion.go similarity index 100% rename from builtin/assertion.go rename to internal/builtin/assertion.go diff --git a/builtin/assertion_test.go b/internal/builtin/assertion_test.go similarity index 100% rename from builtin/assertion_test.go rename to internal/builtin/assertion_test.go diff --git a/builtin/function.go b/internal/builtin/function.go similarity index 100% rename from builtin/function.go rename to internal/builtin/function.go diff --git a/internal/ga/client.go b/internal/ga/client.go new file mode 100644 index 00000000..635c23c4 --- /dev/null +++ b/internal/ga/client.go @@ -0,0 +1,79 @@ +package ga + +import ( + "fmt" + "net/http" + "net/url" + "reflect" + "time" + + "github.com/google/uuid" +) + +const ( + gaAPIDebugURL = "https://www.google-analytics.com/debug/collect" // used for debug + gaAPIURL = "https://www.google-analytics.com/collect" +) + +type GAClient struct { + TrackingID string `form:"tid"` // Tracking ID / Property ID, XX-XXXXXXX-X + ClientID string `form:"cid"` // Anonymous Client ID + Version string `form:"v"` // Version + httpClient *http.Client // http client session +} + +// NewGAClient creates a new GAClient object with the trackingID and clientID. +func NewGAClient(trackingID string) *GAClient { + nodeUUID, _ := uuid.NewUUID() + return &GAClient{ + TrackingID: trackingID, + ClientID: nodeUUID.String(), + Version: "1", // constant v1 + httpClient: &http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +// SendEvent sends one event to Google Analytics +func (g *GAClient) SendEvent(e IEvent) error { + var data url.Values + if event, ok := e.(UserTimingTracking); ok { + event.duration = time.Since(event.startTime) + data = event.ToUrlValues() + } else { + data = e.ToUrlValues() + } + + // append common params + data.Add("v", g.Version) + data.Add("tid", g.TrackingID) + data.Add("cid", g.ClientID) + + resp, err := g.httpClient.PostForm(gaAPIURL, data) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("response status: %d", resp.StatusCode) + } + return nil +} + +func structToUrlValues(i interface{}) (values url.Values) { + values = url.Values{} + iVal := reflect.ValueOf(i) + for i := 0; i < iVal.NumField(); i++ { + formTagName := iVal.Type().Field(i).Tag.Get("form") + if formTagName == "" { + continue + } + if iVal.Field(i).IsZero() { + continue + } + values.Set(formTagName, fmt.Sprint(iVal.Field(i))) + } + return +} diff --git a/internal/ga/client_test.go b/internal/ga/client_test.go new file mode 100644 index 00000000..6340a3d3 --- /dev/null +++ b/internal/ga/client_test.go @@ -0,0 +1,28 @@ +package ga + +import ( + "testing" +) + +func TestSendEvents(t *testing.T) { + event := EventTracking{ + Category: "unittest", + Action: "SendEvents", + } + err := gaClient.SendEvent(event) + if err != nil { + t.Fatal(err) + } +} + +func TestStructToUrlValues(t *testing.T) { + event := EventTracking{ + Category: "unittest", + Action: "convert", + Label: "StructToUrlValues", + } + val := structToUrlValues(event) + if val.Encode() != "ea=convert&ec=unittest&el=StructToUrlValues" { + t.Fail() + } +} diff --git a/internal/ga/events.go b/internal/ga/events.go new file mode 100644 index 00000000..2d0a10f7 --- /dev/null +++ b/internal/ga/events.go @@ -0,0 +1,67 @@ +package ga + +import ( + "fmt" + "net/url" + "time" +) + +type IEvent interface { + ToUrlValues() url.Values +} + +type EventTracking struct { + HitType string `form:"t"` // Event hit type = event + Category string `form:"ec"` // Required. Event Category. + Action string `form:"ea"` // Required. Event Action. + Label string `form:"el"` // Optional. Event label + Value int `form:"ev"` // Optional. Event value +} + +func (e EventTracking) StartTiming(variable string) UserTimingTracking { + return UserTimingTracking{ + HitType: "timing", + Category: e.Category, + Variable: variable, + Label: e.Label, + startTime: time.Now(), // starts the timer + } +} + +func (e EventTracking) ToUrlValues() url.Values { + e.HitType = "event" + return structToUrlValues(e) +} + +type UserTimingTracking struct { + HitType string `form:"t"` // Timing hit type + Category string `form:"utc"` // Required. user timing category. e.g. jsonLoader + Variable string `form:"utv"` // Required. timing variable. e.g. load + Duration string `form:"utt"` // Required. time took duration. Required. + Label string `form:"utl"` // Optional. user timing label. e.g jQuery + startTime time.Time + duration time.Duration // time took duration +} + +func (e UserTimingTracking) ToUrlValues() url.Values { + e.HitType = "timing" + e.Duration = fmt.Sprintf("%d", int64(e.duration.Seconds()*1000)) + return structToUrlValues(e) +} + +type Exception struct { + HitType string `form:"t"` // Hit Type = exception + Description string `form:"exd"` // exception description. i.e. IOException + IsFatal string `form:"exf"` // if the exception was fatal + isFatal bool +} + +func (e Exception) ToUrlValues() url.Values { + e.HitType = "exception" + if e.isFatal { + e.IsFatal = "1" + } else { + e.IsFatal = "0" + } + return structToUrlValues(e) +} diff --git a/internal/ga/init.go b/internal/ga/init.go new file mode 100644 index 00000000..f484ae20 --- /dev/null +++ b/internal/ga/init.go @@ -0,0 +1,15 @@ +package ga + +const ( + trackingID = "UA-114587036-1" // Tracking ID for Google Analytics +) + +var gaClient *GAClient + +func init() { + gaClient = NewGAClient(trackingID) +} + +func SendEvent(e IEvent) error { + return gaClient.SendEvent(e) +} diff --git a/internal/sentry/init.go b/internal/sentry/init.go new file mode 100644 index 00000000..8b6c4844 --- /dev/null +++ b/internal/sentry/init.go @@ -0,0 +1,27 @@ +package sentry + +import ( + "time" + + "github.com/getsentry/sentry-go" + + "github.com/httprunner/hrp/internal/version" +) + +func init() { + // init sentry sdk + err := sentry.Init(sentry.ClientOptions{ + Dsn: "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292", + Release: version.VERSION, + }) + if err != nil { + panic("init sentry sdk failed!") + } + sentry.ConfigureScope(func(scope *sentry.Scope) { + scope.SetLevel(sentry.LevelError) + }) +} + +func Flush() { + sentry.Flush(3 * time.Second) +} diff --git a/internal/version/init.go b/internal/version/init.go new file mode 100644 index 00000000..e5b480e1 --- /dev/null +++ b/internal/version/init.go @@ -0,0 +1,3 @@ +package version + +const VERSION = "v0.2.1" diff --git a/log.go b/log.go index 9f21f9c1..af5dcf9c 100644 --- a/log.go +++ b/log.go @@ -4,25 +4,10 @@ import ( "os" "strings" - "github.com/getsentry/sentry-go" "github.com/rs/zerolog" zlog "github.com/rs/zerolog/log" ) -func init() { - // init sentry sdk - err := sentry.Init(sentry.ClientOptions{ - Dsn: "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292", - Release: VERSION, - }) - if err != nil { - log.Fatal().Err(err).Msg("init sentry sdk failed!") - } - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetLevel(sentry.LevelError) - }) -} - var log = zlog.Logger func SetLogLevel(level string) { diff --git a/mkdocs.yml b/mkdocs.yml index 296bd1a7..a46a3000 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,9 @@ extra: link: https://debugtalk.com - icon: fontawesome/brands/github-alt link: 'https://github.com/httprunner' + analytics: + provider: google + property: G-N2DPN3VP7K # index pages nav: diff --git a/parser.go b/parser.go index 21e98bd7..9bc0df8a 100644 --- a/parser.go +++ b/parser.go @@ -10,7 +10,7 @@ import ( "github.com/maja42/goval" - "github.com/httprunner/hrp/builtin" + "github.com/httprunner/hrp/internal/builtin" ) func buildURL(baseURL, stepURL string) string { diff --git a/response.go b/response.go index 15638492..80d0e5cc 100644 --- a/response.go +++ b/response.go @@ -9,7 +9,7 @@ import ( "github.com/jmespath/go-jmespath" - "github.com/httprunner/hrp/builtin" + "github.com/httprunner/hrp/internal/builtin" ) func NewResponseObject(t *testing.T, resp *http.Response) (*ResponseObject, error) { diff --git a/runner.go b/runner.go index a04eeda7..dfc2cf24 100644 --- a/runner.go +++ b/runner.go @@ -16,6 +16,8 @@ import ( "github.com/jinzhu/copier" "github.com/pkg/errors" + + "github.com/httprunner/hrp/internal/ga" ) // run API test with default configs @@ -69,6 +71,15 @@ func (r *Runner) SetProxyUrl(proxyUrl string) *Runner { } func (r *Runner) Run(testcases ...ITestCase) error { + event := ga.EventTracking{ + Category: "RunAPITests", + Action: "hrp run", + } + // report start event + go ga.SendEvent(event) + // report execution timing event + defer ga.SendEvent(event.StartTiming("execution")) + for _, iTestCase := range testcases { testcase, err := iTestCase.ToTestCase() if err != nil { diff --git a/version.go b/version.go deleted file mode 100644 index f0c8ef22..00000000 --- a/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package hrp - -const VERSION = "v0.2.0"