mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-09 01:39:39 +08:00
refactor: move hrp/ to root folder
This commit is contained in:
211
internal/sdk/ga4.go
Normal file
211
internal/sdk/ga4.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
)
|
||||
|
||||
// Measurement Protocol (Google Analytics 4) docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4
|
||||
// debugging tools: https://ga-dev-tools.google/ga4/event-builder/
|
||||
const (
|
||||
ga4APISecret = "w7lKNQIrQsKNS4ikgMPp0Q"
|
||||
ga4MeasurementID = "G-9KHR3VC2LN"
|
||||
)
|
||||
|
||||
var (
|
||||
ga4Client *GA4Client
|
||||
userID string
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
userID, err = machineid.ProtectedID("hrp")
|
||||
if err != nil {
|
||||
userID = uuid.NewV1().String()
|
||||
}
|
||||
|
||||
// init GA4 client
|
||||
ga4Client = NewGA4Client(ga4MeasurementID, ga4APISecret, false)
|
||||
}
|
||||
|
||||
type GA4Client struct {
|
||||
apiSecret string // Measurement Protocol API secret value
|
||||
measurementID string // MEASUREMENT ID, G-XXXXXXXXXX
|
||||
userID string // A unique identifier for a user
|
||||
httpClient *http.Client // http client session
|
||||
debug bool // send events for validation, used for debug
|
||||
}
|
||||
|
||||
// NewGA4Client creates a new GA4Client object with the measurementID and apiSecret.
|
||||
func NewGA4Client(measurementID, apiSecret string, debug ...bool) *GA4Client {
|
||||
dbg := false
|
||||
if len(debug) > 0 {
|
||||
dbg = debug[0]
|
||||
}
|
||||
|
||||
return &GA4Client{
|
||||
measurementID: measurementID,
|
||||
apiSecret: apiSecret,
|
||||
userID: userID,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
debug: dbg,
|
||||
}
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
// Required. The name for the event.
|
||||
Name string `json:"name"`
|
||||
// Optional. The parameters for the event.
|
||||
// engagement_time_msec/session_id
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// payload docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag
|
||||
type Payload struct {
|
||||
// Required. Uniquely identifies a user instance of a web client
|
||||
ClientID string `json:"client_id"`
|
||||
// Optional. A unique identifier for a user
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
// Optional. A Unix timestamp (in microseconds) for the time to associate with the event.
|
||||
// This should only be set to record events that happened in the past.
|
||||
// This value can be overridden via user_property or event timestamps.
|
||||
// Events can be backdated up to 3 calendar days based on the property's timezone.
|
||||
TimestampMicros int64 `json:"timestamp_micros,omitempty"`
|
||||
// Optional. The user properties for the measurement.
|
||||
UserProperties map[string]string `json:"user_properties,omitempty"`
|
||||
// Optional. Set to true to indicate these events should not be used for personalized ads.
|
||||
NonPersonalizedAds bool `json:"non_personalized_ads,omitempty"`
|
||||
// Required. An array of event items. Up to 25 events can be sent per request.
|
||||
Events []Event `json:"events"`
|
||||
}
|
||||
|
||||
// validation docs reference:
|
||||
// https://developers.google.com/analytics/devguides/collection/protocol/ga4/validating-events?client_type=gtag
|
||||
type ValidationResponse struct {
|
||||
ValidationMessages []ValidationMessage `json:"validationMessages"` // An array of validation messages.
|
||||
}
|
||||
|
||||
type ValidationMessage struct {
|
||||
FieldPath string `json:"fieldPath"` // The path to the field that was invalid.
|
||||
Description string `json:"description"` // A description of the error.
|
||||
ValidationCode ValidationCode `json:"validationCode"` // A ValidationCode that corresponds to the error.
|
||||
}
|
||||
|
||||
type ValidationCode string
|
||||
|
||||
const (
|
||||
VALUE_INVALID ValidationCode = "VALUE_INVALID" // The value provided for a fieldPath was invalid.
|
||||
VALUE_REQUIRED ValidationCode = "VALUE_REQUIRED" // A required value for a fieldPath was not provided.
|
||||
NAME_INVALID ValidationCode = "NAME_INVALID" // The name provided was invalid.
|
||||
NAME_RESERVED ValidationCode = "NAME_RESERVED" // The name provided was one of the reserved names.
|
||||
VALUE_OUT_OF_BOUNDS ValidationCode = "VALUE_OUT_OF_BOUNDS" // The value provided was too large.
|
||||
EXCEEDED_MAX_ENTITIES ValidationCode = "EXCEEDED_MAX_ENTITIES" // There were too many parameters in the request.
|
||||
NAME_DUPLICATED ValidationCode = "NAME_DUPLICATED" // The same name was provided more than once in the request.
|
||||
)
|
||||
|
||||
// SendEvent sends one event to Google Analytics
|
||||
func (g *GA4Client) SendEvent(event Event) error {
|
||||
query := url.Values{}
|
||||
query.Add("api_secret", g.apiSecret)
|
||||
query.Add("measurement_id", g.measurementID)
|
||||
|
||||
var uri string
|
||||
if g.debug {
|
||||
uri = fmt.Sprintf("https://www.google-analytics.com/debug/mp/collect?%s", query.Encode())
|
||||
} else {
|
||||
uri = fmt.Sprintf("https://www.google-analytics.com/mp/collect?%s", query.Encode())
|
||||
}
|
||||
|
||||
// append event params
|
||||
if event.Params == nil {
|
||||
event.Params = map[string]interface{}{}
|
||||
}
|
||||
event.Params["os"] = runtime.GOOS
|
||||
event.Params["arch"] = runtime.GOARCH
|
||||
event.Params["go_version"] = runtime.Version()
|
||||
event.Params["hrp_version"] = version.VERSION
|
||||
|
||||
payload := Payload{
|
||||
ClientID: fmt.Sprintf("%d.%d", rand.Int31(), time.Now().Unix()),
|
||||
UserID: g.userID,
|
||||
TimestampMicros: time.Now().UnixMicro(),
|
||||
Events: []Event{event},
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(payload)
|
||||
if g.debug {
|
||||
log.Debug().
|
||||
Str("uri", uri).
|
||||
Interface("payload", payload).
|
||||
Msg("send GA4 event")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "marshal GA4 request payload failed")
|
||||
}
|
||||
|
||||
body := bytes.NewReader(bs)
|
||||
res, err := g.httpClient.Post(uri, "application/json", body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "request GA4 failed")
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
return fmt.Errorf("validation response got unexpected status %d", res.StatusCode)
|
||||
}
|
||||
|
||||
if !g.debug {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs, err = io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "read GA4 response body failed")
|
||||
}
|
||||
|
||||
validationResponse := ValidationResponse{}
|
||||
err = json.Unmarshal(bs, &validationResponse)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unmarshal GA4 response body failed")
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Int("statusCode", res.StatusCode).
|
||||
Interface("validationResponse", validationResponse).
|
||||
Msg("get GA4 validation response")
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendGA4Event(name string, params map[string]interface{}) {
|
||||
if os.Getenv("DISABLE_GA") == "true" {
|
||||
// do not send GA4 events in CI environment
|
||||
return
|
||||
}
|
||||
|
||||
event := Event{
|
||||
Name: name,
|
||||
Params: params,
|
||||
}
|
||||
err := ga4Client.SendEvent(event)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("send GA4 event failed")
|
||||
}
|
||||
}
|
||||
15
internal/sdk/ga4_test.go
Normal file
15
internal/sdk/ga4_test.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGA4(t *testing.T) {
|
||||
ga4Client := NewGA4Client(ga4MeasurementID, ga4APISecret, false)
|
||||
|
||||
event := Event{
|
||||
Name: "hrp_debug_event",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
ga4Client.SendEvent(event)
|
||||
}
|
||||
37
internal/sdk/sentry.go
Normal file
37
internal/sdk/sentry.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
sentryDSN = "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// init sentry sdk
|
||||
if os.Getenv("DISABLE_SENTRY") == "true" {
|
||||
return
|
||||
}
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: sentryDSN,
|
||||
Release: fmt.Sprintf("httprunner@%s", version.VERSION),
|
||||
AttachStacktrace: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("init sentry sdk failed!")
|
||||
return
|
||||
}
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
scope.SetUser(sentry.User{
|
||||
ID: userID,
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user