From 700ef0dcc611c866588da6e6f798978785ad7c61 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Thu, 5 May 2022 15:26:10 +0200 Subject: [PATCH 01/19] Made standart project layout for ender service --- backend/build.sh | 2 +- backend/{services => cmd}/ender/main.go | 27 +++++++++---------- .../ender => internal}/builder/builder.go | 0 .../ender => internal}/builder/builderMap.go | 0 .../builder/clikRageDetector.go | 0 .../builder/cpuIssueFinder.go | 0 .../builder/deadClickDetector.go | 0 .../builder/domDropDetector.go | 0 .../builder/inputEventBuilder.go | 0 .../builder/memoryIssueFinder.go | 0 .../builder/pageEventBuilder.go | 0 .../builder/performanceTrackAggrBuilder.go | 0 backend/internal/config/ender/config.go | 25 +++++++++++++++++ backend/services/ender/build_hack | 0 14 files changed, 38 insertions(+), 16 deletions(-) rename backend/{services => cmd}/ender/main.go (69%) rename backend/{services/ender => internal}/builder/builder.go (100%) rename backend/{services/ender => internal}/builder/builderMap.go (100%) rename backend/{services/ender => internal}/builder/clikRageDetector.go (100%) rename backend/{services/ender => internal}/builder/cpuIssueFinder.go (100%) rename backend/{services/ender => internal}/builder/deadClickDetector.go (100%) rename backend/{services/ender => internal}/builder/domDropDetector.go (100%) rename backend/{services/ender => internal}/builder/inputEventBuilder.go (100%) rename backend/{services/ender => internal}/builder/memoryIssueFinder.go (100%) rename backend/{services/ender => internal}/builder/pageEventBuilder.go (100%) rename backend/{services/ender => internal}/builder/performanceTrackAggrBuilder.go (100%) create mode 100644 backend/internal/config/ender/config.go create mode 100644 backend/services/ender/build_hack diff --git a/backend/build.sh b/backend/build.sh index b4de3c2de..b78d71fd8 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -23,7 +23,7 @@ function build_service() { image="$1" echo "BUILDING $image" case "$image" in - http | db) + http | db | ender) echo build http docker build -t ${DOCKER_REPO:-'local'}/$image:${git_sha1} --build-arg SERVICE_NAME=$image -f ./cmd/Dockerfile . [[ $PUSH_IMAGE -eq 1 ]] && { diff --git a/backend/services/ender/main.go b/backend/cmd/ender/main.go similarity index 69% rename from backend/services/ender/main.go rename to backend/cmd/ender/main.go index 4170a178e..b54a8dc15 100644 --- a/backend/services/ender/main.go +++ b/backend/cmd/ender/main.go @@ -2,37 +2,34 @@ package main import ( "log" + "openreplay/backend/internal/builder" + "openreplay/backend/internal/config/ender" "time" "os" "os/signal" "syscall" - "openreplay/backend/pkg/env" "openreplay/backend/pkg/intervals" logger "openreplay/backend/pkg/log" "openreplay/backend/pkg/messages" "openreplay/backend/pkg/queue" "openreplay/backend/pkg/queue/types" - "openreplay/backend/services/ender/builder" ) func main() { log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) - GROUP_EVENTS := env.String("GROUP_ENDER") - TOPIC_TRIGGER := env.String("TOPIC_TRIGGER") + cfg := ender.New() builderMap := builder.NewBuilderMap() - - statsLogger := logger.NewQueueStats(env.Int("LOG_QUEUE_STATS_INTERVAL_SEC")) - + statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) producer := queue.NewProducer() consumer := queue.NewMessageConsumer( - GROUP_EVENTS, + cfg.GroupEvents, []string{ - env.String("TOPIC_RAW_WEB"), - env.String("TOPIC_RAW_IOS"), + cfg.TopicRawWeb, + cfg.TopicRawIOS, }, func(sessionID uint64, msg messages.Message, meta *types.Meta) { statsLogger.Collect(sessionID, meta) @@ -51,17 +48,17 @@ func main() { select { case sig := <-sigchan: log.Printf("Caught signal %v: terminating\n", sig) - producer.Close(2000) - consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP) + producer.Close(cfg.ProducerTimeout) + consumer.Commit() consumer.Close() os.Exit(0) case <-tick: builderMap.IterateReadyMessages(time.Now().UnixMilli(), func(sessionID uint64, readyMsg messages.Message) { - producer.Produce(TOPIC_TRIGGER, sessionID, messages.Encode(readyMsg)) + producer.Produce(cfg.TopicTrigger, sessionID, messages.Encode(readyMsg)) }) // TODO: why exactly do we need Flush here and not in any other place? - producer.Flush(2000) - consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP) + producer.Flush(cfg.ProducerTimeout) + consumer.Commit() default: if err := consumer.ConsumeNext(); err != nil { log.Fatalf("Error on consuming: %v", err) diff --git a/backend/services/ender/builder/builder.go b/backend/internal/builder/builder.go similarity index 100% rename from backend/services/ender/builder/builder.go rename to backend/internal/builder/builder.go diff --git a/backend/services/ender/builder/builderMap.go b/backend/internal/builder/builderMap.go similarity index 100% rename from backend/services/ender/builder/builderMap.go rename to backend/internal/builder/builderMap.go diff --git a/backend/services/ender/builder/clikRageDetector.go b/backend/internal/builder/clikRageDetector.go similarity index 100% rename from backend/services/ender/builder/clikRageDetector.go rename to backend/internal/builder/clikRageDetector.go diff --git a/backend/services/ender/builder/cpuIssueFinder.go b/backend/internal/builder/cpuIssueFinder.go similarity index 100% rename from backend/services/ender/builder/cpuIssueFinder.go rename to backend/internal/builder/cpuIssueFinder.go diff --git a/backend/services/ender/builder/deadClickDetector.go b/backend/internal/builder/deadClickDetector.go similarity index 100% rename from backend/services/ender/builder/deadClickDetector.go rename to backend/internal/builder/deadClickDetector.go diff --git a/backend/services/ender/builder/domDropDetector.go b/backend/internal/builder/domDropDetector.go similarity index 100% rename from backend/services/ender/builder/domDropDetector.go rename to backend/internal/builder/domDropDetector.go diff --git a/backend/services/ender/builder/inputEventBuilder.go b/backend/internal/builder/inputEventBuilder.go similarity index 100% rename from backend/services/ender/builder/inputEventBuilder.go rename to backend/internal/builder/inputEventBuilder.go diff --git a/backend/services/ender/builder/memoryIssueFinder.go b/backend/internal/builder/memoryIssueFinder.go similarity index 100% rename from backend/services/ender/builder/memoryIssueFinder.go rename to backend/internal/builder/memoryIssueFinder.go diff --git a/backend/services/ender/builder/pageEventBuilder.go b/backend/internal/builder/pageEventBuilder.go similarity index 100% rename from backend/services/ender/builder/pageEventBuilder.go rename to backend/internal/builder/pageEventBuilder.go diff --git a/backend/services/ender/builder/performanceTrackAggrBuilder.go b/backend/internal/builder/performanceTrackAggrBuilder.go similarity index 100% rename from backend/services/ender/builder/performanceTrackAggrBuilder.go rename to backend/internal/builder/performanceTrackAggrBuilder.go diff --git a/backend/internal/config/ender/config.go b/backend/internal/config/ender/config.go new file mode 100644 index 000000000..e39fbc240 --- /dev/null +++ b/backend/internal/config/ender/config.go @@ -0,0 +1,25 @@ +package ender + +import ( + "openreplay/backend/pkg/env" +) + +type Config struct { + GroupEvents string + TopicTrigger string + LoggerTimeout int + TopicRawWeb string + TopicRawIOS string + ProducerTimeout int +} + +func New() *Config { + return &Config{ + GroupEvents: env.String("GROUP_ENDER"), + TopicTrigger: env.String("TOPIC_TRIGGER"), + LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"), + TopicRawWeb: env.String("TOPIC_RAW_WEB"), + TopicRawIOS: env.String("TOPIC_RAW_IOS"), + ProducerTimeout: 2000, + } +} diff --git a/backend/services/ender/build_hack b/backend/services/ender/build_hack new file mode 100644 index 000000000..e69de29bb From f4212d6eaaf497623816b446c4515d33e5e54c8c Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Thu, 5 May 2022 17:37:05 +0200 Subject: [PATCH 02/19] Split ender into 2 services (ender and heuristics) --- backend/cmd/ender/main.go | 7 ++- backend/cmd/heuristics/main.go | 68 ++++++++++++++++++++++++++ backend/internal/builder/builder.go | 33 +++++++------ backend/internal/builder/builderMap.go | 12 ----- backend/internal/ender/builder.go | 56 +++++++++++++++++++++ backend/internal/ender/builderMap.go | 36 ++++++++++++++ backend/pkg/intervals/intervals.go | 12 ++--- 7 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 backend/cmd/heuristics/main.go create mode 100644 backend/internal/ender/builder.go create mode 100644 backend/internal/ender/builderMap.go diff --git a/backend/cmd/ender/main.go b/backend/cmd/ender/main.go index b54a8dc15..dbe2fa212 100644 --- a/backend/cmd/ender/main.go +++ b/backend/cmd/ender/main.go @@ -2,8 +2,8 @@ package main import ( "log" - "openreplay/backend/internal/builder" "openreplay/backend/internal/config/ender" + builder "openreplay/backend/internal/ender" "time" "os" @@ -49,16 +49,15 @@ func main() { case sig := <-sigchan: log.Printf("Caught signal %v: terminating\n", sig) producer.Close(cfg.ProducerTimeout) - consumer.Commit() + consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP) consumer.Close() os.Exit(0) case <-tick: builderMap.IterateReadyMessages(time.Now().UnixMilli(), func(sessionID uint64, readyMsg messages.Message) { producer.Produce(cfg.TopicTrigger, sessionID, messages.Encode(readyMsg)) }) - // TODO: why exactly do we need Flush here and not in any other place? producer.Flush(cfg.ProducerTimeout) - consumer.Commit() + consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP) default: if err := consumer.ConsumeNext(); err != nil { log.Fatalf("Error on consuming: %v", err) diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go new file mode 100644 index 000000000..b33511bd1 --- /dev/null +++ b/backend/cmd/heuristics/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "log" + "openreplay/backend/internal/builder" + "openreplay/backend/internal/config/ender" + "openreplay/backend/pkg/intervals" + logger "openreplay/backend/pkg/log" + "openreplay/backend/pkg/messages" + "openreplay/backend/pkg/queue" + "openreplay/backend/pkg/queue/types" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) + + cfg := ender.New() + + builderMap := builder.NewBuilderMap() + statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) + producer := queue.NewProducer() + consumer := queue.NewMessageConsumer( + cfg.GroupEvents, + []string{ + cfg.TopicRawWeb, + cfg.TopicRawIOS, + }, + func(sessionID uint64, msg messages.Message, meta *types.Meta) { + statsLogger.Collect(sessionID, meta) + builderMap.HandleMessage(sessionID, msg, msg.Meta().Index) + }, + false, + ) + + tick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) + + sigchan := make(chan os.Signal, 1) + signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) + + log.Printf("Ender service started\n") + for { + select { + case sig := <-sigchan: + log.Printf("Caught signal %v: terminating\n", sig) + producer.Close(cfg.ProducerTimeout) + consumer.Commit() + consumer.Close() + os.Exit(0) + case <-tick: + builderMap.IterateReadyMessages(time.Now().UnixMilli(), func(sessionID uint64, readyMsg messages.Message) { + producer.Produce(cfg.TopicTrigger, sessionID, messages.Encode(readyMsg)) + }) + producer.Flush(cfg.ProducerTimeout) + consumer.Commit() + default: + if err := consumer.ConsumeNext(); err != nil { + log.Fatalf("Error on consuming: %v", err) + } + } + } + + // Config + +} diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index 1a89f67b6..bd9f26b19 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -44,9 +44,9 @@ type builder struct { readyMsgs []Message timestamp uint64 lastProcessedTimestamp int64 - peBuilder *pageEventBuilder + peBuilder *pageEventBuilder // TODO: DB ptaBuilder *performanceTrackAggrBuilder - ieBuilder *inputEventBuilder + ieBuilder *inputEventBuilder // TODO: DB ciFinder *cpuIssueFinder miFinder *memoryIssueFinder ddDetector *domDropDetector @@ -117,6 +117,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { b.lastProcessedTimestamp = time.Now().UnixMilli() // Might happen before the first timestamp. + // TODO: to DB switch msg := message.(type) { case *SessionStart, *Metadata, @@ -137,7 +138,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { return } switch msg := message.(type) { - case *SetPageLocation: + case *SetPageLocation: // TODO: DB if msg.NavigationStart == 0 { b.appendReadyMessage(&PageEvent{ URL: msg.URL, @@ -154,11 +155,11 @@ func (b *builder) handleMessage(message Message, messageID uint64) { b.miFinder.HandleSetPageLocation(msg) b.ciFinder.HandleSetPageLocation(msg) } - case *PageLoadTiming: + case *PageLoadTiming: // TODO: DB if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil { b.appendReadyMessage(rm) } - case *PageRenderTiming: + case *PageRenderTiming: // TODO: DB if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil { b.appendReadyMessage(rm) } @@ -172,20 +173,20 @@ func (b *builder) handleMessage(message Message, messageID uint64) { if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { b.appendReadyMessage(rm) } - case *SetInputTarget: + case *SetInputTarget: // TODO: DB if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil { b.appendReadyMessage(rm) } - case *SetInputValue: + case *SetInputValue: // TODO: DB if rm := b.ieBuilder.HandleSetInputValue(msg, messageID, b.timestamp); rm != nil { b.appendReadyMessage(rm) } - case *MouseClick: + case *MouseClick: // TODO: DB b.buildInputEvent() if rm := b.crDetector.HandleMouseClick(msg, messageID, b.timestamp); rm != nil { b.appendReadyMessage(rm) } - if msg.Label != "" { + if msg.Label != "" { // TODO: DB b.appendReadyMessage(&ClickEvent{ MessageID: messageID, Label: msg.Label, @@ -195,7 +196,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { }) } case *JSException: - b.appendReadyMessage(&ErrorEvent{ + b.appendReadyMessage(&ErrorEvent{ // TODO: DB MessageID: messageID, Timestamp: b.timestamp, Source: "js_exception", @@ -206,7 +207,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { case *ResourceTiming: tp := getResourceType(msg.Initiator, msg.URL) success := msg.Duration != 0 - b.appendReadyMessage(&ResourceEvent{ + b.appendReadyMessage(&ResourceEvent{ // TODO: DB MessageID: messageID, Timestamp: msg.Timestamp, Duration: msg.Duration, @@ -231,14 +232,14 @@ func (b *builder) handleMessage(message Message, messageID uint64) { }) } case *RawCustomEvent: - b.appendReadyMessage(&CustomEvent{ + b.appendReadyMessage(&CustomEvent{ // TODO: DB MessageID: messageID, Timestamp: b.timestamp, Name: msg.Name, Payload: msg.Payload, }) case *CustomIssue: - b.appendReadyMessage(&IssueEvent{ + b.appendReadyMessage(&IssueEvent{ // TODO: DB Type: "custom", Timestamp: b.timestamp, MessageID: messageID, @@ -246,7 +247,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { Payload: msg.Payload, }) case *Fetch: - b.appendReadyMessage(&FetchEvent{ + b.appendReadyMessage(&FetchEvent{ // TODO: DB MessageID: messageID, Timestamp: msg.Timestamp, Method: msg.Method, @@ -265,7 +266,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { }) } case *GraphQL: - b.appendReadyMessage(&GraphQLEvent{ + b.appendReadyMessage(&GraphQLEvent{ // TODO: DB MessageID: messageID, Timestamp: b.timestamp, OperationKind: msg.OperationKind, @@ -274,7 +275,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { Response: msg.Response, }) case *StateAction: - b.appendReadyMessage(&StateActionEvent{ + b.appendReadyMessage(&StateActionEvent{ // TODO: DB MessageID: messageID, Timestamp: b.timestamp, Type: msg.Type, diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index 3f3e4d6e3..b7885da92 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -26,18 +26,6 @@ func (m builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint6 b.handleMessage(msg, messageID) } -func (m builderMap) IterateSessionReadyMessages(sessionID uint64, operatingTs int64, iter func(msg Message)) { - b, ok := m[sessionID] - if !ok { - return - } - sessionEnded := b.checkTimeouts(operatingTs) - b.iterateReadyMessage(iter) - if sessionEnded { - delete(m, sessionID) - } -} - func (m builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) { for sessionID, b := range m { sessionEnded := b.checkTimeouts(operatingTs) diff --git a/backend/internal/ender/builder.go b/backend/internal/ender/builder.go new file mode 100644 index 000000000..0389f74d1 --- /dev/null +++ b/backend/internal/ender/builder.go @@ -0,0 +1,56 @@ +package builder + +import ( + "log" + "openreplay/backend/pkg/intervals" + . "openreplay/backend/pkg/messages" +) + +type builder struct { + readyMsgs []Message + timestamp uint64 + sid uint64 +} + +func NewBuilder() *builder { + return &builder{} +} + +func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value + b.readyMsgs = append(b.readyMsgs, msg) +} + +func (b *builder) buildSessionEnd() { + if b.timestamp == 0 { + return + } + sessionEnd := &SessionEnd{ + Timestamp: b.timestamp, + } + b.appendReadyMessage(sessionEnd) +} + +func (b *builder) handleMessage(message Message, messageID uint64) { + timestamp := GetTimestamp(message) + if b.timestamp < timestamp { + b.timestamp = timestamp + } + + if b.timestamp == 0 { + log.Printf("Empty timestamp, sessionID: %d, messageID: %d", b.sid, messageID) + return + } +} + +func (b *builder) checkTimeouts(ts int64) bool { + if b.timestamp == 0 { + return false // There was no timestamp events yet + } + + lastTsGap := ts - int64(b.timestamp) + if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT { + b.buildSessionEnd() + return true + } + return false +} diff --git a/backend/internal/ender/builderMap.go b/backend/internal/ender/builderMap.go new file mode 100644 index 000000000..6eba1f9ad --- /dev/null +++ b/backend/internal/ender/builderMap.go @@ -0,0 +1,36 @@ +package builder + +import ( + . "openreplay/backend/pkg/messages" +) + +type builderMap map[uint64]*builder + +func NewBuilderMap() builderMap { + return make(builderMap) +} + +func (m builderMap) GetBuilder(sessionID uint64) *builder { + b := m[sessionID] + if b == nil { + b = NewBuilder() + m[sessionID] = b + b.sid = sessionID + + } + return b +} + +func (m builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint64) { + b := m.GetBuilder(sessionID) + b.handleMessage(msg, messageID) +} + +func (m builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) { + for sessionID, b := range m { + sessionEnded := b.checkTimeouts(operatingTs) + if sessionEnded { + delete(m, sessionID) + } + } +} diff --git a/backend/pkg/intervals/intervals.go b/backend/pkg/intervals/intervals.go index c4dfbc835..2ce13ed5e 100644 --- a/backend/pkg/intervals/intervals.go +++ b/backend/pkg/intervals/intervals.go @@ -1,11 +1,11 @@ package intervals -const EVENTS_COMMIT_INTERVAL = 30 * 1000 -const HEARTBEAT_INTERVAL = 2 * 60 * 1000 -const INTEGRATIONS_REQUEST_INTERVAL = 1 * 60 * 1000 -const EVENTS_PAGE_EVENT_TIMEOUT = 2 * 60 * 1000 -const EVENTS_INPUT_EVENT_TIMEOUT = 2 * 60 * 1000 +const EVENTS_COMMIT_INTERVAL = 30 * 1000 // как часто комитим сообщения в кафке (ender) +const HEARTBEAT_INTERVAL = 2 * 60 * 1000 // максимальный таймаут от трекера в рамках сессии +const INTEGRATIONS_REQUEST_INTERVAL = 1 * 60 * 1000 // интеграции +const EVENTS_PAGE_EVENT_TIMEOUT = 2 * 60 * 1000 // таймаут пейдж ивента +const EVENTS_INPUT_EVENT_TIMEOUT = 2 * 60 * 1000 // const EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT = 2 * 60 * 1000 const EVENTS_SESSION_END_TIMEOUT = HEARTBEAT_INTERVAL + 30*1000 const EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS = HEARTBEAT_INTERVAL + 3*60*1000 -const EVENTS_BACK_COMMIT_GAP = EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS + 1*60*1000 +const EVENTS_BACK_COMMIT_GAP = EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS + 1*60*1000 // для бэк коммита From 2b3728d8dabd2fc0244f4eb661e6162e455f54a7 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Fri, 6 May 2022 12:16:24 +0200 Subject: [PATCH 03/19] Finished refactoring for session ender service --- backend/cmd/ender/main.go | 30 +++++++++---- backend/internal/ender/builder.go | 56 ----------------------- backend/internal/ender/builderMap.go | 36 --------------- backend/internal/sessionender/ender.go | 62 ++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 101 deletions(-) delete mode 100644 backend/internal/ender/builder.go delete mode 100644 backend/internal/ender/builderMap.go create mode 100644 backend/internal/sessionender/ender.go diff --git a/backend/cmd/ender/main.go b/backend/cmd/ender/main.go index dbe2fa212..5d82b67db 100644 --- a/backend/cmd/ender/main.go +++ b/backend/cmd/ender/main.go @@ -3,7 +3,7 @@ package main import ( "log" "openreplay/backend/internal/config/ender" - builder "openreplay/backend/internal/ender" + "openreplay/backend/internal/sessionender" "time" "os" @@ -20,10 +20,12 @@ import ( func main() { log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) + // Load service configuration cfg := ender.New() - builderMap := builder.NewBuilderMap() + // Init all modules statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) + sessions := sessionender.New(intervals.EVENTS_SESSION_END_TIMEOUT) producer := queue.NewProducer() consumer := queue.NewMessageConsumer( cfg.GroupEvents, @@ -33,31 +35,41 @@ func main() { }, func(sessionID uint64, msg messages.Message, meta *types.Meta) { statsLogger.Collect(sessionID, meta) - builderMap.HandleMessage(sessionID, msg, msg.Meta().Index) + sessions.UpdateSession(sessionID, messages.GetTimestamp(msg)) }, false, ) - tick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) + log.Printf("Ender service started\n") sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) - log.Printf("Ender service started\n") + tick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) for { select { case sig := <-sigchan: log.Printf("Caught signal %v: terminating\n", sig) producer.Close(cfg.ProducerTimeout) - consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP) + if err := consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP); err != nil { + log.Printf("can't commit messages with offset: %s", err) + } consumer.Close() os.Exit(0) case <-tick: - builderMap.IterateReadyMessages(time.Now().UnixMilli(), func(sessionID uint64, readyMsg messages.Message) { - producer.Produce(cfg.TopicTrigger, sessionID, messages.Encode(readyMsg)) + // Find ended sessions and send notification to other services + sessions.HandleEndedSessions(func(sessionID uint64, timestamp int64) bool { + msg := &messages.SessionEnd{Timestamp: uint64(timestamp)} + if err := producer.Produce(cfg.TopicTrigger, sessionID, messages.Encode(msg)); err != nil { + log.Printf("can't send message to queue: %s", err) + return false + } + return true }) producer.Flush(cfg.ProducerTimeout) - consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP) + if err := consumer.CommitBack(intervals.EVENTS_BACK_COMMIT_GAP); err != nil { + log.Printf("can't commit messages with offset: %s", err) + } default: if err := consumer.ConsumeNext(); err != nil { log.Fatalf("Error on consuming: %v", err) diff --git a/backend/internal/ender/builder.go b/backend/internal/ender/builder.go deleted file mode 100644 index 0389f74d1..000000000 --- a/backend/internal/ender/builder.go +++ /dev/null @@ -1,56 +0,0 @@ -package builder - -import ( - "log" - "openreplay/backend/pkg/intervals" - . "openreplay/backend/pkg/messages" -) - -type builder struct { - readyMsgs []Message - timestamp uint64 - sid uint64 -} - -func NewBuilder() *builder { - return &builder{} -} - -func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value - b.readyMsgs = append(b.readyMsgs, msg) -} - -func (b *builder) buildSessionEnd() { - if b.timestamp == 0 { - return - } - sessionEnd := &SessionEnd{ - Timestamp: b.timestamp, - } - b.appendReadyMessage(sessionEnd) -} - -func (b *builder) handleMessage(message Message, messageID uint64) { - timestamp := GetTimestamp(message) - if b.timestamp < timestamp { - b.timestamp = timestamp - } - - if b.timestamp == 0 { - log.Printf("Empty timestamp, sessionID: %d, messageID: %d", b.sid, messageID) - return - } -} - -func (b *builder) checkTimeouts(ts int64) bool { - if b.timestamp == 0 { - return false // There was no timestamp events yet - } - - lastTsGap := ts - int64(b.timestamp) - if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT { - b.buildSessionEnd() - return true - } - return false -} diff --git a/backend/internal/ender/builderMap.go b/backend/internal/ender/builderMap.go deleted file mode 100644 index 6eba1f9ad..000000000 --- a/backend/internal/ender/builderMap.go +++ /dev/null @@ -1,36 +0,0 @@ -package builder - -import ( - . "openreplay/backend/pkg/messages" -) - -type builderMap map[uint64]*builder - -func NewBuilderMap() builderMap { - return make(builderMap) -} - -func (m builderMap) GetBuilder(sessionID uint64) *builder { - b := m[sessionID] - if b == nil { - b = NewBuilder() - m[sessionID] = b - b.sid = sessionID - - } - return b -} - -func (m builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint64) { - b := m.GetBuilder(sessionID) - b.handleMessage(msg, messageID) -} - -func (m builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) { - for sessionID, b := range m { - sessionEnded := b.checkTimeouts(operatingTs) - if sessionEnded { - delete(m, sessionID) - } - } -} diff --git a/backend/internal/sessionender/ender.go b/backend/internal/sessionender/ender.go new file mode 100644 index 000000000..54bd399ac --- /dev/null +++ b/backend/internal/sessionender/ender.go @@ -0,0 +1,62 @@ +package sessionender + +import ( + "log" + "time" +) + +// EndedSessionHandler handler for ended sessions +type EndedSessionHandler func(sessionID uint64, timestamp int64) bool + +// session holds information about user's session live status +type session struct { + lastTimestamp int64 + isEnded bool +} + +// SessionEnder updates timestamp of last message for each session +type SessionEnder struct { + timeout int64 + sessions map[uint64]*session // map[sessionID]session +} + +func New(timeout int64) *SessionEnder { + return &SessionEnder{ + timeout: timeout, + sessions: make(map[uint64]*session), + } +} + +// UpdateSession save timestamp for new sessions and update for existing sessions +func (se *SessionEnder) UpdateSession(sessionID, timestamp uint64) { + currTS := int64(timestamp) + if currTS == 0 { + log.Printf("got empty timestamp for sessionID: %d", sessionID) + return + } + sess, ok := se.sessions[sessionID] + if !ok { + se.sessions[sessionID] = &session{ + lastTimestamp: currTS, + isEnded: false, + } + return + } + if currTS > sess.lastTimestamp { + sess.lastTimestamp = currTS + sess.isEnded = false + } +} + +// HandleEndedSessions runs handler for each ended session and delete information about session in successful case +func (se *SessionEnder) HandleEndedSessions(handler EndedSessionHandler) { + deadLine := time.Now().UnixMilli() - se.timeout + for sessID, sess := range se.sessions { + if sess.isEnded || sess.lastTimestamp < deadLine { + sess.isEnded = true + if handler(sessID, sess.lastTimestamp) { + delete(se.sessions, sessID) + } + } + } +} From 967034a89cedeb05473ed67e70c0e8e16c7e4b44 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Fri, 6 May 2022 16:12:06 +0200 Subject: [PATCH 04/19] Create first version of heuristics service with the same logic as old ender --- backend/build.sh | 2 +- backend/cmd/heuristics/main.go | 7 ++----- backend/services/heuristics/build_hack | 0 3 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 backend/services/heuristics/build_hack diff --git a/backend/build.sh b/backend/build.sh index b78d71fd8..70a29c5af 100755 --- a/backend/build.sh +++ b/backend/build.sh @@ -23,7 +23,7 @@ function build_service() { image="$1" echo "BUILDING $image" case "$image" in - http | db | ender) + http | db | ender | heuristics) echo build http docker build -t ${DOCKER_REPO:-'local'}/$image:${git_sha1} --build-arg SERVICE_NAME=$image -f ./cmd/Dockerfile . [[ $PUSH_IMAGE -eq 1 ]] && { diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index b33511bd1..2778685d3 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -36,12 +36,12 @@ func main() { false, ) - tick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) + log.Printf("Ender service started\n") sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) - log.Printf("Ender service started\n") + tick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) for { select { case sig := <-sigchan: @@ -62,7 +62,4 @@ func main() { } } } - - // Config - } diff --git a/backend/services/heuristics/build_hack b/backend/services/heuristics/build_hack new file mode 100644 index 000000000..e69de29bb From 8c432b8ba379dc2018e9252d36bb8e28d0a1eb61 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Fri, 6 May 2022 16:39:29 +0200 Subject: [PATCH 05/19] Removed from heuristics extra logic --- backend/internal/builder/builder.go | 185 +----------------- .../inputEventBuilder.go | 0 .../pageEventBuilder.go | 0 3 files changed, 1 insertion(+), 184 deletions(-) rename backend/internal/{builder => heuristics}/inputEventBuilder.go (100%) rename backend/internal/{builder => heuristics}/pageEventBuilder.go (100%) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index bd9f26b19..b3dc909b7 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -44,9 +44,7 @@ type builder struct { readyMsgs []Message timestamp uint64 lastProcessedTimestamp int64 - peBuilder *pageEventBuilder // TODO: DB ptaBuilder *performanceTrackAggrBuilder - ieBuilder *inputEventBuilder // TODO: DB ciFinder *cpuIssueFinder miFinder *memoryIssueFinder ddDetector *domDropDetector @@ -59,9 +57,7 @@ type builder struct { func NewBuilder() *builder { return &builder{ - peBuilder: &pageEventBuilder{}, ptaBuilder: &performanceTrackAggrBuilder{}, - ieBuilder: NewInputEventBuilder(), ciFinder: &cpuIssueFinder{}, miFinder: &memoryIssueFinder{}, ddDetector: &domDropDetector{}, @@ -82,87 +78,24 @@ func (b *builder) iterateReadyMessage(iter func(msg Message)) { b.readyMsgs = nil } -func (b *builder) buildSessionEnd() { - if b.timestamp == 0 { - return - } - sessionEnd := &SessionEnd{ - Timestamp: b.timestamp, // + delay? - } - b.appendReadyMessage(sessionEnd) -} - -func (b *builder) buildPageEvent() { - if msg := b.peBuilder.Build(); msg != nil { - b.appendReadyMessage(msg) - } -} func (b *builder) buildPerformanceTrackAggr() { if msg := b.ptaBuilder.Build(); msg != nil { b.appendReadyMessage(msg) } } -func (b *builder) buildInputEvent() { - if msg := b.ieBuilder.Build(); msg != nil { - b.appendReadyMessage(msg) - } -} func (b *builder) handleMessage(message Message, messageID uint64) { timestamp := GetTimestamp(message) - if b.timestamp < timestamp { // unnecessary? TODO: test and remove + if b.timestamp < timestamp { b.timestamp = timestamp } b.lastProcessedTimestamp = time.Now().UnixMilli() - // Might happen before the first timestamp. - // TODO: to DB - switch msg := message.(type) { - case *SessionStart, - *Metadata, - *UserID, - *UserAnonymousID: - b.appendReadyMessage(msg) - case *RawErrorEvent: - b.appendReadyMessage(&ErrorEvent{ - MessageID: messageID, - Timestamp: msg.Timestamp, - Source: msg.Source, - Name: msg.Name, - Message: msg.Message, - Payload: msg.Payload, - }) - } if b.timestamp == 0 { return } switch msg := message.(type) { - case *SetPageLocation: // TODO: DB - if msg.NavigationStart == 0 { - b.appendReadyMessage(&PageEvent{ - URL: msg.URL, - Referrer: msg.Referrer, - Loaded: false, - MessageID: messageID, - Timestamp: b.timestamp, - }) - } else { - b.buildPageEvent() - b.buildInputEvent() - b.ieBuilder.ClearLabels() - b.peBuilder.HandleSetPageLocation(msg, messageID, b.timestamp) - b.miFinder.HandleSetPageLocation(msg) - b.ciFinder.HandleSetPageLocation(msg) - } - case *PageLoadTiming: // TODO: DB - if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil { - b.appendReadyMessage(rm) - } - case *PageRenderTiming: // TODO: DB - if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil { - b.appendReadyMessage(rm) - } case *PerformanceTrack: if rm := b.ptaBuilder.HandlePerformanceTrack(msg, b.timestamp); rm != nil { b.appendReadyMessage(rm) @@ -173,113 +106,6 @@ func (b *builder) handleMessage(message Message, messageID uint64) { if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { b.appendReadyMessage(rm) } - case *SetInputTarget: // TODO: DB - if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil { - b.appendReadyMessage(rm) - } - case *SetInputValue: // TODO: DB - if rm := b.ieBuilder.HandleSetInputValue(msg, messageID, b.timestamp); rm != nil { - b.appendReadyMessage(rm) - } - case *MouseClick: // TODO: DB - b.buildInputEvent() - if rm := b.crDetector.HandleMouseClick(msg, messageID, b.timestamp); rm != nil { - b.appendReadyMessage(rm) - } - if msg.Label != "" { // TODO: DB - b.appendReadyMessage(&ClickEvent{ - MessageID: messageID, - Label: msg.Label, - HesitationTime: msg.HesitationTime, - Timestamp: b.timestamp, - Selector: msg.Selector, - }) - } - case *JSException: - b.appendReadyMessage(&ErrorEvent{ // TODO: DB - MessageID: messageID, - Timestamp: b.timestamp, - Source: "js_exception", - Name: msg.Name, - Message: msg.Message, - Payload: msg.Payload, - }) - case *ResourceTiming: - tp := getResourceType(msg.Initiator, msg.URL) - success := msg.Duration != 0 - b.appendReadyMessage(&ResourceEvent{ // TODO: DB - MessageID: messageID, - Timestamp: msg.Timestamp, - Duration: msg.Duration, - TTFB: msg.TTFB, - HeaderSize: msg.HeaderSize, - EncodedBodySize: msg.EncodedBodySize, - DecodedBodySize: msg.DecodedBodySize, - URL: msg.URL, - Type: tp, - Success: success, - }) - if !success { - issueType := "missing_resource" - if tp == "fetch" { - issueType = "bad_request" - } - b.appendReadyMessage(&IssueEvent{ - Type: issueType, - MessageID: messageID, - Timestamp: msg.Timestamp, - ContextString: msg.URL, - }) - } - case *RawCustomEvent: - b.appendReadyMessage(&CustomEvent{ // TODO: DB - MessageID: messageID, - Timestamp: b.timestamp, - Name: msg.Name, - Payload: msg.Payload, - }) - case *CustomIssue: - b.appendReadyMessage(&IssueEvent{ // TODO: DB - Type: "custom", - Timestamp: b.timestamp, - MessageID: messageID, - ContextString: msg.Name, - Payload: msg.Payload, - }) - case *Fetch: - b.appendReadyMessage(&FetchEvent{ // TODO: DB - MessageID: messageID, - Timestamp: msg.Timestamp, - Method: msg.Method, - URL: msg.URL, - Request: msg.Request, - Response: msg.Response, - Status: msg.Status, - Duration: msg.Duration, - }) - if msg.Status >= 400 { - b.appendReadyMessage(&IssueEvent{ - Type: "bad_request", - MessageID: messageID, - Timestamp: msg.Timestamp, - ContextString: msg.URL, - }) - } - case *GraphQL: - b.appendReadyMessage(&GraphQLEvent{ // TODO: DB - MessageID: messageID, - Timestamp: b.timestamp, - OperationKind: msg.OperationKind, - OperationName: msg.OperationName, - Variables: msg.Variables, - Response: msg.Response, - }) - case *StateAction: - b.appendReadyMessage(&StateActionEvent{ // TODO: DB - MessageID: messageID, - Timestamp: b.timestamp, - Type: msg.Type, - }) case *CreateElementNode, *CreateTextNode: b.ddDetector.HandleNodeCreation() @@ -300,19 +126,11 @@ func (b *builder) checkTimeouts(ts int64) bool { return false // There was no timestamp events yet } - if b.peBuilder.HasInstance() && int64(b.peBuilder.GetTimestamp())+intervals.EVENTS_PAGE_EVENT_TIMEOUT < ts { - b.buildPageEvent() - } - if b.ieBuilder.HasInstance() && int64(b.ieBuilder.GetTimestamp())+intervals.EVENTS_INPUT_EVENT_TIMEOUT < ts { - b.buildInputEvent() - } if b.ptaBuilder.HasInstance() && int64(b.ptaBuilder.GetStartTimestamp())+intervals.EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT < ts { b.buildPerformanceTrackAggr() } lastTsGap := ts - int64(b.timestamp) - //b.lastProcessedTimestamp - //log.Printf("checking timeouts for sess %v: %v now, %v sesstime; gap %v",b.sid, ts, b.timestamp, lastTsGap) if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT { if rm := b.ddDetector.Build(); rm != nil { b.appendReadyMessage(rm) @@ -329,7 +147,6 @@ func (b *builder) checkTimeouts(ts int64) bool { if rm := b.dcDetector.HandleReaction(b.timestamp); rm != nil { b.appendReadyMessage(rm) } - b.buildSessionEnd() return true } return false diff --git a/backend/internal/builder/inputEventBuilder.go b/backend/internal/heuristics/inputEventBuilder.go similarity index 100% rename from backend/internal/builder/inputEventBuilder.go rename to backend/internal/heuristics/inputEventBuilder.go diff --git a/backend/internal/builder/pageEventBuilder.go b/backend/internal/heuristics/pageEventBuilder.go similarity index 100% rename from backend/internal/builder/pageEventBuilder.go rename to backend/internal/heuristics/pageEventBuilder.go From 432c0da4e2ea3c0fe0ab5c202d74c85cb809567f Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Sat, 7 May 2022 15:04:17 +0200 Subject: [PATCH 06/19] chore(backend-heuristics): Remove redundant lines --- backend/internal/builder/builder.go | 57 +++++------------------------ 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index b3dc909b7..6b8ece5d5 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -9,37 +9,6 @@ import ( . "openreplay/backend/pkg/messages" ) -func getURLExtention(URL string) string { - u, err := url.Parse(URL) - if err != nil { - return "" - } - i := strings.LastIndex(u.Path, ".") - return u.Path[i+1:] -} - -func getResourceType(initiator string, URL string) string { - switch initiator { - case "xmlhttprequest", "fetch": - return "fetch" - case "img": - return "img" - default: - switch getURLExtention(URL) { - case "css": - return "stylesheet" - case "js": - return "script" - case "png", "gif", "jpg", "jpeg", "svg": - return "img" - case "mp4", "mkv", "ogg", "webm", "avi", "mp3": - return "media" - default: - return "other" - } - } -} - type builder struct { readyMsgs []Message timestamp uint64 @@ -50,20 +19,16 @@ type builder struct { ddDetector *domDropDetector crDetector *clickRageDetector dcDetector *deadClickDetector - integrationsWaiting bool - - sid uint64 } func NewBuilder() *builder { return &builder{ - ptaBuilder: &performanceTrackAggrBuilder{}, - ciFinder: &cpuIssueFinder{}, - miFinder: &memoryIssueFinder{}, - ddDetector: &domDropDetector{}, - crDetector: &clickRageDetector{}, - dcDetector: &deadClickDetector{}, - integrationsWaiting: true, + ptaBuilder: &performanceTrackAggrBuilder{}, + ciFinder: &cpuIssueFinder{}, + miFinder: &memoryIssueFinder{}, + ddDetector: &domDropDetector{}, + crDetector: &clickRageDetector{}, + dcDetector: &deadClickDetector{}, } } @@ -78,12 +43,6 @@ func (b *builder) iterateReadyMessage(iter func(msg Message)) { b.readyMsgs = nil } -func (b *builder) buildPerformanceTrackAggr() { - if msg := b.ptaBuilder.Build(); msg != nil { - b.appendReadyMessage(msg) - } -} - func (b *builder) handleMessage(message Message, messageID uint64) { timestamp := GetTimestamp(message) if b.timestamp < timestamp { @@ -127,7 +86,9 @@ func (b *builder) checkTimeouts(ts int64) bool { } if b.ptaBuilder.HasInstance() && int64(b.ptaBuilder.GetStartTimestamp())+intervals.EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT < ts { - b.buildPerformanceTrackAggr() + if msg := b.ptaBuilder.Build(); msg != nil { + b.appendReadyMessage(msg) + } } lastTsGap := ts - int64(b.timestamp) From 62b36bd70a577853952afcba5eabea3dbd7ff1e2 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Sat, 7 May 2022 21:29:40 +0200 Subject: [PATCH 07/19] refactor(backend-heuristics): bring all sub-bilders to common interface --- backend/internal/builder/builder.go | 104 ++++------- backend/internal/builder/builderMap.go | 2 - backend/internal/builder/clikRageDetector.go | 55 +++--- backend/internal/builder/cpuIssueFinder.go | 58 +++--- backend/internal/builder/deadClickDetector.go | 63 ++++--- backend/internal/builder/domDropDetector.go | 41 +++-- backend/internal/builder/memoryIssueFinder.go | 48 ++--- .../builder/performanceTrackAggrBuilder.go | 167 +++++++++--------- backend/pkg/intervals/intervals.go | 1 - 9 files changed, 269 insertions(+), 270 deletions(-) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index 6b8ece5d5..7e062e6b2 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -1,41 +1,34 @@ package builder import ( - "net/url" - "strings" - "time" - "openreplay/backend/pkg/intervals" . "openreplay/backend/pkg/messages" ) +type messageProcessor interface { + Handle(message Message, messageID uint64, timestamp uint64) Message + Build() Message +} + type builder struct { - readyMsgs []Message - timestamp uint64 - lastProcessedTimestamp int64 - ptaBuilder *performanceTrackAggrBuilder - ciFinder *cpuIssueFinder - miFinder *memoryIssueFinder - ddDetector *domDropDetector - crDetector *clickRageDetector - dcDetector *deadClickDetector + readyMsgs []Message + timestamp uint64 + processors []messageProcessor } func NewBuilder() *builder { return &builder{ - ptaBuilder: &performanceTrackAggrBuilder{}, - ciFinder: &cpuIssueFinder{}, - miFinder: &memoryIssueFinder{}, - ddDetector: &domDropDetector{}, - crDetector: &clickRageDetector{}, - dcDetector: &deadClickDetector{}, + processors: []messageProcessor{ + &performanceTrackAggrBuilder{}, + &cpuIssueFinder{}, + &memoryIssueFinder{}, + // &domDropDetector{}, + &clickRageDetector{}, + &deadClickDetector{}, + }, } } -func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value - b.readyMsgs = append(b.readyMsgs, msg) -} - func (b *builder) iterateReadyMessage(iter func(msg Message)) { for _, readyMsg := range b.readyMsgs { iter(readyMsg) @@ -48,65 +41,38 @@ func (b *builder) handleMessage(message Message, messageID uint64) { if b.timestamp < timestamp { b.timestamp = timestamp } - - b.lastProcessedTimestamp = time.Now().UnixMilli() - if b.timestamp == 0 { + // in case of SessionStart. TODO: make timestamp system transparent return } - switch msg := message.(type) { - case *PerformanceTrack: - if rm := b.ptaBuilder.HandlePerformanceTrack(msg, b.timestamp); rm != nil { - b.appendReadyMessage(rm) + + for _, p := range b.processors { + /* If nil is not returned explicitely by Handle, but as the typed nil + ("var i *IssueEvent; return i;") + The `rm != nil` will be true. + TODO: enforce nil to be nil(?) or add `isNil() bool` to the Message types + because this part is expected to be etendable by user with custom messageProcessor's. + Use of reflrction will be probably bad on millions of messages? + */ + if rm := p.Handle(message, messageID, b.timestamp); rm != nil { + b.readyMsgs = append(b.readyMsgs, rm) } - if rm := b.ciFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { - b.appendReadyMessage(rm) - } - if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { - b.appendReadyMessage(rm) - } - case *CreateElementNode, - *CreateTextNode: - b.ddDetector.HandleNodeCreation() - case *RemoveNode: - b.ddDetector.HandleNodeRemoval(b.timestamp) - case *CreateDocument: - if rm := b.ddDetector.Build(); rm != nil { - b.appendReadyMessage(rm) - } - } - if rm := b.dcDetector.HandleMessage(message, messageID, b.timestamp); rm != nil { - b.appendReadyMessage(rm) } } func (b *builder) checkTimeouts(ts int64) bool { if b.timestamp == 0 { - return false // There was no timestamp events yet - } - - if b.ptaBuilder.HasInstance() && int64(b.ptaBuilder.GetStartTimestamp())+intervals.EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT < ts { - if msg := b.ptaBuilder.Build(); msg != nil { - b.appendReadyMessage(msg) - } + return false // SessionStart happened only } lastTsGap := ts - int64(b.timestamp) + // Maybe listen for `trigger` and react on SessionEnd instead (less reliable) if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT { - if rm := b.ddDetector.Build(); rm != nil { - b.appendReadyMessage(rm) - } - if rm := b.ciFinder.Build(); rm != nil { - b.appendReadyMessage(rm) - } - if rm := b.miFinder.Build(); rm != nil { - b.appendReadyMessage(rm) - } - if rm := b.crDetector.Build(); rm != nil { - b.appendReadyMessage(rm) - } - if rm := b.dcDetector.HandleReaction(b.timestamp); rm != nil { - b.appendReadyMessage(rm) + for _, p := range b.processors { + // TODO: same as above + if rm := p.Build(); rm != nil { + b.readyMsgs = append(b.readyMsgs, rm) + } } return true } diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index b7885da92..6caf18e4f 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -15,8 +15,6 @@ func (m builderMap) GetBuilder(sessionID uint64) *builder { if b == nil { b = NewBuilder() m[sessionID] = b - b.sid = sessionID - } return b } diff --git a/backend/internal/builder/clikRageDetector.go b/backend/internal/builder/clikRageDetector.go index f25efbcd9..1140027b3 100644 --- a/backend/internal/builder/clikRageDetector.go +++ b/backend/internal/builder/clikRageDetector.go @@ -6,7 +6,7 @@ import ( . "openreplay/backend/pkg/messages" ) -const CLICK_TIME_DIFF = 300 +const MAX_TIME_DIFF = 300 const MIN_CLICKS_IN_A_ROW = 3 type clickRageDetector struct { @@ -17,39 +17,50 @@ type clickRageDetector struct { countsInARow int } -func (crd *clickRageDetector) Build() *IssueEvent { - var i *IssueEvent - if crd.countsInARow >= MIN_CLICKS_IN_A_ROW { - payload, _ := json.Marshal(struct{ Count int }{crd.countsInARow}) - i = &IssueEvent{ - Type: "click_rage", - ContextString: crd.lastLabel, - Payload: string(payload), // TODO: json encoder - Timestamp: crd.firstInARawTimestamp, - MessageID: crd.firstInARawMessageId, - } - } +func (crd *clickRageDetector) reset() { crd.lastTimestamp = 0 crd.lastLabel = "" crd.firstInARawTimestamp = 0 crd.firstInARawMessageId = 0 crd.countsInARow = 0 - return i } -func (crd *clickRageDetector) HandleMouseClick(msg *MouseClick, messageID uint64, timestamp uint64) *IssueEvent { - if crd.lastTimestamp+CLICK_TIME_DIFF > timestamp && crd.lastLabel == msg.Label { - crd.lastTimestamp = timestamp - crd.countsInARow += 1 - return nil +func (crd *clickRageDetector) Build() Message { + if crd.countsInARow >= MIN_CLICKS_IN_A_ROW { + payload, _ := json.Marshal(struct{ Count int }{crd.countsInARow}) + i := &IssueEvent{ + Type: "click_rage", + ContextString: crd.lastLabel, + Payload: string(payload), // TODO: json message field type + Timestamp: crd.firstInARawTimestamp, + MessageID: crd.firstInARawMessageId, + } + crd.reset() + return i } - i := crd.Build() - if msg.Label != "" { + crd.reset() + return nil +} + +func (crd *clickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *MouseClick: + // TODO: check if we it is ok to capture clickrages without the connected CleckEvent in db. + if msg.Label == "" { + return crd.Build() + } + if crd.lastLabel == msg.Label && timestamp-crd.lastTimestamp < MAX_TIME_DIFF { + crd.lastTimestamp = timestamp + crd.countsInARow += 1 + return nil + } + i := crd.Build() crd.lastTimestamp = timestamp crd.lastLabel = msg.Label crd.firstInARawTimestamp = timestamp crd.firstInARawMessageId = messageID crd.countsInARow = 1 + return i } - return i + return nil } diff --git a/backend/internal/builder/cpuIssueFinder.go b/backend/internal/builder/cpuIssueFinder.go index 1af867ea3..feb694a86 100644 --- a/backend/internal/builder/cpuIssueFinder.go +++ b/backend/internal/builder/cpuIssueFinder.go @@ -18,7 +18,7 @@ type cpuIssueFinder struct { contextString string } -func (f *cpuIssueFinder) Build() *IssueEvent { +func (f *cpuIssueFinder) Build() Message { if f.startTimestamp == 0 { return nil } @@ -47,35 +47,35 @@ func (f *cpuIssueFinder) Build() *IssueEvent { } } -func (f *cpuIssueFinder) HandleSetPageLocation(msg *SetPageLocation) { - f.contextString = msg.URL -} - -func (f *cpuIssueFinder) HandlePerformanceTrack(msg *PerformanceTrack, messageID uint64, timestamp uint64) *IssueEvent { - dt := performance.TimeDiff(timestamp, f.lastTimestamp) - if dt == 0 { - return nil // TODO: handle error - } - - f.lastTimestamp = timestamp - - if msg.Frames == -1 || msg.Ticks == -1 { - return f.Build() - } - - cpuRate := performance.CPURate(msg.Ticks, dt) - - if cpuRate >= CPU_THRESHOLD { - if f.startTimestamp == 0 { - f.startTimestamp = timestamp - f.startMessageID = messageID +func (f *cpuIssueFinder) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *PerformanceTrack: + dt := performance.TimeDiff(timestamp, f.lastTimestamp) + if dt == 0 { + return nil // TODO: handle error } - if f.maxRate < cpuRate { - f.maxRate = cpuRate - } - } else { - return f.Build() - } + f.lastTimestamp = timestamp + + if msg.Frames == -1 || msg.Ticks == -1 { + return f.Build() + } + + cpuRate := performance.CPURate(msg.Ticks, dt) + + if cpuRate >= CPU_THRESHOLD { + if f.startTimestamp == 0 { + f.startTimestamp = timestamp + f.startMessageID = messageID + } + if f.maxRate < cpuRate { + f.maxRate = cpuRate + } + } else { + return f.Build() + } + case *SetPageLocation: + f.contextString = msg.URL + } return nil } diff --git a/backend/internal/builder/deadClickDetector.go b/backend/internal/builder/deadClickDetector.go index de977b7bd..f83c0bedd 100644 --- a/backend/internal/builder/deadClickDetector.go +++ b/backend/internal/builder/deadClickDetector.go @@ -7,50 +7,61 @@ import ( const CLICK_RELATION_TIME = 1400 type deadClickDetector struct { - lastMouseClick *MouseClick - lastTimestamp uint64 - lastMessageID uint64 - inputIDSet map[uint64]bool + lastTimestamp uint64 + lastMouseClick *MouseClick + lastClickTimestamp uint64 + lastMessageID uint64 + inputIDSet map[uint64]bool } -func (d *deadClickDetector) HandleReaction(timestamp uint64) *IssueEvent { - var i *IssueEvent - if d.lastMouseClick != nil && d.lastTimestamp+CLICK_RELATION_TIME < timestamp { - i = &IssueEvent{ - Type: "dead_click", - ContextString: d.lastMouseClick.Label, - Timestamp: d.lastTimestamp, - MessageID: d.lastMessageID, - } - } +func (d *deadClickDetector) reset() { d.inputIDSet = nil d.lastMouseClick = nil - d.lastTimestamp = 0 + d.lastClickTimestamp = 0 d.lastMessageID = 0 +} + +func (d *deadClickDetector) handleReaction(timestamp uint64) Message { + if d.lastMouseClick == nil || d.lastClickTimestamp+CLICK_RELATION_TIME > timestamp { // riaction is instant + d.reset() + return nil + } + i := &IssueEvent{ + Type: "dead_click", + ContextString: d.lastMouseClick.Label, + Timestamp: d.lastClickTimestamp, + MessageID: d.lastMessageID, + } + d.reset() return i } -func (d *deadClickDetector) HandleMessage(msg Message, messageID uint64, timestamp uint64) *IssueEvent { - var i *IssueEvent - switch m := msg.(type) { +func (d *deadClickDetector) Build() Message { + return d.handleReaction(d.lastTimestamp) +} + +func (d *deadClickDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { + d.lastTimestamp = timestamp + switch msg := message.(type) { case *SetInputTarget: if d.inputIDSet == nil { d.inputIDSet = make(map[uint64]bool) } - d.inputIDSet[m.ID] = true + d.inputIDSet[msg.ID] = true case *CreateDocument: d.inputIDSet = nil case *MouseClick: - if m.Label == "" { + if msg.Label == "" { return nil } - i = d.HandleReaction(timestamp) - if d.inputIDSet[m.ID] { // ignore if input + i := d.handleReaction(timestamp) + if d.inputIDSet[msg.ID] { // ignore if input return i } - d.lastMouseClick = m - d.lastTimestamp = timestamp + d.lastMouseClick = msg + d.lastClickTimestamp = timestamp d.lastMessageID = messageID + return i case *SetNodeAttribute, *RemoveNodeAttribute, *CreateElementNode, @@ -60,7 +71,7 @@ func (d *deadClickDetector) HandleMessage(msg Message, messageID uint64, timesta *SetCSSData, *CSSInsertRule, *CSSDeleteRule: - i = d.HandleReaction(timestamp) + return d.handleReaction(timestamp) } - return i + return nil } diff --git a/backend/internal/builder/domDropDetector.go b/backend/internal/builder/domDropDetector.go index 3643038c1..473937a9d 100644 --- a/backend/internal/builder/domDropDetector.go +++ b/backend/internal/builder/domDropDetector.go @@ -4,36 +4,45 @@ import ( . "openreplay/backend/pkg/messages" ) +const DROP_WINDOW = 200 //ms +const CRITICAL_COUNT = 1 // Our login page contains 20. But on crush it removes only roots (1-3 nodes). +// TODO: smart detection (making whole DOM tree would eat all memory) + type domDropDetector struct { removedCount int lastDropTimestamp uint64 } -const DROP_WINDOW = 200 //ms -const CRITICAL_COUNT = 1 // Our login page contains 20. But on crush it removes only roots (1-3 nodes). - -func (dd *domDropDetector) HandleNodeCreation() { +func (dd *domDropDetector) reset() { dd.removedCount = 0 dd.lastDropTimestamp = 0 } -func (dd *domDropDetector) HandleNodeRemoval(ts uint64) { - if dd.lastDropTimestamp+DROP_WINDOW > ts { - dd.removedCount += 1 - } else { - dd.removedCount = 1 +func (dd *domDropDetector) Handle(message Message, _ uint64, timestamp uint64) Message { + switch message.(type) { + case *CreateElementNode, + *CreateTextNode: + dd.removedCount = 0 + dd.lastDropTimestamp = 0 + case *RemoveNode: + if dd.lastDropTimestamp+DROP_WINDOW > timestamp { + dd.removedCount += 1 + } else { + dd.removedCount = 1 + } + dd.lastDropTimestamp = timestamp } - dd.lastDropTimestamp = ts + return nil } -func (dd *domDropDetector) Build() *DOMDrop { - var domDrop *DOMDrop +func (dd *domDropDetector) Build() Message { if dd.removedCount >= CRITICAL_COUNT { - domDrop = &DOMDrop{ + domDrop := &DOMDrop{ Timestamp: dd.lastDropTimestamp, } + dd.reset() + return domDrop } - dd.removedCount = 0 - dd.lastDropTimestamp = 0 - return domDrop + dd.reset() + return nil } diff --git a/backend/internal/builder/memoryIssueFinder.go b/backend/internal/builder/memoryIssueFinder.go index 0d6d71420..2f04343bc 100644 --- a/backend/internal/builder/memoryIssueFinder.go +++ b/backend/internal/builder/memoryIssueFinder.go @@ -19,7 +19,7 @@ type memoryIssueFinder struct { contextString string } -func (f *memoryIssueFinder) Build() *IssueEvent { +func (f *memoryIssueFinder) Build() Message { if f.startTimestamp == 0 { return nil } @@ -37,34 +37,34 @@ func (f *memoryIssueFinder) Build() *IssueEvent { return i } -func (f *memoryIssueFinder) HandleSetPageLocation(msg *SetPageLocation) { - f.contextString = msg.URL -} +func (f *memoryIssueFinder) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *PerformanceTrack: + if f.count < MIN_COUNT { + f.sum += float64(msg.UsedJSHeapSize) + f.count++ + return nil + } + + average := f.sum / f.count + rate := int(math.Round(float64(msg.UsedJSHeapSize) / average * 100)) -func (f *memoryIssueFinder) HandlePerformanceTrack(msg *PerformanceTrack, messageID uint64, timestamp uint64) *IssueEvent { - if f.count < MIN_COUNT { f.sum += float64(msg.UsedJSHeapSize) f.count++ - return nil - } - average := f.sum / f.count - rate := int(math.Round(float64(msg.UsedJSHeapSize) / average * 100)) - - f.sum += float64(msg.UsedJSHeapSize) - f.count++ - - if rate >= MEM_RATE_THRESHOLD { - if f.startTimestamp == 0 { - f.startTimestamp = timestamp - f.startMessageID = messageID + if rate >= MEM_RATE_THRESHOLD { + if f.startTimestamp == 0 { + f.startTimestamp = timestamp + f.startMessageID = messageID + } + if f.rate < rate { + f.rate = rate + } + } else { + return f.Build() } - if f.rate < rate { - f.rate = rate - } - } else { - return f.Build() + case *SetPageLocation: + f.contextString = msg.URL } - return nil } diff --git a/backend/internal/builder/performanceTrackAggrBuilder.go b/backend/internal/builder/performanceTrackAggrBuilder.go index 70b751f55..4396e8a05 100644 --- a/backend/internal/builder/performanceTrackAggrBuilder.go +++ b/backend/internal/builder/performanceTrackAggrBuilder.go @@ -7,100 +7,105 @@ import ( "openreplay/backend/pkg/messages/performance" ) +const AGGREGATION_WINDOW = 2 * 60 * 1000 + type performanceTrackAggrBuilder struct { - performanceTrackAggr *PerformanceTrackAggr - lastTimestamp uint64 - count float64 - sumFrameRate float64 - sumTickRate float64 - sumTotalJSHeapSize float64 - sumUsedJSHeapSize float64 + *PerformanceTrackAggr + lastTimestamp uint64 + count float64 + sumFrameRate float64 + sumTickRate float64 + sumTotalJSHeapSize float64 + sumUsedJSHeapSize float64 } func (b *performanceTrackAggrBuilder) start(timestamp uint64) { - b.performanceTrackAggr = &PerformanceTrackAggr{ + b.PerformanceTrackAggr = &PerformanceTrackAggr{ TimestampStart: timestamp, } b.lastTimestamp = timestamp } -func (b *performanceTrackAggrBuilder) HandlePerformanceTrack(msg *PerformanceTrack, timestamp uint64) *PerformanceTrackAggr { - if msg.Frames == -1 || msg.Ticks == -1 || !b.HasInstance() { - performanceTrackAggr := b.Build() - b.start(timestamp) - return performanceTrackAggr - } - - dt := performance.TimeDiff(timestamp, b.lastTimestamp) - if dt == 0 { - return nil // TODO: handle error - } - - frameRate := performance.FrameRate(msg.Frames, dt) - tickRate := performance.TickRate(msg.Ticks, dt) - - fps := uint64(math.Round(frameRate)) - cpu := performance.CPURateFromTickRate(tickRate) - if fps < b.performanceTrackAggr.MinFPS || b.performanceTrackAggr.MinFPS == 0 { - b.performanceTrackAggr.MinFPS = fps - } - if fps > b.performanceTrackAggr.MaxFPS { - b.performanceTrackAggr.MaxFPS = fps - } - if cpu < b.performanceTrackAggr.MinCPU || b.performanceTrackAggr.MinCPU == 0 { - b.performanceTrackAggr.MinCPU = cpu - } - if cpu > b.performanceTrackAggr.MaxCPU { - b.performanceTrackAggr.MaxCPU = cpu - } - if msg.TotalJSHeapSize < b.performanceTrackAggr.MinTotalJSHeapSize || b.performanceTrackAggr.MinTotalJSHeapSize == 0 { - b.performanceTrackAggr.MinTotalJSHeapSize = msg.TotalJSHeapSize - } - if msg.TotalJSHeapSize > b.performanceTrackAggr.MaxTotalJSHeapSize { - b.performanceTrackAggr.MaxTotalJSHeapSize = msg.TotalJSHeapSize - } - if msg.UsedJSHeapSize < b.performanceTrackAggr.MinUsedJSHeapSize || b.performanceTrackAggr.MinUsedJSHeapSize == 0 { - b.performanceTrackAggr.MinUsedJSHeapSize = msg.UsedJSHeapSize - } - if msg.UsedJSHeapSize > b.performanceTrackAggr.MaxUsedJSHeapSize { - b.performanceTrackAggr.MaxUsedJSHeapSize = msg.UsedJSHeapSize - } - b.sumFrameRate += frameRate - b.sumTickRate += tickRate - b.sumTotalJSHeapSize += float64(msg.TotalJSHeapSize) - b.sumUsedJSHeapSize += float64(msg.UsedJSHeapSize) - b.count += 1 - b.lastTimestamp = timestamp - return nil -} - -func (b *performanceTrackAggrBuilder) HasInstance() bool { - return b.performanceTrackAggr != nil -} - -func (b *performanceTrackAggrBuilder) GetStartTimestamp() uint64 { - if b.performanceTrackAggr == nil { - return 0 - } - return b.performanceTrackAggr.TimestampStart -} - -func (b *performanceTrackAggrBuilder) Build() *PerformanceTrackAggr { - var performanceTrackAggr *PerformanceTrackAggr - if b.HasInstance() && b.GetStartTimestamp() != b.lastTimestamp && b.count != 0 { - performanceTrackAggr = b.performanceTrackAggr - performanceTrackAggr.TimestampEnd = b.lastTimestamp - performanceTrackAggr.AvgFPS = uint64(math.Round(b.sumFrameRate / b.count)) - performanceTrackAggr.AvgCPU = 100 - uint64(math.Round(b.sumTickRate*100/b.count)) - performanceTrackAggr.AvgTotalJSHeapSize = uint64(math.Round(b.sumTotalJSHeapSize / b.count)) - performanceTrackAggr.AvgUsedJSHeapSize = uint64(math.Round(b.sumUsedJSHeapSize / b.count)) - } - b.performanceTrackAggr = nil +func (b *performanceTrackAggrBuilder) reset() { + b.PerformanceTrackAggr = nil b.count = 0 b.sumFrameRate = 0 b.sumTickRate = 0 b.sumTotalJSHeapSize = 0 b.sumUsedJSHeapSize = 0 b.lastTimestamp = 0 - return performanceTrackAggr +} + +func (b *performanceTrackAggrBuilder) Handle(message Message, _ uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *PerformanceTrack: + if b.PerformanceTrackAggr == nil || msg.Frames == -1 || msg.Ticks == -1 { + pta := b.Build() + b.start(timestamp) + return pta + } + + dt := performance.TimeDiff(timestamp, b.lastTimestamp) + if dt == 0 { + return nil // shouldn't happen + } + + frameRate := performance.FrameRate(msg.Frames, dt) + tickRate := performance.TickRate(msg.Ticks, dt) + + fps := uint64(math.Round(frameRate)) + cpu := performance.CPURateFromTickRate(tickRate) + if fps < b.MinFPS || b.MinFPS == 0 { + b.MinFPS = fps + } + if fps > b.MaxFPS { + b.MaxFPS = fps + } + if cpu < b.MinCPU || b.MinCPU == 0 { + b.MinCPU = cpu + } + if cpu > b.MaxCPU { + b.MaxCPU = cpu + } + if msg.TotalJSHeapSize < b.MinTotalJSHeapSize || b.MinTotalJSHeapSize == 0 { + b.MinTotalJSHeapSize = msg.TotalJSHeapSize + } + if msg.TotalJSHeapSize > b.MaxTotalJSHeapSize { + b.MaxTotalJSHeapSize = msg.TotalJSHeapSize + } + if msg.UsedJSHeapSize < b.MinUsedJSHeapSize || b.MinUsedJSHeapSize == 0 { + b.MinUsedJSHeapSize = msg.UsedJSHeapSize + } + if msg.UsedJSHeapSize > b.MaxUsedJSHeapSize { + b.MaxUsedJSHeapSize = msg.UsedJSHeapSize + } + b.sumFrameRate += frameRate + b.sumTickRate += tickRate + b.sumTotalJSHeapSize += float64(msg.TotalJSHeapSize) + b.sumUsedJSHeapSize += float64(msg.UsedJSHeapSize) + b.count += 1 + b.lastTimestamp = timestamp + } + if b.PerformanceTrackAggr != nil && + timestamp-b.PerformanceTrackAggr.TimestampStart >= AGGREGATION_WINDOW { + return b.Build() + } + return nil +} + +func (b *performanceTrackAggrBuilder) Build() Message { + if b.PerformanceTrackAggr == nil { + return nil + } + if b.count != 0 && b.PerformanceTrackAggr.TimestampStart < b.lastTimestamp { // the last one shouldn't happen + b.PerformanceTrackAggr.TimestampEnd = b.lastTimestamp + b.PerformanceTrackAggr.AvgFPS = uint64(math.Round(b.sumFrameRate / b.count)) + b.PerformanceTrackAggr.AvgCPU = 100 - uint64(math.Round(b.sumTickRate*100/b.count)) + b.PerformanceTrackAggr.AvgTotalJSHeapSize = uint64(math.Round(b.sumTotalJSHeapSize / b.count)) + b.PerformanceTrackAggr.AvgUsedJSHeapSize = uint64(math.Round(b.sumUsedJSHeapSize / b.count)) + b.reset() + return b.PerformanceTrackAggr + } + b.reset() + return nil } diff --git a/backend/pkg/intervals/intervals.go b/backend/pkg/intervals/intervals.go index 2ce13ed5e..649ceca1a 100644 --- a/backend/pkg/intervals/intervals.go +++ b/backend/pkg/intervals/intervals.go @@ -5,7 +5,6 @@ const HEARTBEAT_INTERVAL = 2 * 60 * 1000 // максимальный const INTEGRATIONS_REQUEST_INTERVAL = 1 * 60 * 1000 // интеграции const EVENTS_PAGE_EVENT_TIMEOUT = 2 * 60 * 1000 // таймаут пейдж ивента const EVENTS_INPUT_EVENT_TIMEOUT = 2 * 60 * 1000 // -const EVENTS_PERFORMANCE_AGGREGATION_TIMEOUT = 2 * 60 * 1000 const EVENTS_SESSION_END_TIMEOUT = HEARTBEAT_INTERVAL + 30*1000 const EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS = HEARTBEAT_INTERVAL + 3*60*1000 const EVENTS_BACK_COMMIT_GAP = EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS + 1*60*1000 // для бэк коммита From 6ab6d342c035a65cde98fbe73b5509db03e8a344 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Sat, 7 May 2022 22:16:15 +0200 Subject: [PATCH 08/19] chore(backend-heuristics/db): remove redundant --- backend/cmd/db/main.go | 2 +- backend/internal/heuristics/heuristics.go | 40 +++---------------- .../internal/heuristics/inputEventBuilder.go | 2 +- .../internal/heuristics/pageEventBuilder.go | 2 +- backend/internal/heuristics/session.go | 10 +++-- 5 files changed, 15 insertions(+), 41 deletions(-) diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 962057213..f6cd481a7 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -60,7 +60,7 @@ func main() { } // Handle heuristics and save to temporary queue in memory - heurFinder.HandleMessage(session, msg) + heurFinder.HandleMessage(sessionID, msg) // Process saved heuristics messages as usual messages above in the code heurFinder.IterateSessionReadyMessages(sessionID, func(msg messages.Message) { diff --git a/backend/internal/heuristics/heuristics.go b/backend/internal/heuristics/heuristics.go index 677574951..c55ad33b3 100644 --- a/backend/internal/heuristics/heuristics.go +++ b/backend/internal/heuristics/heuristics.go @@ -1,43 +1,26 @@ package heuristics import ( - . "openreplay/backend/pkg/db/types" . "openreplay/backend/pkg/messages" ) -type MessageHandler interface { - HandleMessage(Message) -} -type ReadyMessagesIterator interface { - IterateReadyMessages(func(Message)) -} - -type Handler interface { - MessageHandler - ReadyMessagesIterator -} - type mainHandler map[uint64]*sessHandler func NewHandler() mainHandler { return make(mainHandler) } -func (m mainHandler) getSessHandler(session *Session) *sessHandler { - if session == nil { - //AAAA - return nil - } - s := m[session.SessionID] +func (m mainHandler) getSessHandler(sessionID uint64) *sessHandler { + s := m[sessionID] if s == nil { - s = newSessHandler(session) - m[session.SessionID] = s + s = newSessHandler() + m[sessionID] = s } return s } -func (m mainHandler) HandleMessage(session *Session, msg Message) { - s := m.getSessHandler(session) +func (m mainHandler) HandleMessage(sessionID uint64, msg Message) { + s := m.getSessHandler(sessionID) s.HandleMessage(msg) } @@ -51,14 +34,3 @@ func (m mainHandler) IterateSessionReadyMessages(sessionID uint64, iter func(msg delete(m, sessionID) } } - -func (m mainHandler) IterateReadyMessages(iter func(sessionID uint64, msg Message)) { - for sessionID, s := range m { - s.IterateReadyMessages(func(msg Message) { - iter(sessionID, msg) - }) - if s.IsEnded() { - delete(m, sessionID) - } - } -} diff --git a/backend/internal/heuristics/inputEventBuilder.go b/backend/internal/heuristics/inputEventBuilder.go index ce1b710ca..624e15e47 100644 --- a/backend/internal/heuristics/inputEventBuilder.go +++ b/backend/internal/heuristics/inputEventBuilder.go @@ -1,4 +1,4 @@ -package builder +package heuristics import ( . "openreplay/backend/pkg/messages" diff --git a/backend/internal/heuristics/pageEventBuilder.go b/backend/internal/heuristics/pageEventBuilder.go index 2b0665894..96a1b287e 100644 --- a/backend/internal/heuristics/pageEventBuilder.go +++ b/backend/internal/heuristics/pageEventBuilder.go @@ -1,4 +1,4 @@ -package builder +package heuristics import ( . "openreplay/backend/pkg/messages" diff --git a/backend/internal/heuristics/session.go b/backend/internal/heuristics/session.go index 3946bf918..a49db948b 100644 --- a/backend/internal/heuristics/session.go +++ b/backend/internal/heuristics/session.go @@ -1,19 +1,21 @@ package heuristics import ( - . "openreplay/backend/pkg/db/types" . "openreplay/backend/pkg/messages" ) +type Handler interface { + HandleMessage(Message) + IterateReadyMessages(func(Message)) +} + type sessHandler struct { - session *Session handlers []Handler ended bool } -func newSessHandler(session *Session) *sessHandler { +func newSessHandler() *sessHandler { return &sessHandler{ - session: session, handlers: []Handler{ new(clickrage), new(performanceAggregator), From ca9d76624b505ecb31b43890512e21cf3ff33a08 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Mon, 9 May 2022 16:51:10 +0200 Subject: [PATCH 09/19] feat(backend/heuristics): message handlers refactoring --- backend/cmd/heuristics/main.go | 30 ++++++++- backend/internal/builder/builder.go | 19 ++---- backend/internal/builder/builderMap.go | 31 ++++++---- .../internal/handlers/custom/customHandler.go | 16 +++++ .../ios/appNotResponding.go} | 51 ++++++++++------ .../ios/clickRage.go} | 61 +++++++++++-------- .../ios/performanceAggregator.go} | 59 ++++++++++-------- backend/internal/handlers/messageProcessor.go | 11 ++++ .../readyMessageStore.go | 8 +-- .../web/clickRage.go} | 12 ++-- .../web/cpuIssue.go} | 10 +-- .../web/deadClick.go} | 14 +++-- .../web/domDrop.go} | 4 +- .../web/memoryIssue.go} | 10 +-- .../web/performanceAggregator.go} | 12 ++-- backend/internal/heuristics/session.go | 6 +- 16 files changed, 223 insertions(+), 131 deletions(-) create mode 100644 backend/internal/handlers/custom/customHandler.go rename backend/internal/{heuristics/anr.go => handlers/ios/appNotResponding.go} (66%) rename backend/internal/{heuristics/clickrage.go => handlers/ios/clickRage.go} (60%) rename backend/internal/{heuristics/performance.go => handlers/ios/performanceAggregator.go} (78%) create mode 100644 backend/internal/handlers/messageProcessor.go rename backend/internal/{heuristics => handlers}/readyMessageStore.go (51%) rename backend/internal/{builder/clikRageDetector.go => handlers/web/clickRage.go} (85%) rename backend/internal/{builder/cpuIssueFinder.go => handlers/web/cpuIssue.go} (86%) rename backend/internal/{builder/deadClickDetector.go => handlers/web/deadClick.go} (82%) rename backend/internal/{builder/domDropDetector.go => handlers/web/domDrop.go} (94%) rename backend/internal/{builder/memoryIssueFinder.go => handlers/web/memoryIssue.go} (83%) rename backend/internal/{builder/performanceTrackAggrBuilder.go => handlers/web/performanceAggregator.go} (89%) diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index 2778685d3..f5e3b675f 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -4,6 +4,10 @@ import ( "log" "openreplay/backend/internal/builder" "openreplay/backend/internal/config/ender" + "openreplay/backend/internal/handlers" + "openreplay/backend/internal/handlers/custom" + "openreplay/backend/internal/handlers/ios" + "openreplay/backend/internal/handlers/web" "openreplay/backend/pkg/intervals" logger "openreplay/backend/pkg/log" "openreplay/backend/pkg/messages" @@ -18,10 +22,32 @@ import ( func main() { log.SetFlags(log.LstdFlags | log.LUTC | log.Llongfile) + // Load service configuration cfg := ender.New() - builderMap := builder.NewBuilderMap() + // Declare message handlers we want to apply for each incoming message + msgHandlers := []handlers.MessageProcessor{ + // web handlers + &web.ClickRageDetector{}, + &web.CpuIssueDetector{}, + &web.DeadClickDetector{}, + &web.MemoryIssueDetector{}, + &web.PerformanceAggregator{}, + // iOS handlers + &ios.AppNotResponding{}, + &ios.ClickRageDetector{}, + &ios.PerformanceAggregator{}, + // Other handlers (you can add your custom handlers here) + &custom.CustomHandler{}, + } + + // Create handler's aggregator + builderMap := builder.NewBuilderMap(msgHandlers...) + + // Init logger statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) + + // Init producer and consumer for data bus producer := queue.NewProducer() consumer := queue.NewMessageConsumer( cfg.GroupEvents, @@ -36,7 +62,7 @@ func main() { false, ) - log.Printf("Ender service started\n") + log.Printf("Heuristics service started\n") sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index 7e062e6b2..c35457d62 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -1,31 +1,20 @@ package builder import ( + "openreplay/backend/internal/handlers" "openreplay/backend/pkg/intervals" . "openreplay/backend/pkg/messages" ) -type messageProcessor interface { - Handle(message Message, messageID uint64, timestamp uint64) Message - Build() Message -} - type builder struct { readyMsgs []Message timestamp uint64 - processors []messageProcessor + processors []handlers.MessageProcessor } -func NewBuilder() *builder { +func NewBuilder(handlers ...handlers.MessageProcessor) *builder { return &builder{ - processors: []messageProcessor{ - &performanceTrackAggrBuilder{}, - &cpuIssueFinder{}, - &memoryIssueFinder{}, - // &domDropDetector{}, - &clickRageDetector{}, - &deadClickDetector{}, - }, + processors: handlers, } } diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index 6caf18e4f..5bc01e78d 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -1,37 +1,44 @@ package builder import ( + "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" ) -type builderMap map[uint64]*builder - -func NewBuilderMap() builderMap { - return make(builderMap) +type builderMap struct { + handlers []handlers.MessageProcessor + sessions map[uint64]*builder } -func (m builderMap) GetBuilder(sessionID uint64) *builder { - b := m[sessionID] +func NewBuilderMap(handlers ...handlers.MessageProcessor) *builderMap { + return &builderMap{ + handlers: handlers, + sessions: make(map[uint64]*builder), + } +} + +func (m *builderMap) GetBuilder(sessionID uint64) *builder { + b := m.sessions[sessionID] if b == nil { - b = NewBuilder() - m[sessionID] = b + b = NewBuilder(m.handlers...) + m.sessions[sessionID] = b } return b } -func (m builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint64) { +func (m *builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint64) { b := m.GetBuilder(sessionID) b.handleMessage(msg, messageID) } -func (m builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) { - for sessionID, b := range m { +func (m *builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) { + for sessionID, b := range m.sessions { sessionEnded := b.checkTimeouts(operatingTs) b.iterateReadyMessage(func(msg Message) { iter(sessionID, msg) }) if sessionEnded { - delete(m, sessionID) + delete(m.sessions, sessionID) } } } diff --git a/backend/internal/handlers/custom/customHandler.go b/backend/internal/handlers/custom/customHandler.go new file mode 100644 index 000000000..9b191189e --- /dev/null +++ b/backend/internal/handlers/custom/customHandler.go @@ -0,0 +1,16 @@ +package custom + +import . "openreplay/backend/pkg/messages" + +type CustomHandler struct { + lastTimestamp uint64 +} + +func (h *CustomHandler) Handle(message Message, messageID uint64, timestamp uint64) Message { + h.lastTimestamp = timestamp + return nil +} + +func (h *CustomHandler) Build() Message { + return nil +} diff --git a/backend/internal/heuristics/anr.go b/backend/internal/handlers/ios/appNotResponding.go similarity index 66% rename from backend/internal/heuristics/anr.go rename to backend/internal/handlers/ios/appNotResponding.go index 7cec8fc97..1241648db 100644 --- a/backend/internal/heuristics/anr.go +++ b/backend/internal/handlers/ios/appNotResponding.go @@ -1,34 +1,23 @@ -package heuristics +package ios import ( + "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" ) +// app is not responding detector + const MIN_TIME_AFTER_LAST_HEARTBEAT = 60 * 1000 -type anr struct { - readyMessageStore +type AppNotResponding struct { + handlers.ReadyMessageStore lastLabel string lastHeartbeatTimestamp uint64 lastHeartbeatIndex uint64 } -func (h *anr) buildIf(timestamp uint64) { - if h.lastHeartbeatTimestamp != 0 && h.lastHeartbeatTimestamp+MIN_TIME_AFTER_LAST_HEARTBEAT <= timestamp { - m := &IOSIssueEvent{ - Type: "anr", - ContextString: h.lastLabel, - } - m.Timestamp = h.lastHeartbeatTimestamp - m.Index = h.lastHeartbeatIndex // Associated Index/ MessageID ? - h.append(m) - h.lastHeartbeatTimestamp = 0 - h.lastHeartbeatIndex = 0 - } -} - -func (h *anr) HandleMessage(msg Message) { - switch m := msg.(type) { +func (h *AppNotResponding) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch m := message.(type) { case *IOSClickEvent: h.buildIf(m.Timestamp) h.lastLabel = m.Label @@ -46,4 +35,28 @@ func (h *anr) HandleMessage(msg Message) { case *IOSSessionEnd: h.buildIf(m.Timestamp) } + return nil +} + +func (h *AppNotResponding) Build() Message { + //TODO implement me + panic("implement me") +} + +func (h *AppNotResponding) buildIf(timestamp uint64) { + if h.lastHeartbeatTimestamp != 0 && h.lastHeartbeatTimestamp+MIN_TIME_AFTER_LAST_HEARTBEAT <= timestamp { + m := &IOSIssueEvent{ + Type: "anr", + ContextString: h.lastLabel, + } + m.Timestamp = h.lastHeartbeatTimestamp + m.Index = h.lastHeartbeatIndex // Associated Index/ MessageID ? + h.Append(m) + h.lastHeartbeatTimestamp = 0 + h.lastHeartbeatIndex = 0 + } +} + +func (h *AppNotResponding) HandleMessage(msg Message) { + // TODO: delete it } diff --git a/backend/internal/heuristics/clickrage.go b/backend/internal/handlers/ios/clickRage.go similarity index 60% rename from backend/internal/heuristics/clickrage.go rename to backend/internal/handlers/ios/clickRage.go index 4d19bf92e..2707c04d5 100644 --- a/backend/internal/heuristics/clickrage.go +++ b/backend/internal/handlers/ios/clickRage.go @@ -1,14 +1,17 @@ -package heuristics +package ios import ( + "openreplay/backend/internal/handlers" + "openreplay/backend/internal/handlers/web" . "openreplay/backend/pkg/messages" ) const CLICK_TIME_DIFF = 200 -const MIN_CLICKS_IN_A_ROW = 3 -type clickrage struct { - readyMessageStore +//const MIN_CLICKS_IN_A_ROW = 3 + +type ClickRageDetector struct { + handlers.ReadyMessageStore lastTimestamp uint64 lastLabel string firstInARawTimestamp uint64 @@ -16,30 +19,13 @@ type clickrage struct { countsInARow int } -func (h *clickrage) build() { - if h.countsInARow >= MIN_CLICKS_IN_A_ROW { - m := &IOSIssueEvent{ - Type: "click_rage", - ContextString: h.lastLabel, - } - m.Timestamp = h.firstInARawTimestamp - m.Index = h.firstInARawSeqIndex // Associated Index/ MessageID ? - h.append(m) - } - h.lastTimestamp = 0 - h.lastLabel = "" - h.firstInARawTimestamp = 0 - h.firstInARawSeqIndex = 0 - h.countsInARow = 0 -} - -func (h *clickrage) HandleMessage(msg Message) { - switch m := msg.(type) { +func (h *ClickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch m := message.(type) { case *IOSClickEvent: if h.lastTimestamp+CLICK_TIME_DIFF < m.Timestamp && h.lastLabel == m.Label { h.lastTimestamp = m.Timestamp h.countsInARow += 1 - return + return nil } h.build() if m.Label != "" { @@ -52,4 +38,31 @@ func (h *clickrage) HandleMessage(msg Message) { case *IOSSessionEnd: h.build() } + return nil +} + +func (h *ClickRageDetector) Build() Message { + //TODO implement me + panic("implement me") +} + +func (h *ClickRageDetector) build() { + if h.countsInARow >= web.MIN_CLICKS_IN_A_ROW { + m := &IOSIssueEvent{ + Type: "click_rage", + ContextString: h.lastLabel, + } + m.Timestamp = h.firstInARawTimestamp + m.Index = h.firstInARawSeqIndex // Associated Index/ MessageID ? + h.Append(m) + } + h.lastTimestamp = 0 + h.lastLabel = "" + h.firstInARawTimestamp = 0 + h.firstInARawSeqIndex = 0 + h.countsInARow = 0 +} + +func (h *ClickRageDetector) HandleMessage(msg Message) { + // TODO: delete it } diff --git a/backend/internal/heuristics/performance.go b/backend/internal/handlers/ios/performanceAggregator.go similarity index 78% rename from backend/internal/heuristics/performance.go rename to backend/internal/handlers/ios/performanceAggregator.go index c7494a793..1525127b8 100644 --- a/backend/internal/heuristics/performance.go +++ b/backend/internal/handlers/ios/performanceAggregator.go @@ -1,6 +1,7 @@ -package heuristics +package ios import ( + "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" ) @@ -18,8 +19,8 @@ func (va *valueAggregator) aggregate() uint64 { return uint64(va.sum / va.count) } -type performanceAggregator struct { - readyMessageStore +type PerformanceAggregator struct { + handlers.ReadyMessageStore pa *IOSPerformanceAggregated fps valueAggregator cpu valueAggregator @@ -27,30 +28,11 @@ type performanceAggregator struct { battery valueAggregator } -func (h *performanceAggregator) build(timestamp uint64) { - if h.pa == nil { - return - } - h.pa.TimestampEnd = timestamp - h.pa.AvgFPS = h.fps.aggregate() - h.pa.AvgCPU = h.cpu.aggregate() - h.pa.AvgMemory = h.memory.aggregate() - h.pa.AvgBattery = h.battery.aggregate() - - h.append(h.pa) - - h.pa = &IOSPerformanceAggregated{} - for _, agg := range []valueAggregator{h.fps, h.cpu, h.memory, h.battery} { - agg.sum = 0 - agg.count = 0 - } -} - -func (h *performanceAggregator) HandleMessage(msg Message) { +func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timestamp uint64) Message { if h.pa == nil { h.pa = &IOSPerformanceAggregated{} // TODO: struct type in messages } - switch m := msg.(type) { // TODO: All Timestampe messages + switch m := message.(type) { // TODO: All Timestampe messages case *IOSPerformanceEvent: if h.pa.TimestampStart == 0 { h.pa.TimestampStart = m.Timestamp @@ -99,4 +81,33 @@ func (h *performanceAggregator) HandleMessage(msg Message) { case *IOSSessionEnd: h.build(m.Timestamp) } + return nil +} + +func (h *PerformanceAggregator) Build() Message { + //TODO implement me + panic("implement me") +} + +func (h *PerformanceAggregator) build(timestamp uint64) { + if h.pa == nil { + return + } + h.pa.TimestampEnd = timestamp + h.pa.AvgFPS = h.fps.aggregate() + h.pa.AvgCPU = h.cpu.aggregate() + h.pa.AvgMemory = h.memory.aggregate() + h.pa.AvgBattery = h.battery.aggregate() + + h.Append(h.pa) + + h.pa = &IOSPerformanceAggregated{} + for _, agg := range []valueAggregator{h.fps, h.cpu, h.memory, h.battery} { + agg.sum = 0 + agg.count = 0 + } +} + +func (h *PerformanceAggregator) HandleMessage(msg Message) { + // TODO: delete it } diff --git a/backend/internal/handlers/messageProcessor.go b/backend/internal/handlers/messageProcessor.go new file mode 100644 index 000000000..c4235c18b --- /dev/null +++ b/backend/internal/handlers/messageProcessor.go @@ -0,0 +1,11 @@ +package handlers + +import . "openreplay/backend/pkg/messages" + +// Heuristic interface - common interface for user's realisations +// U can create your own message handler and easily connect to heuristics service + +type MessageProcessor interface { + Handle(message Message, messageID uint64, timestamp uint64) Message + Build() Message +} diff --git a/backend/internal/heuristics/readyMessageStore.go b/backend/internal/handlers/readyMessageStore.go similarity index 51% rename from backend/internal/heuristics/readyMessageStore.go rename to backend/internal/handlers/readyMessageStore.go index bbe77585d..c0c386571 100644 --- a/backend/internal/heuristics/readyMessageStore.go +++ b/backend/internal/handlers/readyMessageStore.go @@ -1,18 +1,18 @@ -package heuristics +package handlers import ( . "openreplay/backend/pkg/messages" ) -type readyMessageStore struct { +type ReadyMessageStore struct { store []Message } -func (s *readyMessageStore) append(msg Message) { +func (s *ReadyMessageStore) Append(msg Message) { s.store = append(s.store, msg) } -func (s *readyMessageStore) IterateReadyMessages(cb func(msg Message)) { +func (s *ReadyMessageStore) IterateReadyMessages(cb func(msg Message)) { for _, msg := range s.store { cb(msg) } diff --git a/backend/internal/builder/clikRageDetector.go b/backend/internal/handlers/web/clickRage.go similarity index 85% rename from backend/internal/builder/clikRageDetector.go rename to backend/internal/handlers/web/clickRage.go index 1140027b3..db22a9667 100644 --- a/backend/internal/builder/clikRageDetector.go +++ b/backend/internal/handlers/web/clickRage.go @@ -1,4 +1,4 @@ -package builder +package web import ( "encoding/json" @@ -6,10 +6,12 @@ import ( . "openreplay/backend/pkg/messages" ) +// TODO: Description of click rage detector + const MAX_TIME_DIFF = 300 const MIN_CLICKS_IN_A_ROW = 3 -type clickRageDetector struct { +type ClickRageDetector struct { lastTimestamp uint64 lastLabel string firstInARawTimestamp uint64 @@ -17,7 +19,7 @@ type clickRageDetector struct { countsInARow int } -func (crd *clickRageDetector) reset() { +func (crd *ClickRageDetector) reset() { crd.lastTimestamp = 0 crd.lastLabel = "" crd.firstInARawTimestamp = 0 @@ -25,7 +27,7 @@ func (crd *clickRageDetector) reset() { crd.countsInARow = 0 } -func (crd *clickRageDetector) Build() Message { +func (crd *ClickRageDetector) Build() Message { if crd.countsInARow >= MIN_CLICKS_IN_A_ROW { payload, _ := json.Marshal(struct{ Count int }{crd.countsInARow}) i := &IssueEvent{ @@ -42,7 +44,7 @@ func (crd *clickRageDetector) Build() Message { return nil } -func (crd *clickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { +func (crd *ClickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { switch msg := message.(type) { case *MouseClick: // TODO: check if we it is ok to capture clickrages without the connected CleckEvent in db. diff --git a/backend/internal/builder/cpuIssueFinder.go b/backend/internal/handlers/web/cpuIssue.go similarity index 86% rename from backend/internal/builder/cpuIssueFinder.go rename to backend/internal/handlers/web/cpuIssue.go index feb694a86..5cc12be68 100644 --- a/backend/internal/builder/cpuIssueFinder.go +++ b/backend/internal/handlers/web/cpuIssue.go @@ -1,4 +1,4 @@ -package builder +package web import ( "encoding/json" @@ -7,10 +7,12 @@ import ( "openreplay/backend/pkg/messages/performance" ) +// TODO: Description of cpu issue detector + const CPU_THRESHOLD = 70 // % out of 100 const CPU_MIN_DURATION_TRIGGER = 6 * 1000 -type cpuIssueFinder struct { +type CpuIssueDetector struct { startTimestamp uint64 startMessageID uint64 lastTimestamp uint64 @@ -18,7 +20,7 @@ type cpuIssueFinder struct { contextString string } -func (f *cpuIssueFinder) Build() Message { +func (f *CpuIssueDetector) Build() Message { if f.startTimestamp == 0 { return nil } @@ -47,7 +49,7 @@ func (f *cpuIssueFinder) Build() Message { } } -func (f *cpuIssueFinder) Handle(message Message, messageID uint64, timestamp uint64) Message { +func (f *CpuIssueDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { switch msg := message.(type) { case *PerformanceTrack: dt := performance.TimeDiff(timestamp, f.lastTimestamp) diff --git a/backend/internal/builder/deadClickDetector.go b/backend/internal/handlers/web/deadClick.go similarity index 82% rename from backend/internal/builder/deadClickDetector.go rename to backend/internal/handlers/web/deadClick.go index f83c0bedd..a04da9be9 100644 --- a/backend/internal/builder/deadClickDetector.go +++ b/backend/internal/handlers/web/deadClick.go @@ -1,12 +1,14 @@ -package builder +package web import ( . "openreplay/backend/pkg/messages" ) +// TODO: Description of dead click detector + const CLICK_RELATION_TIME = 1400 -type deadClickDetector struct { +type DeadClickDetector struct { lastTimestamp uint64 lastMouseClick *MouseClick lastClickTimestamp uint64 @@ -14,14 +16,14 @@ type deadClickDetector struct { inputIDSet map[uint64]bool } -func (d *deadClickDetector) reset() { +func (d *DeadClickDetector) reset() { d.inputIDSet = nil d.lastMouseClick = nil d.lastClickTimestamp = 0 d.lastMessageID = 0 } -func (d *deadClickDetector) handleReaction(timestamp uint64) Message { +func (d *DeadClickDetector) handleReaction(timestamp uint64) Message { if d.lastMouseClick == nil || d.lastClickTimestamp+CLICK_RELATION_TIME > timestamp { // riaction is instant d.reset() return nil @@ -36,11 +38,11 @@ func (d *deadClickDetector) handleReaction(timestamp uint64) Message { return i } -func (d *deadClickDetector) Build() Message { +func (d *DeadClickDetector) Build() Message { return d.handleReaction(d.lastTimestamp) } -func (d *deadClickDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { +func (d *DeadClickDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { d.lastTimestamp = timestamp switch msg := message.(type) { case *SetInputTarget: diff --git a/backend/internal/builder/domDropDetector.go b/backend/internal/handlers/web/domDrop.go similarity index 94% rename from backend/internal/builder/domDropDetector.go rename to backend/internal/handlers/web/domDrop.go index 473937a9d..c89fab2c4 100644 --- a/backend/internal/builder/domDropDetector.go +++ b/backend/internal/handlers/web/domDrop.go @@ -1,9 +1,11 @@ -package builder +package web import ( . "openreplay/backend/pkg/messages" ) +// TODO: Description of dom drop detector + const DROP_WINDOW = 200 //ms const CRITICAL_COUNT = 1 // Our login page contains 20. But on crush it removes only roots (1-3 nodes). // TODO: smart detection (making whole DOM tree would eat all memory) diff --git a/backend/internal/builder/memoryIssueFinder.go b/backend/internal/handlers/web/memoryIssue.go similarity index 83% rename from backend/internal/builder/memoryIssueFinder.go rename to backend/internal/handlers/web/memoryIssue.go index 2f04343bc..ac8ca8a14 100644 --- a/backend/internal/builder/memoryIssueFinder.go +++ b/backend/internal/handlers/web/memoryIssue.go @@ -1,4 +1,4 @@ -package builder +package web import ( "encoding/json" @@ -7,10 +7,12 @@ import ( . "openreplay/backend/pkg/messages" ) +// TODO: Description of memory issue detector + const MIN_COUNT = 3 const MEM_RATE_THRESHOLD = 300 // % to average -type memoryIssueFinder struct { +type MemoryIssueDetector struct { startMessageID uint64 startTimestamp uint64 rate int @@ -19,7 +21,7 @@ type memoryIssueFinder struct { contextString string } -func (f *memoryIssueFinder) Build() Message { +func (f *MemoryIssueDetector) Build() Message { if f.startTimestamp == 0 { return nil } @@ -37,7 +39,7 @@ func (f *memoryIssueFinder) Build() Message { return i } -func (f *memoryIssueFinder) Handle(message Message, messageID uint64, timestamp uint64) Message { +func (f *MemoryIssueDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { switch msg := message.(type) { case *PerformanceTrack: if f.count < MIN_COUNT { diff --git a/backend/internal/builder/performanceTrackAggrBuilder.go b/backend/internal/handlers/web/performanceAggregator.go similarity index 89% rename from backend/internal/builder/performanceTrackAggrBuilder.go rename to backend/internal/handlers/web/performanceAggregator.go index 4396e8a05..a7bf79f9f 100644 --- a/backend/internal/builder/performanceTrackAggrBuilder.go +++ b/backend/internal/handlers/web/performanceAggregator.go @@ -1,4 +1,4 @@ -package builder +package web import ( "math" @@ -9,7 +9,7 @@ import ( const AGGREGATION_WINDOW = 2 * 60 * 1000 -type performanceTrackAggrBuilder struct { +type PerformanceAggregator struct { *PerformanceTrackAggr lastTimestamp uint64 count float64 @@ -19,14 +19,14 @@ type performanceTrackAggrBuilder struct { sumUsedJSHeapSize float64 } -func (b *performanceTrackAggrBuilder) start(timestamp uint64) { +func (b *PerformanceAggregator) start(timestamp uint64) { b.PerformanceTrackAggr = &PerformanceTrackAggr{ TimestampStart: timestamp, } b.lastTimestamp = timestamp } -func (b *performanceTrackAggrBuilder) reset() { +func (b *PerformanceAggregator) reset() { b.PerformanceTrackAggr = nil b.count = 0 b.sumFrameRate = 0 @@ -36,7 +36,7 @@ func (b *performanceTrackAggrBuilder) reset() { b.lastTimestamp = 0 } -func (b *performanceTrackAggrBuilder) Handle(message Message, _ uint64, timestamp uint64) Message { +func (b *PerformanceAggregator) Handle(message Message, _ uint64, timestamp uint64) Message { switch msg := message.(type) { case *PerformanceTrack: if b.PerformanceTrackAggr == nil || msg.Frames == -1 || msg.Ticks == -1 { @@ -93,7 +93,7 @@ func (b *performanceTrackAggrBuilder) Handle(message Message, _ uint64, timestam return nil } -func (b *performanceTrackAggrBuilder) Build() Message { +func (b *PerformanceAggregator) Build() Message { if b.PerformanceTrackAggr == nil { return nil } diff --git a/backend/internal/heuristics/session.go b/backend/internal/heuristics/session.go index a49db948b..3c7951750 100644 --- a/backend/internal/heuristics/session.go +++ b/backend/internal/heuristics/session.go @@ -16,11 +16,7 @@ type sessHandler struct { func newSessHandler() *sessHandler { return &sessHandler{ - handlers: []Handler{ - new(clickrage), - new(performanceAggregator), - new(anr), - }, + handlers: []Handler{}, } } From 47007eb9d776db1b1fa0a9d635ead42d9ba7560a Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Tue, 10 May 2022 14:11:41 +0200 Subject: [PATCH 10/19] feat(backend/db): prepared db service for refactoring --- backend/cmd/db/main.go | 31 +- backend/cmd/heuristics/main.go | 1 + backend/internal/builder/builder.go | 8 + backend/internal/builder/builderMap.go | 11 + backend/internal/config/db/config.go | 2 + .../custom}/inputEventBuilder.go | 19 +- .../internal/handlers/custom/mainHandler.go | 288 ++++++++++++++++++ .../custom}/pageEventBuilder.go | 22 +- backend/internal/heuristics/heuristics.go | 36 --- backend/internal/heuristics/session.go | 43 --- 10 files changed, 368 insertions(+), 93 deletions(-) rename backend/internal/{heuristics => handlers/custom}/inputEventBuilder.go (80%) create mode 100644 backend/internal/handlers/custom/mainHandler.go rename backend/internal/{heuristics => handlers/custom}/pageEventBuilder.go (83%) delete mode 100644 backend/internal/heuristics/heuristics.go delete mode 100644 backend/internal/heuristics/session.go diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index f6cd481a7..564fcbab5 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -2,9 +2,12 @@ package main import ( "log" + "openreplay/backend/internal/builder" "openreplay/backend/internal/config/db" "openreplay/backend/internal/datasaver" - "openreplay/backend/internal/heuristics" + "openreplay/backend/internal/handlers" + "openreplay/backend/internal/handlers/custom" + "openreplay/backend/pkg/intervals" "time" "os" @@ -28,8 +31,17 @@ func main() { pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres), cfg.ProjectExpirationTimeoutMs) defer pg.Close() + // Declare message handlers we want to apply for each incoming message + msgHandlers := []handlers.MessageProcessor{ + custom.NewMainHandler(), + custom.NewInputEventBuilder(), + custom.NewPageEventBuilder(), + } + + // Create handler's aggregator + builderMap := builder.NewBuilderMap(msgHandlers...) + // Init modules - heurFinder := heuristics.NewHandler() saver := datasaver.New(pg) statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) @@ -42,6 +54,7 @@ func main() { if !postgres.IsPkeyViolation(err) { log.Printf("Message Insertion Error %v, SessionID: %v, Message: %v", err, sessionID, msg) } + // TODO: can we lose data here because of db error? return } @@ -60,10 +73,10 @@ func main() { } // Handle heuristics and save to temporary queue in memory - heurFinder.HandleMessage(sessionID, msg) + builderMap.HandleMessage(sessionID, msg, msg.Meta().Index) // Process saved heuristics messages as usual messages above in the code - heurFinder.IterateSessionReadyMessages(sessionID, func(msg messages.Message) { + builderMap.IterateSessionReadyMessages(sessionID, func(msg messages.Message) { // TODO: DRY code (carefully with the return statement logic) if err := saver.InsertMessage(sessionID, msg); err != nil { if !postgres.IsPkeyViolation(err) { @@ -82,8 +95,9 @@ func main() { consumer := queue.NewMessageConsumer( cfg.GroupDB, []string{ + cfg.TopicRawWeb, // TODO: is it necessary or not? cfg.TopicRawIOS, - cfg.TopicTrigger, + cfg.TopicTrigger, // to receive SessionEnd events }, handler, false, @@ -94,19 +108,22 @@ func main() { sigchan := make(chan os.Signal, 1) signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) - tick := time.Tick(cfg.CommitBatchTimeout) + commitTick := time.Tick(cfg.CommitBatchTimeout) + checkTick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) for { select { case sig := <-sigchan: log.Printf("Caught signal %v: terminating\n", sig) consumer.Close() os.Exit(0) - case <-tick: + case <-commitTick: pg.CommitBatches() // TODO?: separate stats & regular messages if err := consumer.Commit(); err != nil { log.Printf("Error on consumer commit: %v", err) } + case <-checkTick: + // checkTimeout default: err := consumer.ConsumeNext() if err != nil { diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index f5e3b675f..ddfd29095 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -54,6 +54,7 @@ func main() { []string{ cfg.TopicRawWeb, cfg.TopicRawIOS, + cfg.TopicTrigger, // to receive SessionEnd events }, func(sessionID uint64, msg messages.Message, meta *types.Meta) { statsLogger.Collect(sessionID, meta) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index c35457d62..a00ad194a 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -10,6 +10,7 @@ type builder struct { readyMsgs []Message timestamp uint64 processors []handlers.MessageProcessor + ended bool } func NewBuilder(handlers ...handlers.MessageProcessor) *builder { @@ -35,6 +36,13 @@ func (b *builder) handleMessage(message Message, messageID uint64) { return } + if _, isEnd := message.(*IOSSessionEnd); isEnd { + b.ended = true + } + if _, isEnd := message.(*SessionEnd); isEnd { + b.ended = true + } + for _, p := range b.processors { /* If nil is not returned explicitely by Handle, but as the typed nil ("var i *IssueEvent; return i;") diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index 5bc01e78d..6b2c22bec 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -42,3 +42,14 @@ func (m *builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID } } } + +func (m *builderMap) IterateSessionReadyMessages(sessionID uint64, iter func(msg Message)) { + session, ok := m.sessions[sessionID] + if !ok { + return + } + session.iterateReadyMessage(iter) + if session.ended { + delete(m.sessions, sessionID) + } +} diff --git a/backend/internal/config/db/config.go b/backend/internal/config/db/config.go index fb35a199c..e074399dc 100644 --- a/backend/internal/config/db/config.go +++ b/backend/internal/config/db/config.go @@ -10,6 +10,7 @@ type Config struct { ProjectExpirationTimeoutMs int64 LoggerTimeout int GroupDB string + TopicRawWeb string TopicRawIOS string TopicTrigger string CommitBatchTimeout time.Duration @@ -21,6 +22,7 @@ func New() *Config { ProjectExpirationTimeoutMs: 1000 * 60 * 20, LoggerTimeout: env.Int("LOG_QUEUE_STATS_INTERVAL_SEC"), GroupDB: env.String("GROUP_DB"), + TopicRawWeb: env.String("TOPIC_RAW_WEB"), TopicRawIOS: env.String("TOPIC_RAW_IOS"), TopicTrigger: env.String("TOPIC_TRIGGER"), CommitBatchTimeout: 15 * time.Second, diff --git a/backend/internal/heuristics/inputEventBuilder.go b/backend/internal/handlers/custom/inputEventBuilder.go similarity index 80% rename from backend/internal/heuristics/inputEventBuilder.go rename to backend/internal/handlers/custom/inputEventBuilder.go index 624e15e47..770e714af 100644 --- a/backend/internal/heuristics/inputEventBuilder.go +++ b/backend/internal/handlers/custom/inputEventBuilder.go @@ -1,4 +1,4 @@ -package heuristics +package custom import ( . "openreplay/backend/pkg/messages" @@ -12,6 +12,17 @@ type inputEventBuilder struct { inputID uint64 } +func (b *inputEventBuilder) Handle(message Message, messageID uint64, timestamp uint64) Message { + //TODO implement me + panic("implement me") +} + +func (b *inputEventBuilder) Build() Message { + // b.build() + //TODO implement me + panic("implement me") +} + func NewInputEventBuilder() *inputEventBuilder { ieBuilder := &inputEventBuilder{} ieBuilder.ClearLabels() @@ -25,7 +36,7 @@ func (b *inputEventBuilder) ClearLabels() { func (b *inputEventBuilder) HandleSetInputTarget(msg *SetInputTarget) *InputEvent { var inputEvent *InputEvent if b.inputID != msg.ID { - inputEvent = b.Build() + inputEvent = b.build() b.inputID = msg.ID } b.inputLabels[msg.ID] = msg.Label @@ -35,7 +46,7 @@ func (b *inputEventBuilder) HandleSetInputTarget(msg *SetInputTarget) *InputEven func (b *inputEventBuilder) HandleSetInputValue(msg *SetInputValue, messageID uint64, timestamp uint64) *InputEvent { var inputEvent *InputEvent if b.inputID != msg.ID { - inputEvent = b.Build() + inputEvent = b.build() b.inputID = msg.ID } if b.inputEvent == nil { @@ -63,7 +74,7 @@ func (b *inputEventBuilder) GetTimestamp() uint64 { return b.inputEvent.Timestamp } -func (b *inputEventBuilder) Build() *InputEvent { +func (b *inputEventBuilder) build() *InputEvent { if b.inputEvent == nil { return nil } diff --git a/backend/internal/handlers/custom/mainHandler.go b/backend/internal/handlers/custom/mainHandler.go new file mode 100644 index 000000000..7e653c250 --- /dev/null +++ b/backend/internal/handlers/custom/mainHandler.go @@ -0,0 +1,288 @@ +package custom + +import ( + "net/url" + "openreplay/backend/pkg/intervals" + "strings" + "time" + + . "openreplay/backend/pkg/messages" +) + +func getURLExtention(URL string) string { + u, err := url.Parse(URL) + if err != nil { + return "" + } + i := strings.LastIndex(u.Path, ".") + return u.Path[i+1:] +} + +func getResourceType(initiator string, URL string) string { + switch initiator { + case "xmlhttprequest", "fetch": + return "fetch" + case "img": + return "img" + default: + switch getURLExtention(URL) { + case "css": + return "stylesheet" + case "js": + return "script" + case "png", "gif", "jpg", "jpeg", "svg": + return "img" + case "mp4", "mkv", "ogg", "webm", "avi", "mp3": + return "media" + default: + return "other" + } + } +} + +type builder struct { + readyMsgs []Message + timestamp uint64 + lastProcessedTimestamp int64 + peBuilder *pageEventBuilder + ieBuilder *inputEventBuilder + integrationsWaiting bool + sid uint64 +} + +func (b *builder) Build() Message { + //TODO implement me + panic("implement me") +} + +func NewMainHandler() *builder { + return &builder{ + peBuilder: &pageEventBuilder{}, + ieBuilder: NewInputEventBuilder(), + integrationsWaiting: true, + } +} + +func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value + b.readyMsgs = append(b.readyMsgs, msg) +} + +func (b *builder) iterateReadyMessage(iter func(msg Message)) { + for _, readyMsg := range b.readyMsgs { + iter(readyMsg) + } + b.readyMsgs = nil +} + +func (b *builder) buildPageEvent() { + if msg := b.peBuilder.Build(); msg != nil { + b.appendReadyMessage(msg) + } +} + +func (b *builder) buildInputEvent() { + if msg := b.ieBuilder.Build(); msg != nil { + b.appendReadyMessage(msg) + } +} + +func (b *builder) Handle(message Message, messageID uint64, timestamp uint64) Message { + b.timestamp = timestamp + b.lastProcessedTimestamp = time.Now().UnixMilli() + + // Might happen before the first timestamp. + switch msg := message.(type) { + case *SessionStart, + *Metadata, + *UserID, + *UserAnonymousID: + b.appendReadyMessage(msg) + case *RawErrorEvent: + b.appendReadyMessage(&ErrorEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Source: msg.Source, + Name: msg.Name, + Message: msg.Message, + Payload: msg.Payload, + }) + } + if b.timestamp == 0 { + return nil + } + switch msg := message.(type) { + case *SetPageLocation: + if msg.NavigationStart == 0 { + b.appendReadyMessage(&PageEvent{ + URL: msg.URL, + Referrer: msg.Referrer, + Loaded: false, + MessageID: messageID, + Timestamp: b.timestamp, + }) + } else { + b.buildPageEvent() + b.buildInputEvent() + b.ieBuilder.ClearLabels() + b.peBuilder.HandleSetPageLocation(msg, messageID, b.timestamp) + // TODO: what to do with this code? + //b.miFinder.HandleSetPageLocation(msg) + //b.ciFinder.HandleSetPageLocation(msg) + } + case *PageLoadTiming: + if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil { + b.appendReadyMessage(rm) + } + case *PageRenderTiming: + if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil { + b.appendReadyMessage(rm) + } + case *PerformanceTrack: + // TODO: what to do with this code? + //if rm := b.ptaBuilder.HandlePerformanceTrack(msg, b.timestamp); rm != nil { + // b.appendReadyMessage(rm) + //} + //if rm := b.ciFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { + // b.appendReadyMessage(rm) + //} + //if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { + // b.appendReadyMessage(rm) + //} + case *SetInputTarget: + if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil { + b.appendReadyMessage(rm) + } + case *SetInputValue: + if rm := b.ieBuilder.HandleSetInputValue(msg, messageID, b.timestamp); rm != nil { + b.appendReadyMessage(rm) + } + case *MouseClick: + b.buildInputEvent() + // TODO: what to do with this code? + //if rm := b.crDetector.HandleMouseClick(msg, messageID, b.timestamp); rm != nil { + // b.appendReadyMessage(rm) + //} + if msg.Label != "" { + b.appendReadyMessage(&ClickEvent{ + MessageID: messageID, + Label: msg.Label, + HesitationTime: msg.HesitationTime, + Timestamp: b.timestamp, + Selector: msg.Selector, + }) + } + case *JSException: + b.appendReadyMessage(&ErrorEvent{ + MessageID: messageID, + Timestamp: b.timestamp, + Source: "js_exception", + Name: msg.Name, + Message: msg.Message, + Payload: msg.Payload, + }) + case *ResourceTiming: + tp := getResourceType(msg.Initiator, msg.URL) + success := msg.Duration != 0 + b.appendReadyMessage(&ResourceEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Duration: msg.Duration, + TTFB: msg.TTFB, + HeaderSize: msg.HeaderSize, + EncodedBodySize: msg.EncodedBodySize, + DecodedBodySize: msg.DecodedBodySize, + URL: msg.URL, + Type: tp, + Success: success, + }) + if !success { + issueType := "missing_resource" + if tp == "fetch" { + issueType = "bad_request" + } + b.appendReadyMessage(&IssueEvent{ + Type: issueType, + MessageID: messageID, + Timestamp: msg.Timestamp, + ContextString: msg.URL, + }) + } + case *RawCustomEvent: + b.appendReadyMessage(&CustomEvent{ + MessageID: messageID, + Timestamp: b.timestamp, + Name: msg.Name, + Payload: msg.Payload, + }) + case *CustomIssue: + b.appendReadyMessage(&IssueEvent{ + Type: "custom", + Timestamp: b.timestamp, + MessageID: messageID, + ContextString: msg.Name, + Payload: msg.Payload, + }) + case *Fetch: + b.appendReadyMessage(&FetchEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Method: msg.Method, + URL: msg.URL, + Request: msg.Request, + Response: msg.Response, + Status: msg.Status, + Duration: msg.Duration, + }) + if msg.Status >= 400 { + b.appendReadyMessage(&IssueEvent{ + Type: "bad_request", + MessageID: messageID, + Timestamp: msg.Timestamp, + ContextString: msg.URL, + }) + } + case *GraphQL: + b.appendReadyMessage(&GraphQLEvent{ + MessageID: messageID, + Timestamp: b.timestamp, + OperationKind: msg.OperationKind, + OperationName: msg.OperationName, + Variables: msg.Variables, + Response: msg.Response, + }) + case *StateAction: + b.appendReadyMessage(&StateActionEvent{ + MessageID: messageID, + Timestamp: b.timestamp, + Type: msg.Type, + }) + // TODO: what to do with this code? + //case *CreateElementNode, *CreateTextNode: + // b.ddDetector.HandleNodeCreation() + //case *RemoveNode: + // b.ddDetector.HandleNodeRemoval(b.timestamp) + //case *CreateDocument: + // if rm := b.ddDetector.Build(); rm != nil { + // b.appendReadyMessage(rm) + // } + } + // TODO: what to do with this code? + //if rm := b.dcDetector.HandleMessage(message, messageID, b.timestamp); rm != nil { + // b.appendReadyMessage(rm) + //} + return nil +} + +func (b *builder) checkTimeouts(ts int64) bool { + if b.timestamp == 0 { + return false // There was no timestamp events yet + } + + if b.peBuilder.HasInstance() && int64(b.peBuilder.GetTimestamp())+intervals.EVENTS_PAGE_EVENT_TIMEOUT < ts { + b.buildPageEvent() + } + if b.ieBuilder.HasInstance() && int64(b.ieBuilder.GetTimestamp())+intervals.EVENTS_INPUT_EVENT_TIMEOUT < ts { + b.buildInputEvent() + } + return false +} diff --git a/backend/internal/heuristics/pageEventBuilder.go b/backend/internal/handlers/custom/pageEventBuilder.go similarity index 83% rename from backend/internal/heuristics/pageEventBuilder.go rename to backend/internal/handlers/custom/pageEventBuilder.go index 96a1b287e..765fd31a2 100644 --- a/backend/internal/heuristics/pageEventBuilder.go +++ b/backend/internal/handlers/custom/pageEventBuilder.go @@ -1,4 +1,4 @@ -package heuristics +package custom import ( . "openreplay/backend/pkg/messages" @@ -9,9 +9,25 @@ type pageEventBuilder struct { firstTimingHandled bool } +func (b *pageEventBuilder) Handle(message Message, messageID uint64, timestamp uint64) Message { + //TODO implement me + panic("implement me") +} + +func (b *pageEventBuilder) Build() Message { + // b.build() + //TODO implement me + panic("implement me") +} + +func NewPageEventBuilder() *pageEventBuilder { + ieBuilder := &pageEventBuilder{} + return ieBuilder +} + func (b *pageEventBuilder) buildIfTimingsComplete() *PageEvent { if b.firstTimingHandled { - return b.Build() + return b.build() } b.firstTimingHandled = true return nil @@ -83,7 +99,7 @@ func (b *pageEventBuilder) GetTimestamp() uint64 { return b.pageEvent.Timestamp } -func (b *pageEventBuilder) Build() *PageEvent { +func (b *pageEventBuilder) build() *PageEvent { pageEvent := b.pageEvent b.pageEvent = nil b.firstTimingHandled = false diff --git a/backend/internal/heuristics/heuristics.go b/backend/internal/heuristics/heuristics.go deleted file mode 100644 index c55ad33b3..000000000 --- a/backend/internal/heuristics/heuristics.go +++ /dev/null @@ -1,36 +0,0 @@ -package heuristics - -import ( - . "openreplay/backend/pkg/messages" -) - -type mainHandler map[uint64]*sessHandler - -func NewHandler() mainHandler { - return make(mainHandler) -} - -func (m mainHandler) getSessHandler(sessionID uint64) *sessHandler { - s := m[sessionID] - if s == nil { - s = newSessHandler() - m[sessionID] = s - } - return s -} - -func (m mainHandler) HandleMessage(sessionID uint64, msg Message) { - s := m.getSessHandler(sessionID) - s.HandleMessage(msg) -} - -func (m mainHandler) IterateSessionReadyMessages(sessionID uint64, iter func(msg Message)) { - s, ok := m[sessionID] - if !ok { - return - } - s.IterateReadyMessages(iter) - if s.IsEnded() { - delete(m, sessionID) - } -} diff --git a/backend/internal/heuristics/session.go b/backend/internal/heuristics/session.go deleted file mode 100644 index 3c7951750..000000000 --- a/backend/internal/heuristics/session.go +++ /dev/null @@ -1,43 +0,0 @@ -package heuristics - -import ( - . "openreplay/backend/pkg/messages" -) - -type Handler interface { - HandleMessage(Message) - IterateReadyMessages(func(Message)) -} - -type sessHandler struct { - handlers []Handler - ended bool -} - -func newSessHandler() *sessHandler { - return &sessHandler{ - handlers: []Handler{}, - } -} - -func (s *sessHandler) HandleMessage(msg Message) { - for _, h := range s.handlers { - h.HandleMessage(msg) - } - if _, isEnd := msg.(*IOSSessionEnd); isEnd { - s.ended = true - } - if _, isEnd := msg.(*SessionEnd); isEnd { - s.ended = true - } -} - -func (s *sessHandler) IterateReadyMessages(cb func(msg Message)) { - for _, h := range s.handlers { - h.IterateReadyMessages(cb) - } -} - -func (s *sessHandler) IsEnded() bool { - return s.ended -} From 26e23d594f31079e8c991768407ee67c88240aca Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Tue, 10 May 2022 15:40:55 +0200 Subject: [PATCH 11/19] feat(backend/handlers): refactored web and ios message handlers --- .../internal/handlers/ios/appNotResponding.go | 40 +++++++++++-------- backend/internal/handlers/ios/clickRage.go | 34 ++++++++-------- .../handlers/ios/performanceAggregator.go | 32 +++++++++------ backend/internal/handlers/web/clickRage.go | 27 ++++++++----- backend/internal/handlers/web/cpuIssue.go | 14 ++++++- backend/internal/handlers/web/deadClick.go | 38 ++++++++++++------ backend/internal/handlers/web/domDrop.go | 11 +++-- backend/internal/handlers/web/memoryIssue.go | 27 +++++++++---- .../handlers/web/performanceAggregator.go | 6 +++ backend/pkg/messages/messages.go | 2 +- 10 files changed, 148 insertions(+), 83 deletions(-) diff --git a/backend/internal/handlers/ios/appNotResponding.go b/backend/internal/handlers/ios/appNotResponding.go index 1241648db..097361c00 100644 --- a/backend/internal/handlers/ios/appNotResponding.go +++ b/backend/internal/handlers/ios/appNotResponding.go @@ -3,9 +3,17 @@ package ios import ( "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" + "time" ) -// app is not responding detector +/* + Handler name: AppNotResponding + Input events: IOSClickEvent, + IOSInputEvent, + IOSPerformanceEvent, + IOSSessionEnd + Output event: IOSIssueEvent +*/ const MIN_TIME_AFTER_LAST_HEARTBEAT = 60 * 1000 @@ -17,46 +25,44 @@ type AppNotResponding struct { } func (h *AppNotResponding) Handle(message Message, messageID uint64, timestamp uint64) Message { + var event Message = nil switch m := message.(type) { case *IOSClickEvent: - h.buildIf(m.Timestamp) + event = h.build(m.Timestamp) h.lastLabel = m.Label h.lastHeartbeatTimestamp = m.Timestamp h.lastHeartbeatIndex = m.Index case *IOSInputEvent: - h.buildIf(m.Timestamp) + event = h.build(m.Timestamp) h.lastLabel = m.Label h.lastHeartbeatTimestamp = m.Timestamp h.lastHeartbeatIndex = m.Index case *IOSPerformanceEvent: - h.buildIf(m.Timestamp) + event = h.build(m.Timestamp) h.lastHeartbeatTimestamp = m.Timestamp h.lastHeartbeatIndex = m.Index case *IOSSessionEnd: - h.buildIf(m.Timestamp) + event = h.build(m.Timestamp) } - return nil + return event } func (h *AppNotResponding) Build() Message { - //TODO implement me - panic("implement me") + return h.build(uint64(time.Now().Unix())) } -func (h *AppNotResponding) buildIf(timestamp uint64) { +func (h *AppNotResponding) build(timestamp uint64) Message { if h.lastHeartbeatTimestamp != 0 && h.lastHeartbeatTimestamp+MIN_TIME_AFTER_LAST_HEARTBEAT <= timestamp { - m := &IOSIssueEvent{ + event := &IOSIssueEvent{ Type: "anr", ContextString: h.lastLabel, + Timestamp: h.lastHeartbeatTimestamp, } - m.Timestamp = h.lastHeartbeatTimestamp - m.Index = h.lastHeartbeatIndex // Associated Index/ MessageID ? - h.Append(m) + event.Index = h.lastHeartbeatIndex // Associated Index/ MessageID ? + // Reset h.lastHeartbeatTimestamp = 0 h.lastHeartbeatIndex = 0 + return event } -} - -func (h *AppNotResponding) HandleMessage(msg Message) { - // TODO: delete it + return nil } diff --git a/backend/internal/handlers/ios/clickRage.go b/backend/internal/handlers/ios/clickRage.go index 2707c04d5..6562e05c1 100644 --- a/backend/internal/handlers/ios/clickRage.go +++ b/backend/internal/handlers/ios/clickRage.go @@ -6,9 +6,14 @@ import ( . "openreplay/backend/pkg/messages" ) -const CLICK_TIME_DIFF = 200 +/* + Handler name: ClickRage + Input events: IOSClickEvent, + IOSSessionEnd + Output event: IOSIssueEvent +*/ -//const MIN_CLICKS_IN_A_ROW = 3 +const CLICK_TIME_DIFF = 200 type ClickRageDetector struct { handlers.ReadyMessageStore @@ -20,6 +25,7 @@ type ClickRageDetector struct { } func (h *ClickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { + var event Message = nil switch m := message.(type) { case *IOSClickEvent: if h.lastTimestamp+CLICK_TIME_DIFF < m.Timestamp && h.lastLabel == m.Label { @@ -27,7 +33,7 @@ func (h *ClickRageDetector) Handle(message Message, messageID uint64, timestamp h.countsInARow += 1 return nil } - h.build() + event = h.Build() if m.Label != "" { h.lastTimestamp = m.Timestamp h.lastLabel = m.Label @@ -36,33 +42,25 @@ func (h *ClickRageDetector) Handle(message Message, messageID uint64, timestamp h.countsInARow = 1 } case *IOSSessionEnd: - h.build() + event = h.Build() } - return nil + return event } func (h *ClickRageDetector) Build() Message { - //TODO implement me - panic("implement me") -} - -func (h *ClickRageDetector) build() { if h.countsInARow >= web.MIN_CLICKS_IN_A_ROW { - m := &IOSIssueEvent{ + event := &IOSIssueEvent{ Type: "click_rage", ContextString: h.lastLabel, } - m.Timestamp = h.firstInARawTimestamp - m.Index = h.firstInARawSeqIndex // Associated Index/ MessageID ? - h.Append(m) + event.Timestamp = h.firstInARawTimestamp + event.Index = h.firstInARawSeqIndex // Associated Index/ MessageID ? + return event } h.lastTimestamp = 0 h.lastLabel = "" h.firstInARawTimestamp = 0 h.firstInARawSeqIndex = 0 h.countsInARow = 0 -} - -func (h *ClickRageDetector) HandleMessage(msg Message) { - // TODO: delete it + return nil } diff --git a/backend/internal/handlers/ios/performanceAggregator.go b/backend/internal/handlers/ios/performanceAggregator.go index 1525127b8..b4bc812c7 100644 --- a/backend/internal/handlers/ios/performanceAggregator.go +++ b/backend/internal/handlers/ios/performanceAggregator.go @@ -3,8 +3,16 @@ package ios import ( "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" + "time" ) +/* + Handler name: PerformanceAggregator + Input events: IOSPerformanceEvent, + IOSSessionEnd + Output event: IssueEvent +*/ + const AGGR_TIME = 15 * 60 * 1000 type valueAggregator struct { @@ -32,13 +40,14 @@ func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timest if h.pa == nil { h.pa = &IOSPerformanceAggregated{} // TODO: struct type in messages } - switch m := message.(type) { // TODO: All Timestampe messages + var event Message = nil + switch m := message.(type) { // TODO: All Timestamp messages case *IOSPerformanceEvent: if h.pa.TimestampStart == 0 { h.pa.TimestampStart = m.Timestamp } if h.pa.TimestampStart+AGGR_TIME <= m.Timestamp { - h.build(m.Timestamp) + event = h.build(m.Timestamp) } switch m.Name { case "fps": @@ -79,35 +88,32 @@ func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timest } } case *IOSSessionEnd: - h.build(m.Timestamp) + event = h.build(m.Timestamp) } - return nil + return event } func (h *PerformanceAggregator) Build() Message { - //TODO implement me - panic("implement me") + return h.build(uint64(time.Now().Unix())) } -func (h *PerformanceAggregator) build(timestamp uint64) { +func (h *PerformanceAggregator) build(timestamp uint64) Message { if h.pa == nil { - return + return nil } + h.pa.TimestampEnd = timestamp h.pa.AvgFPS = h.fps.aggregate() h.pa.AvgCPU = h.cpu.aggregate() h.pa.AvgMemory = h.memory.aggregate() h.pa.AvgBattery = h.battery.aggregate() - h.Append(h.pa) + event := h.pa h.pa = &IOSPerformanceAggregated{} for _, agg := range []valueAggregator{h.fps, h.cpu, h.memory, h.battery} { agg.sum = 0 agg.count = 0 } -} - -func (h *PerformanceAggregator) HandleMessage(msg Message) { - // TODO: delete it + return event } diff --git a/backend/internal/handlers/web/clickRage.go b/backend/internal/handlers/web/clickRage.go index db22a9667..e22eb6454 100644 --- a/backend/internal/handlers/web/clickRage.go +++ b/backend/internal/handlers/web/clickRage.go @@ -2,11 +2,16 @@ package web import ( "encoding/json" + "log" . "openreplay/backend/pkg/messages" ) -// TODO: Description of click rage detector +/* + Handler name: ClickRage + Input event: MouseClick + Output event: IssueEvent +*/ const MAX_TIME_DIFF = 300 const MIN_CLICKS_IN_A_ROW = 3 @@ -28,26 +33,28 @@ func (crd *ClickRageDetector) reset() { } func (crd *ClickRageDetector) Build() Message { + defer crd.reset() if crd.countsInARow >= MIN_CLICKS_IN_A_ROW { - payload, _ := json.Marshal(struct{ Count int }{crd.countsInARow}) - i := &IssueEvent{ + payload, err := json.Marshal(struct{ Count int }{crd.countsInARow}) + if err != nil { + log.Printf("can't marshal ClickRage payload to json: %s", err) + } + event := &IssueEvent{ Type: "click_rage", ContextString: crd.lastLabel, - Payload: string(payload), // TODO: json message field type + Payload: string(payload), Timestamp: crd.firstInARawTimestamp, MessageID: crd.firstInARawMessageId, } - crd.reset() - return i + return event } - crd.reset() return nil } func (crd *ClickRageDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { switch msg := message.(type) { case *MouseClick: - // TODO: check if we it is ok to capture clickrages without the connected CleckEvent in db. + // TODO: check if we it is ok to capture clickRage event without the connected ClickEvent in db. if msg.Label == "" { return crd.Build() } @@ -56,13 +63,13 @@ func (crd *ClickRageDetector) Handle(message Message, messageID uint64, timestam crd.countsInARow += 1 return nil } - i := crd.Build() + event := crd.Build() crd.lastTimestamp = timestamp crd.lastLabel = msg.Label crd.firstInARawTimestamp = timestamp crd.firstInARawMessageId = messageID crd.countsInARow = 1 - return i + return event } return nil } diff --git a/backend/internal/handlers/web/cpuIssue.go b/backend/internal/handlers/web/cpuIssue.go index 5cc12be68..56f483e8b 100644 --- a/backend/internal/handlers/web/cpuIssue.go +++ b/backend/internal/handlers/web/cpuIssue.go @@ -2,12 +2,18 @@ package web import ( "encoding/json" + "log" . "openreplay/backend/pkg/messages" "openreplay/backend/pkg/messages/performance" ) -// TODO: Description of cpu issue detector +/* + Handler name: CpuIssue + Input events: PerformanceTrack, + SetPageLocation + Output event: IssueEvent +*/ const CPU_THRESHOLD = 70 // % out of 100 const CPU_MIN_DURATION_TRIGGER = 6 * 1000 @@ -36,10 +42,14 @@ func (f *CpuIssueDetector) Build() Message { return nil } - payload, _ := json.Marshal(struct { + payload, err := json.Marshal(struct { Duration uint64 Rate uint64 }{duration, maxRate}) + if err != nil { + log.Printf("can't marshal CpuIssue payload to json: %s", err) + } + return &IssueEvent{ Type: "cpu", Timestamp: timestamp, diff --git a/backend/internal/handlers/web/deadClick.go b/backend/internal/handlers/web/deadClick.go index a04da9be9..6377b074e 100644 --- a/backend/internal/handlers/web/deadClick.go +++ b/backend/internal/handlers/web/deadClick.go @@ -4,7 +4,22 @@ import ( . "openreplay/backend/pkg/messages" ) -// TODO: Description of dead click detector +/* + Handler name: DeadClick + Input events: SetInputTarget, + CreateDocument, + MouseClick, + SetNodeAttribute, + RemoveNodeAttribute, + CreateElementNode, + CreateTextNode, + MoveNode, + RemoveNode, + SetCSSData, + CSSInsertRule, + CSSDeleteRule + Output event: IssueEvent +*/ const CLICK_RELATION_TIME = 1400 @@ -23,23 +38,22 @@ func (d *DeadClickDetector) reset() { d.lastMessageID = 0 } -func (d *DeadClickDetector) handleReaction(timestamp uint64) Message { - if d.lastMouseClick == nil || d.lastClickTimestamp+CLICK_RELATION_TIME > timestamp { // riaction is instant - d.reset() +func (d *DeadClickDetector) build(timestamp uint64) Message { + defer d.reset() + if d.lastMouseClick == nil || d.lastClickTimestamp+CLICK_RELATION_TIME > timestamp { // reaction is instant return nil } - i := &IssueEvent{ + event := &IssueEvent{ Type: "dead_click", ContextString: d.lastMouseClick.Label, Timestamp: d.lastClickTimestamp, MessageID: d.lastMessageID, } - d.reset() - return i + return event } func (d *DeadClickDetector) Build() Message { - return d.handleReaction(d.lastTimestamp) + return d.build(d.lastTimestamp) } func (d *DeadClickDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { @@ -56,14 +70,14 @@ func (d *DeadClickDetector) Handle(message Message, messageID uint64, timestamp if msg.Label == "" { return nil } - i := d.handleReaction(timestamp) + event := d.build(timestamp) if d.inputIDSet[msg.ID] { // ignore if input - return i + return event } d.lastMouseClick = msg d.lastClickTimestamp = timestamp d.lastMessageID = messageID - return i + return event case *SetNodeAttribute, *RemoveNodeAttribute, *CreateElementNode, @@ -73,7 +87,7 @@ func (d *DeadClickDetector) Handle(message Message, messageID uint64, timestamp *SetCSSData, *CSSInsertRule, *CSSDeleteRule: - return d.handleReaction(timestamp) + return d.build(timestamp) } return nil } diff --git a/backend/internal/handlers/web/domDrop.go b/backend/internal/handlers/web/domDrop.go index c89fab2c4..4a3ec2065 100644 --- a/backend/internal/handlers/web/domDrop.go +++ b/backend/internal/handlers/web/domDrop.go @@ -4,7 +4,13 @@ import ( . "openreplay/backend/pkg/messages" ) -// TODO: Description of dom drop detector +/* + Handler name: DomDrop + Input events: CreateElementNode, + CreateTextNode, + RemoveNode + Output event: DOMDrop +*/ const DROP_WINDOW = 200 //ms const CRITICAL_COUNT = 1 // Our login page contains 20. But on crush it removes only roots (1-3 nodes). @@ -38,13 +44,12 @@ func (dd *domDropDetector) Handle(message Message, _ uint64, timestamp uint64) M } func (dd *domDropDetector) Build() Message { + defer dd.reset() if dd.removedCount >= CRITICAL_COUNT { domDrop := &DOMDrop{ Timestamp: dd.lastDropTimestamp, } - dd.reset() return domDrop } - dd.reset() return nil } diff --git a/backend/internal/handlers/web/memoryIssue.go b/backend/internal/handlers/web/memoryIssue.go index ac8ca8a14..487c396a9 100644 --- a/backend/internal/handlers/web/memoryIssue.go +++ b/backend/internal/handlers/web/memoryIssue.go @@ -2,12 +2,18 @@ package web import ( "encoding/json" + "log" "math" . "openreplay/backend/pkg/messages" ) -// TODO: Description of memory issue detector +/* + Handler name: MemoryIssue + Input events: PerformanceTrack, + SetPageLocation + Output event: IssueEvent +*/ const MIN_COUNT = 3 const MEM_RATE_THRESHOLD = 300 // % to average @@ -21,22 +27,29 @@ type MemoryIssueDetector struct { contextString string } +func (f *MemoryIssueDetector) reset() { + f.startTimestamp = 0 + f.startMessageID = 0 + f.rate = 0 +} + func (f *MemoryIssueDetector) Build() Message { if f.startTimestamp == 0 { return nil } - payload, _ := json.Marshal(struct{ Rate int }{f.rate - 100}) - i := &IssueEvent{ + payload, err := json.Marshal(struct{ Rate int }{f.rate - 100}) + if err != nil { + log.Printf("can't marshal MemoryIssue payload to json: %s", err) + } + event := &IssueEvent{ Type: "memory", Timestamp: f.startTimestamp, MessageID: f.startMessageID, ContextString: f.contextString, Payload: string(payload), } - f.startTimestamp = 0 - f.startMessageID = 0 - f.rate = 0 - return i + f.reset() + return event } func (f *MemoryIssueDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { diff --git a/backend/internal/handlers/web/performanceAggregator.go b/backend/internal/handlers/web/performanceAggregator.go index a7bf79f9f..928cedeb9 100644 --- a/backend/internal/handlers/web/performanceAggregator.go +++ b/backend/internal/handlers/web/performanceAggregator.go @@ -7,6 +7,12 @@ import ( "openreplay/backend/pkg/messages/performance" ) +/* + Handler name: PerformanceAggregator + Input event: PerformanceTrack + Output event: PerformanceTrackAggr +*/ + const AGGREGATION_WINDOW = 2 * 60 * 1000 type PerformanceAggregator struct { diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index e9aec5788..418c47342 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -1168,7 +1168,7 @@ type IssueEvent struct { Type string ContextString string Context string - Payload string + Payload string // TODO: check, maybe it's better to use empty interface here } func (msg *IssueEvent) Encode() []byte { From c77966a78997ea78a9c114e81a11e07d2576d10c Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Wed, 11 May 2022 16:45:31 +0200 Subject: [PATCH 12/19] feat(backend/handlers): removed unix timestamp from header builders --- backend/cmd/db/main.go | 17 ++++--- backend/cmd/heuristics/main.go | 2 +- backend/internal/builder/builder.go | 44 +++++-------------- backend/internal/builder/builderMap.go | 12 +++-- .../internal/handlers/custom/mainHandler.go | 31 ------------- .../internal/handlers/ios/appNotResponding.go | 5 ++- .../handlers/ios/performanceAggregator.go | 15 ++++--- 7 files changed, 39 insertions(+), 87 deletions(-) diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 564fcbab5..8236586c2 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -7,7 +7,6 @@ import ( "openreplay/backend/internal/datasaver" "openreplay/backend/internal/handlers" "openreplay/backend/internal/handlers/custom" - "openreplay/backend/pkg/intervals" "time" "os" @@ -33,9 +32,9 @@ func main() { // Declare message handlers we want to apply for each incoming message msgHandlers := []handlers.MessageProcessor{ - custom.NewMainHandler(), - custom.NewInputEventBuilder(), - custom.NewPageEventBuilder(), + custom.NewMainHandler(), // TODO: separate to several handler + //custom.NewInputEventBuilder(), + //custom.NewPageEventBuilder(), } // Create handler's aggregator @@ -54,7 +53,6 @@ func main() { if !postgres.IsPkeyViolation(err) { log.Printf("Message Insertion Error %v, SessionID: %v, Message: %v", err, sessionID, msg) } - // TODO: can we lose data here because of db error? return } @@ -95,7 +93,7 @@ func main() { consumer := queue.NewMessageConsumer( cfg.GroupDB, []string{ - cfg.TopicRawWeb, // TODO: is it necessary or not? + cfg.TopicRawWeb, cfg.TopicRawIOS, cfg.TopicTrigger, // to receive SessionEnd events }, @@ -109,7 +107,6 @@ func main() { signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) commitTick := time.Tick(cfg.CommitBatchTimeout) - checkTick := time.Tick(intervals.EVENTS_COMMIT_INTERVAL * time.Millisecond) for { select { case sig := <-sigchan: @@ -118,12 +115,14 @@ func main() { os.Exit(0) case <-commitTick: pg.CommitBatches() + // TODO: ee commit stats !!! + //if err := commitStats(); err != nil { + // log.Printf("Error on stats commit: %v", err) + //} // TODO?: separate stats & regular messages if err := consumer.Commit(); err != nil { log.Printf("Error on consumer commit: %v", err) } - case <-checkTick: - // checkTimeout default: err := consumer.ConsumeNext() if err != nil { diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index ddfd29095..9c77cb4ba 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -78,7 +78,7 @@ func main() { consumer.Close() os.Exit(0) case <-tick: - builderMap.IterateReadyMessages(time.Now().UnixMilli(), func(sessionID uint64, readyMsg messages.Message) { + builderMap.IterateReadyMessages(func(sessionID uint64, readyMsg messages.Message) { producer.Produce(cfg.TopicTrigger, sessionID, messages.Encode(readyMsg)) }) producer.Flush(cfg.ProducerTimeout) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index a00ad194a..4916764cd 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -2,7 +2,6 @@ package builder import ( "openreplay/backend/internal/handlers" - "openreplay/backend/pkg/intervals" . "openreplay/backend/pkg/messages" ) @@ -26,6 +25,15 @@ func (b *builder) iterateReadyMessage(iter func(msg Message)) { b.readyMsgs = nil } +func (b *builder) checkSessionEnd(message Message) { + if _, isEnd := message.(*IOSSessionEnd); isEnd { + b.ended = true + } + if _, isEnd := message.(*SessionEnd); isEnd { + b.ended = true + } +} + func (b *builder) handleMessage(message Message, messageID uint64) { timestamp := GetTimestamp(message) if b.timestamp < timestamp { @@ -36,42 +44,10 @@ func (b *builder) handleMessage(message Message, messageID uint64) { return } - if _, isEnd := message.(*IOSSessionEnd); isEnd { - b.ended = true - } - if _, isEnd := message.(*SessionEnd); isEnd { - b.ended = true - } - + b.checkSessionEnd(message) for _, p := range b.processors { - /* If nil is not returned explicitely by Handle, but as the typed nil - ("var i *IssueEvent; return i;") - The `rm != nil` will be true. - TODO: enforce nil to be nil(?) or add `isNil() bool` to the Message types - because this part is expected to be etendable by user with custom messageProcessor's. - Use of reflrction will be probably bad on millions of messages? - */ if rm := p.Handle(message, messageID, b.timestamp); rm != nil { b.readyMsgs = append(b.readyMsgs, rm) } } } - -func (b *builder) checkTimeouts(ts int64) bool { - if b.timestamp == 0 { - return false // SessionStart happened only - } - - lastTsGap := ts - int64(b.timestamp) - // Maybe listen for `trigger` and react on SessionEnd instead (less reliable) - if lastTsGap > intervals.EVENTS_SESSION_END_TIMEOUT { - for _, p := range b.processors { - // TODO: same as above - if rm := p.Build(); rm != nil { - b.readyMsgs = append(b.readyMsgs, rm) - } - } - return true - } - return false -} diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index 6b2c22bec..b393bcd28 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -31,13 +31,19 @@ func (m *builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint b.handleMessage(msg, messageID) } -func (m *builderMap) IterateReadyMessages(operatingTs int64, iter func(sessionID uint64, msg Message)) { +func (m *builderMap) IterateReadyMessages(iter func(sessionID uint64, msg Message)) { for sessionID, b := range m.sessions { - sessionEnded := b.checkTimeouts(operatingTs) + if b.ended { + for _, p := range b.processors { + if rm := p.Build(); rm != nil { + b.readyMsgs = append(b.readyMsgs, rm) + } + } + } b.iterateReadyMessage(func(msg Message) { iter(sessionID, msg) }) - if sessionEnded { + if b.ended { delete(m.sessions, sessionID) } } diff --git a/backend/internal/handlers/custom/mainHandler.go b/backend/internal/handlers/custom/mainHandler.go index 7e653c250..52a6278c0 100644 --- a/backend/internal/handlers/custom/mainHandler.go +++ b/backend/internal/handlers/custom/mainHandler.go @@ -125,9 +125,6 @@ func (b *builder) Handle(message Message, messageID uint64, timestamp uint64) Me b.buildInputEvent() b.ieBuilder.ClearLabels() b.peBuilder.HandleSetPageLocation(msg, messageID, b.timestamp) - // TODO: what to do with this code? - //b.miFinder.HandleSetPageLocation(msg) - //b.ciFinder.HandleSetPageLocation(msg) } case *PageLoadTiming: if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil { @@ -137,17 +134,6 @@ func (b *builder) Handle(message Message, messageID uint64, timestamp uint64) Me if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil { b.appendReadyMessage(rm) } - case *PerformanceTrack: - // TODO: what to do with this code? - //if rm := b.ptaBuilder.HandlePerformanceTrack(msg, b.timestamp); rm != nil { - // b.appendReadyMessage(rm) - //} - //if rm := b.ciFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { - // b.appendReadyMessage(rm) - //} - //if rm := b.miFinder.HandlePerformanceTrack(msg, messageID, b.timestamp); rm != nil { - // b.appendReadyMessage(rm) - //} case *SetInputTarget: if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil { b.appendReadyMessage(rm) @@ -158,10 +144,6 @@ func (b *builder) Handle(message Message, messageID uint64, timestamp uint64) Me } case *MouseClick: b.buildInputEvent() - // TODO: what to do with this code? - //if rm := b.crDetector.HandleMouseClick(msg, messageID, b.timestamp); rm != nil { - // b.appendReadyMessage(rm) - //} if msg.Label != "" { b.appendReadyMessage(&ClickEvent{ MessageID: messageID, @@ -256,20 +238,7 @@ func (b *builder) Handle(message Message, messageID uint64, timestamp uint64) Me Timestamp: b.timestamp, Type: msg.Type, }) - // TODO: what to do with this code? - //case *CreateElementNode, *CreateTextNode: - // b.ddDetector.HandleNodeCreation() - //case *RemoveNode: - // b.ddDetector.HandleNodeRemoval(b.timestamp) - //case *CreateDocument: - // if rm := b.ddDetector.Build(); rm != nil { - // b.appendReadyMessage(rm) - // } } - // TODO: what to do with this code? - //if rm := b.dcDetector.HandleMessage(message, messageID, b.timestamp); rm != nil { - // b.appendReadyMessage(rm) - //} return nil } diff --git a/backend/internal/handlers/ios/appNotResponding.go b/backend/internal/handlers/ios/appNotResponding.go index 097361c00..b5f6cd2f0 100644 --- a/backend/internal/handlers/ios/appNotResponding.go +++ b/backend/internal/handlers/ios/appNotResponding.go @@ -3,7 +3,6 @@ package ios import ( "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" - "time" ) /* @@ -22,9 +21,11 @@ type AppNotResponding struct { lastLabel string lastHeartbeatTimestamp uint64 lastHeartbeatIndex uint64 + lastTimestamp uint64 } func (h *AppNotResponding) Handle(message Message, messageID uint64, timestamp uint64) Message { + h.lastTimestamp = timestamp var event Message = nil switch m := message.(type) { case *IOSClickEvent: @@ -48,7 +49,7 @@ func (h *AppNotResponding) Handle(message Message, messageID uint64, timestamp u } func (h *AppNotResponding) Build() Message { - return h.build(uint64(time.Now().Unix())) + return h.build(h.lastTimestamp) } func (h *AppNotResponding) build(timestamp uint64) Message { diff --git a/backend/internal/handlers/ios/performanceAggregator.go b/backend/internal/handlers/ios/performanceAggregator.go index b4bc812c7..2a9401748 100644 --- a/backend/internal/handlers/ios/performanceAggregator.go +++ b/backend/internal/handlers/ios/performanceAggregator.go @@ -3,7 +3,6 @@ package ios import ( "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" - "time" ) /* @@ -29,14 +28,16 @@ func (va *valueAggregator) aggregate() uint64 { type PerformanceAggregator struct { handlers.ReadyMessageStore - pa *IOSPerformanceAggregated - fps valueAggregator - cpu valueAggregator - memory valueAggregator - battery valueAggregator + pa *IOSPerformanceAggregated + fps valueAggregator + cpu valueAggregator + memory valueAggregator + battery valueAggregator + lastTimestamp uint64 } func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timestamp uint64) Message { + h.lastTimestamp = timestamp if h.pa == nil { h.pa = &IOSPerformanceAggregated{} // TODO: struct type in messages } @@ -94,7 +95,7 @@ func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timest } func (h *PerformanceAggregator) Build() Message { - return h.build(uint64(time.Now().Unix())) + return h.build(h.lastTimestamp) } func (h *PerformanceAggregator) build(timestamp uint64) Message { From e65fa58ab56e3198e46e298090f396c36ec111d0 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Wed, 11 May 2022 18:51:55 +0200 Subject: [PATCH 13/19] refactor(backend-internal): dry builder --- backend/internal/builder/builder.go | 4 +-- backend/internal/builder/builderMap.go | 45 +++++++++++++++----------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index 4916764cd..ff3d91e1b 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -18,7 +18,7 @@ func NewBuilder(handlers ...handlers.MessageProcessor) *builder { } } -func (b *builder) iterateReadyMessage(iter func(msg Message)) { +func (b *builder) iterateReadyMessages(iter func(msg Message)) { for _, readyMsg := range b.readyMsgs { iter(readyMsg) } @@ -44,10 +44,10 @@ func (b *builder) handleMessage(message Message, messageID uint64) { return } - b.checkSessionEnd(message) for _, p := range b.processors { if rm := p.Handle(message, messageID, b.timestamp); rm != nil { b.readyMsgs = append(b.readyMsgs, rm) } } + b.checkSessionEnd(message) } diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index b393bcd28..f6d81b995 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -20,7 +20,7 @@ func NewBuilderMap(handlers ...handlers.MessageProcessor) *builderMap { func (m *builderMap) GetBuilder(sessionID uint64) *builder { b := m.sessions[sessionID] if b == nil { - b = NewBuilder(m.handlers...) + b = NewBuilder(m.handlers...) // Should create new instances m.sessions[sessionID] = b } return b @@ -31,21 +31,29 @@ func (m *builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint b.handleMessage(msg, messageID) } -func (m *builderMap) IterateReadyMessages(iter func(sessionID uint64, msg Message)) { - for sessionID, b := range m.sessions { - if b.ended { - for _, p := range b.processors { - if rm := p.Build(); rm != nil { - b.readyMsgs = append(b.readyMsgs, rm) - } +func (m *builderMap) iterateSessionReadyMessages(sessionID uint64, b *builder, iter func(msg Message)) { + if b.ended { + for _, p := range b.processors { + if rm := p.Build(); rm != nil { + b.readyMsgs = append(b.readyMsgs, rm) } } - b.iterateReadyMessage(func(msg Message) { - iter(sessionID, msg) - }) - if b.ended { - delete(m.sessions, sessionID) - } + } + b.iterateReadyMessage(iter) + if b.ended { + delete(m.sessions, sessionID) + } +} + +func (m *builderMap) IterateReadyMessages(iter func(sessionID uint64, msg Message)) { + for sessionID, session := range m.sessions { + m.iterateSessionReadyMessages( + sessionID, + session, + func(msg Message) { + iter(sessionID, msg) + }, + ) } } @@ -54,8 +62,9 @@ func (m *builderMap) IterateSessionReadyMessages(sessionID uint64, iter func(msg if !ok { return } - session.iterateReadyMessage(iter) - if session.ended { - delete(m.sessions, sessionID) - } + m.iterateSessionReadyMessages( + sessionID, + session, + inter, + ) } From a6f8857b8912747491236f2e7440673678f0f2ec Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Wed, 11 May 2022 19:04:14 +0200 Subject: [PATCH 14/19] refactor-fix(backend-heuristics/db): create handlers for each session separately --- backend/cmd/db/main.go | 14 ++++++----- backend/cmd/heuristics/main.go | 32 ++++++++++++++------------ backend/internal/builder/builderMap.go | 12 +++++----- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 8236586c2..d3ba45017 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -30,15 +30,17 @@ func main() { pg := cache.NewPGCache(postgres.NewConn(cfg.Postgres), cfg.ProjectExpirationTimeoutMs) defer pg.Close() - // Declare message handlers we want to apply for each incoming message - msgHandlers := []handlers.MessageProcessor{ - custom.NewMainHandler(), // TODO: separate to several handler - //custom.NewInputEventBuilder(), - //custom.NewPageEventBuilder(), + // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. + handlersFabric := func() { + return []handlers.MessageProcessor{ + custom.NewMainHandler(), // TODO: separate to several handler + //custom.NewInputEventBuilder(), + //custom.NewPageEventBuilder(), + } } // Create handler's aggregator - builderMap := builder.NewBuilderMap(msgHandlers...) + builderMap := builder.NewBuilderMap(handlersFabric) // Init modules saver := datasaver.New(pg) diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index 9c77cb4ba..5543f85e1 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -25,24 +25,26 @@ func main() { // Load service configuration cfg := ender.New() - // Declare message handlers we want to apply for each incoming message - msgHandlers := []handlers.MessageProcessor{ - // web handlers - &web.ClickRageDetector{}, - &web.CpuIssueDetector{}, - &web.DeadClickDetector{}, - &web.MemoryIssueDetector{}, - &web.PerformanceAggregator{}, - // iOS handlers - &ios.AppNotResponding{}, - &ios.ClickRageDetector{}, - &ios.PerformanceAggregator{}, - // Other handlers (you can add your custom handlers here) - &custom.CustomHandler{}, + // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. + handlersFabric := func() { + return []handlers.MessageProcessor{ + // web handlers + &web.ClickRageDetector{}, + &web.CpuIssueDetector{}, + &web.DeadClickDetector{}, + &web.MemoryIssueDetector{}, + &web.PerformanceAggregator{}, + // iOS handlers + &ios.AppNotResponding{}, + &ios.ClickRageDetector{}, + &ios.PerformanceAggregator{}, + // Other handlers (you can add your custom handlers here) + &custom.CustomHandler{}, + } } // Create handler's aggregator - builderMap := builder.NewBuilderMap(msgHandlers...) + builderMap := builder.NewBuilderMap(handlersFabric) // Init logger statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index f6d81b995..ed47abdce 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -6,21 +6,21 @@ import ( ) type builderMap struct { - handlers []handlers.MessageProcessor - sessions map[uint64]*builder + handlersFabric func() []handlers.MessageProcessor + sessions map[uint64]*builder } -func NewBuilderMap(handlers ...handlers.MessageProcessor) *builderMap { +func NewBuilderMap(handlersFabric func() []handlers.MessageProcessor) *builderMap { return &builderMap{ - handlers: handlers, - sessions: make(map[uint64]*builder), + handlersFabric: handlersFabric, + sessions: make(map[uint64]*builder), } } func (m *builderMap) GetBuilder(sessionID uint64) *builder { b := m.sessions[sessionID] if b == nil { - b = NewBuilder(m.handlers...) // Should create new instances + b = NewBuilder(m.handlersFabric()) // Should create new instances m.sessions[sessionID] = b } return b From 85b87e17dfbf91eab8e3ebe239fd3e668f224cf1 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Wed, 11 May 2022 21:14:23 +0200 Subject: [PATCH 15/19] refactor(backend/internals): builder: message order & timestamps check --- backend/internal/builder/builder.go | 30 ++++++++++++++++++-------- backend/internal/builder/builderMap.go | 6 +++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index ff3d91e1b..38a64ab54 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -6,10 +6,12 @@ import ( ) type builder struct { - readyMsgs []Message - timestamp uint64 - processors []handlers.MessageProcessor - ended bool + readyMsgs []Message + timestamp uint64 + lastMessageID uint64 + lastSystemTimestamp int64 + processors []handlers.MessageProcessor + ended bool } func NewBuilder(handlers ...handlers.MessageProcessor) *builder { @@ -35,15 +37,25 @@ func (b *builder) checkSessionEnd(message Message) { } func (b *builder) handleMessage(message Message, messageID uint64) { - timestamp := GetTimestamp(message) - if b.timestamp < timestamp { - b.timestamp = timestamp + if messageID < b.lastMessageID { + // May happen in case of duplicated messages in kafka (if `idempotence: false`) + return } - if b.timestamp == 0 { - // in case of SessionStart. TODO: make timestamp system transparent + timestamp := GetTimestamp(message) + if timestamp == 0 { + // May happen in case of messages that are single-in-batch, + // e.g. SessionStart or RawErrorEvent (emitted by `integrations`). + + // TODO: make timestamp system transparent; + return + } + if timestamp < b.timestamp { + // Shouldn't happen after messageID check which is done above. TODO: log this case. return } + b.timestamp = timestamp + b.lastSystemTimestamp = time.Now().UnixMilli() for _, p := range b.processors { if rm := p.Handle(message, messageID, b.timestamp); rm != nil { b.readyMsgs = append(b.readyMsgs, rm) diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index ed47abdce..fcd52b0cc 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -1,10 +1,14 @@ package builder import ( + "time" + "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" ) +const FORCE_DELETE_TIMEOUT = 4 * time.Hour + type builderMap struct { handlersFabric func() []handlers.MessageProcessor sessions map[uint64]*builder @@ -32,7 +36,7 @@ func (m *builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint } func (m *builderMap) iterateSessionReadyMessages(sessionID uint64, b *builder, iter func(msg Message)) { - if b.ended { + if b.ended || b.lastSystemTimestamp+FORCE_DELETE_TIMEOUT < time.Now().UnixMilli() { for _, p := range b.processors { if rm := p.Build(); rm != nil { b.readyMsgs = append(b.readyMsgs, rm) From 6d2bfc0e77d6379f07445fc1efa94820722b31d6 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Wed, 11 May 2022 21:25:41 +0200 Subject: [PATCH 16/19] fix(backend/internals): builder codefix --- backend/internal/builder/builder.go | 16 +++++++++------- backend/internal/builder/builderMap.go | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/internal/builder/builder.go b/backend/internal/builder/builder.go index 38a64ab54..dd7bb675a 100644 --- a/backend/internal/builder/builder.go +++ b/backend/internal/builder/builder.go @@ -1,17 +1,19 @@ package builder import ( + "time" + "openreplay/backend/internal/handlers" . "openreplay/backend/pkg/messages" ) type builder struct { - readyMsgs []Message - timestamp uint64 - lastMessageID uint64 - lastSystemTimestamp int64 - processors []handlers.MessageProcessor - ended bool + readyMsgs []Message + timestamp uint64 + lastMessageID uint64 + lastSystemTime time.Time + processors []handlers.MessageProcessor + ended bool } func NewBuilder(handlers ...handlers.MessageProcessor) *builder { @@ -55,7 +57,7 @@ func (b *builder) handleMessage(message Message, messageID uint64) { } b.timestamp = timestamp - b.lastSystemTimestamp = time.Now().UnixMilli() + b.lastSystemTime = time.Now() for _, p := range b.processors { if rm := p.Handle(message, messageID, b.timestamp); rm != nil { b.readyMsgs = append(b.readyMsgs, rm) diff --git a/backend/internal/builder/builderMap.go b/backend/internal/builder/builderMap.go index fcd52b0cc..af2ecf0d5 100644 --- a/backend/internal/builder/builderMap.go +++ b/backend/internal/builder/builderMap.go @@ -24,7 +24,7 @@ func NewBuilderMap(handlersFabric func() []handlers.MessageProcessor) *builderMa func (m *builderMap) GetBuilder(sessionID uint64) *builder { b := m.sessions[sessionID] if b == nil { - b = NewBuilder(m.handlersFabric()) // Should create new instances + b = NewBuilder(m.handlersFabric()...) // Should create new instances m.sessions[sessionID] = b } return b @@ -36,14 +36,14 @@ func (m *builderMap) HandleMessage(sessionID uint64, msg Message, messageID uint } func (m *builderMap) iterateSessionReadyMessages(sessionID uint64, b *builder, iter func(msg Message)) { - if b.ended || b.lastSystemTimestamp+FORCE_DELETE_TIMEOUT < time.Now().UnixMilli() { + if b.ended || b.lastSystemTime.Add(FORCE_DELETE_TIMEOUT).Before(time.Now()) { for _, p := range b.processors { if rm := p.Build(); rm != nil { b.readyMsgs = append(b.readyMsgs, rm) } } } - b.iterateReadyMessage(iter) + b.iterateReadyMessages(iter) if b.ended { delete(m.sessions, sessionID) } @@ -69,6 +69,6 @@ func (m *builderMap) IterateSessionReadyMessages(sessionID uint64, iter func(msg m.iterateSessionReadyMessages( sessionID, session, - inter, + iter, ) } From 88bec7ab6065ade07f3e4ecf1941df27ae5f8c9f Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Wed, 11 May 2022 21:27:18 +0200 Subject: [PATCH 17/19] refactor(): separate ieBuilder, peBuilder & networkIssueDeterctor from EventMapper --- backend/cmd/db/main.go | 6 +- backend/cmd/heuristics/main.go | 1 + .../internal/handlers/custom/eventMapper.go | 135 +++++++++ .../handlers/custom/inputEventBuilder.go | 95 +++---- .../internal/handlers/custom/mainHandler.go | 257 ------------------ .../handlers/custom/pageEventBuilder.go | 159 ++++++----- .../handlers/ios/performanceAggregator.go | 10 +- backend/internal/handlers/web/networkIssue.go | 47 ++++ backend/pkg/intervals/intervals.go | 2 - 9 files changed, 309 insertions(+), 403 deletions(-) create mode 100644 backend/internal/handlers/custom/eventMapper.go delete mode 100644 backend/internal/handlers/custom/mainHandler.go create mode 100644 backend/internal/handlers/web/networkIssue.go diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index d3ba45017..d3d786242 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -33,9 +33,9 @@ func main() { // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. handlersFabric := func() { return []handlers.MessageProcessor{ - custom.NewMainHandler(), // TODO: separate to several handler - //custom.NewInputEventBuilder(), - //custom.NewPageEventBuilder(), + custom.EventMapper{}, + custom.NewInputEventBuilder(), + custom.NewPageEventBuilder(), } } diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index 5543f85e1..6712c927c 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -33,6 +33,7 @@ func main() { &web.CpuIssueDetector{}, &web.DeadClickDetector{}, &web.MemoryIssueDetector{}, + &web.NetworkIssueDetector{}, &web.PerformanceAggregator{}, // iOS handlers &ios.AppNotResponding{}, diff --git a/backend/internal/handlers/custom/eventMapper.go b/backend/internal/handlers/custom/eventMapper.go new file mode 100644 index 000000000..5d118ff7d --- /dev/null +++ b/backend/internal/handlers/custom/eventMapper.go @@ -0,0 +1,135 @@ +package custom + +import ( + "net/url" + "strings" + + . "openreplay/backend/pkg/messages" +) + +func getURLExtention(URL string) string { + u, err := url.Parse(URL) + if err != nil { + return "" + } + i := strings.LastIndex(u.Path, ".") + return u.Path[i+1:] +} + +func getResourceType(initiator string, URL string) string { + switch initiator { + case "xmlhttprequest", "fetch": + return "fetch" + case "img": + return "img" + default: + switch getURLExtention(URL) { + case "css": + return "stylesheet" + case "js": + return "script" + case "png", "gif", "jpg", "jpeg", "svg": + return "img" + case "mp4", "mkv", "ogg", "webm", "avi", "mp3": + return "media" + default: + return "other" + } + } +} + +type EventMapper struct{} + +func (b *EventMapper) Build() Message { + return nil +} + +func (b *EventMapper) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *RawErrorEvent: + // !!! This won't be handled because the Meta() timestamp emitted by `integrations` will be 0 + // TODO: move to db directly + return &ErrorEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Source: msg.Source, + Name: msg.Name, + Message: msg.Message, + Payload: msg.Payload, + } + case *MouseClick: + if msg.Label != "" { + return &ClickEvent{ + MessageID: messageID, + Label: msg.Label, + HesitationTime: msg.HesitationTime, + Timestamp: timestamp, + Selector: msg.Selector, + } + } + case *JSException: + return &ErrorEvent{ + MessageID: messageID, + Timestamp: timestamp, + Source: "js_exception", + Name: msg.Name, + Message: msg.Message, + Payload: msg.Payload, + } + case *ResourceTiming: + return &ResourceEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Duration: msg.Duration, + TTFB: msg.TTFB, + HeaderSize: msg.HeaderSize, + EncodedBodySize: msg.EncodedBodySize, + DecodedBodySize: msg.DecodedBodySize, + URL: msg.URL, + Type: getResourceType(msg.Initiator, msg.URL), + Success: msg.Duration != 0, + } + case *RawCustomEvent: + return &CustomEvent{ + MessageID: messageID, + Timestamp: timestamp, + Name: msg.Name, + Payload: msg.Payload, + } + case *CustomIssue: + return &IssueEvent{ + Type: "custom", + Timestamp: timestamp, + MessageID: messageID, + ContextString: msg.Name, + Payload: msg.Payload, + } + case *Fetch: + return &FetchEvent{ + MessageID: messageID, + Timestamp: msg.Timestamp, + Method: msg.Method, + URL: msg.URL, + Request: msg.Request, + Response: msg.Response, + Status: msg.Status, + Duration: msg.Duration, + } + case *GraphQL: + return &GraphQLEvent{ + MessageID: messageID, + Timestamp: timestamp, + OperationKind: msg.OperationKind, + OperationName: msg.OperationName, + Variables: msg.Variables, + Response: msg.Response, + } + case *StateAction: + return &StateActionEvent{ + MessageID: messageID, + Timestamp: timestamp, + Type: msg.Type, + } + } + return nil +} diff --git a/backend/internal/handlers/custom/inputEventBuilder.go b/backend/internal/handlers/custom/inputEventBuilder.go index 770e714af..e07470f37 100644 --- a/backend/internal/handlers/custom/inputEventBuilder.go +++ b/backend/internal/handlers/custom/inputEventBuilder.go @@ -4,6 +4,8 @@ import ( . "openreplay/backend/pkg/messages" ) +const INPUT_EVENT_TIMEOUT = 1 * 60 * 1000 + type inputLabels map[uint64]string type inputEventBuilder struct { @@ -12,78 +14,63 @@ type inputEventBuilder struct { inputID uint64 } -func (b *inputEventBuilder) Handle(message Message, messageID uint64, timestamp uint64) Message { - //TODO implement me - panic("implement me") -} - -func (b *inputEventBuilder) Build() Message { - // b.build() - //TODO implement me - panic("implement me") -} - func NewInputEventBuilder() *inputEventBuilder { ieBuilder := &inputEventBuilder{} - ieBuilder.ClearLabels() + ieBuilder.clearLabels() return ieBuilder } -func (b *inputEventBuilder) ClearLabels() { +func (b *inputEventBuilder) clearLabels() { b.inputLabels = make(inputLabels) } -func (b *inputEventBuilder) HandleSetInputTarget(msg *SetInputTarget) *InputEvent { - var inputEvent *InputEvent - if b.inputID != msg.ID { - inputEvent = b.build() - b.inputID = msg.ID - } - b.inputLabels[msg.ID] = msg.Label - return inputEvent -} - -func (b *inputEventBuilder) HandleSetInputValue(msg *SetInputValue, messageID uint64, timestamp uint64) *InputEvent { - var inputEvent *InputEvent - if b.inputID != msg.ID { - inputEvent = b.build() - b.inputID = msg.ID - } - if b.inputEvent == nil { - b.inputEvent = &InputEvent{ - MessageID: messageID, - Timestamp: timestamp, - Value: msg.Value, - ValueMasked: msg.Mask > 0, +func (b *inputEventBuilder) Handle(message Message, messageID uint64, timestamp uint64) Message { + var inputEvent Message = nil + switch msg := message.(type) { + case *SetInputTarget: + if b.inputID != msg.ID { + inputEvent = b.Build() + b.inputID = msg.ID } - } else { - b.inputEvent.Value = msg.Value - b.inputEvent.ValueMasked = msg.Mask > 0 + b.inputLabels[msg.ID] = msg.Label + return inputEvent + case *SetInputValue: + if b.inputID != msg.ID { + inputEvent = b.Build() + b.inputID = msg.ID + } + if b.inputEvent == nil { + b.inputEvent = &InputEvent{ + MessageID: messageID, + Timestamp: timestamp, + Value: msg.Value, + ValueMasked: msg.Mask > 0, + } + } else { + b.inputEvent.Value = msg.Value + b.inputEvent.ValueMasked = msg.Mask > 0 + } + return inputEvent + case *CreateDocument: + inputEvent = b.Build() + b.clearLabels() + return inputEvent + case *MouseClick: + return b.Build() } - return inputEvent -} -func (b *inputEventBuilder) HasInstance() bool { - return b.inputEvent != nil -} - -func (b *inputEventBuilder) GetTimestamp() uint64 { - if b.inputEvent == nil { - return 0 + if b.inputEvent != nil && b.inputEvent.Timestamp+INPUT_EVENT_TIMEOUT < timestamp { + return b.Build() } - return b.inputEvent.Timestamp + return nil } -func (b *inputEventBuilder) build() *InputEvent { +func (b *inputEventBuilder) Build() Message { if b.inputEvent == nil { return nil } inputEvent := b.inputEvent - label, exists := b.inputLabels[b.inputID] - if !exists { - return nil - } - inputEvent.Label = label + inputEvent.Label = b.inputLabels[b.inputID] // might be empty string b.inputEvent = nil return inputEvent diff --git a/backend/internal/handlers/custom/mainHandler.go b/backend/internal/handlers/custom/mainHandler.go deleted file mode 100644 index 52a6278c0..000000000 --- a/backend/internal/handlers/custom/mainHandler.go +++ /dev/null @@ -1,257 +0,0 @@ -package custom - -import ( - "net/url" - "openreplay/backend/pkg/intervals" - "strings" - "time" - - . "openreplay/backend/pkg/messages" -) - -func getURLExtention(URL string) string { - u, err := url.Parse(URL) - if err != nil { - return "" - } - i := strings.LastIndex(u.Path, ".") - return u.Path[i+1:] -} - -func getResourceType(initiator string, URL string) string { - switch initiator { - case "xmlhttprequest", "fetch": - return "fetch" - case "img": - return "img" - default: - switch getURLExtention(URL) { - case "css": - return "stylesheet" - case "js": - return "script" - case "png", "gif", "jpg", "jpeg", "svg": - return "img" - case "mp4", "mkv", "ogg", "webm", "avi", "mp3": - return "media" - default: - return "other" - } - } -} - -type builder struct { - readyMsgs []Message - timestamp uint64 - lastProcessedTimestamp int64 - peBuilder *pageEventBuilder - ieBuilder *inputEventBuilder - integrationsWaiting bool - sid uint64 -} - -func (b *builder) Build() Message { - //TODO implement me - panic("implement me") -} - -func NewMainHandler() *builder { - return &builder{ - peBuilder: &pageEventBuilder{}, - ieBuilder: NewInputEventBuilder(), - integrationsWaiting: true, - } -} - -func (b *builder) appendReadyMessage(msg Message) { // interface is never nil even if it holds nil value - b.readyMsgs = append(b.readyMsgs, msg) -} - -func (b *builder) iterateReadyMessage(iter func(msg Message)) { - for _, readyMsg := range b.readyMsgs { - iter(readyMsg) - } - b.readyMsgs = nil -} - -func (b *builder) buildPageEvent() { - if msg := b.peBuilder.Build(); msg != nil { - b.appendReadyMessage(msg) - } -} - -func (b *builder) buildInputEvent() { - if msg := b.ieBuilder.Build(); msg != nil { - b.appendReadyMessage(msg) - } -} - -func (b *builder) Handle(message Message, messageID uint64, timestamp uint64) Message { - b.timestamp = timestamp - b.lastProcessedTimestamp = time.Now().UnixMilli() - - // Might happen before the first timestamp. - switch msg := message.(type) { - case *SessionStart, - *Metadata, - *UserID, - *UserAnonymousID: - b.appendReadyMessage(msg) - case *RawErrorEvent: - b.appendReadyMessage(&ErrorEvent{ - MessageID: messageID, - Timestamp: msg.Timestamp, - Source: msg.Source, - Name: msg.Name, - Message: msg.Message, - Payload: msg.Payload, - }) - } - if b.timestamp == 0 { - return nil - } - switch msg := message.(type) { - case *SetPageLocation: - if msg.NavigationStart == 0 { - b.appendReadyMessage(&PageEvent{ - URL: msg.URL, - Referrer: msg.Referrer, - Loaded: false, - MessageID: messageID, - Timestamp: b.timestamp, - }) - } else { - b.buildPageEvent() - b.buildInputEvent() - b.ieBuilder.ClearLabels() - b.peBuilder.HandleSetPageLocation(msg, messageID, b.timestamp) - } - case *PageLoadTiming: - if rm := b.peBuilder.HandlePageLoadTiming(msg); rm != nil { - b.appendReadyMessage(rm) - } - case *PageRenderTiming: - if rm := b.peBuilder.HandlePageRenderTiming(msg); rm != nil { - b.appendReadyMessage(rm) - } - case *SetInputTarget: - if rm := b.ieBuilder.HandleSetInputTarget(msg); rm != nil { - b.appendReadyMessage(rm) - } - case *SetInputValue: - if rm := b.ieBuilder.HandleSetInputValue(msg, messageID, b.timestamp); rm != nil { - b.appendReadyMessage(rm) - } - case *MouseClick: - b.buildInputEvent() - if msg.Label != "" { - b.appendReadyMessage(&ClickEvent{ - MessageID: messageID, - Label: msg.Label, - HesitationTime: msg.HesitationTime, - Timestamp: b.timestamp, - Selector: msg.Selector, - }) - } - case *JSException: - b.appendReadyMessage(&ErrorEvent{ - MessageID: messageID, - Timestamp: b.timestamp, - Source: "js_exception", - Name: msg.Name, - Message: msg.Message, - Payload: msg.Payload, - }) - case *ResourceTiming: - tp := getResourceType(msg.Initiator, msg.URL) - success := msg.Duration != 0 - b.appendReadyMessage(&ResourceEvent{ - MessageID: messageID, - Timestamp: msg.Timestamp, - Duration: msg.Duration, - TTFB: msg.TTFB, - HeaderSize: msg.HeaderSize, - EncodedBodySize: msg.EncodedBodySize, - DecodedBodySize: msg.DecodedBodySize, - URL: msg.URL, - Type: tp, - Success: success, - }) - if !success { - issueType := "missing_resource" - if tp == "fetch" { - issueType = "bad_request" - } - b.appendReadyMessage(&IssueEvent{ - Type: issueType, - MessageID: messageID, - Timestamp: msg.Timestamp, - ContextString: msg.URL, - }) - } - case *RawCustomEvent: - b.appendReadyMessage(&CustomEvent{ - MessageID: messageID, - Timestamp: b.timestamp, - Name: msg.Name, - Payload: msg.Payload, - }) - case *CustomIssue: - b.appendReadyMessage(&IssueEvent{ - Type: "custom", - Timestamp: b.timestamp, - MessageID: messageID, - ContextString: msg.Name, - Payload: msg.Payload, - }) - case *Fetch: - b.appendReadyMessage(&FetchEvent{ - MessageID: messageID, - Timestamp: msg.Timestamp, - Method: msg.Method, - URL: msg.URL, - Request: msg.Request, - Response: msg.Response, - Status: msg.Status, - Duration: msg.Duration, - }) - if msg.Status >= 400 { - b.appendReadyMessage(&IssueEvent{ - Type: "bad_request", - MessageID: messageID, - Timestamp: msg.Timestamp, - ContextString: msg.URL, - }) - } - case *GraphQL: - b.appendReadyMessage(&GraphQLEvent{ - MessageID: messageID, - Timestamp: b.timestamp, - OperationKind: msg.OperationKind, - OperationName: msg.OperationName, - Variables: msg.Variables, - Response: msg.Response, - }) - case *StateAction: - b.appendReadyMessage(&StateActionEvent{ - MessageID: messageID, - Timestamp: b.timestamp, - Type: msg.Type, - }) - } - return nil -} - -func (b *builder) checkTimeouts(ts int64) bool { - if b.timestamp == 0 { - return false // There was no timestamp events yet - } - - if b.peBuilder.HasInstance() && int64(b.peBuilder.GetTimestamp())+intervals.EVENTS_PAGE_EVENT_TIMEOUT < ts { - b.buildPageEvent() - } - if b.ieBuilder.HasInstance() && int64(b.ieBuilder.GetTimestamp())+intervals.EVENTS_INPUT_EVENT_TIMEOUT < ts { - b.buildInputEvent() - } - return false -} diff --git a/backend/internal/handlers/custom/pageEventBuilder.go b/backend/internal/handlers/custom/pageEventBuilder.go index 765fd31a2..d95768983 100644 --- a/backend/internal/handlers/custom/pageEventBuilder.go +++ b/backend/internal/handlers/custom/pageEventBuilder.go @@ -4,104 +4,103 @@ import ( . "openreplay/backend/pkg/messages" ) +const PAGE_EVENT_TIMEOUT = 1 * 60 * 1000 + type pageEventBuilder struct { pageEvent *PageEvent firstTimingHandled bool } -func (b *pageEventBuilder) Handle(message Message, messageID uint64, timestamp uint64) Message { - //TODO implement me - panic("implement me") -} - -func (b *pageEventBuilder) Build() Message { - // b.build() - //TODO implement me - panic("implement me") -} - func NewPageEventBuilder() *pageEventBuilder { ieBuilder := &pageEventBuilder{} return ieBuilder } -func (b *pageEventBuilder) buildIfTimingsComplete() *PageEvent { - if b.firstTimingHandled { - return b.build() +func (b *pageEventBuilder) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *SetPageLocation: + if msg.NavigationStart == 0 { // routing without new page loading + return &PageEvent{ + URL: msg.URL, + Referrer: msg.Referrer, + Loaded: false, + MessageID: messageID, + Timestamp: timestamp, + } + } else { + pageEvent := b.Build() + b.pageEvent = &PageEvent{ + URL: msg.URL, + Referrer: msg.Referrer, + Loaded: true, + MessageID: messageID, + Timestamp: timestamp, + } + return pageEvent + } + case *PageLoadTiming: + if b.pageEvent == nil { + break + } + if msg.RequestStart <= 30000 { + b.pageEvent.RequestStart = msg.RequestStart + } + if msg.ResponseStart <= 30000 { + b.pageEvent.ResponseStart = msg.ResponseStart + } + if msg.ResponseEnd <= 30000 { + b.pageEvent.ResponseEnd = msg.ResponseEnd + } + if msg.DomContentLoadedEventStart <= 30000 { + b.pageEvent.DomContentLoadedEventStart = msg.DomContentLoadedEventStart + } + if msg.DomContentLoadedEventEnd <= 30000 { + b.pageEvent.DomContentLoadedEventEnd = msg.DomContentLoadedEventEnd + } + if msg.LoadEventStart <= 30000 { + b.pageEvent.LoadEventStart = msg.LoadEventStart + } + if msg.LoadEventEnd <= 30000 { + b.pageEvent.LoadEventEnd = msg.LoadEventEnd + } + if msg.FirstPaint <= 30000 { + b.pageEvent.FirstPaint = msg.FirstPaint + } + if msg.FirstContentfulPaint <= 30000 { + b.pageEvent.FirstContentfulPaint = msg.FirstContentfulPaint + } + return b.buildIfTimingsComplete() + case *PageRenderTiming: + if b.pageEvent == nil { + break + } + b.pageEvent.SpeedIndex = msg.SpeedIndex + b.pageEvent.VisuallyComplete = msg.VisuallyComplete + b.pageEvent.TimeToInteractive = msg.TimeToInteractive + return b.buildIfTimingsComplete() + + } + + if b.pageEvent != nil && b.pageEvent.Timestamp+PAGE_EVENT_TIMEOUT < timestamp { + return b.Build() } - b.firstTimingHandled = true return nil } -// Only for Loaded: true -func (b *pageEventBuilder) HandleSetPageLocation(msg *SetPageLocation, messageID uint64, timestamp uint64) { - b.pageEvent = &PageEvent{ - URL: msg.URL, - Referrer: msg.Referrer, - Loaded: true, - MessageID: messageID, - Timestamp: timestamp, - } -} - -func (b *pageEventBuilder) HandlePageLoadTiming(msg *PageLoadTiming) *PageEvent { - if !b.HasInstance() { - return nil - } - if msg.RequestStart <= 30000 { - b.pageEvent.RequestStart = msg.RequestStart - } - if msg.ResponseStart <= 30000 { - b.pageEvent.ResponseStart = msg.ResponseStart - } - if msg.ResponseEnd <= 30000 { - b.pageEvent.ResponseEnd = msg.ResponseEnd - } - if msg.DomContentLoadedEventStart <= 30000 { - b.pageEvent.DomContentLoadedEventStart = msg.DomContentLoadedEventStart - } - if msg.DomContentLoadedEventEnd <= 30000 { - b.pageEvent.DomContentLoadedEventEnd = msg.DomContentLoadedEventEnd - } - if msg.LoadEventStart <= 30000 { - b.pageEvent.LoadEventStart = msg.LoadEventStart - } - if msg.LoadEventEnd <= 30000 { - b.pageEvent.LoadEventEnd = msg.LoadEventEnd - } - if msg.FirstPaint <= 30000 { - b.pageEvent.FirstPaint = msg.FirstPaint - } - if msg.FirstContentfulPaint <= 30000 { - b.pageEvent.FirstContentfulPaint = msg.FirstContentfulPaint - } - return b.buildIfTimingsComplete() -} - -func (b *pageEventBuilder) HandlePageRenderTiming(msg *PageRenderTiming) *PageEvent { - if !b.HasInstance() { - return nil - } - b.pageEvent.SpeedIndex = msg.SpeedIndex - b.pageEvent.VisuallyComplete = msg.VisuallyComplete - b.pageEvent.TimeToInteractive = msg.TimeToInteractive - return b.buildIfTimingsComplete() -} - -func (b *pageEventBuilder) HasInstance() bool { - return b.pageEvent != nil -} - -func (b *pageEventBuilder) GetTimestamp() uint64 { +func (b *pageEventBuilder) Build() Message { if b.pageEvent == nil { - return 0 + return nil } - return b.pageEvent.Timestamp -} - -func (b *pageEventBuilder) build() *PageEvent { pageEvent := b.pageEvent b.pageEvent = nil b.firstTimingHandled = false return pageEvent } + +func (b *pageEventBuilder) buildIfTimingsComplete() Message { + if b.firstTimingHandled { + return b.Build() + } + b.firstTimingHandled = true + return nil +} diff --git a/backend/internal/handlers/ios/performanceAggregator.go b/backend/internal/handlers/ios/performanceAggregator.go index 2a9401748..df87298bd 100644 --- a/backend/internal/handlers/ios/performanceAggregator.go +++ b/backend/internal/handlers/ios/performanceAggregator.go @@ -48,7 +48,7 @@ func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timest h.pa.TimestampStart = m.Timestamp } if h.pa.TimestampStart+AGGR_TIME <= m.Timestamp { - event = h.build(m.Timestamp) + event = h.Build() } switch m.Name { case "fps": @@ -89,21 +89,17 @@ func (h *PerformanceAggregator) Handle(message Message, messageID uint64, timest } } case *IOSSessionEnd: - event = h.build(m.Timestamp) + event = h.Build() } return event } func (h *PerformanceAggregator) Build() Message { - return h.build(h.lastTimestamp) -} - -func (h *PerformanceAggregator) build(timestamp uint64) Message { if h.pa == nil { return nil } - h.pa.TimestampEnd = timestamp + h.pa.TimestampEnd = h.lastTimestamp h.pa.AvgFPS = h.fps.aggregate() h.pa.AvgCPU = h.cpu.aggregate() h.pa.AvgMemory = h.memory.aggregate() diff --git a/backend/internal/handlers/web/networkIssue.go b/backend/internal/handlers/web/networkIssue.go new file mode 100644 index 000000000..ed51351e5 --- /dev/null +++ b/backend/internal/handlers/web/networkIssue.go @@ -0,0 +1,47 @@ +package web + +import ( + . "openreplay/backend/pkg/messages" +) + +/* + Handler name: NetworkIssue + Input events: ResourceTiming, + Fetch + Output event: IssueEvent +*/ + +type NetworkIssueDetector struct{} + +func (f *NetworkIssueDetector) Build() Message { + return nil +} + +func (f *NetworkIssueDetector) Handle(message Message, messageID uint64, timestamp uint64) Message { + switch msg := message.(type) { + case *ResourceTiming: + success := msg.Duration != 0 // The only available way here + if !success { + issueType := "missing_resource" + if msg.Initiator == "fetch" || msg.Initiator == "xmlhttprequest" { + issueType = "bad_request" + } + return &IssueEvent{ + Type: issueType, + MessageID: messageID, + Timestamp: msg.Timestamp, + ContextString: msg.URL, + } + } + case *Fetch: + if msg.Status >= 400 { + return &IssueEvent{ + Type: "bad_request", + MessageID: messageID, + Timestamp: msg.Timestamp, + ContextString: msg.URL, + } + } + } + return nil +} diff --git a/backend/pkg/intervals/intervals.go b/backend/pkg/intervals/intervals.go index 649ceca1a..226d79d35 100644 --- a/backend/pkg/intervals/intervals.go +++ b/backend/pkg/intervals/intervals.go @@ -3,8 +3,6 @@ package intervals const EVENTS_COMMIT_INTERVAL = 30 * 1000 // как часто комитим сообщения в кафке (ender) const HEARTBEAT_INTERVAL = 2 * 60 * 1000 // максимальный таймаут от трекера в рамках сессии const INTEGRATIONS_REQUEST_INTERVAL = 1 * 60 * 1000 // интеграции -const EVENTS_PAGE_EVENT_TIMEOUT = 2 * 60 * 1000 // таймаут пейдж ивента -const EVENTS_INPUT_EVENT_TIMEOUT = 2 * 60 * 1000 // const EVENTS_SESSION_END_TIMEOUT = HEARTBEAT_INTERVAL + 30*1000 const EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS = HEARTBEAT_INTERVAL + 3*60*1000 const EVENTS_BACK_COMMIT_GAP = EVENTS_SESSION_END_TIMEOUT_WITH_INTEGRATIONS + 1*60*1000 // для бэк коммита From ae6af1449c8d30864155859ff3e8f14b1c3f5b9a Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Thu, 12 May 2022 09:59:09 +0200 Subject: [PATCH 18/19] feat(backend-db/heuristics): fixed errors in main files --- backend/cmd/db/main.go | 4 ++-- backend/cmd/heuristics/main.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index d3d786242..20d6ce55b 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -31,9 +31,9 @@ func main() { defer pg.Close() // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. - handlersFabric := func() { + handlersFabric := func() []handlers.MessageProcessor { return []handlers.MessageProcessor{ - custom.EventMapper{}, + &custom.EventMapper{}, custom.NewInputEventBuilder(), custom.NewPageEventBuilder(), } diff --git a/backend/cmd/heuristics/main.go b/backend/cmd/heuristics/main.go index 6712c927c..6edf01a92 100644 --- a/backend/cmd/heuristics/main.go +++ b/backend/cmd/heuristics/main.go @@ -26,7 +26,7 @@ func main() { cfg := ender.New() // HandlersFabric returns the list of message handlers we want to be applied to each incoming message. - handlersFabric := func() { + handlersFabric := func() []handlers.MessageProcessor { return []handlers.MessageProcessor{ // web handlers &web.ClickRageDetector{}, From 44dae11886839152c84ce6f01218aa7be9e07f27 Mon Sep 17 00:00:00 2001 From: Alexander Zavorotynskiy Date: Fri, 13 May 2022 17:00:09 +0200 Subject: [PATCH 19/19] feat(backend/db): fixed ee version --- backend/cmd/db/main.go | 8 +-- backend/internal/datasaver/stats.go | 8 +++ ee/backend/internal/datasaver/stats.go | 79 ++++++++++++++++++++++++ ee/backend/services/db/stats.go | 83 -------------------------- 4 files changed, 91 insertions(+), 87 deletions(-) create mode 100644 ee/backend/internal/datasaver/stats.go delete mode 100644 ee/backend/services/db/stats.go diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 20d6ce55b..1c1d3bf0e 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -44,6 +44,7 @@ func main() { // Init modules saver := datasaver.New(pg) + saver.InitStats() statsLogger := logger.NewQueueStats(cfg.LoggerTimeout) // Handler logic @@ -117,10 +118,9 @@ func main() { os.Exit(0) case <-commitTick: pg.CommitBatches() - // TODO: ee commit stats !!! - //if err := commitStats(); err != nil { - // log.Printf("Error on stats commit: %v", err) - //} + if err := saver.CommitStats(); err != nil { + log.Printf("Error on stats commit: %v", err) + } // TODO?: separate stats & regular messages if err := consumer.Commit(); err != nil { log.Printf("Error on consumer commit: %v", err) diff --git a/backend/internal/datasaver/stats.go b/backend/internal/datasaver/stats.go index a57d91824..26efe51b5 100644 --- a/backend/internal/datasaver/stats.go +++ b/backend/internal/datasaver/stats.go @@ -5,6 +5,10 @@ import ( . "openreplay/backend/pkg/messages" ) +func (si *Saver) InitStats() { + // noop +} + func (si *Saver) InsertStats(session *Session, msg Message) error { switch m := msg.(type) { // Web @@ -17,3 +21,7 @@ func (si *Saver) InsertStats(session *Session, msg Message) error { } return nil } + +func (si *Saver) CommitStats() error { + return nil +} diff --git a/ee/backend/internal/datasaver/stats.go b/ee/backend/internal/datasaver/stats.go new file mode 100644 index 000000000..501a861aa --- /dev/null +++ b/ee/backend/internal/datasaver/stats.go @@ -0,0 +1,79 @@ +package datasaver + +import ( + "log" + "time" + + "openreplay/backend/pkg/db/clickhouse" + "openreplay/backend/pkg/env" +) + +var ch *clickhouse.Connector +var finalizeTicker <-chan time.Time + +func (si *Saver) InitStats() { + ch = clickhouse.NewConnector(env.String("CLICKHOUSE_STRING")) + if err := ch.Prepare(); err != nil { + log.Fatalf("Clickhouse prepare error: %v\n", err) + } + + finalizeTicker = time.Tick(20 * time.Minute) + +} + +func (si *Saver) InsertStats(session *Session, msg Message) error { + switch m := msg.(type) { + // Web + case *SessionEnd: + return si.pg.InsertWebSession(session) + case *PerformanceTrackAggr: + return si.pg.InsertWebPerformanceTrackAggr(session, m) + case *ClickEvent: + return si.pg.InsertWebClickEvent(session, m) + case *InputEvent: + return si.pg.InsertWebInputEvent(session, m) + // Unique for Web + case *PageEvent: + si.pg.InsertWebPageEvent(session, m) + case *ResourceEvent: + return si.pg.InsertWebResourceEvent(session, m) + case *ErrorEvent: + return si.pg.InsertWebErrorEvent(session, m) + case *LongTask: + return si.pg.InsertLongtask(session, m) + + // IOS + case *IOSSessionEnd: + return si.pg.InsertIOSSession(session) + case *IOSPerformanceAggregated: + return si.pg.InsertIOSPerformanceAggregated(session, m) + case *IOSClickEvent: + return si.pg.InsertIOSClickEvent(session, m) + case *IOSInputEvent: + return si.pg.InsertIOSInputEvent(session, m) + // Unique for Web + case *IOSScreenEnter: + //ch.InsertIOSView(session, m) + case *IOSCrash: + return si.pg.InsertIOSCrash(session, m) + case *IOSNetworkCall: + return si.pg.InsertIOSNetworkCall(session, m) + } + return nil +} + +func (si *Saver) CommitStats() error { + select { + case <-finalizeTicker: + if err := ch.FinaliseSessionsTable(); err != nil { + log.Printf("Stats: FinaliseSessionsTable returned an error. %v", err) + } + default: + } + errCommit := ch.Commit() + errPrepare := ch.Prepare() + if errCommit != nil { + return errCommit + } + return errPrepare +} diff --git a/ee/backend/services/db/stats.go b/ee/backend/services/db/stats.go deleted file mode 100644 index 9d250fc51..000000000 --- a/ee/backend/services/db/stats.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "log" - "time" - - - . "openreplay/backend/pkg/messages" - . "openreplay/backend/pkg/db/types" - "openreplay/backend/pkg/db/clickhouse" - "openreplay/backend/pkg/env" -) - -var ch *clickhouse.Connector -var finalizeTicker <-chan time.Time - -func initStats() { - ch = clickhouse.NewConnector(env.String("CLICKHOUSE_STRING")) - if err := ch.Prepare(); err != nil { - log.Fatalf("Clickhouse prepare error: %v\n", err) - } - - finalizeTicker = time.Tick(20 * time.Minute) - -} - -func insertStats(session *Session, msg Message) error { - switch m := msg.(type) { - // Web - case *SessionEnd: - return ch.InsertWebSession(session) - case *PerformanceTrackAggr: - return ch.InsertWebPerformanceTrackAggr(session, m) - case *ClickEvent: - return ch.InsertWebClickEvent(session, m) - case *InputEvent: - return ch.InsertWebInputEvent(session, m) - // Unique for Web - case *PageEvent: - ch.InsertWebPageEvent(session, m) - case *ResourceEvent: - return ch.InsertWebResourceEvent(session, m) - case *ErrorEvent: - return ch.InsertWebErrorEvent(session, m) - case *LongTask: - return ch.InsertLongtask(session, m) - - // IOS - case *IOSSessionEnd: - return ch.InsertIOSSession(session) - case *IOSPerformanceAggregated: - return ch.InsertIOSPerformanceAggregated(session, m) - case *IOSClickEvent: - return ch.InsertIOSClickEvent(session, m) - case *IOSInputEvent: - return ch.InsertIOSInputEvent(session, m) - // Unique for Web - case *IOSScreenEnter: - //ch.InsertIOSView(session, m) - case *IOSCrash: - return ch.InsertIOSCrash(session, m) - case *IOSNetworkCall: - return ch.InsertIOSNetworkCall(session, m) - } - return nil -} - -func commitStats() error { - select { - case <-finalizeTicker: - if err := ch.FinaliseSessionsTable(); err != nil { - log.Printf("Stats: FinaliseSessionsTable returned an error. %v", err) - } - default: - } - errCommit := ch.Commit() - errPrepare := ch.Prepare() - if errCommit != nil { - return errCommit - } - return errPrepare -} -