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:
Alexander 2024-01-22 14:52:02 +01:00 committed by GitHub
parent 2ffeaff813
commit 9e3548e9ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 158 additions and 3 deletions

View file

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

View file

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

View file

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

View file

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

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