Conditional recording (#1849)
* feat(backend): added mocked endpoint for conditional recordings * feat(backend): disabled project check * feat(backend): adapt getConditions endpoint for API implementation * feat(backend): added condition option to start request * feat(backend): added missing file * feat(backend): debug log
This commit is contained in:
parent
2ffeaff813
commit
9e3548e9ce
5 changed files with 158 additions and 3 deletions
|
|
@ -149,6 +149,18 @@ func (e *Router) startSessionHandlerWeb(w http.ResponseWriter, r *http.Request)
|
|||
tokenData, err := e.services.Tokenizer.Parse(req.Token)
|
||||
if err != nil || req.Reset { // Starting the new one
|
||||
dice := byte(rand.Intn(100)) // [0, 100)
|
||||
// Use condition rate if it's set
|
||||
if req.Condition != "" {
|
||||
rate, err := e.services.Conditions.GetRate(p.ProjectID, req.Condition)
|
||||
if err != nil {
|
||||
log.Printf("can't get condition rate: %s", err)
|
||||
} else {
|
||||
log.Printf("condition rate: %d", rate)
|
||||
p.SampleRate = byte(rate) // why byte?
|
||||
}
|
||||
} else {
|
||||
log.Printf("project sample rate: %d", p.SampleRate)
|
||||
}
|
||||
if dice >= p.SampleRate {
|
||||
ResponseWithError(w, http.StatusForbidden, errors.New("cancel"), startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
|
|
@ -616,3 +628,32 @@ func (e *Router) getTags(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
ResponseWithJSON(w, &UrlResponse{Tags: tags}, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
func (e *Router) getConditions(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
// Check authorization
|
||||
_, err := e.services.Tokenizer.ParseFromHTTPRequest(r)
|
||||
if err != nil {
|
||||
ResponseWithError(w, http.StatusUnauthorized, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// Get taskID
|
||||
vars := mux.Vars(r)
|
||||
projID := vars["project"]
|
||||
projectID, err := strconv.Atoi(projID)
|
||||
if err != nil {
|
||||
ResponseWithError(w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// Get task info
|
||||
info, err := e.services.Conditions.Get(uint32(projectID))
|
||||
if err != nil {
|
||||
ResponseWithError(w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
ResponseWithJSON(w, info, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type StartSessionRequest struct {
|
|||
DoNotRecord bool `json:"doNotRecord"` // start record session or not
|
||||
BufferDiff uint64 `json:"bufferDiff"` // buffer diff in ms for start record session
|
||||
IsOffline bool `json:"isOffline"` // to indicate that we have to use user's start timestamp
|
||||
Condition string `json:"condition"` // condition for start record session
|
||||
}
|
||||
|
||||
type StartSessionResponse struct {
|
||||
|
|
|
|||
|
|
@ -114,9 +114,10 @@ func (e *Router) init() {
|
|||
"/v1/web/uxt/signals/task": e.sendUXTaskSignal,
|
||||
}
|
||||
getHandlers := map[string]func(http.ResponseWriter, *http.Request){
|
||||
"/v1/web/uxt/test/{id}": e.getUXTestInfo,
|
||||
"/v1/web/uxt/upload-url": e.getUXUploadUrl,
|
||||
"/v1/web/tags": e.getTags,
|
||||
"/v1/web/uxt/test/{id}": e.getUXTestInfo,
|
||||
"/v1/web/uxt/upload-url": e.getUXUploadUrl,
|
||||
"/v1/web/tags": e.getTags,
|
||||
"/v1/web/conditions/{project}": e.getConditions,
|
||||
}
|
||||
prefix := "/ingest"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"openreplay/backend/internal/config/http"
|
||||
"openreplay/backend/internal/http/geoip"
|
||||
"openreplay/backend/internal/http/uaparser"
|
||||
"openreplay/backend/pkg/conditions"
|
||||
"openreplay/backend/pkg/db/postgres/pool"
|
||||
"openreplay/backend/pkg/db/redis"
|
||||
"openreplay/backend/pkg/featureflags"
|
||||
|
|
@ -31,6 +32,7 @@ type ServicesBuilder struct {
|
|||
ObjStorage objectstorage.ObjectStorage
|
||||
UXTesting uxtesting.UXTesting
|
||||
Tags tags.Tags
|
||||
Conditions conditions.Conditions
|
||||
}
|
||||
|
||||
func New(cfg *http.Config, producer types.Producer, pgconn pool.Pool, redis *redis.Client) (*ServicesBuilder, error) {
|
||||
|
|
@ -52,5 +54,6 @@ func New(cfg *http.Config, producer types.Producer, pgconn pool.Pool, redis *red
|
|||
ObjStorage: objStore,
|
||||
UXTesting: uxtesting.New(pgconn),
|
||||
Tags: tags.New(pgconn),
|
||||
Conditions: conditions.New(pgconn),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
109
backend/pkg/conditions/conditions.go
Normal file
109
backend/pkg/conditions/conditions.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package conditions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"openreplay/backend/pkg/db/postgres/pool"
|
||||
)
|
||||
|
||||
type Conditions interface {
|
||||
Get(projectID uint32) (*Response, error)
|
||||
GetRate(projectID uint32, condition string) (int, error)
|
||||
}
|
||||
|
||||
type conditionsImpl struct {
|
||||
db pool.Pool
|
||||
cache map[uint32]map[string]int // projectID -> condition -> rate
|
||||
}
|
||||
|
||||
func New(db pool.Pool) Conditions {
|
||||
return &conditionsImpl{
|
||||
db: db,
|
||||
cache: make(map[uint32]map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
type ConditionType string
|
||||
|
||||
const (
|
||||
VisitedURL ConditionType = "visited_url"
|
||||
RequestURL ConditionType = "request_url"
|
||||
ClickLabel ConditionType = "click_label"
|
||||
ClickSelector ConditionType = "click_selector"
|
||||
CustomEvent ConditionType = "custom_event"
|
||||
Exception ConditionType = "exception"
|
||||
FeatureFlag ConditionType = "feature_flag"
|
||||
SessionDuration ConditionType = "session_duration"
|
||||
)
|
||||
|
||||
type ConditionOperator string
|
||||
|
||||
const (
|
||||
Is ConditionOperator = "is"
|
||||
IsNot ConditionOperator = "isNot"
|
||||
Contains ConditionOperator = "contains"
|
||||
NotContains ConditionOperator = "notContains"
|
||||
StartsWith ConditionOperator = "startsWith"
|
||||
EndsWith ConditionOperator = "endsWith"
|
||||
)
|
||||
|
||||
type Condition struct {
|
||||
Type ConditionType `json:"type"`
|
||||
Operator ConditionOperator `json:"operator"`
|
||||
Values []string `json:"value"`
|
||||
}
|
||||
|
||||
type ConditionSet struct {
|
||||
Name string `json:"name"`
|
||||
Filters interface{} `json:"filters"`
|
||||
Rate int `json:"capture_rate"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Conditions interface{} `json:"conditions"`
|
||||
}
|
||||
|
||||
func (c *conditionsImpl) getConditions(projectID uint32) ([]ConditionSet, error) {
|
||||
var conditions []ConditionSet
|
||||
err := c.db.QueryRow(`
|
||||
SELECT conditions
|
||||
FROM projects
|
||||
WHERE project_id = $1
|
||||
`, projectID).Scan(&conditions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save project's conditions to cache
|
||||
conditionSet := make(map[string]int)
|
||||
for _, condition := range conditions {
|
||||
conditionSet[condition.Name] = condition.Rate
|
||||
}
|
||||
c.cache[projectID] = conditionSet
|
||||
|
||||
return conditions, nil
|
||||
}
|
||||
|
||||
func (c *conditionsImpl) Get(projectID uint32) (*Response, error) {
|
||||
conditions, err := c.getConditions(projectID)
|
||||
return &Response{Conditions: conditions}, err
|
||||
}
|
||||
|
||||
func (c *conditionsImpl) GetRate(projectID uint32, condition string) (int, error) {
|
||||
proj, ok := c.cache[projectID]
|
||||
if ok {
|
||||
rate, ok := proj[condition]
|
||||
if ok {
|
||||
return rate, nil
|
||||
}
|
||||
}
|
||||
// Don't have project's conditions in cache or particular condition
|
||||
_, err := c.getConditions(projectID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rate, ok := c.cache[projectID][condition]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("condition %s not found", condition)
|
||||
}
|
||||
return rate, nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue