Merge pull request #968 from openreplay/new-frustrations

feat(tracker): add input hesitation, change input change event handling
This commit is contained in:
Delirium 2023-03-17 10:32:30 +01:00 committed by GitHub
commit df90d3a8ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1277 additions and 283 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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]);

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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[] = []

View file

@ -0,0 +1,7 @@
.openreplay-selection-start {
border: 2px solid red;
}
.openreplay-selection-end {
border: 2px solid red;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,9 +31,9 @@ export interface Options {
controlConfirm: ConfirmOptions;
recordingConfirm: ConfirmOptions;
// @depricated
// @deprecated
confirmText?: string;
// @depricated
// @deprecated
confirmStyle?: Properties;
config: RTCConfiguration;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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