Merge pull request #968 from openreplay/new-frustrations
feat(tracker): add input hesitation, change input change event handling
|
|
@ -38,7 +38,7 @@ func main() {
|
|||
messages.MsgFetch, messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction,
|
||||
messages.MsgSetInputTarget, messages.MsgSetInputValue, messages.MsgCreateDocument, messages.MsgMouseClick,
|
||||
messages.MsgSetPageLocation, messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
||||
messages.MsgInputEvent, messages.MsgPageEvent}
|
||||
messages.MsgInputEvent, messages.MsgPageEvent, messages.MsgMouseThrashing, messages.MsgInputChange}
|
||||
|
||||
// Init consumer
|
||||
consumer := queue.NewConsumer(
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ func (s *saverImpl) handleMessage(msg Message) error {
|
|||
return s.pg.InsertWebJSException(m)
|
||||
case *IntegrationEvent:
|
||||
return s.pg.InsertWebIntegrationEvent(m)
|
||||
case *InputChange:
|
||||
return s.pg.InsertWebInputDuration(m)
|
||||
case *MouseThrashing:
|
||||
return s.pg.InsertMouseThrashing(m)
|
||||
case *IOSSessionStart:
|
||||
return s.pg.InsertIOSSessionStart(m)
|
||||
case *IOSSessionEnd:
|
||||
|
|
|
|||
18
backend/pkg/db/cache/messages-web.go
vendored
|
|
@ -180,3 +180,21 @@ func (c *PGCache) InsertWebInputEvent(e *InputEvent) error {
|
|||
}
|
||||
return c.Conn.InsertWebInputEvent(sessionID, session.ProjectID, e)
|
||||
}
|
||||
|
||||
func (c *PGCache) InsertWebInputDuration(e *InputChange) error {
|
||||
sessionID := e.SessionID()
|
||||
session, err := c.Cache.GetSession(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.InsertWebInputDuration(sessionID, session.ProjectID, e)
|
||||
}
|
||||
|
||||
func (c *PGCache) InsertMouseThrashing(e *MouseThrashing) error {
|
||||
sessionID := e.SessionID()
|
||||
session, err := c.Cache.GetSession(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Conn.InsertMouseThrashing(sessionID, session.ProjectID, e)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type bulksTask struct {
|
|||
}
|
||||
|
||||
func NewBulksTask() *bulksTask {
|
||||
return &bulksTask{bulks: make([]Bulk, 0, 14)}
|
||||
return &bulksTask{bulks: make([]Bulk, 0, 15)}
|
||||
}
|
||||
|
||||
type BulkSet struct {
|
||||
|
|
@ -19,6 +19,7 @@ type BulkSet struct {
|
|||
customEvents Bulk
|
||||
webPageEvents Bulk
|
||||
webInputEvents Bulk
|
||||
webInputDurations Bulk
|
||||
webGraphQL Bulk
|
||||
webErrors Bulk
|
||||
webErrorEvents Bulk
|
||||
|
|
@ -57,6 +58,8 @@ func (conn *BulkSet) Get(name string) Bulk {
|
|||
return conn.webPageEvents
|
||||
case "webInputEvents":
|
||||
return conn.webInputEvents
|
||||
case "webInputDurations":
|
||||
return conn.webInputDurations
|
||||
case "webGraphQL":
|
||||
return conn.webGraphQL
|
||||
case "webErrors":
|
||||
|
|
@ -126,6 +129,14 @@ func (conn *BulkSet) initBulks() {
|
|||
if err != nil {
|
||||
log.Fatalf("can't create webPageEvents bulk: %s", err)
|
||||
}
|
||||
conn.webInputDurations, err = NewBulk(conn.c,
|
||||
"events.inputs",
|
||||
"(session_id, message_id, timestamp, value, label, hesitation, duration)",
|
||||
"($%d, $%d, $%d, LEFT($%d, 2000), NULLIF(LEFT($%d, 2000),''), $%d, $%d)",
|
||||
7, 200)
|
||||
if err != nil {
|
||||
log.Fatalf("can't create webPageEvents bulk: %s", err)
|
||||
}
|
||||
conn.webGraphQL, err = NewBulk(conn.c,
|
||||
"events.graphql",
|
||||
"(session_id, timestamp, message_id, name, request_body, response_body)",
|
||||
|
|
@ -184,9 +195,9 @@ func (conn *BulkSet) initBulks() {
|
|||
}
|
||||
conn.webClickEvents, err = NewBulk(conn.c,
|
||||
"events.clicks",
|
||||
"(session_id, message_id, timestamp, label, selector, url, path)",
|
||||
"($%d, $%d, $%d, NULLIF(LEFT($%d, 2000), ''), LEFT($%d, 8000), LEFT($%d, 2000), LEFT($%d, 2000))",
|
||||
7, 200)
|
||||
"(session_id, message_id, timestamp, label, selector, url, path, hesitation)",
|
||||
"($%d, $%d, $%d, NULLIF(LEFT($%d, 2000), ''), LEFT($%d, 8000), LEFT($%d, 2000), LEFT($%d, 2000), $%d)",
|
||||
8, 200)
|
||||
if err != nil {
|
||||
log.Fatalf("can't create webClickEvents bulk: %s", err)
|
||||
}
|
||||
|
|
@ -209,6 +220,7 @@ func (conn *BulkSet) Send() {
|
|||
newTask.bulks = append(newTask.bulks, conn.customEvents)
|
||||
newTask.bulks = append(newTask.bulks, conn.webPageEvents)
|
||||
newTask.bulks = append(newTask.bulks, conn.webInputEvents)
|
||||
newTask.bulks = append(newTask.bulks, conn.webInputDurations)
|
||||
newTask.bulks = append(newTask.bulks, conn.webGraphQL)
|
||||
newTask.bulks = append(newTask.bulks, conn.webErrors)
|
||||
newTask.bulks = append(newTask.bulks, conn.webErrorEvents)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package postgres
|
|||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"openreplay/backend/pkg/db/types"
|
||||
"openreplay/backend/pkg/hashid"
|
||||
. "openreplay/backend/pkg/messages"
|
||||
"openreplay/backend/pkg/url"
|
||||
)
|
||||
|
|
@ -63,7 +63,7 @@ func (conn *Conn) InsertWebClickEvent(sessionID uint64, projectID uint32, e *Mou
|
|||
}
|
||||
var host, path string
|
||||
host, path, _, _ = url.GetURLParts(e.Url)
|
||||
if err := conn.bulks.Get("webClickEvents").Append(sessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path); err != nil {
|
||||
if err := conn.bulks.Get("webClickEvents").Append(sessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path, e.HesitationTime); err != nil {
|
||||
log.Printf("insert web click err: %s", err)
|
||||
}
|
||||
// Accumulate session updates and exec inside batch with another sql commands
|
||||
|
|
@ -89,6 +89,24 @@ func (conn *Conn) InsertWebInputEvent(sessionID uint64, projectID uint32, e *Inp
|
|||
return nil
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertWebInputDuration(sessionID uint64, projectID uint32, e *InputChange) error {
|
||||
// Debug log
|
||||
log.Printf("new InputDuration event: %v", e)
|
||||
if e.Label == "" {
|
||||
return nil
|
||||
}
|
||||
value := &e.Value
|
||||
if e.ValueMasked {
|
||||
value = nil
|
||||
}
|
||||
if err := conn.bulks.Get("webInputDurations").Append(sessionID, truncSqIdx(e.ID), e.Timestamp, value, e.Label, e.HesitationTime, e.InputDuration); err != nil {
|
||||
log.Printf("insert web input event err: %s", err)
|
||||
}
|
||||
conn.updateSessionEvents(sessionID, 1, 0)
|
||||
conn.insertAutocompleteValue(sessionID, projectID, "INPUT", e.Label)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertWebErrorEvent(sessionID uint64, projectID uint32, e *types.ErrorEvent) error {
|
||||
errorID := e.ID(projectID)
|
||||
if err := conn.bulks.Get("webErrors").Append(errorID, projectID, e.Source, e.Name, e.Message, e.Payload); err != nil {
|
||||
|
|
@ -145,3 +163,18 @@ func (conn *Conn) InsertSessionReferrer(sessionID uint64, referrer string) error
|
|||
WHERE session_id = $3 AND referrer IS NULL`,
|
||||
referrer, url.DiscardURLQuery(referrer), sessionID)
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertMouseThrashing(sessionID uint64, projectID uint32, e *MouseThrashing) error {
|
||||
// Debug log
|
||||
log.Printf("new MouseThrashing event: %v", e)
|
||||
//
|
||||
issueID := hashid.MouseThrashingID(projectID, sessionID, e.Timestamp)
|
||||
if err := conn.bulks.Get("webIssues").Append(projectID, issueID, "mouse_thrashing", e.Url); err != nil {
|
||||
log.Printf("insert web issue err: %s", err)
|
||||
}
|
||||
if err := conn.bulks.Get("webIssueEvents").Append(sessionID, issueID, e.Timestamp, truncSqIdx(e.MsgID()), nil); err != nil {
|
||||
log.Printf("insert web issue event err: %s", err)
|
||||
}
|
||||
conn.updateSessionIssues(sessionID, 0, 50)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,3 +23,11 @@ func IOSCrashID(projectID uint32, crash *messages.IOSCrash) string {
|
|||
hash.Write([]byte(crash.Stacktrace))
|
||||
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func MouseThrashingID(projectID uint32, sessID, ts uint64) string {
|
||||
hash := fnv.New128a()
|
||||
hash.Write([]byte("mouse_trashing"))
|
||||
hash.Write([]byte(strconv.FormatUint(sessID, 10)))
|
||||
hash.Write([]byte(strconv.FormatUint(ts, 10)))
|
||||
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
package messages
|
||||
|
||||
func IsReplayerType(id int) bool {
|
||||
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 125 != id && 126 != id && 127 != id && 107 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 99 != id && 101 != id && 104 != id && 110 != id && 111 != id
|
||||
return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 35 != id && 42 != id && 52 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 125 != id && 126 != id && 127 != id && 112 != id && 107 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 99 != id && 101 != id && 104 != id && 110 != id && 111 != id
|
||||
}
|
||||
|
||||
func IsIOSType(id int) bool {
|
||||
|
|
@ -10,5 +10,6 @@ func IsIOSType(id int) bool {
|
|||
}
|
||||
|
||||
func IsDOMType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
}
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,102 +2,108 @@
|
|||
package messages
|
||||
|
||||
const (
|
||||
MsgTimestamp = 0
|
||||
MsgSessionStart = 1
|
||||
MsgSessionEndDeprecated = 3
|
||||
MsgSetPageLocation = 4
|
||||
MsgSetViewportSize = 5
|
||||
MsgSetViewportScroll = 6
|
||||
MsgCreateDocument = 7
|
||||
MsgCreateElementNode = 8
|
||||
MsgCreateTextNode = 9
|
||||
MsgMoveNode = 10
|
||||
MsgRemoveNode = 11
|
||||
MsgSetNodeAttribute = 12
|
||||
MsgRemoveNodeAttribute = 13
|
||||
MsgSetNodeData = 14
|
||||
MsgSetCSSData = 15
|
||||
MsgSetNodeScroll = 16
|
||||
MsgSetInputTarget = 17
|
||||
MsgSetInputValue = 18
|
||||
MsgSetInputChecked = 19
|
||||
MsgMouseMove = 20
|
||||
MsgNetworkRequest = 21
|
||||
MsgConsoleLog = 22
|
||||
MsgPageLoadTiming = 23
|
||||
MsgPageRenderTiming = 24
|
||||
MsgJSExceptionDeprecated = 25
|
||||
MsgIntegrationEvent = 26
|
||||
MsgCustomEvent = 27
|
||||
MsgUserID = 28
|
||||
MsgUserAnonymousID = 29
|
||||
MsgMetadata = 30
|
||||
MsgPageEvent = 31
|
||||
MsgInputEvent = 32
|
||||
MsgCSSInsertRule = 37
|
||||
MsgCSSDeleteRule = 38
|
||||
MsgFetch = 39
|
||||
MsgProfiler = 40
|
||||
MsgOTable = 41
|
||||
MsgStateAction = 42
|
||||
MsgRedux = 44
|
||||
MsgVuex = 45
|
||||
MsgMobX = 46
|
||||
MsgNgRx = 47
|
||||
MsgGraphQL = 48
|
||||
MsgPerformanceTrack = 49
|
||||
MsgStringDict = 50
|
||||
MsgSetNodeAttributeDict = 51
|
||||
MsgResourceTiming = 53
|
||||
MsgConnectionInformation = 54
|
||||
MsgSetPageVisibility = 55
|
||||
MsgPerformanceTrackAggr = 56
|
||||
MsgLoadFontFace = 57
|
||||
MsgSetNodeFocus = 58
|
||||
MsgLongTask = 59
|
||||
MsgSetNodeAttributeURLBased = 60
|
||||
MsgSetCSSDataURLBased = 61
|
||||
MsgIssueEventDeprecated = 62
|
||||
MsgTechnicalInfo = 63
|
||||
MsgCustomIssue = 64
|
||||
MsgAssetCache = 66
|
||||
MsgCSSInsertRuleURLBased = 67
|
||||
MsgMouseClick = 69
|
||||
MsgCreateIFrameDocument = 70
|
||||
MsgAdoptedSSReplaceURLBased = 71
|
||||
MsgAdoptedSSReplace = 72
|
||||
MsgAdoptedSSInsertRuleURLBased = 73
|
||||
MsgAdoptedSSInsertRule = 74
|
||||
MsgAdoptedSSDeleteRule = 75
|
||||
MsgAdoptedSSAddOwner = 76
|
||||
MsgAdoptedSSRemoveOwner = 77
|
||||
MsgJSException = 78
|
||||
MsgZustand = 79
|
||||
MsgBatchMeta = 80
|
||||
MsgBatchMetadata = 81
|
||||
MsgPartitionedMessage = 82
|
||||
MsgIssueEvent = 125
|
||||
MsgSessionEnd = 126
|
||||
MsgSessionSearch = 127
|
||||
MsgIOSBatchMeta = 107
|
||||
MsgIOSSessionStart = 90
|
||||
MsgIOSSessionEnd = 91
|
||||
MsgIOSMetadata = 92
|
||||
MsgIOSCustomEvent = 93
|
||||
MsgIOSUserID = 94
|
||||
MsgIOSUserAnonymousID = 95
|
||||
MsgIOSScreenChanges = 96
|
||||
MsgIOSCrash = 97
|
||||
MsgIOSScreenEnter = 98
|
||||
MsgIOSScreenLeave = 99
|
||||
MsgIOSClickEvent = 100
|
||||
MsgIOSInputEvent = 101
|
||||
MsgIOSPerformanceEvent = 102
|
||||
MsgIOSLog = 103
|
||||
MsgIOSInternalError = 104
|
||||
MsgIOSNetworkCall = 105
|
||||
MsgIOSPerformanceAggregated = 110
|
||||
MsgIOSIssueEvent = 111
|
||||
MsgTimestamp = 0
|
||||
MsgSessionStart = 1
|
||||
MsgSessionEndDeprecated = 3
|
||||
MsgSetPageLocation = 4
|
||||
MsgSetViewportSize = 5
|
||||
MsgSetViewportScroll = 6
|
||||
MsgCreateDocument = 7
|
||||
MsgCreateElementNode = 8
|
||||
MsgCreateTextNode = 9
|
||||
MsgMoveNode = 10
|
||||
MsgRemoveNode = 11
|
||||
MsgSetNodeAttribute = 12
|
||||
MsgRemoveNodeAttribute = 13
|
||||
MsgSetNodeData = 14
|
||||
MsgSetCSSData = 15
|
||||
MsgSetNodeScroll = 16
|
||||
MsgSetInputTarget = 17
|
||||
MsgSetInputValue = 18
|
||||
MsgSetInputChecked = 19
|
||||
MsgMouseMove = 20
|
||||
MsgNetworkRequest = 21
|
||||
MsgConsoleLog = 22
|
||||
MsgPageLoadTiming = 23
|
||||
MsgPageRenderTiming = 24
|
||||
MsgJSExceptionDeprecated = 25
|
||||
MsgIntegrationEvent = 26
|
||||
MsgCustomEvent = 27
|
||||
MsgUserID = 28
|
||||
MsgUserAnonymousID = 29
|
||||
MsgMetadata = 30
|
||||
MsgPageEvent = 31
|
||||
MsgInputEvent = 32
|
||||
MsgClickEvent = 33
|
||||
MsgResourceEvent = 35
|
||||
MsgCSSInsertRule = 37
|
||||
MsgCSSDeleteRule = 38
|
||||
MsgFetch = 39
|
||||
MsgProfiler = 40
|
||||
MsgOTable = 41
|
||||
MsgStateAction = 42
|
||||
MsgRedux = 44
|
||||
MsgVuex = 45
|
||||
MsgMobX = 46
|
||||
MsgNgRx = 47
|
||||
MsgGraphQL = 48
|
||||
MsgPerformanceTrack = 49
|
||||
MsgStringDict = 50
|
||||
MsgSetNodeAttributeDict = 51
|
||||
MsgDOMDrop = 52
|
||||
MsgResourceTiming = 53
|
||||
MsgConnectionInformation = 54
|
||||
MsgSetPageVisibility = 55
|
||||
MsgPerformanceTrackAggr = 56
|
||||
MsgLoadFontFace = 57
|
||||
MsgSetNodeFocus = 58
|
||||
MsgLongTask = 59
|
||||
MsgSetNodeAttributeURLBased = 60
|
||||
MsgSetCSSDataURLBased = 61
|
||||
MsgIssueEventDeprecated = 62
|
||||
MsgTechnicalInfo = 63
|
||||
MsgCustomIssue = 64
|
||||
MsgAssetCache = 66
|
||||
MsgCSSInsertRuleURLBased = 67
|
||||
MsgMouseClick = 69
|
||||
MsgCreateIFrameDocument = 70
|
||||
MsgAdoptedSSReplaceURLBased = 71
|
||||
MsgAdoptedSSReplace = 72
|
||||
MsgAdoptedSSInsertRuleURLBased = 73
|
||||
MsgAdoptedSSInsertRule = 74
|
||||
MsgAdoptedSSDeleteRule = 75
|
||||
MsgAdoptedSSAddOwner = 76
|
||||
MsgAdoptedSSRemoveOwner = 77
|
||||
MsgJSException = 78
|
||||
MsgZustand = 79
|
||||
MsgBatchMeta = 80
|
||||
MsgBatchMetadata = 81
|
||||
MsgPartitionedMessage = 82
|
||||
MsgIssueEvent = 125
|
||||
MsgSessionEnd = 126
|
||||
MsgSessionSearch = 127
|
||||
MsgInputChange = 112
|
||||
MsgSelectionChange = 113
|
||||
MsgMouseThrashing = 114
|
||||
MsgIOSBatchMeta = 107
|
||||
MsgIOSSessionStart = 90
|
||||
MsgIOSSessionEnd = 91
|
||||
MsgIOSMetadata = 92
|
||||
MsgIOSCustomEvent = 93
|
||||
MsgIOSUserID = 94
|
||||
MsgIOSUserAnonymousID = 95
|
||||
MsgIOSScreenChanges = 96
|
||||
MsgIOSCrash = 97
|
||||
MsgIOSScreenEnter = 98
|
||||
MsgIOSScreenLeave = 99
|
||||
MsgIOSClickEvent = 100
|
||||
MsgIOSInputEvent = 101
|
||||
MsgIOSPerformanceEvent = 102
|
||||
MsgIOSLog = 103
|
||||
MsgIOSInternalError = 104
|
||||
MsgIOSNetworkCall = 105
|
||||
MsgIOSPerformanceAggregated = 110
|
||||
MsgIOSIssueEvent = 111
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2102,6 +2108,83 @@ func (msg *SessionSearch) TypeID() int {
|
|||
return 127
|
||||
}
|
||||
|
||||
type InputChange struct {
|
||||
message
|
||||
ID uint64
|
||||
Value string
|
||||
ValueMasked bool
|
||||
Label string
|
||||
HesitationTime int64
|
||||
InputDuration int64
|
||||
}
|
||||
|
||||
func (msg *InputChange) Encode() []byte {
|
||||
buf := make([]byte, 61+len(msg.Value)+len(msg.Label))
|
||||
buf[0] = 112
|
||||
p := 1
|
||||
p = WriteUint(msg.ID, buf, p)
|
||||
p = WriteString(msg.Value, buf, p)
|
||||
p = WriteBoolean(msg.ValueMasked, buf, p)
|
||||
p = WriteString(msg.Label, buf, p)
|
||||
p = WriteInt(msg.HesitationTime, buf, p)
|
||||
p = WriteInt(msg.InputDuration, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *InputChange) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *InputChange) TypeID() int {
|
||||
return 112
|
||||
}
|
||||
|
||||
type SelectionChange struct {
|
||||
message
|
||||
SelectionStart uint64
|
||||
SelectionEnd uint64
|
||||
Selection string
|
||||
}
|
||||
|
||||
func (msg *SelectionChange) Encode() []byte {
|
||||
buf := make([]byte, 31+len(msg.Selection))
|
||||
buf[0] = 113
|
||||
p := 1
|
||||
p = WriteUint(msg.SelectionStart, buf, p)
|
||||
p = WriteUint(msg.SelectionEnd, buf, p)
|
||||
p = WriteString(msg.Selection, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *SelectionChange) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *SelectionChange) TypeID() int {
|
||||
return 113
|
||||
}
|
||||
|
||||
type MouseThrashing struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
}
|
||||
|
||||
func (msg *MouseThrashing) Encode() []byte {
|
||||
buf := make([]byte, 11)
|
||||
buf[0] = 114
|
||||
p := 1
|
||||
p = WriteUint(msg.Timestamp, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *MouseThrashing) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *MouseThrashing) TypeID() int {
|
||||
return 114
|
||||
}
|
||||
|
||||
type IOSBatchMeta struct {
|
||||
message
|
||||
Timestamp uint64
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,54 @@ func DecodeSessionSearch(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeInputChange(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &InputChange{}
|
||||
if msg.ID, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Value, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.ValueMasked, err = reader.ReadBoolean(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Label, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.HesitationTime, err = reader.ReadInt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.InputDuration, err = reader.ReadInt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeSelectionChange(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &SelectionChange{}
|
||||
if msg.SelectionStart, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.SelectionEnd, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg.Selection, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeMouseThrashing(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &MouseThrashing{}
|
||||
if msg.Timestamp, err = reader.ReadUint(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeIOSBatchMeta(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &IOSBatchMeta{}
|
||||
|
|
@ -1830,6 +1878,12 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodeSessionEnd(reader)
|
||||
case 127:
|
||||
return DecodeSessionSearch(reader)
|
||||
case 112:
|
||||
return DecodeInputChange(reader)
|
||||
case 113:
|
||||
return DecodeSelectionChange(reader)
|
||||
case 114:
|
||||
return DecodeMouseThrashing(reader)
|
||||
case 107:
|
||||
return DecodeIOSBatchMeta(reader)
|
||||
case 90:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ func (s *saverImpl) handleExtraMessage(msg Message) error {
|
|||
}
|
||||
case *GraphQL:
|
||||
return s.ch.InsertGraphQL(session, m)
|
||||
case *InputChange:
|
||||
return s.ch.InsertWebInputDuration(session, m)
|
||||
case *MouseThrashing:
|
||||
return s.ch.InsertMouseThrashing(session, m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ type Connector interface {
|
|||
InsertCustom(session *types.Session, msg *messages.CustomEvent) error
|
||||
InsertGraphQL(session *types.Session, msg *messages.GraphQL) error
|
||||
InsertIssue(session *types.Session, msg *messages.IssueEvent) error
|
||||
InsertWebInputDuration(session *types.Session, msg *messages.InputChange) error
|
||||
InsertMouseThrashing(session *types.Session, msg *messages.MouseThrashing) error
|
||||
}
|
||||
|
||||
type task struct {
|
||||
|
|
@ -97,7 +99,7 @@ var batches = map[string]string{
|
|||
"autocompletes": "INSERT INTO experimental.autocomplete (project_id, type, value) VALUES (?, ?, ?)",
|
||||
"pages": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint_time, speed_index, visually_complete, time_to_interactive, event_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"clicks": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, hesitation_time, event_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"inputs": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, event_type) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
"inputs": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, event_type, duration, hesitation_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"errors": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, source, name, message, error_id, event_type, error_tags_keys, error_tags_values) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"performance": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_total_js_heap_size, avg_total_js_heap_size, max_total_js_heap_size, min_used_js_heap_size, avg_used_js_heap_size, max_used_js_heap_size, event_type) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"requests": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, url, request_body, response_body, status, method, duration, success, event_type) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?)",
|
||||
|
|
@ -164,11 +166,59 @@ func (c *connectorImpl) checkError(name string, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *connectorImpl) InsertWebInputDuration(session *types.Session, msg *messages.InputChange) error {
|
||||
if msg.Label == "" {
|
||||
return nil
|
||||
}
|
||||
if err := c.batches["inputs"].Append(
|
||||
session.SessionID,
|
||||
uint16(session.ProjectID),
|
||||
msg.MsgID(),
|
||||
datetime(msg.Timestamp),
|
||||
msg.Label,
|
||||
"INPUT",
|
||||
nullableUint16(uint16(msg.InputDuration)),
|
||||
nullableUint32(uint32(msg.HesitationTime)),
|
||||
); err != nil {
|
||||
c.checkError("inputs", err)
|
||||
return fmt.Errorf("can't append to inputs batch: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *connectorImpl) InsertMouseThrashing(session *types.Session, msg *messages.MouseThrashing) error {
|
||||
issueID := hashid.MouseThrashingID(session.ProjectID, session.SessionID, msg.Timestamp)
|
||||
// Insert issue event to batches
|
||||
if err := c.batches["issuesEvents"].Append(
|
||||
session.SessionID,
|
||||
uint16(session.ProjectID),
|
||||
msg.MsgID(),
|
||||
datetime(msg.Timestamp),
|
||||
issueID,
|
||||
"mouse_thrashing",
|
||||
"ISSUE",
|
||||
msg.Url,
|
||||
); err != nil {
|
||||
c.checkError("issuesEvents", err)
|
||||
return fmt.Errorf("can't append to issuesEvents batch: %s", err)
|
||||
}
|
||||
if err := c.batches["issues"].Append(
|
||||
uint16(session.ProjectID),
|
||||
issueID,
|
||||
"mouse_thrashing",
|
||||
msg.Url,
|
||||
); err != nil {
|
||||
c.checkError("issues", err)
|
||||
return fmt.Errorf("can't append to issues batch: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *connectorImpl) InsertIssue(session *types.Session, msg *messages.IssueEvent) error {
|
||||
issueID := hashid.IssueID(session.ProjectID, msg)
|
||||
// Check issue type before insert to avoid panic from clickhouse lib
|
||||
switch msg.Type {
|
||||
case "click_rage", "dead_click", "excessive_scrolling", "bad_request", "missing_resource", "memory", "cpu", "slow_resource", "slow_page_load", "crash", "ml_cpu", "ml_memory", "ml_dead_click", "ml_click_rage", "ml_mouse_thrashing", "ml_excessive_scrolling", "ml_slow_resources", "custom", "js_exception":
|
||||
case "click_rage", "dead_click", "excessive_scrolling", "bad_request", "missing_resource", "memory", "cpu", "slow_resource", "slow_page_load", "crash", "ml_cpu", "ml_memory", "ml_dead_click", "ml_click_rage", "ml_mouse_thrashing", "ml_excessive_scrolling", "ml_slow_resources", "custom", "js_exception", "mouse_thrashing":
|
||||
default:
|
||||
return fmt.Errorf("unknown issueType: %s", msg.Type)
|
||||
}
|
||||
|
|
@ -323,6 +373,8 @@ func (c *connectorImpl) InsertWebInputEvent(session *types.Session, msg *message
|
|||
datetime(msg.Timestamp),
|
||||
msg.Label,
|
||||
"INPUT",
|
||||
nil,
|
||||
nil,
|
||||
); err != nil {
|
||||
c.checkError("inputs", err)
|
||||
return fmt.Errorf("can't append to inputs batch: %s", err)
|
||||
|
|
|
|||
|
|
@ -737,6 +737,34 @@ class SessionSearch(Message):
|
|||
self.partition = partition
|
||||
|
||||
|
||||
class InputChange(Message):
|
||||
__id__ = 112
|
||||
|
||||
def __init__(self, id, value, value_masked, label, hesitation_time, input_duration):
|
||||
self.id = id
|
||||
self.value = value
|
||||
self.value_masked = value_masked
|
||||
self.label = label
|
||||
self.hesitation_time = hesitation_time
|
||||
self.input_duration = input_duration
|
||||
|
||||
|
||||
class SelectionChange(Message):
|
||||
__id__ = 113
|
||||
|
||||
def __init__(self, selection_start, selection_end, selection):
|
||||
self.selection_start = selection_start
|
||||
self.selection_end = selection_end
|
||||
self.selection = selection
|
||||
|
||||
|
||||
class MouseThrashing(Message):
|
||||
__id__ = 114
|
||||
|
||||
def __init__(self, timestamp):
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
class IOSBatchMeta(Message):
|
||||
__id__ = 107
|
||||
|
||||
|
|
|
|||
|
|
@ -653,6 +653,28 @@ class MessageCodec(Codec):
|
|||
partition=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 112:
|
||||
return InputChange(
|
||||
id=self.read_uint(reader),
|
||||
value=self.read_string(reader),
|
||||
value_masked=self.read_boolean(reader),
|
||||
label=self.read_string(reader),
|
||||
hesitation_time=self.read_int(reader),
|
||||
input_duration=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 113:
|
||||
return SelectionChange(
|
||||
selection_start=self.read_uint(reader),
|
||||
selection_end=self.read_uint(reader),
|
||||
selection=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 114:
|
||||
return MouseThrashing(
|
||||
timestamp=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 107:
|
||||
return IOSBatchMeta(
|
||||
timestamp=self.read_uint(reader),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import cn from 'classnames';
|
||||
import { Icon, TextEllipsis } from 'UI';
|
||||
import { Icon, TextEllipsis, Tooltip } from 'UI';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { prorata } from 'App/utils';
|
||||
import withOverlay from 'Components/hocs/withOverlay';
|
||||
|
|
@ -9,6 +9,16 @@ import LoadInfo from './LoadInfo';
|
|||
import cls from './event.module.css';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
function isFrustrationEvent(evt) {
|
||||
if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE) {
|
||||
return true;
|
||||
}
|
||||
if (evt.type === TYPES.CLICK || evt.type === TYPES.INPUT) {
|
||||
return evt.hesitation > 1000
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@withOverlay()
|
||||
export default class Event extends React.PureComponent {
|
||||
state = {
|
||||
|
|
@ -44,35 +54,50 @@ export default class Event extends React.PureComponent {
|
|||
const { event } = this.props;
|
||||
let title = event.type;
|
||||
let body;
|
||||
let icon;
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
const tooltip = { disabled: true, text: '' }
|
||||
|
||||
switch (event.type) {
|
||||
case TYPES.LOCATION:
|
||||
title = 'Visited';
|
||||
body = event.url;
|
||||
icon = 'location';
|
||||
break;
|
||||
case TYPES.CLICK:
|
||||
title = 'Clicked';
|
||||
body = event.label;
|
||||
icon = isFrustration ? 'click_hesitation' : 'click';
|
||||
isFrustration ? Object.assign(tooltip, { disabled: false, text: `User hesitated to click for ${Math.round(event.hesitation/1000)}s`, }) : null;
|
||||
break;
|
||||
case TYPES.INPUT:
|
||||
title = 'Input';
|
||||
body = event.value;
|
||||
icon = isFrustration ? 'input_hesitation' : 'input';
|
||||
isFrustration ? Object.assign(tooltip, { disabled: false, text: `User hesitated to enter a value for ${Math.round(event.hesitation/1000)}s`, }) : null;
|
||||
break;
|
||||
case TYPES.CLICKRAGE:
|
||||
title = `${ event.count } Clicks`;
|
||||
body = event.label;
|
||||
icon = 'clickrage'
|
||||
break;
|
||||
case TYPES.IOS_VIEW:
|
||||
title = 'View';
|
||||
body = event.name;
|
||||
icon = 'ios_view'
|
||||
break;
|
||||
case 'mouse_thrashing':
|
||||
title = 'Mouse Thrashing';
|
||||
icon = 'mouse_thrashing'
|
||||
break;
|
||||
}
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
const isClickrage = event.type === TYPES.CLICKRAGE;
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip.text} disabled={tooltip.disabled} placement={"left"} anchorClassName={"w-full"} containerClassName={"w-full"}>
|
||||
<div className={ cn(cls.main, 'flex flex-col w-full') } >
|
||||
<div className="flex items-center w-full">
|
||||
{ event.type && <Icon name={`event/${event.type.toLowerCase()}`} size="16" color={isClickrage? 'red' : 'gray-dark' } /> }
|
||||
{ event.type && <Icon name={`event/${icon}`} size="16" color={'gray-dark' } /> }
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex items-center w-full" style={{ minWidth: '0'}}>
|
||||
|
|
@ -100,6 +125,7 @@ export default class Event extends React.PureComponent {
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -110,17 +136,15 @@ export default class Event extends React.PureComponent {
|
|||
isCurrent,
|
||||
onClick,
|
||||
showSelection,
|
||||
onCheckboxClick,
|
||||
showLoadInfo,
|
||||
toggleLoadInfo,
|
||||
isRed,
|
||||
extended,
|
||||
highlight = false,
|
||||
presentInSearch = false,
|
||||
isLastInGroup,
|
||||
whiteBg,
|
||||
} = this.props;
|
||||
const { menuOpen } = this.state;
|
||||
|
||||
const isFrustration = isFrustrationEvent(event);
|
||||
return (
|
||||
<div
|
||||
ref={ ref => { this.wrapper = ref } }
|
||||
|
|
@ -135,7 +159,7 @@ export default class Event extends React.PureComponent {
|
|||
[ cls.red ]: isRed,
|
||||
[ cls.clickType ]: event.type === TYPES.CLICK,
|
||||
[ cls.inputType ]: event.type === TYPES.INPUT,
|
||||
[ cls.clickrageType ]: event.type === TYPES.CLICKRAGE,
|
||||
[ cls.frustration ]: isFrustration,
|
||||
[ cls.highlight ] : presentInSearch,
|
||||
[ cls.lastInGroup ]: whiteBg,
|
||||
}) }
|
||||
|
|
@ -146,13 +170,10 @@ export default class Event extends React.PureComponent {
|
|||
{ event.target ? 'Copy CSS' : 'Copy URL' }
|
||||
</button>
|
||||
}
|
||||
<div className={ cls.topBlock }>
|
||||
<div className={ cls.firstLine }>
|
||||
<div className={ cn(cls.topBlock, 'w-full') }>
|
||||
<div className={ cn(cls.firstLine, 'w-full') }>
|
||||
{ this.renderBody() }
|
||||
</div>
|
||||
{/* { event.type === TYPES.LOCATION &&
|
||||
<div className="text-sm font-normal color-gray-medium">{event.url}</div>
|
||||
} */}
|
||||
</div>
|
||||
{ event.type === TYPES.LOCATION && (event.fcpTime || event.visuallyComplete || event.timeToInteractive) &&
|
||||
<LoadInfo
|
||||
|
|
|
|||
|
|
@ -141,6 +141,13 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.frustration {
|
||||
background-color: rgba(204, 0, 0, 0.1)!important;
|
||||
box-shadow:
|
||||
2px 2px 1px 1px white,
|
||||
2px 2px 0px 1px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.clickrageType {
|
||||
background-color: #FFF3F3;
|
||||
border: 1px solid #CC0000;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
|
|||
import { toggleBottomBlock } from 'Duck/components/player';
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import EventRow from './components/EventRow';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { connect } from 'react-redux';
|
||||
import TimelineScale from './components/TimelineScale';
|
||||
import FeatureSelection, { HELP_MESSAGE } from './components/FeatureSelection/FeatureSelection';
|
||||
|
|
@ -28,6 +27,7 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
performanceChartData,
|
||||
stackList: stackEventList,
|
||||
eventList: eventsList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
resourceList: resourceListUnmap,
|
||||
fetchList,
|
||||
|
|
@ -46,8 +46,8 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
NETWORK: resourceList,
|
||||
ERRORS: exceptionsList,
|
||||
EVENTS: stackEventList,
|
||||
CLICKRAGE: eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE),
|
||||
PERFORMANCE: performanceChartData,
|
||||
FRUSTRATIONS: frustrationsList,
|
||||
};
|
||||
}, [dataLoaded]);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import { Checkbox, Tooltip } from 'UI';
|
|||
const NETWORK = 'NETWORK';
|
||||
const ERRORS = 'ERRORS';
|
||||
const EVENTS = 'EVENTS';
|
||||
const CLICKRAGE = 'CLICKRAGE';
|
||||
const FRUSTRATIONS = 'FRUSTRATIONS';
|
||||
const PERFORMANCE = 'PERFORMANCE';
|
||||
|
||||
export const HELP_MESSAGE: any = {
|
||||
NETWORK: 'Network requests made in this session',
|
||||
EVENTS: 'Visualizes the events that takes place in the DOM',
|
||||
ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.',
|
||||
CLICKRAGE: 'Indicates user frustration when repeated clicks are recorded',
|
||||
PERFORMANCE: 'Summary of this session’s memory, and CPU consumption on the timeline',
|
||||
FRUSTRATIONS: 'Indicates user frustrations in the session',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
|
@ -21,7 +21,7 @@ interface Props {
|
|||
}
|
||||
function FeatureSelection(props: Props) {
|
||||
const { list } = props;
|
||||
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
|
||||
const features = [NETWORK, ERRORS, EVENTS, PERFORMANCE, FRUSTRATIONS];
|
||||
const disabled = list.length >= 5;
|
||||
|
||||
return (
|
||||
|
|
@ -30,7 +30,7 @@ function FeatureSelection(props: Props) {
|
|||
const checked = list.includes(feature);
|
||||
const _disabled = disabled && !checked;
|
||||
return (
|
||||
<Tooltip key={index} title="X-RAY supports up to 3 views" disabled={!_disabled} delay={0}>
|
||||
<Tooltip key={index} title="X-RAY supports up to 5 views" disabled={!_disabled} delay={0}>
|
||||
<Checkbox
|
||||
key={index}
|
||||
label={feature}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorD
|
|||
import FetchDetails from 'Shared/FetchDetailsModal';
|
||||
import GraphQLDetailsModal from 'Shared/GraphQLDetailsModal';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { TYPES } from 'App/types/session/event'
|
||||
import { types as issueTypes } from 'App/types/session/issue'
|
||||
|
||||
interface Props {
|
||||
pointer: any;
|
||||
type: any;
|
||||
type: 'ERRORS' | 'EVENT' | 'NETWORK' | 'FRUSTRATIONS' | 'EVENTS' | 'PERFORMANCE';
|
||||
noClick?: boolean;
|
||||
fetchPresented?: boolean;
|
||||
}
|
||||
|
|
@ -71,19 +73,25 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderClickRageElement = (item: any) => {
|
||||
const renderFrustrationElement = (item: any) => {
|
||||
const elData = { name: '', icon: ''}
|
||||
if (item.type === TYPES.CLICK) Object.assign(elData, { name: `User hesitated to click for ${Math.round(item.hesitation/1000)}s`, icon: 'click-hesitation' })
|
||||
if (item.type === TYPES.INPUT) Object.assign(elData, { name: `User hesitated to enter a value for ${Math.round(item.hesitation/1000)}s`, icon: 'input-hesitation' })
|
||||
if (item.type === TYPES.CLICKRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' })
|
||||
if (item.type === issueTypes.MOUSE_THRASHING) Object.assign(elData, { name: 'Mouse Thrashing', icon: 'cursor-trash' })
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<div className="">
|
||||
<b>{'Click Rage'}</b>
|
||||
<b>{elData.name}</b>
|
||||
</div>
|
||||
}
|
||||
delay={0}
|
||||
placement="top"
|
||||
>
|
||||
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
|
||||
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
|
||||
<Icon name={elData.icon} color="black" size="16" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -158,8 +166,8 @@ const TimelinePointer = React.memo((props: Props) => {
|
|||
if (type === 'NETWORK') {
|
||||
return renderNetworkElement(pointer);
|
||||
}
|
||||
if (type === 'CLICKRAGE') {
|
||||
return renderClickRageElement(pointer);
|
||||
if (type === 'FRUSTRATIONS') {
|
||||
return renderFrustrationElement(pointer);
|
||||
}
|
||||
if (type === 'ERRORS') {
|
||||
return renderExceptionElement(pointer);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ function Tooltip(props: Props) {
|
|||
placement,
|
||||
className = '',
|
||||
anchorClassName = '',
|
||||
containerClassName = '',
|
||||
delay = 500,
|
||||
style = {},
|
||||
offset = 5,
|
||||
|
|
@ -39,7 +40,7 @@ function Tooltip(props: Props) {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={cn("relative", containerClassName)}>
|
||||
<TooltipAnchor className={anchorClassName} state={state}>{props.children}</TooltipAnchor>
|
||||
<FloatingTooltip
|
||||
state={state}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ListWalkerWithMarks from '../common/ListWalkerWithMarks';
|
|||
import type { Timed } from '../common/types';
|
||||
|
||||
|
||||
const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const
|
||||
const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles", "frustrations"] as const
|
||||
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const
|
||||
//const entityNamesSimple = [ "event", "profile" ];
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import WindowNodeCounter from './managers/WindowNodeCounter';
|
|||
import ActivityManager from './managers/ActivityManager';
|
||||
|
||||
import MFileReader from './messages/MFileReader';
|
||||
import { MType } from './messages';
|
||||
import { MouseThrashing, MType } from "./messages";
|
||||
import { isDOMType } from './messages/filters.gen';
|
||||
import type {
|
||||
Message,
|
||||
|
|
@ -100,6 +100,7 @@ export default class MessageManager {
|
|||
private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager();
|
||||
private windowNodeCounter: WindowNodeCounter = new WindowNodeCounter();
|
||||
private clickManager: ListWalker<MouseClick> = new ListWalker();
|
||||
private mouseThrashingManager: ListWalker<MouseThrashing> = new ListWalker();
|
||||
|
||||
private resizeManager: ListWalker<SetViewportSize> = new ListWalker([]);
|
||||
private pagesManager: PagesManager;
|
||||
|
|
@ -345,9 +346,13 @@ export default class MessageManager {
|
|||
// Moving mouse and setting :hover classes on ready view
|
||||
this.mouseMoveManager.move(t);
|
||||
const lastClick = this.clickManager.moveGetLast(t);
|
||||
if (!!lastClick && t - lastClick.time < 600) { // happend during last 600ms
|
||||
if (!!lastClick && t - lastClick.time < 600) { // happened during last 600ms
|
||||
this.screen.cursor.click();
|
||||
}
|
||||
const lastThrashing = this.mouseThrashingManager.moveGetLast(t)
|
||||
if (!!lastThrashing && t - lastThrashing.time < 300) {
|
||||
this.screen.cursor.shake();
|
||||
}
|
||||
})
|
||||
|
||||
if (this.waitingForFiles && this.lastMessageTime <= t && t !== this.session.duration.milliseconds) {
|
||||
|
|
@ -388,6 +393,9 @@ export default class MessageManager {
|
|||
case MType.SetViewportSize:
|
||||
this.resizeManager.append(msg);
|
||||
break;
|
||||
case MType.MouseThrashing:
|
||||
this.mouseThrashingManager.append(msg);
|
||||
break;
|
||||
case MType.MouseMove:
|
||||
this.mouseMoveManager.append(msg);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import styles from './cursor.module.css';
|
|||
|
||||
|
||||
export default class Cursor {
|
||||
private readonly isMobile: boolean;
|
||||
private readonly cursor: HTMLDivElement;
|
||||
private tagElement: HTMLDivElement;
|
||||
private isMobile: boolean;
|
||||
private coords = { x: 0, y: 0 };
|
||||
private isMoving = false;
|
||||
|
||||
constructor(overlay: HTMLDivElement, isMobile: boolean) {
|
||||
this.cursor = document.createElement('div');
|
||||
|
|
@ -50,8 +52,24 @@ export default class Cursor {
|
|||
}
|
||||
|
||||
move({ x, y }: Point) {
|
||||
this.isMoving = true;
|
||||
this.cursor.style.left = x + 'px';
|
||||
this.cursor.style.top = y + 'px';
|
||||
this.coords = { x, y };
|
||||
setTimeout(() => this.isMoving = false, 60)
|
||||
}
|
||||
|
||||
setDefaultStyle() {
|
||||
this.cursor.style.width = 18 + 'px'
|
||||
this.cursor.style.height = 30 + 'px'
|
||||
this.cursor.style.transition = 'top .125s linear, left .125s linear'
|
||||
}
|
||||
|
||||
shake() {
|
||||
this.cursor.classList.add(styles.shaking)
|
||||
setTimeout(() => {
|
||||
this.cursor.classList.remove(styles.shaking)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
click() {
|
||||
|
|
@ -62,7 +80,7 @@ export default class Cursor {
|
|||
}, 600)
|
||||
}
|
||||
|
||||
// TODO (to keep on a different playig speed):
|
||||
// TODO (to keep on a different playing speed):
|
||||
// transition
|
||||
// setTransitionSpeed()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
.cursor {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 13px;
|
||||
height: 20px;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M302.189 329.126H196.105l55.831 135.993c3.889 9.428-.555 19.999-9.444 23.999l-49.165 21.427c-9.165 4-19.443-.571-23.332-9.714l-53.053-129.136-86.664 89.138C18.729 472.71 0 463.554 0 447.977V18.299C0 1.899 19.921-6.096 30.277 5.443l284.412 292.542c11.472 11.179 3.007 31.141-12.5 31.141z"/></svg>');
|
||||
width: 18px;
|
||||
height: 30px;
|
||||
background-image: url('data:image/svg+xml;utf8, <svg viewBox="0 0 486 647" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M400.189 402.126H294.105L349.936 538.119C353.825 547.547 349.381 558.118 340.492 562.118L291.327 583.545C282.162 587.545 271.884 582.974 267.995 573.831L214.942 444.695L128.278 533.833C116.729 545.71 98 536.554 98 520.977V91.299C98 74.899 117.921 66.904 128.277 78.443L412.689 370.985C424.161 382.164 415.696 402.126 400.189 402.126Z" fill="black"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
transition: top .125s linear, left .125s linear;
|
||||
|
||||
|
|
@ -108,3 +108,29 @@
|
|||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.cursor.shaking {
|
||||
width: 45px;
|
||||
height: 75px;
|
||||
-webkit-animation: shaking 0.3s linear;
|
||||
animation: shaking 0.3s linear;
|
||||
animation-iteration-count: 2;
|
||||
}
|
||||
|
||||
@keyframes shaking {
|
||||
0% {
|
||||
transform: translate(60px, -60px);
|
||||
}
|
||||
25% {
|
||||
transform: translate(-60px, 60px);
|
||||
}
|
||||
50% {
|
||||
transform: translate(60px, -60px);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-60px, 60px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(60px, -60px);
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ export default class WebPlayer extends Player {
|
|||
let initialLists = live ? {} : {
|
||||
event: session.events || [],
|
||||
stack: session.stackEvents || [],
|
||||
frustrations: session.frustrations || [],
|
||||
exceptions: session.errors?.map(({ name, ...rest }: any) =>
|
||||
Log({
|
||||
level: LogLevel.ERROR,
|
||||
|
|
|
|||
|
|
@ -2,25 +2,24 @@ import logger from 'App/logger';
|
|||
|
||||
import type Screen from '../../Screen/Screen';
|
||||
import type { Message, SetNodeScroll } from '../../messages';
|
||||
|
||||
import { MType } from '../../messages';
|
||||
import ListWalker from '../../../common/ListWalker';
|
||||
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
|
||||
import FocusManager from './FocusManager';
|
||||
import {
|
||||
VElement,
|
||||
VText,
|
||||
VShadowRoot,
|
||||
VDocument,
|
||||
VNode,
|
||||
VStyleElement,
|
||||
PostponedStyleSheet,
|
||||
} from './VirtualDOM';
|
||||
import SelectionManager from './SelectionManager';
|
||||
import type { StyleElement } from './VirtualDOM';
|
||||
import { insertRule, deleteRule } from './safeCSSRules';
|
||||
import {
|
||||
PostponedStyleSheet,
|
||||
VDocument,
|
||||
VElement,
|
||||
VNode,
|
||||
VShadowRoot,
|
||||
VStyleElement,
|
||||
VText,
|
||||
} from './VirtualDOM';
|
||||
import { deleteRule, insertRule } from './safeCSSRules';
|
||||
|
||||
|
||||
type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
|
||||
const IGNORED_ATTRS = [ "autocomplete" ];
|
||||
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
|
||||
|
|
@ -50,7 +49,7 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
private nodeScrollManagers: Map<number, ListWalker<SetNodeScroll>> = new Map()
|
||||
private stylesManager: StylesManager
|
||||
private focusManager: FocusManager = new FocusManager(this.vElements)
|
||||
|
||||
private selectionManager: SelectionManager
|
||||
|
||||
constructor(
|
||||
private readonly screen: Screen,
|
||||
|
|
@ -59,6 +58,7 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
setCssLoading: ConstructorParameters<typeof StylesManager>[1],
|
||||
) {
|
||||
super()
|
||||
this.selectionManager = new SelectionManager(this.vElements, screen)
|
||||
this.stylesManager = new StylesManager(screen, setCssLoading)
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +76,10 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
this.focusManager.append(m)
|
||||
return
|
||||
}
|
||||
if (m.tp === MType.SelectionChange) {
|
||||
this.selectionManager.append(m)
|
||||
return
|
||||
}
|
||||
if (m.tp === MType.CreateElementNode) {
|
||||
if(m.tag === "BODY" && this.upperBodyId === -1) {
|
||||
this.upperBodyId = m.id
|
||||
|
|
@ -287,7 +291,7 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
}
|
||||
return
|
||||
|
||||
// @depricated since 4.0.2 in favor of adopted_ss_insert/delete_rule + add_owner as being common case for StyleSheets
|
||||
// @deprecated since 4.0.2 in favor of adopted_ss_insert/delete_rule + add_owner as being common case for StyleSheets
|
||||
case MType.CssInsertRule:
|
||||
vn = this.vElements.get(msg.id)
|
||||
if (!vn) { logger.error("Node not found", msg); return }
|
||||
|
|
@ -306,7 +310,7 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
}
|
||||
vn.onStyleSheet(sheet => deleteRule(sheet, msg))
|
||||
return
|
||||
// end @depricated
|
||||
// end @deprecated
|
||||
|
||||
case MType.CreateIFrameDocument:
|
||||
vn = this.vElements.get(msg.frameID)
|
||||
|
|
@ -432,6 +436,7 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
return this.stylesManager.moveReady(t).then(() => {
|
||||
// Apply focus
|
||||
this.focusManager.move(t)
|
||||
this.selectionManager.move(t)
|
||||
// Apply all scrolls after the styles got applied
|
||||
this.nodeScrollManagers.forEach(manager => {
|
||||
const msg = manager.moveGetLast(t)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { SetNodeFocus } from '../../messages';
|
|||
import type { VElement } from './VirtualDOM';
|
||||
import ListWalker from '../../../common/ListWalker';
|
||||
|
||||
|
||||
const FOCUS_CLASS = "-openreplay-focus"
|
||||
|
||||
export default class FocusManager extends ListWalker<SetNodeFocus> {
|
||||
|
|
|
|||
75
frontend/app/player/web/managers/DOM/SelectionManager.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { SelectionChange } from '../../messages';
|
||||
import type { VElement } from './VirtualDOM';
|
||||
import ListWalker from '../../../common/ListWalker';
|
||||
import Screen from 'Player/web/Screen/Screen';
|
||||
|
||||
export default class SelectionManager extends ListWalker<SelectionChange> {
|
||||
constructor(private readonly vElements: Map<number, VElement>, private readonly screen: Screen) {
|
||||
super();
|
||||
}
|
||||
|
||||
private selected: [{ id: number, node: Element } | null, { id: number, node: Element } | null] = [null, null];
|
||||
private markers: Element[] = []
|
||||
|
||||
clearSelection() {
|
||||
this.selected[0] && this.screen.overlay.removeChild(this.selected[0].node) && this.selected[0].node.remove();
|
||||
this.selected[1] && this.screen.overlay.removeChild(this.selected[1].node) && this.selected[1].node.remove();
|
||||
this.markers.forEach(marker => marker.remove())
|
||||
|
||||
this.selected = [null, null];
|
||||
this.markers = [];
|
||||
}
|
||||
|
||||
move(t: number) {
|
||||
const msg = this.moveGetLast(t);
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
// in theory: empty selection or selection removed
|
||||
if (msg.selectionStart <= 0) {
|
||||
this.clearSelection()
|
||||
return;
|
||||
}
|
||||
// preventing clones
|
||||
if ((this.selected[0] && this.selected[0].id === msg.selectionStart) && (this.selected[1] && this.selected[1].id === msg.selectionEnd)) return;
|
||||
|
||||
const startVNode = this.vElements.get(msg.selectionStart - 1);
|
||||
const endVNode = this.vElements.get(msg.selectionEnd - 1);
|
||||
|
||||
// only one selection present on page at the same time
|
||||
if (this.selected[0] && this.selected[0]?.id !== msg.selectionStart) this.clearSelection()
|
||||
|
||||
if (startVNode && endVNode) {
|
||||
const startCoords = startVNode.node.getBoundingClientRect();
|
||||
const endCoords = endVNode.node.getBoundingClientRect();
|
||||
|
||||
const startPointer = document.createElement('div');
|
||||
const endPointer = document.createElement('div');
|
||||
|
||||
Object.assign(endPointer.style, {
|
||||
top: endCoords.top + 'px',
|
||||
left: (endCoords.left + (endCoords.width / 2) + 3) + 'px',
|
||||
width: (endCoords.width / 2) + 'px',
|
||||
height: endCoords.height + 'px',
|
||||
borderRight: '2px solid blue',
|
||||
position: 'absolute',
|
||||
boxShadow: '1px 4px 1px -2px blue',
|
||||
});
|
||||
Object.assign(startPointer.style, {
|
||||
top: startCoords.top + 'px',
|
||||
left: (startCoords.left - 3) + 'px',
|
||||
width: (startCoords.width / 2 ) + 'px',
|
||||
height: startCoords.height + 'px',
|
||||
borderLeft: '2px solid blue',
|
||||
position: 'absolute',
|
||||
boxShadow: '1px 4px 1px -2px blue',
|
||||
});
|
||||
|
||||
this.markers.push(startPointer, endPointer);
|
||||
this.screen.overlay.appendChild(startPointer);
|
||||
this.screen.overlay.appendChild(endPointer);
|
||||
|
||||
this.selected = [{ id: msg.selectionStart, node: startPointer }, { id: msg.selectionEnd, node: endPointer }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ export class VElement extends VParent {
|
|||
type StyleSheetCallback = (s: CSSStyleSheet) => void
|
||||
export type StyleElement = HTMLStyleElement | SVGStyleElement
|
||||
|
||||
// @Depricated TODO: remove in favor of PostponedStyleSheet
|
||||
// @deprecated TODO: remove in favor of PostponedStyleSheet
|
||||
export class VStyleElement extends VElement {
|
||||
private loaded = false
|
||||
private stylesheetCallbacks: StyleSheetCallback[] = []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
.openreplay-selection-start {
|
||||
border: 2px solid red;
|
||||
}
|
||||
|
||||
.openreplay-selection-end {
|
||||
border: 2px solid red;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type Screen from '../Screen/Screen'
|
||||
import type { MouseMove } from '../messages'
|
||||
import type { MouseMove } from "../messages";
|
||||
|
||||
import ListWalker from '../../common/ListWalker'
|
||||
|
||||
|
|
|
|||
|
|
@ -627,6 +627,26 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 113: {
|
||||
const selectionStart = this.readUint(); if (selectionStart === null) { return resetPointer() }
|
||||
const selectionEnd = this.readUint(); if (selectionEnd === null) { return resetPointer() }
|
||||
const selection = this.readString(); if (selection === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.SelectionChange,
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selection,
|
||||
};
|
||||
}
|
||||
|
||||
case 114: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.MouseThrashing,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
case 90: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const projectID = this.readUint(); if (projectID === null) { return resetPointer() }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { MType } from './raw.gen'
|
||||
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,90,93,96,100,102,103,105]
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,90,93,96,100,102,103,105]
|
||||
export function isDOMType(t: MType) {
|
||||
return DOM_TYPES.includes(t)
|
||||
}
|
||||
|
|
@ -55,6 +55,8 @@ import type {
|
|||
RawAdoptedSsAddOwner,
|
||||
RawAdoptedSsRemoveOwner,
|
||||
RawZustand,
|
||||
RawSelectionChange,
|
||||
RawMouseThrashing,
|
||||
RawIosSessionStart,
|
||||
RawIosCustomEvent,
|
||||
RawIosScreenChanges,
|
||||
|
|
@ -169,6 +171,10 @@ export type AdoptedSsRemoveOwner = RawAdoptedSsRemoveOwner & Timed
|
|||
|
||||
export type Zustand = RawZustand & Timed
|
||||
|
||||
export type SelectionChange = RawSelectionChange & Timed
|
||||
|
||||
export type MouseThrashing = RawMouseThrashing & Timed
|
||||
|
||||
export type IosSessionStart = RawIosSessionStart & Timed
|
||||
|
||||
export type IosCustomEvent = RawIosCustomEvent & Timed
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export const enum MType {
|
|||
AdoptedSsAddOwner = 76,
|
||||
AdoptedSsRemoveOwner = 77,
|
||||
Zustand = 79,
|
||||
SelectionChange = 113,
|
||||
MouseThrashing = 114,
|
||||
IosSessionStart = 90,
|
||||
IosCustomEvent = 93,
|
||||
IosScreenChanges = 96,
|
||||
|
|
@ -418,6 +420,18 @@ export interface RawZustand {
|
|||
state: string,
|
||||
}
|
||||
|
||||
export interface RawSelectionChange {
|
||||
tp: MType.SelectionChange,
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selection: string,
|
||||
}
|
||||
|
||||
export interface RawMouseThrashing {
|
||||
tp: MType.MouseThrashing,
|
||||
timestamp: number,
|
||||
}
|
||||
|
||||
export interface RawIosSessionStart {
|
||||
tp: MType.IosSessionStart,
|
||||
timestamp: number,
|
||||
|
|
@ -489,4 +503,4 @@ export interface RawIosNetworkCall {
|
|||
}
|
||||
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequest | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTiming | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequest | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTiming | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawSelectionChange | RawMouseThrashing | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ export const TP_MAP = {
|
|||
76: MType.AdoptedSsAddOwner,
|
||||
77: MType.AdoptedSsRemoveOwner,
|
||||
79: MType.Zustand,
|
||||
113: MType.SelectionChange,
|
||||
114: MType.MouseThrashing,
|
||||
90: MType.IosSessionStart,
|
||||
93: MType.IosCustomEvent,
|
||||
96: MType.IosScreenChanges,
|
||||
|
|
|
|||
|
|
@ -429,8 +429,30 @@ type TrPartitionedMessage = [
|
|||
partTotal: number,
|
||||
]
|
||||
|
||||
type TrInputChange = [
|
||||
type: 112,
|
||||
id: number,
|
||||
value: string,
|
||||
valueMasked: boolean,
|
||||
label: string,
|
||||
hesitationTime: number,
|
||||
inputDuration: number,
|
||||
]
|
||||
|
||||
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequest | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage
|
||||
type TrSelectionChange = [
|
||||
type: 113,
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selection: string,
|
||||
]
|
||||
|
||||
type TrMouseThrashing = [
|
||||
type: 114,
|
||||
timestamp: number,
|
||||
]
|
||||
|
||||
|
||||
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequest | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTiming | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrInputChange | TrSelectionChange | TrMouseThrashing
|
||||
|
||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||
switch(tMsg[0]) {
|
||||
|
|
@ -867,6 +889,22 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 113: {
|
||||
return {
|
||||
tp: MType.SelectionChange,
|
||||
selectionStart: tMsg[1],
|
||||
selectionEnd: tMsg[2],
|
||||
selection: tMsg[3],
|
||||
}
|
||||
}
|
||||
|
||||
case 114: {
|
||||
return {
|
||||
tp: MType.MouseThrashing,
|
||||
timestamp: tMsg[1],
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
15
frontend/app/svg/icons/click-hesitation.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg viewBox="0 0 33 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="32" height="32" fill="transparent" transform="translate(1 1.5)"/>
|
||||
|
||||
<g>
|
||||
<rect width="27.6335" height="27.6335" fill="transparent" transform="translate(0.640625 5.68311)"/>
|
||||
<path d="M12.2974 7.4102C12.641 7.4102 12.9704 7.54667 13.2133 7.78959C13.4563 8.03251 13.5927 8.36198 13.5927 8.70552V19.4999C13.5927 19.7289 13.6837 19.9486 13.8457 20.1105C14.0076 20.2724 14.2273 20.3634 14.4563 20.3634C14.6853 20.3634 14.905 20.2724 15.0669 20.1105C15.2289 19.9486 15.3198 19.7289 15.3198 19.4999V15.1251L15.4684 15.1182C16.0159 15.0975 16.5685 15.1044 16.8777 15.1649C17.1091 15.2115 17.3854 15.3307 17.6514 15.4792C17.7844 15.5517 17.9105 15.7331 17.9105 16.0215V19.4999C17.9105 19.7289 18.0015 19.9486 18.1634 20.1105C18.3254 20.2724 18.545 20.3634 18.774 20.3634C19.0031 20.3634 19.2227 20.2724 19.3846 20.1105C19.5466 19.9486 19.6376 19.7289 19.6376 19.4999V16.797C19.6985 16.7906 19.7596 16.7848 19.8206 16.7797C20.3664 16.7383 20.8293 16.7624 21.0434 16.8488C21.2472 16.9282 21.5616 17.2063 21.8828 17.5914C22.0227 17.7572 22.1419 17.9196 22.2282 18.0388V20.3634C22.2282 20.5925 22.3192 20.8121 22.4811 20.974C22.6431 21.136 22.8627 21.227 23.0918 21.227C23.3208 21.227 23.5404 21.136 23.7024 20.974C23.8643 20.8121 23.9553 20.5925 23.9553 20.3634V18.6363H24.546C24.7874 18.6363 25.0261 18.6869 25.2467 18.7848C25.4673 18.8828 25.6649 19.0258 25.8268 19.2048C25.9888 19.3839 26.1114 19.5948 26.1867 19.8241C26.2621 20.0534 26.2886 20.296 26.2644 20.5361L25.7964 25.2252C25.736 25.8278 25.5494 26.4109 25.2489 26.9368L22.8396 31.1543C22.7641 31.2865 22.655 31.3964 22.5234 31.4728C22.3918 31.5492 22.2423 31.5895 22.0901 31.5896H11.0625C10.9203 31.5895 10.7803 31.5543 10.655 31.4871C10.5297 31.4199 10.4229 31.3228 10.3441 31.2044L7.86914 27.4912C7.63935 27.1468 7.49543 26.7523 7.44946 26.3409L6.85361 20.9783C6.82952 20.7634 6.88696 20.5472 7.01456 20.3726C7.14216 20.198 7.33064 20.0776 7.54272 20.0353L9.275 19.6899V21.227C9.275 21.456 9.36598 21.6756 9.52792 21.8376C9.68987 21.9995 9.90952 22.0905 10.1385 22.0905C10.3676 22.0905 10.5872 21.9995 10.7492 21.8376C10.9111 21.6756 11.0021 21.456 11.0021 21.227V8.70552C11.0021 8.36198 11.1386 8.03251 11.3815 7.78959C11.6244 7.54667 11.9539 7.4102 12.2974 7.4102V7.4102ZM15.3198 13.3963V8.70552C15.3198 7.90393 15.0014 7.13516 14.4346 6.56835C13.8678 6.00154 13.099 5.68311 12.2974 5.68311C11.4958 5.68311 10.7271 6.00154 10.1602 6.56835C9.59343 7.13516 9.275 7.90393 9.275 8.70552V17.9282L7.20248 18.3427C6.56722 18.4701 6.00273 18.8309 5.62036 19.3539C5.23799 19.877 5.06549 20.5243 5.13687 21.1683L5.73272 26.5326C5.80935 27.2184 6.04921 27.8757 6.4322 28.4497L8.90713 32.1629C9.14368 32.5178 9.46418 32.8088 9.84018 33.01C10.2162 33.2113 10.6361 33.3166 11.0625 33.3167H22.0901C22.5467 33.3165 22.9952 33.1957 23.3901 32.9664C23.785 32.7371 24.1122 32.4075 24.3387 32.011L26.748 27.7951C27.1692 27.0582 27.4305 26.2408 27.5149 25.3962L27.9829 20.7071C28.0309 20.2269 27.9778 19.742 27.8269 19.2836C27.6761 18.8252 27.4308 18.4035 27.107 18.0457C26.7832 17.6879 26.388 17.4019 25.9469 17.2061C25.5058 17.0104 25.0286 16.9093 24.546 16.9092H23.5425C23.4347 16.763 23.3224 16.6202 23.2058 16.4809C22.8759 16.0854 22.3249 15.4999 21.6859 15.2443C21.0572 14.9922 20.2248 15.0163 19.6859 15.0578L19.458 15.0785C19.2741 14.6078 18.9321 14.2158 18.4908 13.9697C18.0934 13.7406 17.6637 13.5729 17.2162 13.4723C16.6808 13.3652 15.9399 13.3721 15.4027 13.3929L15.3198 13.3963Z" fill-opacity="0.54"/>
|
||||
</g>
|
||||
<g >
|
||||
<rect width="14" height="14" fill="transparent" transform="translate(15 0.683105)"/>
|
||||
<path d="M23.7172 6.89409L23.7171 6.89407L23.7138 6.89751L23.19 7.4329C23.1898 7.4331 23.1896 7.43331 23.1894 7.43351C22.9725 7.65353 22.7581 7.89061 22.6072 8.21025C22.5038 8.42919 22.4371 8.67099 22.4047 8.95345H22.0954C22.1311 8.49095 22.3346 8.07169 22.6463 7.75567L22.6467 7.75533L23.367 7.02342C23.6645 6.73202 23.8398 6.32907 23.8398 5.89095C23.8398 5.00766 23.119 4.28678 22.2357 4.28678C21.5039 4.28678 20.8837 4.78151 20.6924 5.45345H20.3905C20.5879 4.61704 21.3387 3.99512 22.2357 3.99512C23.2832 3.99512 24.1315 4.84341 24.1315 5.89095C24.1315 6.2836 23.971 6.64029 23.7172 6.89409ZM22.2357 1.37012C18.7741 1.37012 15.9648 4.17933 15.9648 7.64095C15.9648 11.1026 18.7741 13.9118 22.2357 13.9118C25.6973 13.9118 28.5065 11.1026 28.5065 7.64095C28.5065 4.17933 25.6973 1.37012 22.2357 1.37012ZM22.0898 11.2868V10.9951H22.3815V11.2868H22.0898Z" fill-opacity="0.87" stroke="white" stroke-width="0.875"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
9
frontend/app/svg/icons/click-rage.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg viewBox="0 0 31 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="27.9992" height="28" fill="transparent" transform="matrix(1 0 -2.76372e-05 1 1.75 1)"/>
|
||||
|
||||
<path d="M6.7336 24.0156C9.12456 26.4067 12.3674 27.75 15.7489 27.75C19.1303 27.75 22.3733 26.4067 24.7644 24.0156C27.1554 21.6245 28.4988 18.3815 28.4989 15C28.499 11.6185 27.1558 8.37548 24.7649 5.98439C22.3739 3.5933 19.131 2.25 15.7496 2.25C12.3682 2.25 9.12519 3.5933 6.7341 5.98439C4.34301 8.37548 2.99966 11.6185 2.99956 15C2.99947 18.3815 4.34264 21.6245 6.7336 24.0156ZM25.2946 24.5459C22.7629 27.0777 19.3292 28.5 15.7489 28.5C12.1685 28.5 8.7349 27.0777 6.2033 24.5459C3.6717 22.0142 2.24951 18.5804 2.24961 15C2.24971 11.4196 3.67209 7.98579 6.20383 5.45405C8.73557 2.92231 12.1693 1.5 15.7496 1.5C19.3299 1.5 22.7636 2.92231 25.2952 5.45405C27.8268 7.98579 29.2489 11.4196 29.2488 15C29.2487 18.5804 27.8264 22.0142 25.2946 24.5459Z" stroke="black"/>
|
||||
<path d="M9.24618 22.7574C9.44714 22.8734 9.68596 22.9048 9.9101 22.8448C10.1343 22.7847 10.3254 22.6381 10.4414 22.4371C10.9789 21.5055 11.7523 20.732 12.6837 20.1943C13.6152 19.6567 14.6719 19.374 15.7473 19.3746C18.0135 19.3746 19.9927 20.6049 21.0531 22.4371C21.17 22.6364 21.3609 22.7815 21.5843 22.8405C21.8077 22.8996 22.0453 22.8679 22.2454 22.7524C22.4455 22.6368 22.5918 22.4468 22.6523 22.2238C22.7129 22.0009 22.6828 21.763 22.5686 21.5621C21.8776 20.3645 20.8834 19.37 19.6859 18.6788C18.4884 17.9876 17.13 17.624 15.7474 17.6246C14.3648 17.6243 13.0065 17.9881 11.8091 18.6793C10.6117 19.3705 9.61729 20.3647 8.92597 21.5621C8.80994 21.7631 8.77849 22.0019 8.83854 22.2261C8.89859 22.4502 9.04522 22.6413 9.24618 22.7574ZM21.4805 8.09237C21.5833 8.03984 21.6955 8.00818 21.8107 7.99922C21.9258 7.99026 22.0416 8.00418 22.1513 8.04017C22.261 8.07617 22.3625 8.13353 22.45 8.20894C22.5374 8.28435 22.6091 8.37633 22.6608 8.47957C22.7125 8.58281 22.7433 8.69527 22.7514 8.81046C22.7594 8.92566 22.7446 9.04131 22.7078 9.15075C22.6709 9.26019 22.6128 9.36126 22.5367 9.44813C22.4606 9.53499 22.3681 9.60593 22.2644 9.65687L20.4987 10.5389C20.8067 11.0114 20.9974 11.6589 20.9974 12.3746C20.9973 13.8236 20.2133 14.9996 19.2474 14.9996C18.2814 14.9996 17.4974 13.8236 17.4975 12.3746C17.4975 11.9424 17.5675 11.5346 17.69 11.1741C17.6086 11.0721 17.551 10.9531 17.5213 10.826C17.4917 10.6989 17.4907 10.5667 17.5186 10.4392C17.5464 10.3116 17.6023 10.1919 17.6822 10.0886C17.7621 9.98539 17.864 9.90127 17.9805 9.84237L21.4805 8.09237V8.09237ZM10.0148 8.09237C9.8076 7.99053 9.56854 7.97472 9.34972 8.04838C9.1309 8.12205 8.95006 8.27922 8.84662 8.48564C8.74317 8.69207 8.72551 8.93101 8.79747 9.15039C8.86943 9.36978 9.02518 9.55183 9.23079 9.65687L10.9965 10.5389C10.6587 11.0908 10.4856 11.7277 10.4977 12.3746C10.4976 13.8236 11.2816 14.9996 12.2476 14.9996C13.2135 14.9996 13.9975 13.8236 13.9976 12.3746C13.9976 11.9424 13.9276 11.5346 13.8051 11.1741C13.8865 11.0721 13.9441 10.9531 13.9738 10.826C14.0035 10.6989 14.0044 10.5667 13.9766 10.4392C13.9488 10.3116 13.8929 10.1919 13.813 10.0886C13.7331 9.98539 13.6312 9.90127 13.5147 9.84237L10.0148 8.09237V8.09237Z" fill="black"/>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
15
frontend/app/svg/icons/cursor-trash.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g >
|
||||
<rect width="32" height="32" fill="transparent"/>
|
||||
|
||||
<g opacity="0.5" >
|
||||
<rect width="24.2141" height="24.2141" fill="transparent" transform="matrix(0.912552 0.40896 -0.408989 0.912539 15.2246 -3.63281)"/>
|
||||
|
||||
<path d="M18.7221 2.84405L18.7221 2.84411L29.3225 13.7986C29.3225 13.7986 29.3226 13.7987 29.3226 13.7987C29.4624 13.9435 29.5575 14.1256 29.5963 14.3232C29.6352 14.5207 29.6162 14.7253 29.5415 14.9123C29.4669 15.0993 29.3399 15.2608 29.1757 15.3774C29.0115 15.4939 28.8172 15.5606 28.6161 15.5694C28.616 15.5694 28.616 15.5694 28.616 15.5694L24.2534 15.7529L23.7405 15.7744L23.9232 16.2541L26.0744 21.9023L26.0745 21.9024C26.1733 22.1616 26.1652 22.4494 26.0518 22.7025C25.9385 22.9556 25.7292 23.1534 25.4701 23.2522C25.2109 23.3511 24.9231 23.343 24.67 23.2296C24.4169 23.1163 24.2192 22.9071 24.1203 22.648C24.1203 22.648 24.1203 22.6479 24.1203 22.6479L21.9705 17.0006L21.7878 16.5208L21.3905 16.8459L18.011 19.6106L18.0108 19.6108C17.8548 19.7385 17.6653 19.8184 17.465 19.8409C17.2647 19.8635 17.0622 19.8276 16.8818 19.7377C16.7014 19.6478 16.5509 19.5076 16.4483 19.3341C16.3457 19.1607 16.2955 18.9612 16.3036 18.7599C16.3036 18.7599 16.3036 18.7598 16.3036 18.7598L16.9259 3.52856L16.9259 3.52847C16.9342 3.32399 17.0024 3.12645 17.1219 2.96033C17.2414 2.79421 17.407 2.66681 17.5983 2.59394C17.7895 2.52107 17.9979 2.50593 18.1977 2.55038C18.3975 2.59484 18.5798 2.69695 18.7221 2.84405Z" stroke="black" stroke-width="0.739831"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.24907 5.52226C7.9014 5.38309 7.52053 5.34903 7.15369 5.42428C6.78684 5.49953 6.45014 5.68079 6.18534 5.9456C5.92054 6.2104 5.73928 6.54709 5.66403 6.91394C5.58877 7.28079 5.62284 7.66165 5.762 8.00932L13.4145 27.1406C13.5516 27.483 13.7848 27.7785 14.0859 27.9914C14.3871 28.2044 14.7434 28.3258 15.1119 28.3409C15.4804 28.3561 15.8455 28.2644 16.1631 28.0769C16.4807 27.8894 16.7374 27.6141 16.9021 27.2841L19.5423 22.0058L25.3161 27.7834C25.675 28.1421 26.1618 28.3436 26.6693 28.3434C27.1768 28.3432 27.6635 28.1414 28.0222 27.7824C28.3809 27.4235 28.5823 26.9367 28.5822 26.4292C28.582 25.9217 28.3802 25.4351 28.0212 25.0763L22.2455 19.2987L27.5257 16.6605C27.8551 16.4953 28.1297 16.2385 28.3167 15.921C28.5036 15.6035 28.5949 15.2388 28.5796 14.8706C28.5642 14.5025 28.4429 14.1466 28.2301 13.8458C28.0174 13.5449 27.7223 13.3119 27.3803 13.1748L8.24907 5.52226Z" fill-opacity="0.87"/>
|
||||
<g opacity="0.3">
|
||||
<path d="M3.83765 12.8923L3.83756 12.8922C3.6146 12.8574 3.38631 12.8887 3.18101 12.9824C2.9757 13.0761 2.8024 13.2279 2.68259 13.4192C2.56278 13.6104 2.50174 13.8326 2.50701 14.0582C2.51229 14.2838 2.58367 14.5029 2.71229 14.6883L2.71235 14.6884L12.2901 28.5028C12.2902 28.5029 12.2902 28.5029 12.2902 28.5029C12.4169 28.6855 12.5937 28.8275 12.7992 28.912C13.0048 28.9964 13.2305 29.0197 13.4489 28.9789C13.6674 28.9381 13.8695 28.835 14.0307 28.682C14.192 28.5291 14.3056 28.3328 14.3578 28.1167L14.3579 28.1165L15.4934 23.4376L15.6269 22.8875L16.1064 23.1882L21.7521 26.7278L3.83765 12.8923ZM3.83765 12.8923L20.4463 15.484L3.83765 12.8923ZM22.9758 24.7729L22.9757 24.7728L17.3283 21.2334L16.8487 20.9328L17.2855 20.5729L21.0018 17.5116C21.0018 17.5116 21.0019 17.5115 21.0019 17.5115C21.173 17.3701 21.2997 17.1823 21.3666 16.9706C21.4336 16.7589 21.438 16.5324 21.3793 16.3183C21.3207 16.1041 21.2014 15.9115 21.0359 15.7635C20.8705 15.6156 20.6659 15.5185 20.4467 15.484L22.9758 24.7729ZM22.9758 24.7729C23.2351 24.9352 23.4192 25.1938 23.4878 25.4919C23.5563 25.79 23.5037 26.103 23.3414 26.3623C23.1791 26.6215 22.9205 26.8056 22.6224 26.8742C22.3245 26.9427 22.0115 26.8901 21.7523 26.7279L22.9758 24.7729Z" stroke="black" stroke-width="0.815817"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
15
frontend/app/svg/icons/event/click_hesitation.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg viewBox="0 0 33 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<rect width="32" height="32" fill="transparent" transform="translate(1 1.5)"/>
|
||||
|
||||
<g>
|
||||
<rect width="27.6335" height="27.6335" fill="transparent" transform="translate(0.640625 5.68311)"/>
|
||||
<path d="M12.2974 7.4102C12.641 7.4102 12.9704 7.54667 13.2133 7.78959C13.4563 8.03251 13.5927 8.36198 13.5927 8.70552V19.4999C13.5927 19.7289 13.6837 19.9486 13.8457 20.1105C14.0076 20.2724 14.2273 20.3634 14.4563 20.3634C14.6853 20.3634 14.905 20.2724 15.0669 20.1105C15.2289 19.9486 15.3198 19.7289 15.3198 19.4999V15.1251L15.4684 15.1182C16.0159 15.0975 16.5685 15.1044 16.8777 15.1649C17.1091 15.2115 17.3854 15.3307 17.6514 15.4792C17.7844 15.5517 17.9105 15.7331 17.9105 16.0215V19.4999C17.9105 19.7289 18.0015 19.9486 18.1634 20.1105C18.3254 20.2724 18.545 20.3634 18.774 20.3634C19.0031 20.3634 19.2227 20.2724 19.3846 20.1105C19.5466 19.9486 19.6376 19.7289 19.6376 19.4999V16.797C19.6985 16.7906 19.7596 16.7848 19.8206 16.7797C20.3664 16.7383 20.8293 16.7624 21.0434 16.8488C21.2472 16.9282 21.5616 17.2063 21.8828 17.5914C22.0227 17.7572 22.1419 17.9196 22.2282 18.0388V20.3634C22.2282 20.5925 22.3192 20.8121 22.4811 20.974C22.6431 21.136 22.8627 21.227 23.0918 21.227C23.3208 21.227 23.5404 21.136 23.7024 20.974C23.8643 20.8121 23.9553 20.5925 23.9553 20.3634V18.6363H24.546C24.7874 18.6363 25.0261 18.6869 25.2467 18.7848C25.4673 18.8828 25.6649 19.0258 25.8268 19.2048C25.9888 19.3839 26.1114 19.5948 26.1867 19.8241C26.2621 20.0534 26.2886 20.296 26.2644 20.5361L25.7964 25.2252C25.736 25.8278 25.5494 26.4109 25.2489 26.9368L22.8396 31.1543C22.7641 31.2865 22.655 31.3964 22.5234 31.4728C22.3918 31.5492 22.2423 31.5895 22.0901 31.5896H11.0625C10.9203 31.5895 10.7803 31.5543 10.655 31.4871C10.5297 31.4199 10.4229 31.3228 10.3441 31.2044L7.86914 27.4912C7.63935 27.1468 7.49543 26.7523 7.44946 26.3409L6.85361 20.9783C6.82952 20.7634 6.88696 20.5472 7.01456 20.3726C7.14216 20.198 7.33064 20.0776 7.54272 20.0353L9.275 19.6899V21.227C9.275 21.456 9.36598 21.6756 9.52792 21.8376C9.68987 21.9995 9.90952 22.0905 10.1385 22.0905C10.3676 22.0905 10.5872 21.9995 10.7492 21.8376C10.9111 21.6756 11.0021 21.456 11.0021 21.227V8.70552C11.0021 8.36198 11.1386 8.03251 11.3815 7.78959C11.6244 7.54667 11.9539 7.4102 12.2974 7.4102V7.4102ZM15.3198 13.3963V8.70552C15.3198 7.90393 15.0014 7.13516 14.4346 6.56835C13.8678 6.00154 13.099 5.68311 12.2974 5.68311C11.4958 5.68311 10.7271 6.00154 10.1602 6.56835C9.59343 7.13516 9.275 7.90393 9.275 8.70552V17.9282L7.20248 18.3427C6.56722 18.4701 6.00273 18.8309 5.62036 19.3539C5.23799 19.877 5.06549 20.5243 5.13687 21.1683L5.73272 26.5326C5.80935 27.2184 6.04921 27.8757 6.4322 28.4497L8.90713 32.1629C9.14368 32.5178 9.46418 32.8088 9.84018 33.01C10.2162 33.2113 10.6361 33.3166 11.0625 33.3167H22.0901C22.5467 33.3165 22.9952 33.1957 23.3901 32.9664C23.785 32.7371 24.1122 32.4075 24.3387 32.011L26.748 27.7951C27.1692 27.0582 27.4305 26.2408 27.5149 25.3962L27.9829 20.7071C28.0309 20.2269 27.9778 19.742 27.8269 19.2836C27.6761 18.8252 27.4308 18.4035 27.107 18.0457C26.7832 17.6879 26.388 17.4019 25.9469 17.2061C25.5058 17.0104 25.0286 16.9093 24.546 16.9092H23.5425C23.4347 16.763 23.3224 16.6202 23.2058 16.4809C22.8759 16.0854 22.3249 15.4999 21.6859 15.2443C21.0572 14.9922 20.2248 15.0163 19.6859 15.0578L19.458 15.0785C19.2741 14.6078 18.9321 14.2158 18.4908 13.9697C18.0934 13.7406 17.6637 13.5729 17.2162 13.4723C16.6808 13.3652 15.9399 13.3721 15.4027 13.3929L15.3198 13.3963Z" fill-opacity="0.54"/>
|
||||
</g>
|
||||
<g >
|
||||
<rect width="14" height="14" fill="transparent" transform="translate(15 0.683105)"/>
|
||||
<path d="M23.7172 6.89409L23.7171 6.89407L23.7138 6.89751L23.19 7.4329C23.1898 7.4331 23.1896 7.43331 23.1894 7.43351C22.9725 7.65353 22.7581 7.89061 22.6072 8.21025C22.5038 8.42919 22.4371 8.67099 22.4047 8.95345H22.0954C22.1311 8.49095 22.3346 8.07169 22.6463 7.75567L22.6467 7.75533L23.367 7.02342C23.6645 6.73202 23.8398 6.32907 23.8398 5.89095C23.8398 5.00766 23.119 4.28678 22.2357 4.28678C21.5039 4.28678 20.8837 4.78151 20.6924 5.45345H20.3905C20.5879 4.61704 21.3387 3.99512 22.2357 3.99512C23.2832 3.99512 24.1315 4.84341 24.1315 5.89095C24.1315 6.2836 23.971 6.64029 23.7172 6.89409ZM22.2357 1.37012C18.7741 1.37012 15.9648 4.17933 15.9648 7.64095C15.9648 11.1026 18.7741 13.9118 22.2357 13.9118C25.6973 13.9118 28.5065 11.1026 28.5065 7.64095C28.5065 4.17933 25.6973 1.37012 22.2357 1.37012ZM22.0898 11.2868V10.9951H22.3815V11.2868H22.0898Z" fill-opacity="0.87" stroke="white" stroke-width="0.875"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
17
frontend/app/svg/icons/event/input_hesitation.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
15
frontend/app/svg/icons/event/mouse_thrashing.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g >
|
||||
<rect width="32" height="32" fill="transparent"/>
|
||||
|
||||
<g opacity="0.5" >
|
||||
<rect width="24.2141" height="24.2141" fill="transparent" transform="matrix(0.912552 0.40896 -0.408989 0.912539 15.2246 -3.63281)"/>
|
||||
|
||||
<path d="M18.7221 2.84405L18.7221 2.84411L29.3225 13.7986C29.3225 13.7986 29.3226 13.7987 29.3226 13.7987C29.4624 13.9435 29.5575 14.1256 29.5963 14.3232C29.6352 14.5207 29.6162 14.7253 29.5415 14.9123C29.4669 15.0993 29.3399 15.2608 29.1757 15.3774C29.0115 15.4939 28.8172 15.5606 28.6161 15.5694C28.616 15.5694 28.616 15.5694 28.616 15.5694L24.2534 15.7529L23.7405 15.7744L23.9232 16.2541L26.0744 21.9023L26.0745 21.9024C26.1733 22.1616 26.1652 22.4494 26.0518 22.7025C25.9385 22.9556 25.7292 23.1534 25.4701 23.2522C25.2109 23.3511 24.9231 23.343 24.67 23.2296C24.4169 23.1163 24.2192 22.9071 24.1203 22.648C24.1203 22.648 24.1203 22.6479 24.1203 22.6479L21.9705 17.0006L21.7878 16.5208L21.3905 16.8459L18.011 19.6106L18.0108 19.6108C17.8548 19.7385 17.6653 19.8184 17.465 19.8409C17.2647 19.8635 17.0622 19.8276 16.8818 19.7377C16.7014 19.6478 16.5509 19.5076 16.4483 19.3341C16.3457 19.1607 16.2955 18.9612 16.3036 18.7599C16.3036 18.7599 16.3036 18.7598 16.3036 18.7598L16.9259 3.52856L16.9259 3.52847C16.9342 3.32399 17.0024 3.12645 17.1219 2.96033C17.2414 2.79421 17.407 2.66681 17.5983 2.59394C17.7895 2.52107 17.9979 2.50593 18.1977 2.55038C18.3975 2.59484 18.5798 2.69695 18.7221 2.84405Z" stroke="black" stroke-width="0.739831"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.24907 5.52226C7.9014 5.38309 7.52053 5.34903 7.15369 5.42428C6.78684 5.49953 6.45014 5.68079 6.18534 5.9456C5.92054 6.2104 5.73928 6.54709 5.66403 6.91394C5.58877 7.28079 5.62284 7.66165 5.762 8.00932L13.4145 27.1406C13.5516 27.483 13.7848 27.7785 14.0859 27.9914C14.3871 28.2044 14.7434 28.3258 15.1119 28.3409C15.4804 28.3561 15.8455 28.2644 16.1631 28.0769C16.4807 27.8894 16.7374 27.6141 16.9021 27.2841L19.5423 22.0058L25.3161 27.7834C25.675 28.1421 26.1618 28.3436 26.6693 28.3434C27.1768 28.3432 27.6635 28.1414 28.0222 27.7824C28.3809 27.4235 28.5823 26.9367 28.5822 26.4292C28.582 25.9217 28.3802 25.4351 28.0212 25.0763L22.2455 19.2987L27.5257 16.6605C27.8551 16.4953 28.1297 16.2385 28.3167 15.921C28.5036 15.6035 28.5949 15.2388 28.5796 14.8706C28.5642 14.5025 28.4429 14.1466 28.2301 13.8458C28.0174 13.5449 27.7223 13.3119 27.3803 13.1748L8.24907 5.52226Z" fill-opacity="0.87"/>
|
||||
<g opacity="0.3">
|
||||
<path d="M3.83765 12.8923L3.83756 12.8922C3.6146 12.8574 3.38631 12.8887 3.18101 12.9824C2.9757 13.0761 2.8024 13.2279 2.68259 13.4192C2.56278 13.6104 2.50174 13.8326 2.50701 14.0582C2.51229 14.2838 2.58367 14.5029 2.71229 14.6883L2.71235 14.6884L12.2901 28.5028C12.2902 28.5029 12.2902 28.5029 12.2902 28.5029C12.4169 28.6855 12.5937 28.8275 12.7992 28.912C13.0048 28.9964 13.2305 29.0197 13.4489 28.9789C13.6674 28.9381 13.8695 28.835 14.0307 28.682C14.192 28.5291 14.3056 28.3328 14.3578 28.1167L14.3579 28.1165L15.4934 23.4376L15.6269 22.8875L16.1064 23.1882L21.7521 26.7278L3.83765 12.8923ZM3.83765 12.8923L20.4463 15.484L3.83765 12.8923ZM22.9758 24.7729L22.9757 24.7728L17.3283 21.2334L16.8487 20.9328L17.2855 20.5729L21.0018 17.5116C21.0018 17.5116 21.0019 17.5115 21.0019 17.5115C21.173 17.3701 21.2997 17.1823 21.3666 16.9706C21.4336 16.7589 21.438 16.5324 21.3793 16.3183C21.3207 16.1041 21.2014 15.9115 21.0359 15.7635C20.8705 15.6156 20.6659 15.5185 20.4467 15.484L22.9758 24.7729ZM22.9758 24.7729C23.2351 24.9352 23.4192 25.1938 23.4878 25.4919C23.5563 25.79 23.5037 26.103 23.3414 26.3623C23.1791 26.6215 22.9205 26.8056 22.6224 26.8742C22.3245 26.9427 22.0115 26.8901 21.7523 26.7279L22.9758 24.7729Z" stroke="black" stroke-width="0.815817"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
17
frontend/app/svg/icons/input-hesitation.svg
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
|
@ -5,12 +5,20 @@ const LOCATION = 'LOCATION';
|
|||
const CUSTOM = 'CUSTOM';
|
||||
const CLICKRAGE = 'CLICKRAGE';
|
||||
const IOS_VIEW = 'VIEW';
|
||||
export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW};
|
||||
export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW };
|
||||
|
||||
export type EventType =
|
||||
| typeof CONSOLE
|
||||
| typeof CLICK
|
||||
| typeof INPUT
|
||||
| typeof LOCATION
|
||||
| typeof CUSTOM
|
||||
| typeof CLICKRAGE;
|
||||
|
||||
interface IEvent {
|
||||
time: number;
|
||||
timestamp: number;
|
||||
type: typeof CONSOLE | typeof CLICK | typeof INPUT | typeof LOCATION | typeof CUSTOM | typeof CLICKRAGE;
|
||||
type: EventType
|
||||
name: string;
|
||||
key: number;
|
||||
label: string;
|
||||
|
|
@ -18,18 +26,24 @@ interface IEvent {
|
|||
target: {
|
||||
path: string;
|
||||
label: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface ConsoleEvent extends IEvent {
|
||||
subtype: string
|
||||
value: string
|
||||
subtype: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ClickEvent extends IEvent {
|
||||
targetContent: string;
|
||||
count: number;
|
||||
hesitation: number;
|
||||
}
|
||||
|
||||
interface InputEvent extends IEvent {
|
||||
value: string;
|
||||
hesitation: number;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface LocationEvent extends IEvent {
|
||||
|
|
@ -51,11 +65,10 @@ export interface LocationEvent extends IEvent {
|
|||
export type EventData = ConsoleEvent | ClickEvent | InputEvent | LocationEvent | IEvent;
|
||||
|
||||
class Event {
|
||||
key: IEvent["key"]
|
||||
time: IEvent["time"];
|
||||
label: IEvent["label"];
|
||||
target: IEvent["target"];
|
||||
|
||||
key: IEvent['key'];
|
||||
time: IEvent['time'];
|
||||
label: IEvent['label'];
|
||||
target: IEvent['target'];
|
||||
|
||||
constructor(event: IEvent) {
|
||||
Object.assign(this, {
|
||||
|
|
@ -64,98 +77,103 @@ class Event {
|
|||
key: event.key,
|
||||
target: {
|
||||
path: event.target?.path || event.targetPath,
|
||||
label: event.target?.label
|
||||
}
|
||||
})
|
||||
label: event.target?.label,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Console extends Event {
|
||||
readonly type = CONSOLE;
|
||||
readonly name = 'Console'
|
||||
readonly name = 'Console';
|
||||
subtype: string;
|
||||
value: string;
|
||||
|
||||
constructor(evt: ConsoleEvent) {
|
||||
super(evt);
|
||||
this.subtype = evt.subtype
|
||||
this.value = evt.value
|
||||
this.subtype = evt.subtype;
|
||||
this.value = evt.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Click extends Event {
|
||||
readonly type: typeof CLICKRAGE | typeof CLICK = CLICK;
|
||||
readonly name = 'Click'
|
||||
readonly name = 'Click';
|
||||
targetContent = '';
|
||||
count: number
|
||||
count: number;
|
||||
hesitation: number = 0;
|
||||
|
||||
constructor(evt: ClickEvent, isClickRage?: boolean) {
|
||||
super(evt);
|
||||
this.targetContent = evt.targetContent
|
||||
this.count = evt.count
|
||||
this.targetContent = evt.targetContent;
|
||||
this.count = evt.count;
|
||||
this.hesitation = evt.hesitation;
|
||||
if (isClickRage) {
|
||||
this.type = CLICKRAGE
|
||||
this.type = CLICKRAGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Input extends Event {
|
||||
readonly type = INPUT;
|
||||
readonly name = 'Input'
|
||||
value = ''
|
||||
readonly name = 'Input';
|
||||
readonly hesitation: number = 0;
|
||||
readonly duration: number = 0;
|
||||
|
||||
value = '';
|
||||
|
||||
constructor(evt: InputEvent) {
|
||||
super(evt);
|
||||
this.value = evt.value
|
||||
this.value = evt.value;
|
||||
this.hesitation = evt.hesitation;
|
||||
this.duration = evt.duration;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Location extends Event {
|
||||
readonly name = 'Location';
|
||||
readonly type = LOCATION;
|
||||
url: LocationEvent["url"]
|
||||
host: LocationEvent["host"];
|
||||
fcpTime: LocationEvent["fcpTime"];
|
||||
loadTime: LocationEvent["loadTime"];
|
||||
domContentLoadedTime: LocationEvent["domContentLoadedTime"];
|
||||
domBuildingTime: LocationEvent["domBuildingTime"];
|
||||
speedIndex: LocationEvent["speedIndex"];
|
||||
visuallyComplete: LocationEvent["visuallyComplete"];
|
||||
timeToInteractive: LocationEvent["timeToInteractive"];
|
||||
referrer: LocationEvent["referrer"];
|
||||
url: LocationEvent['url'];
|
||||
host: LocationEvent['host'];
|
||||
fcpTime: LocationEvent['fcpTime'];
|
||||
loadTime: LocationEvent['loadTime'];
|
||||
domContentLoadedTime: LocationEvent['domContentLoadedTime'];
|
||||
domBuildingTime: LocationEvent['domBuildingTime'];
|
||||
speedIndex: LocationEvent['speedIndex'];
|
||||
visuallyComplete: LocationEvent['visuallyComplete'];
|
||||
timeToInteractive: LocationEvent['timeToInteractive'];
|
||||
referrer: LocationEvent['referrer'];
|
||||
|
||||
constructor(evt: LocationEvent) {
|
||||
super(evt);
|
||||
Object.assign(this, {
|
||||
...evt,
|
||||
fcpTime: evt.firstContentfulPaintTime || evt.firstPaintTime
|
||||
fcpTime: evt.firstContentfulPaintTime || evt.firstPaintTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type InjectedEvent = Console | Click | Input | Location;
|
||||
|
||||
export default function(event: EventData) {
|
||||
export default function (event: EventData) {
|
||||
if (event.type && event.type === CONSOLE) {
|
||||
return new Console(event as ConsoleEvent)
|
||||
return new Console(event as ConsoleEvent);
|
||||
}
|
||||
if (event.type && event.type === CLICK) {
|
||||
return new Click(event as ClickEvent)
|
||||
return new Click(event as ClickEvent);
|
||||
}
|
||||
if (event.type && event.type === INPUT) {
|
||||
return new Input(event as InputEvent)
|
||||
return new Input(event as InputEvent);
|
||||
}
|
||||
if (event.type && event.type === LOCATION) {
|
||||
return new Location(event as LocationEvent)
|
||||
return new Location(event as LocationEvent);
|
||||
}
|
||||
if (event.type && event.type === CLICKRAGE) {
|
||||
return new Click(event as ClickEvent, true)
|
||||
return new Click(event as ClickEvent, true);
|
||||
}
|
||||
// not used right now?
|
||||
// if (event.type === CUSTOM || !event.type) {
|
||||
// return new Event(event)
|
||||
// }
|
||||
console.error(`Unknown event type: ${event.type}`)
|
||||
console.error(`Unknown event type: ${event.type}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import Record from 'Types/Record';
|
||||
|
||||
const types = {
|
||||
export const types = {
|
||||
ALL: 'all',
|
||||
JS_EXCEPTION: 'js_exception',
|
||||
BAD_REQUEST: 'bad_request',
|
||||
CRASH: 'crash',
|
||||
CLICK_RAGE: 'click_rage'
|
||||
CLICK_RAGE: 'click_rage',
|
||||
MOUSE_THRASHING: 'mouse_thrashing',
|
||||
} as const
|
||||
|
||||
type TypeKeys = keyof typeof types
|
||||
|
|
@ -21,6 +22,7 @@ export const issues_types = [
|
|||
{ 'type': types.BAD_REQUEST, 'visible': true, 'order': 2, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' },
|
||||
{ 'type': types.CLICK_RAGE, 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' },
|
||||
{ 'type': types.CRASH, 'visible': true, 'order': 4, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' },
|
||||
{ 'type': types.MOUSE_THRASHING, 'visible': true, 'order': 5, 'name': 'Mouse Thrashing', 'icon': 'close' },
|
||||
// { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' },
|
||||
// { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' },
|
||||
// { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' },
|
||||
|
|
|
|||
|
|
@ -2,13 +2,44 @@ import { Duration } from 'luxon';
|
|||
import SessionEvent, { TYPES, EventData, InjectedEvent } from './event';
|
||||
import StackEvent from './stackEvent';
|
||||
import SessionError, { IError } from './error';
|
||||
import Issue, { IIssue } from './issue';
|
||||
import Issue, { IIssue, types as issueTypes } from './issue';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
import { toJS } from 'mobx';
|
||||
|
||||
const HASH_MOD = 1610612741;
|
||||
const HASH_P = 53;
|
||||
|
||||
function mergeEventLists<T extends Record<string, any>, Y extends Record<string, any>>(arr1: T[], arr2: Y[]): Array<T | Y> {
|
||||
let merged = [];
|
||||
let index1 = 0;
|
||||
let index2 = 0;
|
||||
let current = 0;
|
||||
|
||||
while (current < (arr1.length + arr2.length)) {
|
||||
|
||||
let isArr1Depleted = index1 >= arr1.length;
|
||||
let isArr2Depleted = index2 >= arr2.length;
|
||||
|
||||
if (!isArr1Depleted && (isArr2Depleted || (arr1[index1].timestamp < arr2[index2].timestamp))) {
|
||||
merged[current] = arr1[index1];
|
||||
index1++;
|
||||
} else {
|
||||
merged[current] = arr2[index2];
|
||||
index2++;
|
||||
}
|
||||
|
||||
current++;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
function sortEvents(a: Record<string, any>, b: Record<string, any>) {
|
||||
const aTs = a.timestamp || a.time;
|
||||
const bTs = b.timestamp || b.time;
|
||||
|
||||
return aTs - bTs;
|
||||
}
|
||||
|
||||
function hashString(s: string): number {
|
||||
let mul = 1;
|
||||
let hash = 0;
|
||||
|
|
@ -159,6 +190,8 @@ export default class Session {
|
|||
agentToken: ISession['agentToken'];
|
||||
notes: ISession['notes'];
|
||||
notesWithEvents: ISession['notesWithEvents'];
|
||||
frustrations: Array<IIssue | InjectedEvent>
|
||||
|
||||
fileKey: ISession['fileKey'];
|
||||
durationSeconds: number;
|
||||
|
||||
|
|
@ -220,15 +253,24 @@ export default class Session {
|
|||
(i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })
|
||||
) || [];
|
||||
|
||||
const notesWithEvents =
|
||||
[...rawEvents, ...notes].sort((a, b) => {
|
||||
// @ts-ignore just in case
|
||||
const aTs = a.timestamp || a.time;
|
||||
// @ts-ignore
|
||||
const bTs = b.timestamp || b.time;
|
||||
const rawNotes = notes;
|
||||
|
||||
return aTs - bTs;
|
||||
}) || [];
|
||||
const frustrationEvents = events.filter(ev => {
|
||||
if (ev.type === TYPES.CLICK || ev.type === TYPES.INPUT) {
|
||||
// @ts-ignore
|
||||
return ev.hesitation > 1000
|
||||
}
|
||||
return ev.type === TYPES.CLICKRAGE
|
||||
}
|
||||
)
|
||||
const frustrationIssues = issuesList.filter(i => i.type === issueTypes.MOUSE_THRASHING)
|
||||
|
||||
const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || [];
|
||||
|
||||
const mixedEventsWithIssues = mergeEventLists(
|
||||
mergeEventLists(rawEvents, rawNotes),
|
||||
frustrationIssues
|
||||
).sort(sortEvents)
|
||||
|
||||
Object.assign(this, {
|
||||
...session,
|
||||
|
|
@ -260,7 +302,8 @@ export default class Session {
|
|||
domURL,
|
||||
devtoolsURL,
|
||||
notes,
|
||||
notesWithEvents: notesWithEvents,
|
||||
notesWithEvents: mixedEventsWithIssues,
|
||||
frustrations: frustrationList,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -301,14 +344,32 @@ export default class Session {
|
|||
});
|
||||
}
|
||||
|
||||
const frustrationEvents = events.filter(ev => {
|
||||
if (ev.type === TYPES.CLICK || ev.type === TYPES.INPUT) {
|
||||
// @ts-ignore
|
||||
return ev.hesitation > 1000
|
||||
}
|
||||
return ev.type === TYPES.CLICKRAGE
|
||||
}
|
||||
)
|
||||
const frustrationIssues = issuesList.filter(i => i.type === issueTypes.MOUSE_THRASHING)
|
||||
const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || [];
|
||||
|
||||
const mixedEventsWithIssues = mergeEventLists(
|
||||
rawEvents,
|
||||
frustrationIssues
|
||||
).sort(sortEvents)
|
||||
|
||||
this.events = events;
|
||||
// @ts-ignore
|
||||
this.notesWithEvents = rawEvents;
|
||||
this.notesWithEvents = mixedEventsWithIssues;
|
||||
this.errors = exceptions;
|
||||
this.issues = issuesList;
|
||||
// @ts-ignore legacy code? no idea
|
||||
this.resources = resources;
|
||||
this.stackEvents = stackEventsList;
|
||||
// @ts-ignore
|
||||
this.frustrations = frustrationList;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
|
@ -319,7 +380,7 @@ export default class Session {
|
|||
[...this.notesWithEvents, ...sessionNotes].sort((a, b) => {
|
||||
// @ts-ignore just in case
|
||||
const aTs = a.timestamp || a.time;
|
||||
// @ts-ignore
|
||||
// @ts-ignore supporting old code...
|
||||
const bTs = b.timestamp || b.time;
|
||||
|
||||
return aTs - bTs;
|
||||
|
|
|
|||
|
|
@ -63,12 +63,10 @@ const plugins = (removeFill = true) => {
|
|||
]
|
||||
}
|
||||
}
|
||||
// fs.promises.mkdir('/tmp/a/apple', { recursive: true })
|
||||
// .then(() => {
|
||||
fs.writeFileSync(`${UI_DIRNAME}/SVG.tsx`, `
|
||||
fs.writeFileSync(`${UI_DIRNAME}/SVG.tsx`, `
|
||||
import React from 'react';
|
||||
|
||||
export type IconNames = ${icons.map(icon => "'"+ icon.slice(0, -4) + "'").join(' | ')};
|
||||
export type IconNames = ${icons.map((icon) => "'"+ icon.slice(0, -4) + "'").join(' | ')};
|
||||
|
||||
interface Props {
|
||||
name: IconNames;
|
||||
|
|
@ -88,10 +86,11 @@ ${icons.map(icon => {
|
|||
const { data } = optimize(svg, plugins(canOptimize));
|
||||
return ` case '${icon.slice(0, -4)}': return ${data.replace(/xlink\:href/g, 'xlinkHref')
|
||||
.replace(/xmlns\:xlink/g, 'xmlnsXlink')
|
||||
.replace(/clip-path/g, 'clipPath')
|
||||
.replace(/clip-rule/g, 'clipRule')
|
||||
.replace(/clip\-path/g, 'clipPath')
|
||||
.replace(/clip\-rule/g, 'clipRule')
|
||||
// hack to keep fill rule for some icons like stop recording square
|
||||
.replace(/clipRule="evenoddCustomFill"/g, 'clipRule="evenodd" fillRule="evenodd"')
|
||||
|
||||
.replace(/fill-rule/g, 'fillRule')
|
||||
.replace(/fill-opacity/g, 'fillOpacity')
|
||||
.replace(/stop-color/g, 'stopColor')
|
||||
|
|
|
|||
|
|
@ -446,6 +446,7 @@ message 82, 'PartitionedMessage', :replayer => false do
|
|||
uint 'PartTotal'
|
||||
end
|
||||
|
||||
# 90-111 reserved iOS
|
||||
|
||||
## Backend-only
|
||||
message 125, 'IssueEvent', :replayer => false, :tracker => false do
|
||||
|
|
@ -465,3 +466,24 @@ message 127, 'SessionSearch', :tracker => false, :replayer => false do
|
|||
uint 'Timestamp'
|
||||
uint 'Partition'
|
||||
end
|
||||
|
||||
# since tracker 4.1.10
|
||||
|
||||
message 112, 'InputChange', :replayer => false do
|
||||
uint 'ID'
|
||||
string 'Value'
|
||||
boolean 'ValueMasked'
|
||||
string 'Label'
|
||||
int 'HesitationTime'
|
||||
int 'InputDuration'
|
||||
end
|
||||
|
||||
message 113, 'SelectionChange' do
|
||||
uint 'SelectionStart'
|
||||
uint 'SelectionEnd'
|
||||
string 'Selection'
|
||||
end
|
||||
|
||||
message 114, 'MouseThrashing' do
|
||||
uint 'Timestamp'
|
||||
end
|
||||
|
|
@ -31,9 +31,9 @@ export interface Options {
|
|||
controlConfirm: ConfirmOptions;
|
||||
recordingConfirm: ConfirmOptions;
|
||||
|
||||
// @depricated
|
||||
// @deprecated
|
||||
confirmText?: string;
|
||||
// @depricated
|
||||
// @deprecated
|
||||
confirmStyle?: Properties;
|
||||
|
||||
config: RTCConfiguration;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export interface Options {
|
|||
ignoreHeaders: Array<string> | boolean
|
||||
sanitiser?: (RequestResponseData) => RequestResponseData | null
|
||||
|
||||
// Depricated
|
||||
// @deprecated
|
||||
requestSanitizer?: any
|
||||
responseSanitizer?: any
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export default function(opts: Partial<Options> = {}): (app: App | null) => Windo
|
|||
opts,
|
||||
);
|
||||
if (options.requestSanitizer && options.responseSanitizer) {
|
||||
console.warn("OpenReplay fetch plugin: `requestSanitizer` and `responseSanitizer` options are depricated. Please, use `sanitiser` instead (check out documentation at https://docs.openreplay.com/plugins/fetch).")
|
||||
console.warn("OpenReplay fetch plugin: `requestSanitizer` and `responseSanitizer` options are deprecated. Please, use `sanitiser` instead (check out documentation at https://docs.openreplay.com/plugins/fetch).")
|
||||
}
|
||||
|
||||
return (app: App | null) => {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ export declare const enum Type {
|
|||
Zustand = 79,
|
||||
BatchMetadata = 81,
|
||||
PartitionedMessage = 82,
|
||||
InputChange = 112,
|
||||
SelectionChange = 113,
|
||||
MouseThrashing = 114,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -490,6 +493,28 @@ export type PartitionedMessage = [
|
|||
/*partTotal:*/ number,
|
||||
]
|
||||
|
||||
export type InputChange = [
|
||||
/*type:*/ Type.InputChange,
|
||||
/*id:*/ number,
|
||||
/*value:*/ string,
|
||||
/*valueMasked:*/ boolean,
|
||||
/*label:*/ string,
|
||||
/*hesitationTime:*/ number,
|
||||
/*inputDuration:*/ number,
|
||||
]
|
||||
|
||||
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequest | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTiming | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage
|
||||
export type SelectionChange = [
|
||||
/*type:*/ Type.SelectionChange,
|
||||
/*selectionStart:*/ number,
|
||||
/*selectionEnd:*/ number,
|
||||
/*selection:*/ string,
|
||||
]
|
||||
|
||||
export type MouseThrashing = [
|
||||
/*type:*/ Type.MouseThrashing,
|
||||
/*timestamp:*/ number,
|
||||
]
|
||||
|
||||
|
||||
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequest | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTiming | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | InputChange | SelectionChange | MouseThrashing
|
||||
export default Message
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export default class App {
|
|||
}
|
||||
})
|
||||
|
||||
// @depricated (use sessionHash on start instead)
|
||||
// @deprecated (use sessionHash on start instead)
|
||||
if (sessionToken != null) {
|
||||
this.session.applySessionHash(sessionToken)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -792,3 +792,44 @@ export function PartitionedMessage(
|
|||
]
|
||||
}
|
||||
|
||||
export function InputChange(
|
||||
id: number,
|
||||
value: string,
|
||||
valueMasked: boolean,
|
||||
label: string,
|
||||
hesitationTime: number,
|
||||
inputDuration: number,
|
||||
): Messages.InputChange {
|
||||
return [
|
||||
Messages.Type.InputChange,
|
||||
id,
|
||||
value,
|
||||
valueMasked,
|
||||
label,
|
||||
hesitationTime,
|
||||
inputDuration,
|
||||
]
|
||||
}
|
||||
|
||||
export function SelectionChange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selection: string,
|
||||
): Messages.SelectionChange {
|
||||
return [
|
||||
Messages.Type.SelectionChange,
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selection,
|
||||
]
|
||||
}
|
||||
|
||||
export function MouseThrashing(
|
||||
timestamp: number,
|
||||
): Messages.MouseThrashing {
|
||||
return [
|
||||
Messages.Type.MouseThrashing,
|
||||
timestamp,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ export default class Ticker {
|
|||
this.callbacks = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Callback} callback - repeated cb
|
||||
* @param {number} n - number of turn skips; ticker have a 30 ms cycle
|
||||
* @param {boolean} useSafe - using safe wrapper to check if app is active
|
||||
* @param {object} thisArg - link to <this>
|
||||
* */
|
||||
attach(callback: Callback, n = 0, useSafe = true, thisArg?: any) {
|
||||
if (thisArg) {
|
||||
callback = callback.bind(thisArg)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import Focus from './modules/focus.js'
|
|||
import Fonts from './modules/fonts.js'
|
||||
import Network from './modules/network.js'
|
||||
import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
|
||||
import Selection from './modules/selection.js'
|
||||
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
|
||||
|
||||
import type { Options as AppOptions } from './app/index.js'
|
||||
|
|
@ -131,6 +132,7 @@ export default class API {
|
|||
Focus(app)
|
||||
Fonts(app)
|
||||
Network(app, options.network)
|
||||
Selection(app)
|
||||
;(window as any).__OPENREPLAY__ = this
|
||||
|
||||
if (options.autoResetOnWindowOpen) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import type App from '../app/index.js'
|
||||
import { normSpaces, IN_BROWSER, getLabelAttribute } from '../utils.js'
|
||||
import { normSpaces, IN_BROWSER, getLabelAttribute, now } from '../utils.js'
|
||||
import { hasTag } from '../app/guards.js'
|
||||
import { SetInputTarget, SetInputValue, SetInputChecked } from '../app/messages.gen.js'
|
||||
import { InputChange, SetInputValue, SetInputChecked } from '../app/messages.gen.js'
|
||||
|
||||
const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date', 'tel']
|
||||
|
||||
// TODO: take into consideration "contenteditable" attribute
|
||||
type TextEditableElement = HTMLInputElement | HTMLTextAreaElement
|
||||
function isTextEditable(node: any): node is TextEditableElement {
|
||||
type TextFieldElement = HTMLInputElement | HTMLTextAreaElement
|
||||
|
||||
function isTextFieldElement(node: Node): node is TextFieldElement {
|
||||
if (hasTag(node, 'textarea')) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -18,7 +19,7 @@ function isTextEditable(node: any): node is TextEditableElement {
|
|||
return INPUT_TYPES.includes(node.type)
|
||||
}
|
||||
|
||||
function isCheckable(node: any): node is HTMLInputElement {
|
||||
function isCheckbox(node: Node): node is HTMLInputElement & { type: 'checkbox' | 'radio' } {
|
||||
if (!hasTag(node, 'input')) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -26,7 +27,7 @@ function isCheckable(node: any): node is HTMLInputElement {
|
|||
return type === 'checkbox' || type === 'radio'
|
||||
}
|
||||
|
||||
const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | undefined =
|
||||
const labelElementFor: (element: TextFieldElement) => HTMLLabelElement | undefined =
|
||||
IN_BROWSER && 'labels' in HTMLInputElement.prototype
|
||||
? (node) => {
|
||||
let p: Node | null = node
|
||||
|
|
@ -56,7 +57,7 @@ const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | unde
|
|||
}
|
||||
}
|
||||
|
||||
export function getInputLabel(node: TextEditableElement): string {
|
||||
export function getInputLabel(node: TextFieldElement): string {
|
||||
let label = getLabelAttribute(node)
|
||||
if (label === null) {
|
||||
const labelElement = labelElementFor(node)
|
||||
|
|
@ -94,13 +95,8 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
},
|
||||
opts,
|
||||
)
|
||||
function sendInputTarget(id: number, node: TextEditableElement): void {
|
||||
const label = getInputLabel(node)
|
||||
if (label !== '') {
|
||||
app.send(SetInputTarget(id, label))
|
||||
}
|
||||
}
|
||||
function sendInputValue(id: number, node: TextEditableElement | HTMLSelectElement): void {
|
||||
|
||||
function getInputValue(id: number, node: TextFieldElement | HTMLSelectElement) {
|
||||
let value = node.value
|
||||
let inputMode: InputMode = options.defaultInputMode
|
||||
|
||||
|
|
@ -127,42 +123,63 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
break
|
||||
}
|
||||
|
||||
return { value, mask }
|
||||
}
|
||||
function sendInputValue(id: number, node: TextFieldElement | HTMLSelectElement): void {
|
||||
const { value, mask } = getInputValue(id, node)
|
||||
|
||||
app.send(SetInputValue(id, value, mask))
|
||||
}
|
||||
|
||||
const inputValues: Map<number, string> = new Map()
|
||||
const checkableValues: Map<number, boolean> = new Map()
|
||||
const registeredTargets: Set<number> = new Set()
|
||||
const checkboxValues: Map<number, boolean> = new Map()
|
||||
|
||||
app.attachStopCallback(() => {
|
||||
inputValues.clear()
|
||||
checkableValues.clear()
|
||||
registeredTargets.clear()
|
||||
checkboxValues.clear()
|
||||
})
|
||||
|
||||
app.ticker.attach((): void => {
|
||||
function trackInputValue(id: number, node: TextFieldElement) {
|
||||
if (inputValues.get(id) === node.value) {
|
||||
return
|
||||
}
|
||||
inputValues.set(id, node.value)
|
||||
sendInputValue(id, node)
|
||||
}
|
||||
|
||||
function trackCheckboxValue(id: number, value: boolean) {
|
||||
if (checkboxValues.get(id) === value) {
|
||||
return
|
||||
}
|
||||
checkboxValues.set(id, value)
|
||||
app.send(SetInputChecked(id, value))
|
||||
}
|
||||
|
||||
// The only way (to our knowledge) to track all kinds of input changes, including those made by JS
|
||||
app.ticker.attach(() => {
|
||||
inputValues.forEach((value, id) => {
|
||||
const node = app.nodes.getNode(id) as HTMLInputElement
|
||||
if (!node) return inputValues.delete(id)
|
||||
if (value !== node.value) {
|
||||
inputValues.set(id, node.value)
|
||||
if (!registeredTargets.has(id)) {
|
||||
registeredTargets.add(id)
|
||||
sendInputTarget(id, node)
|
||||
}
|
||||
sendInputValue(id, node)
|
||||
}
|
||||
trackInputValue(id, node)
|
||||
})
|
||||
checkableValues.forEach((checked, id) => {
|
||||
checkboxValues.forEach((checked, id) => {
|
||||
const node = app.nodes.getNode(id) as HTMLInputElement
|
||||
if (!node) return checkableValues.delete(id)
|
||||
if (checked !== node.checked) {
|
||||
checkableValues.set(id, node.checked)
|
||||
app.send(SetInputChecked(id, node.checked))
|
||||
}
|
||||
if (!node) return checkboxValues.delete(id)
|
||||
trackCheckboxValue(id, node.checked)
|
||||
})
|
||||
})
|
||||
app.ticker.attach(Set.prototype.clear, 100, false, registeredTargets)
|
||||
}, 3)
|
||||
|
||||
function sendInputChange(
|
||||
id: number,
|
||||
node: TextFieldElement,
|
||||
hesitationTime: number,
|
||||
inputTime: number,
|
||||
) {
|
||||
const { value, mask } = getInputValue(id, node)
|
||||
const label = getInputLabel(node)
|
||||
|
||||
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime))
|
||||
}
|
||||
|
||||
app.nodes.attachNodeCallback(
|
||||
app.safe((node: Node): void => {
|
||||
|
|
@ -170,21 +187,44 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
// TODO: support multiple select (?): use selectedOptions; Need send target?
|
||||
|
||||
// TODO: support multiple select (?): use selectedOptions;
|
||||
if (hasTag(node, 'select')) {
|
||||
sendInputValue(id, node)
|
||||
app.attachEventListener(node, 'change', () => {
|
||||
sendInputValue(id, node)
|
||||
})
|
||||
app.nodes.attachNodeListener(node, 'change', () => sendInputValue(id, node))
|
||||
}
|
||||
if (isTextEditable(node)) {
|
||||
inputValues.set(id, node.value)
|
||||
sendInputValue(id, node)
|
||||
|
||||
if (isTextFieldElement(node)) {
|
||||
trackInputValue(id, node)
|
||||
let nodeFocusTime = 0
|
||||
let nodeHesitationTime = 0
|
||||
let inputTime = 0
|
||||
|
||||
const onFocus = () => {
|
||||
nodeFocusTime = now()
|
||||
}
|
||||
|
||||
const onInput = () => {
|
||||
if (nodeHesitationTime === 0) {
|
||||
nodeHesitationTime = now() - nodeFocusTime
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = () => {
|
||||
inputTime = now() - nodeFocusTime
|
||||
sendInputChange(id, node, nodeHesitationTime, inputTime)
|
||||
nodeHesitationTime = 0
|
||||
inputTime = 0
|
||||
}
|
||||
app.nodes.attachNodeListener(node, 'focus', onFocus)
|
||||
app.nodes.attachNodeListener(node, 'input', onInput)
|
||||
app.nodes.attachNodeListener(node, 'change', onChange)
|
||||
return
|
||||
}
|
||||
if (isCheckable(node)) {
|
||||
checkableValues.set(id, node.checked)
|
||||
app.send(SetInputChecked(id, node.checked))
|
||||
|
||||
if (isCheckbox(node)) {
|
||||
trackCheckboxValue(id, node.checked)
|
||||
app.nodes.attachNodeListener(node, 'change', () => trackCheckboxValue(id, node.checked))
|
||||
return
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type App from '../app/index.js'
|
||||
import { hasTag, isSVGElement, isDocument } from '../app/guards.js'
|
||||
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js'
|
||||
import { MouseMove, MouseClick } from '../app/messages.gen.js'
|
||||
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute, now } from '../utils.js'
|
||||
import { MouseMove, MouseClick, MouseThrashing } from '../app/messages.gen.js'
|
||||
import { getInputLabel } from './input.js'
|
||||
import { finder } from '@medv/finder'
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ function isClickable(element: Element): boolean {
|
|||
element.getAttribute('role') === 'button'
|
||||
)
|
||||
//|| element.className.includes("btn")
|
||||
// MBTODO: intersept addEventListener
|
||||
// MBTODO: intercept addEventListener
|
||||
}
|
||||
|
||||
//TODO: fix (typescript is not sure about target variable after assignation of svg)
|
||||
|
|
@ -100,12 +100,46 @@ export default function (app: App): void {
|
|||
let mouseTargetTime = 0
|
||||
let selectorMap: { [id: number]: string } = {}
|
||||
|
||||
let velocity = 0
|
||||
let direction = 0
|
||||
let directionChangeCount = 0
|
||||
let distance = 0
|
||||
let checkIntervalId: NodeJS.Timer
|
||||
const shakeThreshold = 0.008
|
||||
const shakeCheckInterval = 225
|
||||
|
||||
function checkMouseShaking() {
|
||||
const nextVelocity = distance / shakeCheckInterval
|
||||
|
||||
if (!velocity) {
|
||||
velocity = nextVelocity
|
||||
return
|
||||
}
|
||||
|
||||
const acceleration = (nextVelocity - velocity) / shakeCheckInterval
|
||||
if (directionChangeCount > 3 && acceleration > shakeThreshold) {
|
||||
console.log('Mouse shake detected!')
|
||||
app.send(MouseThrashing(now()))
|
||||
}
|
||||
|
||||
distance = 0
|
||||
directionChangeCount = 0
|
||||
velocity = nextVelocity
|
||||
}
|
||||
|
||||
app.attachStartCallback(() => {
|
||||
checkIntervalId = setInterval(() => checkMouseShaking(), shakeCheckInterval)
|
||||
})
|
||||
|
||||
app.attachStopCallback(() => {
|
||||
mousePositionX = -1
|
||||
mousePositionY = -1
|
||||
mousePositionChanged = false
|
||||
mouseTarget = null
|
||||
selectorMap = {}
|
||||
if (checkIntervalId) {
|
||||
clearInterval(checkIntervalId)
|
||||
}
|
||||
})
|
||||
|
||||
const sendMouseMove = (): void => {
|
||||
|
|
@ -139,6 +173,14 @@ export default function (app: App): void {
|
|||
mousePositionX = e.clientX + left
|
||||
mousePositionY = e.clientY + top
|
||||
mousePositionChanged = true
|
||||
|
||||
const nextDirection = Math.sign(e.movementX)
|
||||
distance += Math.abs(e.movementX) + Math.abs(e.movementY)
|
||||
|
||||
if (nextDirection !== direction) {
|
||||
direction = nextDirection
|
||||
directionChangeCount++
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
|
|
|||
39
tracker/tracker/src/main/modules/selection.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type App from '../app/index.js'
|
||||
import { SelectionChange } from '../app/messages.gen.js'
|
||||
|
||||
function selection(app: App) {
|
||||
app.attachEventListener(document, 'selectionchange', () => {
|
||||
const selection = document.getSelection()
|
||||
if (selection !== null && !selection.isCollapsed) {
|
||||
const selectionStart = app.nodes.getID(selection.anchorNode!)
|
||||
const selectionEnd = app.nodes.getID(selection.focusNode!)
|
||||
const selectedText = selection.toString().replace(/\s+/g, ' ')
|
||||
if (selectionStart && selectionEnd) {
|
||||
app.send(SelectionChange(selectionStart, selectionEnd, selectedText))
|
||||
}
|
||||
} else {
|
||||
app.send(SelectionChange(-1, -1, ''))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default selection
|
||||
|
||||
/** TODO: research how to get all in-between nodes inside selection range
|
||||
* including nodes between anchor and focus nodes and their children
|
||||
* without recursively searching the dom tree
|
||||
*/
|
||||
|
||||
// if (selection.rangeCount) {
|
||||
// const nodes = [];
|
||||
// for (let i = 0; i < selection.rangeCount; i++) {
|
||||
// const range = selection.getRangeAt(i);
|
||||
// let node: Node | null = range.startContainer;
|
||||
// while (node) {
|
||||
// nodes.push(node);
|
||||
// if (node === range.endContainer) break;
|
||||
// node = node.nextSibling;
|
||||
// }
|
||||
// }
|
||||
// // send selected nodes
|
||||
// }
|
||||
|
|
@ -254,6 +254,18 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
|||
return this.uint(msg[1]) && this.uint(msg[2])
|
||||
break
|
||||
|
||||
case Messages.Type.InputChange:
|
||||
return this.uint(msg[1]) && this.string(msg[2]) && this.boolean(msg[3]) && this.string(msg[4]) && this.int(msg[5]) && this.int(msg[6])
|
||||
break
|
||||
|
||||
case Messages.Type.SelectionChange:
|
||||
return this.uint(msg[1]) && this.uint(msg[2]) && this.string(msg[3])
|
||||
break
|
||||
|
||||
case Messages.Type.MouseThrashing:
|
||||
return this.uint(msg[1])
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||