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
This commit is contained in:
debugtalk
2021-11-22 16:23:47 +08:00
parent ae7710e9d0
commit 7fd26395ed
30 changed files with 286 additions and 42 deletions

View File

@@ -0,0 +1,85 @@
package builtin
import (
"fmt"
"strings"
"github.com/stretchr/testify/assert"
)
var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool{
"equals": assert.EqualValues,
"equal": assert.EqualValues, // alias for equals
"greater_than": assert.Greater,
"less_than": assert.Less,
"greater_or_equals": assert.GreaterOrEqual,
"less_or_equals": assert.LessOrEqual,
"not_equal": assert.NotEqual,
"contains": assert.Contains,
"regex_match": assert.Regexp,
// custom assertions
"startswith": StartsWith, // check if string starts with substring
"endswith": EndsWith, // check if string ends with substring
"length_equals": EqualLength,
"length_equal": EqualLength, // alias for length_equals
}
func StartsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
return false
}
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
return false
}
actualString := actual.(string)
expectedString := expected.(string)
return assert.True(t, strings.HasPrefix(actualString, expectedString), msgAndArgs...)
}
func EndsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
return false
}
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
return false
}
actualString := actual.(string)
expectedString := expected.(string)
return assert.True(t, strings.HasSuffix(actualString, expectedString), msgAndArgs...)
}
func EqualLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
length, err := convertInt(expected)
if err != nil {
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
}
return assert.Len(t, actual, length, msgAndArgs...)
}
func convertInt(value interface{}) (int, error) {
switch v := value.(type) {
case int:
return v, nil
case int8:
return int(v), nil
case int16:
return int(v), nil
case int32:
return int(v), nil
case int64:
return int(v), nil
case uint:
return int(v), nil
case uint8:
return int(v), nil
case uint16:
return int(v), nil
case uint32:
return int(v), nil
case uint64:
return int(v), nil
default:
return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v)
}
}

View File

@@ -0,0 +1,63 @@
package builtin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestStartsWith(t *testing.T) {
testData := []struct {
raw string
expected string
}{
{"", ""},
{"a", "a"},
{"abc", "a"},
{"abc", "ab"},
}
for _, data := range testData {
if !assert.True(t, StartsWith(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestEndsWith(t *testing.T) {
testData := []struct {
raw string
expected string
}{
{"", ""},
{"a", "a"},
{"abc", "c"},
{"abc", "bc"},
}
for _, data := range testData {
if !assert.True(t, EndsWith(t, data.expected, data.raw)) {
t.Fail()
}
}
}
func TestEqualLength(t *testing.T) {
testData := []struct {
raw interface{}
expected int
}{
{"", 0},
{[]string{}, 0},
{map[string]interface{}{}, 0},
{"a", 1},
{[]string{"a"}, 1},
{map[string]interface{}{"a": 123}, 1},
}
for _, data := range testData {
if !assert.True(t, EqualLength(t, data.expected, data.raw)) {
t.Fail()
}
}
}

View File

@@ -0,0 +1,46 @@
package builtin
import (
"crypto/md5"
"encoding/hex"
"math"
"math/rand"
"time"
)
var Functions = map[string]interface{}{
"get_timestamp": getTimestamp, // call without arguments
"sleep": sleep, // call with one argument
"gen_random_string": genRandomString, // call with one argument
"max": math.Max, // call with two arguments
"md5": MD5,
}
func init() {
rand.Seed(time.Now().UnixNano())
}
func getTimestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
func sleep(nSecs int) {
time.Sleep(time.Duration(nSecs) * time.Second)
}
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
func genRandomString(n int) string {
lettersLen := len(letters)
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(lettersLen)]
}
return string(b)
}
func MD5(str string) string {
hasher := md5.New()
hasher.Write([]byte(str))
return hex.EncodeToString(hasher.Sum(nil))
}

79
internal/ga/client.go Normal file
View File

@@ -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
}

View File

@@ -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()
}
}

67
internal/ga/events.go Normal file
View File

@@ -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)
}

15
internal/ga/init.go Normal file
View File

@@ -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)
}

27
internal/sentry/init.go Normal file
View File

@@ -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)
}

3
internal/version/init.go Normal file
View File

@@ -0,0 +1,3 @@
package version
const VERSION = "v0.2.1"