Tracker 14.x.x changes (#2240)

* feat tracker: add document titles to tabs

* feat: titles for tabs

* feat tracker: send initial title, parse titles better

* feat ui: tab name styles

* feat tracker: update changelogs

* fix tracker: fix tests

* fix tracker: fix failing tests, add some coverage

* fix tracker: fix failing tests, add some coverage

* Heatmaps  (#2264)

* feat ui: start heatmaps ui and tracker update

* fix ui: drop clickmap from session

* fix ui: refactor heatmap painter

* fix ui: store click coords as int percent

* feat(backend): insert normalized x and y to PG

* feat(backend): insert normalized x and y to CH

* feat(connector): added missing import

* feat(backend): fixed different uint type issue

* fix tracker: use max scrollable size for doc

* fix gen files

* fix ui: fix random crash, remove demo data generator

* fix ui: rm some dead code

---------

Co-authored-by: Alexander <zavorotynskiy@pm.me>

* fix tracker: add heatmap changelog to tracker CHANGELOG.md

* fix tracker: fix peerjs version to 1.5.4 (was 1.5.1)

* fix document height calculation

* Crossdomain tracking (#2277)

* feat tracker: crossdomain tracking (start commit)

* catch crossdomain messages, add nodeid placeholder

* click scroll

* frame placeholder number -> dynamic

* click rewriter, fix scroll prop

* some docs

* some docs

* fix options merging

* CHANGELOG.md update

* checking that crossdomain will not fire automatically

* simplify func declaration

* update test data

* change clickmap document height calculation to scrollheight (which should be true height)

---------

Co-authored-by: Alexander <zavorotynskiy@pm.me>
This commit is contained in:
Delirium 2024-06-24 13:49:26 +02:00 committed by GitHub
parent 82d2023019
commit 960da9f037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1944 additions and 874 deletions

View file

@ -12,7 +12,7 @@ type bulksTask struct {
}
func NewBulksTask() *bulksTask {
return &bulksTask{bulks: make([]Bulk, 0, 15)}
return &bulksTask{bulks: make([]Bulk, 0, 16)}
}
type BulkSet struct {
@ -32,6 +32,7 @@ type BulkSet struct {
webIssueEvents Bulk
webCustomEvents Bulk
webClickEvents Bulk
webClickXYEvents Bulk
webNetworkRequest Bulk
webCanvasNodes Bulk
webTagTriggers Bulk
@ -82,6 +83,8 @@ func (conn *BulkSet) Get(name string) Bulk {
return conn.webCustomEvents
case "webClickEvents":
return conn.webClickEvents
case "webClickXYEvents":
return conn.webClickXYEvents
case "webNetworkRequest":
return conn.webNetworkRequest
case "canvasNodes":
@ -203,6 +206,14 @@ func (conn *BulkSet) initBulks() {
if err != nil {
conn.log.Fatal(conn.ctx, "can't create webClickEvents bulk: %s", err)
}
conn.webClickXYEvents, err = NewBulk(conn.c,
"events.clicks",
"(session_id, message_id, timestamp, label, selector, url, path, hesitation, normalized_x, normalized_y)",
"($%d, $%d, $%d, NULLIF(LEFT($%d, 2000), ''), LEFT($%d, 8000), LEFT($%d, 2000), LEFT($%d, 2000), $%d, $%d, $%d)",
10, 200)
if err != nil {
conn.log.Fatal(conn.ctx, "can't create webClickEvents bulk: %s", err)
}
conn.webNetworkRequest, err = NewBulk(conn.c,
"events_common.requests",
"(session_id, timestamp, seq_index, url, host, path, query, request_body, response_body, status_code, method, duration, success, transfer_size)",
@ -246,6 +257,7 @@ func (conn *BulkSet) Send() {
newTask.bulks = append(newTask.bulks, conn.webIssueEvents)
newTask.bulks = append(newTask.bulks, conn.webCustomEvents)
newTask.bulks = append(newTask.bulks, conn.webClickEvents)
newTask.bulks = append(newTask.bulks, conn.webClickXYEvents)
newTask.bulks = append(newTask.bulks, conn.webNetworkRequest)
newTask.bulks = append(newTask.bulks, conn.webCanvasNodes)
newTask.bulks = append(newTask.bulks, conn.webTagTriggers)

View file

@ -132,9 +132,16 @@ func (conn *Conn) InsertWebClickEvent(sess *sessions.Session, e *messages.MouseC
}
var host, path string
host, path, _, _ = url.GetURLParts(e.Url)
if err := conn.bulks.Get("webClickEvents").Append(sess.SessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path, e.HesitationTime); err != nil {
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
conn.log.Error(sessCtx, "insert web click event in bulk err: %s", err)
if e.NormalizedX <= 100 && e.NormalizedY <= 100 {
if err := conn.bulks.Get("webClickXYEvents").Append(sess.SessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path, e.HesitationTime, e.NormalizedX, e.NormalizedY); err != nil {
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
conn.log.Error(sessCtx, "insert web click event in bulk err: %s", err)
}
} else {
if err := conn.bulks.Get("webClickEvents").Append(sess.SessionID, truncSqIdx(e.MsgID()), e.Timestamp, e.Label, e.Selector, host+path, path, e.HesitationTime); err != nil {
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
conn.log.Error(sessCtx, "insert web click event in bulk err: %s", err)
}
}
// Add new value set to autocomplete bulk
conn.InsertAutocompleteValue(sess.SessionID, sess.ProjectID, "CLICK", e.Label)

View file

@ -10,5 +10,5 @@ func IsMobileType(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 || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == 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 || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
}

View file

@ -43,6 +43,15 @@ func transformDeprecated(msg Message) Message {
TransferredSize: 0,
Cached: false,
}
case *MouseClickDeprecated:
return &MouseClick{
ID: m.ID,
HesitationTime: m.HesitationTime,
Label: m.Label,
Selector: m.Selector,
NormalizedX: 101, // 101 is a magic number to signal that the value is not present
NormalizedY: 101, // 101 is a magic number to signal that the value is not present
}
}
return msg
}

View file

@ -5,7 +5,7 @@ const (
MsgTimestamp = 0
MsgSessionStart = 1
MsgSessionEndDeprecated = 3
MsgSetPageLocation = 4
MsgSetPageLocationDeprecated = 4
MsgSetViewportSize = 5
MsgSetViewportScroll = 6
MsgCreateDocument = 7
@ -62,7 +62,8 @@ const (
MsgCustomIssue = 64
MsgAssetCache = 66
MsgCSSInsertRuleURLBased = 67
MsgMouseClick = 69
MsgMouseClick = 68
MsgMouseClickDeprecated = 69
MsgCreateIFrameDocument = 70
MsgAdoptedSSReplaceURLBased = 71
MsgAdoptedSSReplace = 72
@ -88,6 +89,7 @@ const (
MsgCanvasNode = 119
MsgTagTrigger = 120
MsgRedux = 121
MsgSetPageLocation = 122
MsgIssueEvent = 125
MsgSessionEnd = 126
MsgSessionSearch = 127
@ -205,14 +207,14 @@ func (msg *SessionEndDeprecated) TypeID() int {
return 3
}
type SetPageLocation struct {
type SetPageLocationDeprecated struct {
message
URL string
Referrer string
NavigationStart uint64
}
func (msg *SetPageLocation) Encode() []byte {
func (msg *SetPageLocationDeprecated) Encode() []byte {
buf := make([]byte, 31+len(msg.URL)+len(msg.Referrer))
buf[0] = 4
p := 1
@ -222,11 +224,11 @@ func (msg *SetPageLocation) Encode() []byte {
return buf[:p]
}
func (msg *SetPageLocation) Decode() Message {
func (msg *SetPageLocationDeprecated) Decode() Message {
return msg
}
func (msg *SetPageLocation) TypeID() int {
func (msg *SetPageLocationDeprecated) TypeID() int {
return 4
}
@ -1693,9 +1695,40 @@ type MouseClick struct {
HesitationTime uint64
Label string
Selector string
NormalizedX uint64
NormalizedY uint64
}
func (msg *MouseClick) Encode() []byte {
buf := make([]byte, 61+len(msg.Label)+len(msg.Selector))
buf[0] = 68
p := 1
p = WriteUint(msg.ID, buf, p)
p = WriteUint(msg.HesitationTime, buf, p)
p = WriteString(msg.Label, buf, p)
p = WriteString(msg.Selector, buf, p)
p = WriteUint(msg.NormalizedX, buf, p)
p = WriteUint(msg.NormalizedY, buf, p)
return buf[:p]
}
func (msg *MouseClick) Decode() Message {
return msg
}
func (msg *MouseClick) TypeID() int {
return 68
}
type MouseClickDeprecated struct {
message
ID uint64
HesitationTime uint64
Label string
Selector string
}
func (msg *MouseClickDeprecated) Encode() []byte {
buf := make([]byte, 41+len(msg.Label)+len(msg.Selector))
buf[0] = 69
p := 1
@ -1706,11 +1739,11 @@ func (msg *MouseClick) Encode() []byte {
return buf[:p]
}
func (msg *MouseClick) Decode() Message {
func (msg *MouseClickDeprecated) Decode() Message {
return msg
}
func (msg *MouseClick) TypeID() int {
func (msg *MouseClickDeprecated) TypeID() int {
return 69
}
@ -2351,6 +2384,33 @@ func (msg *Redux) TypeID() int {
return 121
}
type SetPageLocation struct {
message
URL string
Referrer string
NavigationStart uint64
DocumentTitle string
}
func (msg *SetPageLocation) Encode() []byte {
buf := make([]byte, 41+len(msg.URL)+len(msg.Referrer)+len(msg.DocumentTitle))
buf[0] = 122
p := 1
p = WriteString(msg.URL, buf, p)
p = WriteString(msg.Referrer, buf, p)
p = WriteUint(msg.NavigationStart, buf, p)
p = WriteString(msg.DocumentTitle, buf, p)
return buf[:p]
}
func (msg *SetPageLocation) Decode() Message {
return msg
}
func (msg *SetPageLocation) TypeID() int {
return 122
}
type IssueEvent struct {
message
MessageID uint64

View file

@ -77,9 +77,9 @@ func DecodeSessionEndDeprecated(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeSetPageLocation(reader BytesReader) (Message, error) {
func DecodeSetPageLocationDeprecated(reader BytesReader) (Message, error) {
var err error = nil
msg := &SetPageLocation{}
msg := &SetPageLocationDeprecated{}
if msg.URL, err = reader.ReadString(); err != nil {
return nil, err
}
@ -1032,6 +1032,30 @@ func DecodeMouseClick(reader BytesReader) (Message, error) {
if msg.Selector, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.NormalizedX, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.NormalizedY, err = reader.ReadUint(); err != nil {
return nil, err
}
return msg, err
}
func DecodeMouseClickDeprecated(reader BytesReader) (Message, error) {
var err error = nil
msg := &MouseClickDeprecated{}
if msg.ID, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.HesitationTime, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Label, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Selector, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
@ -1428,6 +1452,24 @@ func DecodeRedux(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeSetPageLocation(reader BytesReader) (Message, error) {
var err error = nil
msg := &SetPageLocation{}
if msg.URL, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Referrer, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.NavigationStart, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.DocumentTitle, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
func DecodeIssueEvent(reader BytesReader) (Message, error) {
var err error = nil
msg := &IssueEvent{}
@ -1899,7 +1941,7 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
case 3:
return DecodeSessionEndDeprecated(reader)
case 4:
return DecodeSetPageLocation(reader)
return DecodeSetPageLocationDeprecated(reader)
case 5:
return DecodeSetViewportSize(reader)
case 6:
@ -2012,8 +2054,10 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeAssetCache(reader)
case 67:
return DecodeCSSInsertRuleURLBased(reader)
case 69:
case 68:
return DecodeMouseClick(reader)
case 69:
return DecodeMouseClickDeprecated(reader)
case 70:
return DecodeCreateIFrameDocument(reader)
case 71:
@ -2064,6 +2108,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeTagTrigger(reader)
case 121:
return DecodeRedux(reader)
case 122:
return DecodeSetPageLocation(reader)
case 125:
return DecodeIssueEvent(reader)
case 126:

View file

@ -119,7 +119,7 @@ var batches = map[string]string{
"resources": "INSERT INTO experimental.resources (session_id, project_id, message_id, datetime, url, type, duration, ttfb, header_size, encoded_body_size, decoded_body_size, success, url_path) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, SUBSTR(?, 1, 8000))",
"autocompletes": "INSERT INTO experimental.autocomplete (project_id, type, value) VALUES (?, ?, SUBSTR(?, 1, 8000))",
"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, url_path, event_type) VALUES (?, ?, ?, ?, SUBSTR(?, 1, 8000), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, SUBSTR(?, 1, 8000), ?)",
"clicks": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, hesitation_time, event_type, selector) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"clicks": "INSERT INTO experimental.events (session_id, project_id, message_id, datetime, label, hesitation_time, event_type, selector, normalized_x, normalized_y) 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), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
@ -398,6 +398,14 @@ func (c *connectorImpl) InsertWebClickEvent(session *sessions.Session, msg *mess
if msg.Label == "" {
return nil
}
var nX *uint32 = nil
var nY *uint32 = nil
if msg.NormalizedX <= 100 && msg.NormalizedY <= 100 {
nXVal := uint32(msg.NormalizedX)
nX = &nXVal
nYVal := uint32(msg.NormalizedY)
nY = &nYVal
}
if err := c.batches["clicks"].Append(
session.SessionID,
uint16(session.ProjectID),
@ -407,6 +415,8 @@ func (c *connectorImpl) InsertWebClickEvent(session *sessions.Session, msg *mess
nullableUint32(uint32(msg.HesitationTime)),
"CLICK",
msg.Selector,
nX,
nY,
); err != nil {
c.checkError("clicks", err)
return fmt.Errorf("can't append to clicks batch: %s", err)

View file

@ -42,7 +42,7 @@ class SessionEndDeprecated(Message):
self.timestamp = timestamp
class SetPageLocation(Message):
class SetPageLocationDeprecated(Message):
__id__ = 4
def __init__(self, url, referrer, navigation_start):
@ -585,6 +585,18 @@ class CSSInsertRuleURLBased(Message):
class MouseClick(Message):
__id__ = 68
def __init__(self, id, hesitation_time, label, selector, normalized_x, normalized_y):
self.id = id
self.hesitation_time = hesitation_time
self.label = label
self.selector = selector
self.normalized_x = normalized_x
self.normalized_y = normalized_y
class MouseClickDeprecated(Message):
__id__ = 69
def __init__(self, id, hesitation_time, label, selector):
@ -825,6 +837,16 @@ class Redux(Message):
self.action_time = action_time
class SetPageLocation(Message):
__id__ = 122
def __init__(self, url, referrer, navigation_start, document_title):
self.url = url
self.referrer = referrer
self.navigation_start = navigation_start
self.document_title = document_title
class IssueEvent(Message):
__id__ = 125
@ -854,7 +876,7 @@ class SessionSearch(Message):
self.partition = partition
class IOSSessionStart(Message):
class MobileSessionStart(Message):
__id__ = 90
def __init__(self, timestamp, project_id, tracker_version, rev_id, user_uuid, user_os, user_os_version, user_device, user_device_type, user_country):
@ -870,14 +892,14 @@ class IOSSessionStart(Message):
self.user_country = user_country
class IOSSessionEnd(Message):
class MobileSessionEnd(Message):
__id__ = 91
def __init__(self, timestamp):
self.timestamp = timestamp
class IOSMetadata(Message):
class MobileMetadata(Message):
__id__ = 92
def __init__(self, timestamp, length, key, value):
@ -887,7 +909,7 @@ class IOSMetadata(Message):
self.value = value
class IOSEvent(Message):
class MobileEvent(Message):
__id__ = 93
def __init__(self, timestamp, length, name, payload):
@ -897,7 +919,7 @@ class IOSEvent(Message):
self.payload = payload
class IOSUserID(Message):
class MobileUserID(Message):
__id__ = 94
def __init__(self, timestamp, length, id):
@ -906,7 +928,7 @@ class IOSUserID(Message):
self.id = id
class IOSUserAnonymousID(Message):
class MobileUserAnonymousID(Message):
__id__ = 95
def __init__(self, timestamp, length, id):
@ -915,7 +937,7 @@ class IOSUserAnonymousID(Message):
self.id = id
class IOSScreenChanges(Message):
class MobileScreenChanges(Message):
__id__ = 96
def __init__(self, timestamp, length, x, y, width, height):
@ -927,7 +949,7 @@ class IOSScreenChanges(Message):
self.height = height
class IOSCrash(Message):
class MobileCrash(Message):
__id__ = 97
def __init__(self, timestamp, length, name, reason, stacktrace):
@ -938,7 +960,7 @@ class IOSCrash(Message):
self.stacktrace = stacktrace
class IOSViewComponentEvent(Message):
class MobileViewComponentEvent(Message):
__id__ = 98
def __init__(self, timestamp, length, screen_name, view_name, visible):
@ -949,7 +971,7 @@ class IOSViewComponentEvent(Message):
self.visible = visible
class IOSClickEvent(Message):
class MobileClickEvent(Message):
__id__ = 100
def __init__(self, timestamp, length, label, x, y):
@ -960,7 +982,7 @@ class IOSClickEvent(Message):
self.y = y
class IOSInputEvent(Message):
class MobileInputEvent(Message):
__id__ = 101
def __init__(self, timestamp, length, value, value_masked, label):
@ -971,7 +993,7 @@ class IOSInputEvent(Message):
self.label = label
class IOSPerformanceEvent(Message):
class MobilePerformanceEvent(Message):
__id__ = 102
def __init__(self, timestamp, length, name, value):
@ -981,7 +1003,7 @@ class IOSPerformanceEvent(Message):
self.value = value
class IOSLog(Message):
class MobileLog(Message):
__id__ = 103
def __init__(self, timestamp, length, severity, content):
@ -991,7 +1013,7 @@ class IOSLog(Message):
self.content = content
class IOSInternalError(Message):
class MobileInternalError(Message):
__id__ = 104
def __init__(self, timestamp, length, content):
@ -1000,7 +1022,7 @@ class IOSInternalError(Message):
self.content = content
class IOSNetworkCall(Message):
class MobileNetworkCall(Message):
__id__ = 105
def __init__(self, timestamp, length, type, method, url, request, response, status, duration):
@ -1015,7 +1037,7 @@ class IOSNetworkCall(Message):
self.duration = duration
class IOSSwipeEvent(Message):
class MobileSwipeEvent(Message):
__id__ = 106
def __init__(self, timestamp, length, label, x, y, direction):
@ -1027,7 +1049,7 @@ class IOSSwipeEvent(Message):
self.direction = direction
class IOSBatchMeta(Message):
class MobileBatchMeta(Message):
__id__ = 107
def __init__(self, timestamp, length, first_index):
@ -1036,7 +1058,7 @@ class IOSBatchMeta(Message):
self.first_index = first_index
class IOSPerformanceAggregated(Message):
class MobilePerformanceAggregated(Message):
__id__ = 110
def __init__(self, timestamp_start, timestamp_end, min_fps, avg_fps, max_fps, min_cpu, avg_cpu, max_cpu, min_memory, avg_memory, max_memory, min_battery, avg_battery, max_battery):
@ -1056,7 +1078,7 @@ class IOSPerformanceAggregated(Message):
self.max_battery = max_battery
class IOSIssueEvent(Message):
class MobileIssueEvent(Message):
__id__ = 111
def __init__(self, timestamp, type, context_string, context, payload):

View file

@ -67,7 +67,7 @@ cdef class SessionEndDeprecated(PyMessage):
self.timestamp = timestamp
cdef class SetPageLocation(PyMessage):
cdef class SetPageLocationDeprecated(PyMessage):
cdef public int __id__
cdef public str url
cdef public str referrer
@ -872,6 +872,25 @@ cdef class MouseClick(PyMessage):
cdef public unsigned long hesitation_time
cdef public str label
cdef public str selector
cdef public unsigned long normalized_x
cdef public unsigned long normalized_y
def __init__(self, unsigned long id, unsigned long hesitation_time, str label, str selector, unsigned long normalized_x, unsigned long normalized_y):
self.__id__ = 68
self.id = id
self.hesitation_time = hesitation_time
self.label = label
self.selector = selector
self.normalized_x = normalized_x
self.normalized_y = normalized_y
cdef class MouseClickDeprecated(PyMessage):
cdef public int __id__
cdef public unsigned long id
cdef public unsigned long hesitation_time
cdef public str label
cdef public str selector
def __init__(self, unsigned long id, unsigned long hesitation_time, str label, str selector):
self.__id__ = 69
@ -1218,6 +1237,21 @@ cdef class Redux(PyMessage):
self.action_time = action_time
cdef class SetPageLocation(PyMessage):
cdef public int __id__
cdef public str url
cdef public str referrer
cdef public unsigned long navigation_start
cdef public str document_title
def __init__(self, str url, str referrer, unsigned long navigation_start, str document_title):
self.__id__ = 122
self.url = url
self.referrer = referrer
self.navigation_start = navigation_start
self.document_title = document_title
cdef class IssueEvent(PyMessage):
cdef public int __id__
cdef public unsigned long message_id
@ -1261,7 +1295,7 @@ cdef class SessionSearch(PyMessage):
self.partition = partition
cdef class IOSSessionStart(PyMessage):
cdef class MobileSessionStart(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long project_id
@ -1288,7 +1322,7 @@ cdef class IOSSessionStart(PyMessage):
self.user_country = user_country
cdef class IOSSessionEnd(PyMessage):
cdef class MobileSessionEnd(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
@ -1297,7 +1331,7 @@ cdef class IOSSessionEnd(PyMessage):
self.timestamp = timestamp
cdef class IOSMetadata(PyMessage):
cdef class MobileMetadata(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1312,7 +1346,7 @@ cdef class IOSMetadata(PyMessage):
self.value = value
cdef class IOSEvent(PyMessage):
cdef class MobileEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1327,7 +1361,7 @@ cdef class IOSEvent(PyMessage):
self.payload = payload
cdef class IOSUserID(PyMessage):
cdef class MobileUserID(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1340,7 +1374,7 @@ cdef class IOSUserID(PyMessage):
self.id = id
cdef class IOSUserAnonymousID(PyMessage):
cdef class MobileUserAnonymousID(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1353,7 +1387,7 @@ cdef class IOSUserAnonymousID(PyMessage):
self.id = id
cdef class IOSScreenChanges(PyMessage):
cdef class MobileScreenChanges(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1372,7 +1406,7 @@ cdef class IOSScreenChanges(PyMessage):
self.height = height
cdef class IOSCrash(PyMessage):
cdef class MobileCrash(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1389,7 +1423,7 @@ cdef class IOSCrash(PyMessage):
self.stacktrace = stacktrace
cdef class IOSViewComponentEvent(PyMessage):
cdef class MobileViewComponentEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1406,7 +1440,7 @@ cdef class IOSViewComponentEvent(PyMessage):
self.visible = visible
cdef class IOSClickEvent(PyMessage):
cdef class MobileClickEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1423,7 +1457,7 @@ cdef class IOSClickEvent(PyMessage):
self.y = y
cdef class IOSInputEvent(PyMessage):
cdef class MobileInputEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1440,7 +1474,7 @@ cdef class IOSInputEvent(PyMessage):
self.label = label
cdef class IOSPerformanceEvent(PyMessage):
cdef class MobilePerformanceEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1455,7 +1489,7 @@ cdef class IOSPerformanceEvent(PyMessage):
self.value = value
cdef class IOSLog(PyMessage):
cdef class MobileLog(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1470,7 +1504,7 @@ cdef class IOSLog(PyMessage):
self.content = content
cdef class IOSInternalError(PyMessage):
cdef class MobileInternalError(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1483,7 +1517,7 @@ cdef class IOSInternalError(PyMessage):
self.content = content
cdef class IOSNetworkCall(PyMessage):
cdef class MobileNetworkCall(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1508,7 +1542,7 @@ cdef class IOSNetworkCall(PyMessage):
self.duration = duration
cdef class IOSSwipeEvent(PyMessage):
cdef class MobileSwipeEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1527,7 +1561,7 @@ cdef class IOSSwipeEvent(PyMessage):
self.direction = direction
cdef class IOSBatchMeta(PyMessage):
cdef class MobileBatchMeta(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public unsigned long length
@ -1540,7 +1574,7 @@ cdef class IOSBatchMeta(PyMessage):
self.first_index = first_index
cdef class IOSPerformanceAggregated(PyMessage):
cdef class MobilePerformanceAggregated(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp_start
cdef public unsigned long timestamp_end
@ -1575,7 +1609,7 @@ cdef class IOSPerformanceAggregated(PyMessage):
self.max_battery = max_battery
cdef class IOSIssueEvent(PyMessage):
cdef class MobileIssueEvent(PyMessage):
cdef public int __id__
cdef public unsigned long timestamp
cdef public str type

View file

@ -124,7 +124,7 @@ class MessageCodec(Codec):
)
if message_id == 4:
return SetPageLocation(
return SetPageLocationDeprecated(
url=self.read_string(reader),
referrer=self.read_string(reader),
navigation_start=self.read_uint(reader)
@ -551,8 +551,18 @@ class MessageCodec(Codec):
base_url=self.read_string(reader)
)
if message_id == 69:
if message_id == 68:
return MouseClick(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader),
selector=self.read_string(reader),
normalized_x=self.read_uint(reader),
normalized_y=self.read_uint(reader)
)
if message_id == 69:
return MouseClickDeprecated(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader),
@ -740,6 +750,14 @@ class MessageCodec(Codec):
action_time=self.read_uint(reader)
)
if message_id == 122:
return SetPageLocation(
url=self.read_string(reader),
referrer=self.read_string(reader),
navigation_start=self.read_uint(reader),
document_title=self.read_string(reader)
)
if message_id == 125:
return IssueEvent(
message_id=self.read_uint(reader),
@ -764,7 +782,7 @@ class MessageCodec(Codec):
)
if message_id == 90:
return IOSSessionStart(
return MobileSessionStart(
timestamp=self.read_uint(reader),
project_id=self.read_uint(reader),
tracker_version=self.read_string(reader),
@ -778,12 +796,12 @@ class MessageCodec(Codec):
)
if message_id == 91:
return IOSSessionEnd(
return MobileSessionEnd(
timestamp=self.read_uint(reader)
)
if message_id == 92:
return IOSMetadata(
return MobileMetadata(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
key=self.read_string(reader),
@ -791,7 +809,7 @@ class MessageCodec(Codec):
)
if message_id == 93:
return IOSEvent(
return MobileEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
@ -799,21 +817,21 @@ class MessageCodec(Codec):
)
if message_id == 94:
return IOSUserID(
return MobileUserID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
id=self.read_string(reader)
)
if message_id == 95:
return IOSUserAnonymousID(
return MobileUserAnonymousID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
id=self.read_string(reader)
)
if message_id == 96:
return IOSScreenChanges(
return MobileScreenChanges(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
x=self.read_uint(reader),
@ -823,7 +841,7 @@ class MessageCodec(Codec):
)
if message_id == 97:
return IOSCrash(
return MobileCrash(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
@ -832,7 +850,7 @@ class MessageCodec(Codec):
)
if message_id == 98:
return IOSViewComponentEvent(
return MobileViewComponentEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
screen_name=self.read_string(reader),
@ -841,7 +859,7 @@ class MessageCodec(Codec):
)
if message_id == 100:
return IOSClickEvent(
return MobileClickEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
label=self.read_string(reader),
@ -850,7 +868,7 @@ class MessageCodec(Codec):
)
if message_id == 101:
return IOSInputEvent(
return MobileInputEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader),
@ -859,7 +877,7 @@ class MessageCodec(Codec):
)
if message_id == 102:
return IOSPerformanceEvent(
return MobilePerformanceEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
@ -867,7 +885,7 @@ class MessageCodec(Codec):
)
if message_id == 103:
return IOSLog(
return MobileLog(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
severity=self.read_string(reader),
@ -875,14 +893,14 @@ class MessageCodec(Codec):
)
if message_id == 104:
return IOSInternalError(
return MobileInternalError(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
content=self.read_string(reader)
)
if message_id == 105:
return IOSNetworkCall(
return MobileNetworkCall(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
type=self.read_string(reader),
@ -895,7 +913,7 @@ class MessageCodec(Codec):
)
if message_id == 106:
return IOSSwipeEvent(
return MobileSwipeEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
label=self.read_string(reader),
@ -905,14 +923,14 @@ class MessageCodec(Codec):
)
if message_id == 107:
return IOSBatchMeta(
return MobileBatchMeta(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
first_index=self.read_uint(reader)
)
if message_id == 110:
return IOSPerformanceAggregated(
return MobilePerformanceAggregated(
timestamp_start=self.read_uint(reader),
timestamp_end=self.read_uint(reader),
min_fps=self.read_uint(reader),
@ -930,7 +948,7 @@ class MessageCodec(Codec):
)
if message_id == 111:
return IOSIssueEvent(
return MobileIssueEvent(
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),

View file

@ -222,7 +222,7 @@ cdef class MessageCodec:
)
if message_id == 4:
return SetPageLocation(
return SetPageLocationDeprecated(
url=self.read_string(reader),
referrer=self.read_string(reader),
navigation_start=self.read_uint(reader)
@ -649,8 +649,18 @@ cdef class MessageCodec:
base_url=self.read_string(reader)
)
if message_id == 69:
if message_id == 68:
return MouseClick(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader),
selector=self.read_string(reader),
normalized_x=self.read_uint(reader),
normalized_y=self.read_uint(reader)
)
if message_id == 69:
return MouseClickDeprecated(
id=self.read_uint(reader),
hesitation_time=self.read_uint(reader),
label=self.read_string(reader),
@ -838,6 +848,14 @@ cdef class MessageCodec:
action_time=self.read_uint(reader)
)
if message_id == 122:
return SetPageLocation(
url=self.read_string(reader),
referrer=self.read_string(reader),
navigation_start=self.read_uint(reader),
document_title=self.read_string(reader)
)
if message_id == 125:
return IssueEvent(
message_id=self.read_uint(reader),
@ -862,7 +880,7 @@ cdef class MessageCodec:
)
if message_id == 90:
return IOSSessionStart(
return MobileSessionStart(
timestamp=self.read_uint(reader),
project_id=self.read_uint(reader),
tracker_version=self.read_string(reader),
@ -876,12 +894,12 @@ cdef class MessageCodec:
)
if message_id == 91:
return IOSSessionEnd(
return MobileSessionEnd(
timestamp=self.read_uint(reader)
)
if message_id == 92:
return IOSMetadata(
return MobileMetadata(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
key=self.read_string(reader),
@ -889,7 +907,7 @@ cdef class MessageCodec:
)
if message_id == 93:
return IOSEvent(
return MobileEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
@ -897,21 +915,21 @@ cdef class MessageCodec:
)
if message_id == 94:
return IOSUserID(
return MobileUserID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
id=self.read_string(reader)
)
if message_id == 95:
return IOSUserAnonymousID(
return MobileUserAnonymousID(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
id=self.read_string(reader)
)
if message_id == 96:
return IOSScreenChanges(
return MobileScreenChanges(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
x=self.read_uint(reader),
@ -921,7 +939,7 @@ cdef class MessageCodec:
)
if message_id == 97:
return IOSCrash(
return MobileCrash(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
@ -930,7 +948,7 @@ cdef class MessageCodec:
)
if message_id == 98:
return IOSViewComponentEvent(
return MobileViewComponentEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
screen_name=self.read_string(reader),
@ -939,7 +957,7 @@ cdef class MessageCodec:
)
if message_id == 100:
return IOSClickEvent(
return MobileClickEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
label=self.read_string(reader),
@ -948,7 +966,7 @@ cdef class MessageCodec:
)
if message_id == 101:
return IOSInputEvent(
return MobileInputEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
value=self.read_string(reader),
@ -957,7 +975,7 @@ cdef class MessageCodec:
)
if message_id == 102:
return IOSPerformanceEvent(
return MobilePerformanceEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
name=self.read_string(reader),
@ -965,7 +983,7 @@ cdef class MessageCodec:
)
if message_id == 103:
return IOSLog(
return MobileLog(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
severity=self.read_string(reader),
@ -973,14 +991,14 @@ cdef class MessageCodec:
)
if message_id == 104:
return IOSInternalError(
return MobileInternalError(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
content=self.read_string(reader)
)
if message_id == 105:
return IOSNetworkCall(
return MobileNetworkCall(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
type=self.read_string(reader),
@ -993,7 +1011,7 @@ cdef class MessageCodec:
)
if message_id == 106:
return IOSSwipeEvent(
return MobileSwipeEvent(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
label=self.read_string(reader),
@ -1003,14 +1021,14 @@ cdef class MessageCodec:
)
if message_id == 107:
return IOSBatchMeta(
return MobileBatchMeta(
timestamp=self.read_uint(reader),
length=self.read_uint(reader),
first_index=self.read_uint(reader)
)
if message_id == 110:
return IOSPerformanceAggregated(
return MobilePerformanceAggregated(
timestamp_start=self.read_uint(reader),
timestamp_end=self.read_uint(reader),
min_fps=self.read_uint(reader),
@ -1028,7 +1046,7 @@ cdef class MessageCodec:
)
if message_id == 111:
return IOSIssueEvent(
return MobileIssueEvent(
timestamp=self.read_uint(reader),
type=self.read_string(reader),
context_string=self.read_string(reader),

View file

@ -3,19 +3,15 @@ import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import ClickMapRenderer from 'App/components/Session/Player/ClickMapRenderer'
import { connect } from 'react-redux'
import { setCustomSession, clearCurrentSession } from 'App/duck/sessions'
import { fetchInsights } from 'Duck/sessions';
import { fetchInsights } from 'App/duck/sessions'
import { NoContent, Icon } from 'App/components/ui'
function ClickMapCard({
setCustomSession,
visitedEvents,
insights,
fetchInsights,
insightsFilters,
host,
clearCurrentSession,
}: any) {
const [customSession, setCustomSession] = React.useState<any>(null)
const { metricStore, dashboardStore } = useStore();
const onMarkerClick = (s: string, innerText: string) => {
metricStore.changeClickMapSearch(s, innerText)
@ -23,22 +19,24 @@ function ClickMapCard({
const mapUrl = metricStore.instance.series[0].filter.filters[0].value[0]
React.useEffect(() => {
return () => clearCurrentSession()
return () => setCustomSession(null)
}, [])
React.useEffect(() => {
if (metricStore.instance.data.domURL) {
setCustomSession(metricStore.instance.data)
setCustomSession(null)
setTimeout(() => {
setCustomSession(metricStore.instance.data)
}, 100)
}
}, [metricStore.instance])
React.useEffect(() => {
if (visitedEvents.length) {
const rangeValue = dashboardStore.drillDownPeriod.rangeValue
const startDate = dashboardStore.drillDownPeriod.start
const endDate = dashboardStore.drillDownPeriod.end
fetchInsights({ ...insightsFilters, url: mapUrl || '/', startDate, endDate, rangeValue, clickRage: metricStore.clickMapFilter })
}
}, [visitedEvents, metricStore.clickMapFilter])
const rangeValue = dashboardStore.drillDownPeriod.rangeValue
const startDate = dashboardStore.drillDownPeriod.start
const endDate = dashboardStore.drillDownPeriod.end
fetchInsights({ ...insightsFilters, url: mapUrl || '/', startDate, endDate, rangeValue, clickRage: metricStore.clickMapFilter })
}, [dashboardStore.drillDownPeriod.start, dashboardStore.drillDownPeriod.end, dashboardStore.drillDownPeriod.rangeValue, metricStore.clickMapFilter])
if (!metricStore.instance.data.domURL || insights.size === 0) {
return (
@ -57,7 +55,8 @@ function ClickMapCard({
/>
)
}
if (!visitedEvents || !visitedEvents.length) {
if (!metricStore.instance.data || !customSession) {
return <div className="py-2">Loading session</div>
}
@ -71,7 +70,7 @@ function ClickMapCard({
return (
<div id="clickmap-render">
<ClickMapRenderer
customSession={metricStore.instance.data}
session={customSession}
jumpTimestamp={jumpTimestamp}
onMarkerClick={onMarkerClick}
/>
@ -86,6 +85,6 @@ export default connect(
insights: state.getIn(['sessions', 'insights']),
host: state.getIn(['sessions', 'host']),
}),
{ setCustomSession, fetchInsights, clearCurrentSession }
{ fetchInsights, }
)
(observer(ClickMapCard))

View file

@ -11,28 +11,42 @@ import { toast } from 'react-toastify'
function WebPlayer(props: any) {
const {
session,
customSession,
insights,
jumpTimestamp,
onMarkerClick,
} = props;
// @ts-ignore
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
const playerRef = React.useRef<any>(null);
useEffect(() => {
const [WebPlayerInst, PlayerStore] = createClickMapPlayer(
customSession,
(state) => makeAutoObservable(state),
toast,
);
setContextValue({ player: WebPlayerInst, store: PlayerStore });
const init = () => {
const [WebPlayerInst, PlayerStore] = createClickMapPlayer(
session,
(state) => makeAutoObservable(state),
toast,
);
playerRef.current = WebPlayerInst;
setContextValue({ player: WebPlayerInst, store: PlayerStore });
}
if (!playerRef.current) {
init()
} else {
playerRef.current.clean()
playerRef.current = null;
setContextValue(defaultContextValue);
init();
}
}, [session.sessionId]);
React.useEffect(() => {
return () => {
WebPlayerInst.clean();
playerRef.current && playerRef.current.clean();
playerRef.current = null;
// @ts-ignore
setContextValue(defaultContextValue);
}
}, [session.sessionId]);
}, [])
const isPlayerReady = contextValue.store?.get().ready
@ -43,7 +57,8 @@ function WebPlayer(props: any) {
contextValue.player.pause()
contextValue.player.jump(jumpTimestamp)
contextValue.player.scale()
setTimeout(() => { contextValue.player.showClickmap(insights, onMarkerClick) }, 250)
setTimeout(() => { contextValue.player.showClickmap(insights) }, 250)
}, 500)
}
return () => {
@ -62,7 +77,6 @@ function WebPlayer(props: any) {
export default connect(
(state: any) => ({
session: state.getIn(['sessions', 'current']),
insights: state.getIn(['sessions', 'insights']),
jwt: state.getIn(['user', 'jwt']),
})

View file

@ -10,7 +10,6 @@ import PlayerBlock from './PlayerBlock';
const TABS = {
EVENTS: 'Activity',
HEATMAPS: 'Click map',
};
interface IProps {

View file

@ -122,7 +122,6 @@ function DropdownAudioPlayer({
audio.pause();
}
if (audio.muted !== isMuted) {
console.log(isMuted, audio.muted);
audio.muted = isMuted;
}
}

View file

@ -38,7 +38,7 @@ function Modal({ tabs, currentTab, changeTab, hideModal }: Props) {
function SessionTabs({ isLive }: { isLive?: boolean }) {
const { showModal, hideModal } = useModal();
const { player, store } = React.useContext(PlayerContext);
const { tabs = new Set('back-compat'), currentTab, closedTabs } = store.get();
const { tabs = new Set('back-compat'), currentTab, closedTabs, tabNames } = store.get();
const tabsArr = Array.from(tabs).map((tab, idx) => ({
tab,
@ -80,6 +80,7 @@ function SessionTabs({ isLive }: { isLive?: boolean }) {
changeTab={changeTab}
isLive={isLive}
isClosed={tab.isClosed}
name={tabNames[tab.tab]}
/>
</React.Fragment>
))}

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Tooltip } from 'antd';
import cn from 'classnames';
import React from 'react';
interface Props {
i: number;
@ -8,13 +9,16 @@ interface Props {
changeTab?: (tab: string) => void;
isLive?: boolean;
isClosed?: boolean;
name?: string;
}
function Tab({ i, tab, currentTab, changeTab, isLive, isClosed }: Props) {
function Tab({ i, tab, currentTab, changeTab, isLive, isClosed, name }: Props) {
return (
<div
key={tab}
style={{ marginBottom: '-2px' }}
style={{
marginBottom: '-2px',
}}
onClick={() => changeTab?.(tab)}
className={cn(
'self-end py-1 px-4 text-sm',
@ -22,10 +26,27 @@ function Tab({ i, tab, currentTab, changeTab, isLive, isClosed }: Props) {
currentTab === tab
? 'border-gray-lighter border-t border-l border-r !border-b-white bg-white rounded-tl rounded-tr font-semibold'
: 'cursor-pointer border-gray-lighter !border-b !border-t-transparent !border-l-transparent !border-r-transparent',
isClosed ? 'line-through': ''
)}
>
Tab {i + 1}
<Tooltip title={name && name.length > 20 ? name : ''}>
<div className={'flex items-center gap-2'}>
<div
className={
'bg-gray-light rounded-full min-w-5 min-h-5 w-5 h-5 flex items-center justify-center text-xs'
}
>
<div>{i + 1}</div>
</div>
<div
className={cn('whitespace-nowrap', isClosed ? 'line-through' : '')}
style={{
maxWidth: 114,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>{name ? name : `Tab ${i + 1}`}</div>
</div>
</Tooltip>
</div>
);
}

View file

@ -24,7 +24,6 @@ import {
const TABS = {
EVENTS: 'Activity',
CLICKMAP: 'Click map',
INSPECTOR: 'Tag',
};

View file

@ -215,6 +215,7 @@ const reducer = (state = initialState, action: IAction) => {
}
});
});
return state
.set('current', session)
.set('eventsIndex', matching)

View file

@ -161,9 +161,6 @@ export default class MessageLoader {
this.messageManager.distributeMessage(msg);
});
logger.info('Messages count: ', msgs.length, msgs, file);
if (file === 'd:dom 2' && 'createTabCloseEvents' in this.messageManager) {
this.messageManager.createTabCloseEvents();
}
this.messageManager.sortDomRemoveMessages(msgs);
this.messageManager.setMessagesLoading(false);
};
@ -180,6 +177,12 @@ export default class MessageLoader {
}
}
createTabCloseEvents() {
if ('createTabCloseEvents' in this.messageManager) {
this.messageManager.createTabCloseEvents();
}
}
preloaded = false;
async preloadFirstFile(data: Uint8Array) {
this.mobParser = this.createNewParser(true, this.processMessages, 'p:dom');
@ -242,6 +245,7 @@ export default class MessageLoader {
);
}
} finally {
this.createTabCloseEvents()
this.store.update({ domLoading: false, devtoolsLoading: false });
}
}

View file

@ -50,6 +50,9 @@ export interface State extends ScreenState {
tabStates: {
[tabId: string]: TabState;
};
tabNames: {
[tabId: string]: string;
}
domContentLoadedTime?: { time: number; value: number };
domBuildingTime?: number;
@ -95,6 +98,7 @@ export default class MessageManager {
tabChangeEvents: [],
closedTabs: [],
sessionStart: 0,
tabNames: {},
};
private clickManager: ListWalker<MouseClick> = new ListWalker();
@ -190,15 +194,20 @@ export default class MessageManager {
public createTabCloseEvents = () => {
const lastMsgArr: [string, number][] = []
Object.entries(this.tabs).forEach((entry, i) => {
const [tabId, tab] = entry
const namesObj: Record<string, string> = {}
for (const [tabId, tab] of Object.entries(this.tabs)) {
const { lastMessageTs } = tab
if (lastMessageTs && tabId) lastMsgArr.push([tabId, lastMessageTs])
})
if (lastMessageTs && tabId) {
lastMsgArr.push([tabId, lastMessageTs])
namesObj[tabId] = ''
}
}
lastMsgArr.sort((a, b) => a[1] - b[1])
lastMsgArr.forEach(([tabId, lastMessageTs]) => {
this.tabCloseManager.append({ tabId, time: lastMessageTs })
})
this.state.update({ tabNames: namesObj })
}
public startLoading = () => {
@ -331,6 +340,7 @@ export default class MessageManager {
case MType.MouseMove:
this.mouseMoveManager.append(msg);
break;
case MType.MouseClickDeprecated:
case MType.MouseClick:
this.clickManager.append(msg);
break;

View file

@ -234,8 +234,8 @@ export default class Screen {
case ScaleMode.AdjustParentHeight:
// we want to scale the document with true height so the clickmap will be scrollable
const usedHeight =
this.document?.body.offsetHeight && this.document?.body.offsetHeight > height
? this.document.body.offsetHeight + 'px'
this.document?.body.scrollHeight && this.document?.body.scrollHeight > height
? this.document.body.scrollHeight + 'px'
: height + 'px';
this.scaleRatio = offsetWidth / width;
translate = 'translate(-50%, 0)';

View file

@ -81,7 +81,7 @@ export default class TabSessionManager {
constructor(
private session: any,
private readonly state: Store<{ tabStates: { [tabId: string]: TabState } }>,
private readonly state: Store<{ tabStates: { [tabId: string]: TabState }, tabNames: { [tabId: string]: string } }>,
private readonly screen: Screen,
private readonly id: string,
private readonly setSize: ({ height, width }: { height: number; width: number }) => void,
@ -176,7 +176,7 @@ export default class TabSessionManager {
switch (msg.tp) {
case MType.CanvasNode:
const managerId = `${msg.timestamp}_${msg.nodeId}`;
if (!this.canvasManagers[managerId]) {
if (!this.canvasManagers[managerId] && this.session.canvasURL?.length) {
const fileId = managerId;
const delta = msg.timestamp - this.sessionStart;
@ -198,6 +198,7 @@ export default class TabSessionManager {
this.canvasReplayWalker.append(msg);
}
break;
case MType.SetPageLocationDeprecated:
case MType.SetPageLocation:
this.locationManager.append(msg);
if (msg.navigationStart > 0) {
@ -336,8 +337,12 @@ export default class TabSessionManager {
/* === */
const lastLocationMsg = this.locationManager.moveGetLast(t, index);
if (!!lastLocationMsg) {
const tabNames = this.state.get().tabNames;
if (lastLocationMsg.documentTitle) {
tabNames[this.id] = lastLocationMsg.documentTitle
}
// @ts-ignore comes from parent state
this.state.update({ location: lastLocationMsg.url });
this.state.update({ location: lastLocationMsg.url, tabNames });
}
const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index);

View file

@ -1,66 +1,54 @@
import type Screen from '../Screen/Screen'
import type { Point } from '../Screen/types'
import type { Store } from '../../common/types'
import { clickmapStyles } from './clickmapStyles'
const zIndexMap = {
400: 3,
200: 4,
100: 5,
50: 6
}
const widths = Object.keys(zIndexMap)
.map(s => parseInt(s, 10))
.sort((a,b) => b - a) as [400, 200, 100, 50]
import type { Store } from '../../common/types';
import type Screen from '../Screen/Screen';
import type { Point } from '../Screen/types';
import { clickmapStyles } from './clickmapStyles';
import heatmapRenderer from './simpleHeatmap';
function getOffset(el: Element, innerWindow: Window) {
const rect = el.getBoundingClientRect();
return {
fixedLeft: rect.left + innerWindow.scrollX,
fixedTop: rect.top + innerWindow.scrollY,
rect,
};
const rect = el.getBoundingClientRect();
return {
fixedLeft: rect.left + innerWindow.scrollX,
fixedTop: rect.top + innerWindow.scrollY,
rect,
};
}
interface BoundingRect {
top: number,
left: number,
width: number,
height: number,
top: number;
left: number;
width: number;
height: number;
}
export interface MarkedTarget {
boundingRect: BoundingRect,
el: Element,
selector: string,
count: number,
index: number,
active?: boolean,
percent: number
boundingRect: BoundingRect;
el: Element;
selector: string;
count: number;
index: number;
active?: boolean;
percent: number;
}
export interface State {
markedTargets: MarkedTarget[] | null,
activeTargetIndex: number,
markedTargets: MarkedTarget[] | null;
activeTargetIndex: number;
}
export default class TargetMarker {
private clickMapOverlay: HTMLDivElement | null = null
private clickContainers: HTMLDivElement[] = []
private smallClicks: HTMLDivElement[] = []
static INITIAL_STATE: State = {
markedTargets: null,
activeTargetIndex: 0
}
private clickMapOverlay: HTMLCanvasElement | null = null;
static INITIAL_STATE: State = {
markedTargets: null,
activeTargetIndex: 0,
};
constructor(
private readonly screen: Screen,
private readonly store: Store<State>,
) {}
constructor(
private readonly screen: Screen,
private readonly store: Store<State>
) {}
updateMarkedTargets() {
const { markedTargets } = this.store.get()
updateMarkedTargets() {
const { markedTargets } = this.store.get();
if (markedTargets) {
this.store.update({
markedTargets: markedTargets.map((mt: any) => ({
@ -69,55 +57,64 @@ export default class TargetMarker {
})),
});
}
}
private calculateRelativeBoundingRect(el: Element): BoundingRect {
const parentEl = this.screen.getParentElement()
if (!parentEl) return {top:0, left:0, width:0,height:0} //TODO: can be initialized(?) on mounted screen only
const { top, left, width, height } = el.getBoundingClientRect()
const s = this.screen.getScale()
const screenRect = this.screen.overlay.getBoundingClientRect() //this.screen.getBoundingClientRect() (now private)
const parentRect = parentEl.getBoundingClientRect()
return {
top: top*s + screenRect.top - parentRect.top,
left: left*s + screenRect.left - parentRect.left,
width: width*s,
height: height*s,
if (heatmapRenderer.checkReady()) {
heatmapRenderer.resize().draw();
}
}
private calculateRelativeBoundingRect(el: Element): BoundingRect {
const parentEl = this.screen.getParentElement();
if (!parentEl) return { top: 0, left: 0, width: 0, height: 0 }; //TODO: can be initialized(?) on mounted screen only
const { top, left, width, height } = el.getBoundingClientRect();
const s = this.screen.getScale();
const screenRect = this.screen.overlay.getBoundingClientRect(); //this.screen.getBoundingClientRect() (now private)
const parentRect = parentEl.getBoundingClientRect();
return {
top: top * s + screenRect.top - parentRect.top,
left: left * s + screenRect.left - parentRect.left,
width: width * s,
height: height * s,
};
}
setActiveTarget(index: number) {
const window = this.screen.window
const markedTargets: MarkedTarget[] | null = this.store.get().markedTargets
const target = markedTargets && markedTargets[index]
const window = this.screen.window;
const markedTargets: MarkedTarget[] | null = this.store.get().markedTargets;
const target = markedTargets && markedTargets[index];
if (target && window) {
const { fixedTop, rect } = getOffset(target.el, window)
const scrollToY = fixedTop - window.innerHeight / 1.5
const { fixedTop, rect } = getOffset(target.el, window);
const scrollToY = fixedTop - window.innerHeight / 1.5;
if (rect.top < 0 || rect.top > window.innerHeight) {
// behavior hack TODO: fix it somehow when they will decide to remove it from browser api
// @ts-ignore
window.scrollTo({ top: scrollToY, behavior: 'instant' })
window.scrollTo({ top: scrollToY, behavior: 'instant' });
setTimeout(() => {
if (!markedTargets) { return }
if (!markedTargets) {
return;
}
this.store.update({
markedTargets: markedTargets.map(t => t === target ? {
...target,
boundingRect: this.calculateRelativeBoundingRect(target.el),
} : t)
})
}, 0)
markedTargets: markedTargets.map((t) =>
t === target
? {
...target,
boundingRect: this.calculateRelativeBoundingRect(target.el),
}
: t
),
});
}, 0);
}
}
this.store.update({ activeTargetIndex: index });
}
private actualScroll: Point | null = null
markTargets(selections: { selector: string, count: number }[] | null) {
private actualScroll: Point | null = null;
markTargets(selections: { selector: string; count: number }[] | null) {
if (selections) {
const totalCount = selections.reduce((a, b) => {
return a + b.count
return a + b.count;
}, 0);
const markedTargets: MarkedTarget[] = [];
let index = 0;
@ -130,131 +127,80 @@ export default class TargetMarker {
el,
index: index++,
percent: Math.round((s.count * 100) / totalCount),
boundingRect: this.calculateRelativeBoundingRect(el),
boundingRect: this.calculateRelativeBoundingRect(el),
count: s.count,
})
});
});
this.actualScroll = this.screen.getCurrentScroll()
this.actualScroll = this.screen.getCurrentScroll();
this.store.update({ markedTargets });
} else {
if (this.actualScroll) {
this.screen.window?.scrollTo(this.actualScroll.x, this.actualScroll.y)
this.actualScroll = null
this.screen.window?.scrollTo(this.actualScroll.x, this.actualScroll.y);
this.actualScroll = null;
}
this.store.update({ markedTargets: null });
}
}
injectTargets(clicks: { normalizedX: number; normalizedY: number }[] | null) {
if (clicks && this.screen.document) {
this.clickMapOverlay?.remove();
const overlay = document.createElement('canvas');
const iframeSize = this.screen.iframeStylesRef;
const scrollHeight = this.screen.document?.documentElement.scrollHeight || 0;
const scrollWidth = this.screen.document?.documentElement.scrollWidth || 0;
const scaleRatio = this.screen.getScale();
Object.assign(
overlay.style,
clickmapStyles.overlayStyle({
height: iframeSize.height,
width: iframeSize.width,
scale: scaleRatio,
})
);
injectTargets(
selections: { selector: string, count: number, clickRage?: boolean }[] | null,
onMarkerClick?: (selector: string, innerText: string) => void,
) {
if (selections) {
const totalCount = selections.reduce((a, b) => {
return a + b.count
}, 0);
this.clickMapOverlay = overlay;
this.screen.getParentElement()?.appendChild(overlay);
this.clickMapOverlay?.remove()
const overlay = document.createElement("div")
const iframeSize = this.screen.iframeStylesRef
const scaleRatio = this.screen.getScale()
Object.assign(overlay.style, clickmapStyles.overlayStyle({ height: iframeSize.height, width: iframeSize.width, scale: scaleRatio }))
const pointMap: Record<string, { times: number; data: number[], original: any }> = {};
const ovWidth = parseInt(iframeSize.width);
const ovHeight = parseInt(iframeSize.height);
overlay.width = ovWidth;
overlay.height = ovHeight;
let maxIntensity = 0;
this.clickMapOverlay = overlay
selections.forEach((s, i) => {
const el = this.screen.getElementBySelector(s.selector);
if (!el) return;
const bubbleContainer = document.createElement("div")
const {top, left, width, height} = el.getBoundingClientRect()
const totalClicks = document.createElement("div")
totalClicks.innerHTML = `${s.count} ${s.count !== 1 ? 'Clicks' : 'Click'}`
Object.assign(totalClicks.style, clickmapStyles.totalClicks)
const percent = document.createElement("div")
percent.style.fontSize = "14px"
percent.innerHTML = `${Math.round((s.count * 100) / totalCount)}% of the clicks recorded in this page`
bubbleContainer.appendChild(totalClicks)
bubbleContainer.appendChild(percent)
const containerId = `clickmap-bubble-${i}`
bubbleContainer.id = containerId
this.clickContainers.push(bubbleContainer)
const frameWidth = iframeSize.width.replace('px', '')
// @ts-ignore
Object.assign(bubbleContainer.style, clickmapStyles.bubbleContainer({ top, left: Math.max(100, frameWidth - left > 250 ? left : frameWidth - 220), height }))
const border = document.createElement("div")
let key = 0
if (width > 50) {
let diff = widths[key] - width
while (diff > 0) {
key++
diff = widths[key] - width
}
clicks.forEach((point) => {
const key = `${point.normalizedY}-${point.normalizedX}`;
if (pointMap[key]) {
const times = pointMap[key].times + 1;
maxIntensity = Math.max(maxIntensity, times);
pointMap[key].times = times;
} else {
key = 3
const clickData = [
(point.normalizedX / 100) * scrollWidth,
(point.normalizedY / 100) * scrollHeight,
];
pointMap[key] = { times: 1, data: clickData, original: point };
}
const borderZindex = zIndexMap[widths[key]]
Object.assign(border.style, clickmapStyles.highlight({ width, height, top, left, zIndex: borderZindex }))
const smallClicksBubble = document.createElement("div")
smallClicksBubble.innerHTML = `${s.count}`
const smallClicksId = containerId + '-small'
smallClicksBubble.id = smallClicksId
this.smallClicks.push(smallClicksBubble)
border.onclick = (e) => {
e.stopPropagation()
const innerText = el.innerText.length > 25 ? `${el.innerText.slice(0, 20)}...` : el.innerText
onMarkerClick?.(s.selector, innerText)
this.clickContainers.forEach(container => {
if (container.id === containerId) {
container.style.visibility = "visible"
} else {
container.style.visibility = "hidden"
}
})
this.smallClicks.forEach(container => {
if (container.id !== smallClicksId) {
container.style.visibility = "visible"
} else {
container.style.visibility = "hidden"
}
})
}
overlay.onclick = (e) => {
e.stopPropagation()
onMarkerClick?.('', '')
this.clickContainers.forEach(container => {
container.style.visibility = "hidden"
})
this.smallClicks.forEach(container => {
container.style.visibility = "visible"
})
}
Object.assign(smallClicksBubble.style, clickmapStyles.clicks({ top, height, isRage: s.clickRage, left }))
border.appendChild(smallClicksBubble)
overlay.appendChild(bubbleContainer)
overlay.appendChild(border)
});
this.screen.getParentElement()?.appendChild(overlay)
const heatmapData: number[][] = [];
for (const key in pointMap) {
const { data, times } = pointMap[key];
heatmapData.push([...data, times]);
}
heatmapRenderer
.setCanvas(overlay)
.setData(heatmapData)
.setRadius(15, 10)
.setMax(maxIntensity)
.resize()
.draw();
} else {
this.store.update({ markedTargets: null });
this.clickMapOverlay?.remove()
this.clickMapOverlay = null
this.smallClicks = []
this.clickContainers = []
this.clickMapOverlay?.remove();
this.clickMapOverlay = null;
}
}
}

View file

@ -0,0 +1,162 @@
/**
* modified version of simpleheat
*
* https://github.com/mourner/simpleheat
* Copyright (c) 2015, Vladimir Agafonkin
*
* */
class SimpleHeatmap {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D | null;
private width: number;
private height: number;
private max: number;
private data: number[][];
private circle: HTMLCanvasElement;
private grad: Uint8ClampedArray;
private r: number;
private defaultRadius = 25;
private defaultGradient = {
0.4: 'blue',
0.6: 'cyan',
0.7: 'lime',
0.8: 'yellow',
1.0: 'red',
};
setCanvas(canvas: HTMLCanvasElement): this {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
this.max = 1;
this.data = [];
return this;
}
setData(data: number[][]): this {
this.data = data;
return this;
}
setMax(max: number): this {
this.max = max;
return this;
}
add(point: number[]): this {
this.data.push(point);
return this;
}
clear(): this {
this.data = [];
return this;
}
setRadius(r: number, blur: number = 15): this {
const circle = this.createCanvas();
const ctx = circle.getContext('2d');
if (!ctx) {
throw new Error('Canvas 2d context is not supported');
}
const r2 = r + blur;
circle.width = circle.height = r2 * 2;
ctx.shadowOffsetX = ctx.shadowOffsetY = r2 * 2;
ctx.shadowBlur = blur;
ctx.shadowColor = 'black';
ctx.beginPath();
ctx.arc(-r2, -r2, r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
this.circle = circle;
this.r = r2;
return this;
}
checkReady(): boolean {
return !!(this.canvas && this.ctx);
}
resize(): this {
this.width = this.canvas.width;
this.height = this.canvas.height;
return this;
}
setGradient(grad: Record<string, string>): this {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Canvas 2d context is not supported');
}
const gradient = ctx.createLinearGradient(0, 0, 0, 256);
canvas.width = 1;
canvas.height = 256;
for (const i in grad) {
gradient.addColorStop(parseFloat(i), grad[i]);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1, 256);
this.grad = ctx.getImageData(0, 0, 1, 256).data;
return this;
}
draw(minOpacity: number = 0.05): this {
if (!this.circle) this.setRadius(this.defaultRadius);
if (!this.grad) this.setGradient(this.defaultGradient);
const ctx = this.ctx;
if (!ctx) {
throw new Error('Canvas 2d context is not supported');
}
ctx.clearRect(0, 0, this.width, this.height);
this.data.forEach((p) => {
ctx.globalAlpha = Math.min(Math.max(p[2] / this.max, minOpacity), 1);
ctx.drawImage(this.circle, p[0] - this.r, p[1] - this.r);
});
const colored = ctx.getImageData(0, 0, this.width, this.height);
this.colorize(colored.data, this.grad);
ctx.putImageData(colored, 0, 0);
return this;
}
private colorize(
pixels: Uint8ClampedArray,
gradient: Uint8ClampedArray
): void {
for (let i = 0, len = pixels.length; i < len; i += 4) {
const j = pixels[i + 3] * 4;
if (j) {
pixels[i] = gradient[j];
pixels[i + 1] = gradient[j + 1];
pixels[i + 2] = gradient[j + 2];
}
}
}
private createCanvas(): HTMLCanvasElement {
return document.createElement('canvas');
}
}
const heatmapRenderer = new SimpleHeatmap();
export default heatmapRenderer;

View file

@ -32,7 +32,7 @@ export default class RawMessageReader extends PrimitiveReader {
const referrer = this.readString(); if (referrer === null) { return resetPointer() }
const navigationStart = this.readUint(); if (navigationStart === null) { return resetPointer() }
return {
tp: MType.SetPageLocation,
tp: MType.SetPageLocationDeprecated,
url,
referrer,
navigationStart,
@ -515,13 +515,31 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 68: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const hesitationTime = this.readUint(); if (hesitationTime === null) { return resetPointer() }
const label = this.readString(); if (label === null) { return resetPointer() }
const selector = this.readString(); if (selector === null) { return resetPointer() }
const normalizedX = this.readUint(); if (normalizedX === null) { return resetPointer() }
const normalizedY = this.readUint(); if (normalizedY === null) { return resetPointer() }
return {
tp: MType.MouseClick,
id,
hesitationTime,
label,
selector,
normalizedX,
normalizedY,
};
}
case 69: {
const id = this.readUint(); if (id === null) { return resetPointer() }
const hesitationTime = this.readUint(); if (hesitationTime === null) { return resetPointer() }
const label = this.readString(); if (label === null) { return resetPointer() }
const selector = this.readString(); if (selector === null) { return resetPointer() }
return {
tp: MType.MouseClick,
tp: MType.MouseClickDeprecated,
id,
hesitationTime,
label,
@ -763,13 +781,27 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 122: {
const url = this.readString(); if (url === null) { return resetPointer() }
const referrer = this.readString(); if (referrer === null) { return resetPointer() }
const navigationStart = this.readUint(); if (navigationStart === null) { return resetPointer() }
const documentTitle = this.readString(); if (documentTitle === null) { return resetPointer() }
return {
tp: MType.SetPageLocation,
url,
referrer,
navigationStart,
documentTitle,
};
}
case 93: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const length = this.readUint(); if (length === null) { return resetPointer() }
const name = this.readString(); if (name === null) { return resetPointer() }
const payload = this.readString(); if (payload === null) { return resetPointer() }
return {
tp: MType.IosEvent,
tp: MType.MobileEvent,
timestamp,
length,
name,
@ -785,7 +817,7 @@ export default class RawMessageReader extends PrimitiveReader {
const width = this.readUint(); if (width === null) { return resetPointer() }
const height = this.readUint(); if (height === null) { return resetPointer() }
return {
tp: MType.IosScreenChanges,
tp: MType.MobileScreenChanges,
timestamp,
length,
x,
@ -802,7 +834,7 @@ export default class RawMessageReader extends PrimitiveReader {
const x = this.readUint(); if (x === null) { return resetPointer() }
const y = this.readUint(); if (y === null) { return resetPointer() }
return {
tp: MType.IosClickEvent,
tp: MType.MobileClickEvent,
timestamp,
length,
label,
@ -818,7 +850,7 @@ export default class RawMessageReader extends PrimitiveReader {
const valueMasked = this.readBoolean(); if (valueMasked === null) { return resetPointer() }
const label = this.readString(); if (label === null) { return resetPointer() }
return {
tp: MType.IosInputEvent,
tp: MType.MobileInputEvent,
timestamp,
length,
value,
@ -833,7 +865,7 @@ export default class RawMessageReader extends PrimitiveReader {
const name = this.readString(); if (name === null) { return resetPointer() }
const value = this.readUint(); if (value === null) { return resetPointer() }
return {
tp: MType.IosPerformanceEvent,
tp: MType.MobilePerformanceEvent,
timestamp,
length,
name,
@ -847,7 +879,7 @@ export default class RawMessageReader extends PrimitiveReader {
const severity = this.readString(); if (severity === null) { return resetPointer() }
const content = this.readString(); if (content === null) { return resetPointer() }
return {
tp: MType.IosLog,
tp: MType.MobileLog,
timestamp,
length,
severity,
@ -860,7 +892,7 @@ export default class RawMessageReader extends PrimitiveReader {
const length = this.readUint(); if (length === null) { return resetPointer() }
const content = this.readString(); if (content === null) { return resetPointer() }
return {
tp: MType.IosInternalError,
tp: MType.MobileInternalError,
timestamp,
length,
content,
@ -878,7 +910,7 @@ export default class RawMessageReader extends PrimitiveReader {
const status = this.readUint(); if (status === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
return {
tp: MType.IosNetworkCall,
tp: MType.MobileNetworkCall,
timestamp,
length,
type,
@ -899,7 +931,7 @@ export default class RawMessageReader extends PrimitiveReader {
const y = this.readUint(); if (y === null) { return resetPointer() }
const direction = this.readString(); if (direction === null) { return resetPointer() }
return {
tp: MType.IosSwipeEvent,
tp: MType.MobileSwipeEvent,
timestamp,
length,
label,
@ -916,7 +948,7 @@ export default class RawMessageReader extends PrimitiveReader {
const context = this.readString(); if (context === null) { return resetPointer() }
const payload = this.readString(); if (payload === null) { return resetPointer() }
return {
tp: MType.IosIssueEvent,
tp: MType.MobileIssueEvent,
timestamp,
type,
contextString,

View file

@ -4,7 +4,7 @@
import { MType } from './raw.gen'
const IOS_TYPES = [90,91,92,93,94,95,96,97,98,100,101,102,103,104,105,106,107,110,111]
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,117,118,119]
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,68,69,70,71,72,73,74,75,76,77,113,114,117,118,119,122]
export function isDOMType(t: MType) {
return DOM_TYPES.includes(t)
}

View file

@ -5,7 +5,7 @@ import type { Timed } from './timed'
import type { RawMessage } from './raw.gen'
import type {
RawTimestamp,
RawSetPageLocation,
RawSetPageLocationDeprecated,
RawSetViewportSize,
RawSetViewportScroll,
RawCreateDocument,
@ -46,6 +46,7 @@ import type {
RawSetCssDataURLBased,
RawCssInsertRuleURLBased,
RawMouseClick,
RawMouseClickDeprecated,
RawCreateIFrameDocument,
RawAdoptedSsReplaceURLBased,
RawAdoptedSsReplace,
@ -65,16 +66,17 @@ import type {
RawCanvasNode,
RawTagTrigger,
RawRedux,
RawIosEvent,
RawIosScreenChanges,
RawIosClickEvent,
RawIosInputEvent,
RawIosPerformanceEvent,
RawIosLog,
RawIosInternalError,
RawIosNetworkCall,
RawIosSwipeEvent,
RawIosIssueEvent,
RawSetPageLocation,
RawMobileEvent,
RawMobileScreenChanges,
RawMobileClickEvent,
RawMobileInputEvent,
RawMobilePerformanceEvent,
RawMobileLog,
RawMobileInternalError,
RawMobileNetworkCall,
RawMobileSwipeEvent,
RawMobileIssueEvent,
} from './raw.gen'
export type Message = RawMessage & Timed
@ -82,7 +84,7 @@ export type Message = RawMessage & Timed
export type Timestamp = RawTimestamp & Timed
export type SetPageLocation = RawSetPageLocation & Timed
export type SetPageLocationDeprecated = RawSetPageLocationDeprecated & Timed
export type SetViewportSize = RawSetViewportSize & Timed
@ -164,6 +166,8 @@ export type CssInsertRuleURLBased = RawCssInsertRuleURLBased & Timed
export type MouseClick = RawMouseClick & Timed
export type MouseClickDeprecated = RawMouseClickDeprecated & Timed
export type CreateIFrameDocument = RawCreateIFrameDocument & Timed
export type AdoptedSsReplaceURLBased = RawAdoptedSsReplaceURLBased & Timed
@ -202,23 +206,25 @@ export type TagTrigger = RawTagTrigger & Timed
export type Redux = RawRedux & Timed
export type IosEvent = RawIosEvent & Timed
export type SetPageLocation = RawSetPageLocation & Timed
export type IosScreenChanges = RawIosScreenChanges & Timed
export type MobileEvent = RawMobileEvent & Timed
export type IosClickEvent = RawIosClickEvent & Timed
export type MobileScreenChanges = RawMobileScreenChanges & Timed
export type IosInputEvent = RawIosInputEvent & Timed
export type MobileClickEvent = RawMobileClickEvent & Timed
export type IosPerformanceEvent = RawIosPerformanceEvent & Timed
export type MobileInputEvent = RawMobileInputEvent & Timed
export type IosLog = RawIosLog & Timed
export type MobilePerformanceEvent = RawMobilePerformanceEvent & Timed
export type IosInternalError = RawIosInternalError & Timed
export type MobileLog = RawMobileLog & Timed
export type IosNetworkCall = RawIosNetworkCall & Timed
export type MobileInternalError = RawMobileInternalError & Timed
export type IosSwipeEvent = RawIosSwipeEvent & Timed
export type MobileNetworkCall = RawMobileNetworkCall & Timed
export type IosIssueEvent = RawIosIssueEvent & Timed
export type MobileSwipeEvent = RawMobileSwipeEvent & Timed
export type MobileIssueEvent = RawMobileIssueEvent & Timed

View file

@ -3,7 +3,7 @@
export const enum MType {
Timestamp = 0,
SetPageLocation = 4,
SetPageLocationDeprecated = 4,
SetViewportSize = 5,
SetViewportScroll = 6,
CreateDocument = 7,
@ -43,7 +43,8 @@ export const enum MType {
SetNodeAttributeURLBased = 60,
SetCssDataURLBased = 61,
CssInsertRuleURLBased = 67,
MouseClick = 69,
MouseClick = 68,
MouseClickDeprecated = 69,
CreateIFrameDocument = 70,
AdoptedSsReplaceURLBased = 71,
AdoptedSsReplace = 72,
@ -63,16 +64,17 @@ export const enum MType {
CanvasNode = 119,
TagTrigger = 120,
Redux = 121,
IosEvent = 93,
IosScreenChanges = 96,
IosClickEvent = 100,
IosInputEvent = 101,
IosPerformanceEvent = 102,
IosLog = 103,
IosInternalError = 104,
IosNetworkCall = 105,
IosSwipeEvent = 106,
IosIssueEvent = 111,
SetPageLocation = 122,
MobileEvent = 93,
MobileScreenChanges = 96,
MobileClickEvent = 100,
MobileInputEvent = 101,
MobilePerformanceEvent = 102,
MobileLog = 103,
MobileInternalError = 104,
MobileNetworkCall = 105,
MobileSwipeEvent = 106,
MobileIssueEvent = 111,
}
@ -81,8 +83,8 @@ export interface RawTimestamp {
timestamp: number,
}
export interface RawSetPageLocation {
tp: MType.SetPageLocation,
export interface RawSetPageLocationDeprecated {
tp: MType.SetPageLocationDeprecated,
url: string,
referrer: string,
navigationStart: number,
@ -371,6 +373,16 @@ export interface RawMouseClick {
hesitationTime: number,
label: string,
selector: string,
normalizedX: number,
normalizedY: number,
}
export interface RawMouseClickDeprecated {
tp: MType.MouseClickDeprecated,
id: number,
hesitationTime: number,
label: string,
selector: string,
}
export interface RawCreateIFrameDocument {
@ -509,16 +521,24 @@ export interface RawRedux {
actionTime: number,
}
export interface RawIosEvent {
tp: MType.IosEvent,
export interface RawSetPageLocation {
tp: MType.SetPageLocation,
url: string,
referrer: string,
navigationStart: number,
documentTitle: string,
}
export interface RawMobileEvent {
tp: MType.MobileEvent,
timestamp: number,
length: number,
name: string,
payload: string,
}
export interface RawIosScreenChanges {
tp: MType.IosScreenChanges,
export interface RawMobileScreenChanges {
tp: MType.MobileScreenChanges,
timestamp: number,
length: number,
x: number,
@ -527,8 +547,8 @@ export interface RawIosScreenChanges {
height: number,
}
export interface RawIosClickEvent {
tp: MType.IosClickEvent,
export interface RawMobileClickEvent {
tp: MType.MobileClickEvent,
timestamp: number,
length: number,
label: string,
@ -536,8 +556,8 @@ export interface RawIosClickEvent {
y: number,
}
export interface RawIosInputEvent {
tp: MType.IosInputEvent,
export interface RawMobileInputEvent {
tp: MType.MobileInputEvent,
timestamp: number,
length: number,
value: string,
@ -545,31 +565,31 @@ export interface RawIosInputEvent {
label: string,
}
export interface RawIosPerformanceEvent {
tp: MType.IosPerformanceEvent,
export interface RawMobilePerformanceEvent {
tp: MType.MobilePerformanceEvent,
timestamp: number,
length: number,
name: string,
value: number,
}
export interface RawIosLog {
tp: MType.IosLog,
export interface RawMobileLog {
tp: MType.MobileLog,
timestamp: number,
length: number,
severity: string,
content: string,
}
export interface RawIosInternalError {
tp: MType.IosInternalError,
export interface RawMobileInternalError {
tp: MType.MobileInternalError,
timestamp: number,
length: number,
content: string,
}
export interface RawIosNetworkCall {
tp: MType.IosNetworkCall,
export interface RawMobileNetworkCall {
tp: MType.MobileNetworkCall,
timestamp: number,
length: number,
type: string,
@ -581,8 +601,8 @@ export interface RawIosNetworkCall {
duration: number,
}
export interface RawIosSwipeEvent {
tp: MType.IosSwipeEvent,
export interface RawMobileSwipeEvent {
tp: MType.MobileSwipeEvent,
timestamp: number,
length: number,
label: string,
@ -591,8 +611,8 @@ export interface RawIosSwipeEvent {
direction: string,
}
export interface RawIosIssueEvent {
tp: MType.IosIssueEvent,
export interface RawMobileIssueEvent {
tp: MType.MobileIssueEvent,
timestamp: number,
type: string,
contextString: string,
@ -601,4 +621,4 @@ export interface RawIosIssueEvent {
}
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;

View file

@ -4,7 +4,7 @@ import { MType } from './raw.gen'
export const TP_MAP = {
0: MType.Timestamp,
4: MType.SetPageLocation,
4: MType.SetPageLocationDeprecated,
5: MType.SetViewportSize,
6: MType.SetViewportScroll,
7: MType.CreateDocument,
@ -44,7 +44,8 @@ export const TP_MAP = {
60: MType.SetNodeAttributeURLBased,
61: MType.SetCssDataURLBased,
67: MType.CssInsertRuleURLBased,
69: MType.MouseClick,
68: MType.MouseClick,
69: MType.MouseClickDeprecated,
70: MType.CreateIFrameDocument,
71: MType.AdoptedSsReplaceURLBased,
72: MType.AdoptedSsReplace,
@ -64,14 +65,15 @@ export const TP_MAP = {
119: MType.CanvasNode,
120: MType.TagTrigger,
121: MType.Redux,
93: MType.IosEvent,
96: MType.IosScreenChanges,
100: MType.IosClickEvent,
101: MType.IosInputEvent,
102: MType.IosPerformanceEvent,
103: MType.IosLog,
104: MType.IosInternalError,
105: MType.IosNetworkCall,
106: MType.IosSwipeEvent,
111: MType.IosIssueEvent,
122: MType.SetPageLocation,
93: MType.MobileEvent,
96: MType.MobileScreenChanges,
100: MType.MobileClickEvent,
101: MType.MobileInputEvent,
102: MType.MobilePerformanceEvent,
103: MType.MobileLog,
104: MType.MobileInternalError,
105: MType.MobileNetworkCall,
106: MType.MobileSwipeEvent,
111: MType.MobileIssueEvent,
} as const

View file

@ -10,7 +10,7 @@ type TrTimestamp = [
timestamp: number,
]
type TrSetPageLocation = [
type TrSetPageLocationDeprecated = [
type: 4,
url: string,
referrer: string,
@ -354,6 +354,16 @@ type TrCSSInsertRuleURLBased = [
]
type TrMouseClick = [
type: 68,
id: number,
hesitationTime: number,
label: string,
selector: string,
normalizedX: number,
normalizedY: number,
]
type TrMouseClickDeprecated = [
type: 69,
id: number,
hesitationTime: number,
@ -522,8 +532,16 @@ type TrRedux = [
actionTime: number,
]
type TrSetPageLocation = [
type: 122,
url: string,
referrer: string,
navigationStart: number,
documentTitle: string,
]
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux
export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
@ -537,7 +555,7 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
case 4: {
return {
tp: MType.SetPageLocation,
tp: MType.SetPageLocationDeprecated,
url: tMsg[1],
referrer: tMsg[2],
navigationStart: tMsg[3],
@ -891,13 +909,25 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 69: {
case 68: {
return {
tp: MType.MouseClick,
id: tMsg[1],
hesitationTime: tMsg[2],
label: tMsg[3],
selector: tMsg[4],
normalizedX: tMsg[5],
normalizedY: tMsg[6],
}
}
case 69: {
return {
tp: MType.MouseClickDeprecated,
id: tMsg[1],
hesitationTime: tMsg[2],
label: tMsg[3],
selector: tMsg[4],
}
}
@ -1058,6 +1088,16 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 122: {
return {
tp: MType.SetPageLocation,
url: tMsg[1],
referrer: tMsg[2],
navigationStart: tMsg[3],
documentTitle: tMsg[4],
}
}
default:
return null
}

View file

@ -28,7 +28,9 @@ end
message 3, 'SessionEndDeprecated', :tracker => false, :replayer => false do
uint 'Timestamp'
end
message 4, 'SetPageLocation' do
# DEPRECATED since 14.0.0 -> goto 122
message 4, 'SetPageLocationDeprecated' do
string 'URL'
string 'Referrer'
uint 'NavigationStart'
@ -360,8 +362,17 @@ message 67, 'CSSInsertRuleURLBased' do
uint 'Index'
string 'BaseURL'
end
## 68
message 69, 'MouseClick' do
message 68, 'MouseClick' do
uint 'ID'
uint 'HesitationTime'
string 'Label'
string 'Selector'
uint 'NormalizedX'
uint 'NormalizedY'
end
message 69, 'MouseClickDeprecated' do
uint 'ID'
uint 'HesitationTime'
string 'Label'
@ -529,6 +540,13 @@ message 121, 'Redux', :replayer => :devtools do
uint 'ActionTime'
end
message 122, 'SetPageLocation' do
string 'URL'
string 'Referrer'
uint 'NavigationStart'
string 'DocumentTitle'
end
## Backend-only
message 125, 'IssueEvent', :replayer => false, :tracker => false do
uint 'MessageID'

Binary file not shown.

View file

@ -31,7 +31,7 @@
"dependencies": {
"csstype": "^3.0.10",
"fflate": "^0.8.2",
"peerjs": "1.5.1",
"peerjs": "1.5.4",
"socket.io-client": "^4.7.2"
},
"peerDependencies": {

View file

@ -1,3 +1,9 @@
# 14.0.0
- titles for tabs
- new `MouseClick` message to introduce heatmaps instead of clickmaps
- crossdomain iframe tracking functionality
# 13.0.2
- more file extensions for canvas

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "13.0.2",
"version": "14.0.1-6",
"keywords": [
"logging",
"replay"

View file

@ -3,7 +3,7 @@
export declare const enum Type {
Timestamp = 0,
SetPageLocation = 4,
SetPageLocationDeprecated = 4,
SetViewportSize = 5,
SetViewportScroll = 6,
CreateDocument = 7,
@ -52,7 +52,8 @@ export declare const enum Type {
TechnicalInfo = 63,
CustomIssue = 64,
CSSInsertRuleURLBased = 67,
MouseClick = 69,
MouseClick = 68,
MouseClickDeprecated = 69,
CreateIFrameDocument = 70,
AdoptedSSReplaceURLBased = 71,
AdoptedSSInsertRuleURLBased = 73,
@ -75,6 +76,7 @@ export declare const enum Type {
CanvasNode = 119,
TagTrigger = 120,
Redux = 121,
SetPageLocation = 122,
}
@ -83,8 +85,8 @@ export type Timestamp = [
/*timestamp:*/ number,
]
export type SetPageLocation = [
/*type:*/ Type.SetPageLocation,
export type SetPageLocationDeprecated = [
/*type:*/ Type.SetPageLocationDeprecated,
/*url:*/ string,
/*referrer:*/ string,
/*navigationStart:*/ number,
@ -432,6 +434,16 @@ export type MouseClick = [
/*hesitationTime:*/ number,
/*label:*/ string,
/*selector:*/ string,
/*normalizedX:*/ number,
/*normalizedY:*/ number,
]
export type MouseClickDeprecated = [
/*type:*/ Type.MouseClickDeprecated,
/*id:*/ number,
/*hesitationTime:*/ number,
/*label:*/ string,
/*selector:*/ string,
]
export type CreateIFrameDocument = [
@ -595,6 +607,14 @@ export type Redux = [
/*actionTime:*/ number,
]
export type SetPageLocation = [
/*type:*/ Type.SetPageLocation,
/*url:*/ string,
/*referrer:*/ string,
/*navigationStart:*/ number,
/*documentTitle:*/ string,
]
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux
type Message = Timestamp | SetPageLocationDeprecated | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation
export default Message

File diff suppressed because it is too large Load diff

View file

@ -14,13 +14,13 @@ export function Timestamp(
]
}
export function SetPageLocation(
export function SetPageLocationDeprecated(
url: string,
referrer: string,
navigationStart: number,
): Messages.SetPageLocation {
): Messages.SetPageLocationDeprecated {
return [
Messages.Type.SetPageLocation,
Messages.Type.SetPageLocationDeprecated,
url,
referrer,
navigationStart,
@ -656,6 +656,8 @@ export function MouseClick(
hesitationTime: number,
label: string,
selector: string,
normalizedX: number,
normalizedY: number,
): Messages.MouseClick {
return [
Messages.Type.MouseClick,
@ -663,6 +665,23 @@ export function MouseClick(
hesitationTime,
label,
selector,
normalizedX,
normalizedY,
]
}
export function MouseClickDeprecated(
id: number,
hesitationTime: number,
label: string,
selector: string,
): Messages.MouseClickDeprecated {
return [
Messages.Type.MouseClickDeprecated,
id,
hesitationTime,
label,
selector,
]
}
@ -966,3 +985,18 @@ export function Redux(
]
}
export function SetPageLocation(
url: string,
referrer: string,
navigationStart: number,
documentTitle: string,
): Messages.SetPageLocation {
return [
Messages.Type.SetPageLocation,
url,
referrer,
navigationStart,
documentTitle,
]
}

View file

@ -8,9 +8,22 @@ export default class Nodes {
private totalNodeAmount = 0
private readonly nodeCallbacks: Array<NodeCallback> = []
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
private nextNodeId = 0
constructor(private readonly node_id: string) {}
syntheticMode(frameOrder: number) {
const maxSafeNumber = 9007199254740900
const placeholderSize = 99999999
const nextFrameId = placeholderSize * frameOrder
// I highly doubt that this will ever happen,
// but it will be easier to debug if it does
if (nextFrameId > maxSafeNumber) {
throw new Error('Placeholder id overflow')
}
this.nextNodeId = nextFrameId
}
// Attached once per Tracker instance
attachNodeCallback(nodeCallback: NodeCallback): void {
this.nodeCallbacks.push(nodeCallback)
@ -38,8 +51,9 @@ export default class Nodes {
let id: number = (node as any)[this.node_id]
const isNew = id === undefined
if (isNew) {
id = this.nextNodeId
this.totalNodeAmount++
id = this.nodes.length
this.nextNodeId++
this.nodes[id] = node
;(node as any)[this.node_id] = id
}
@ -102,6 +116,7 @@ export default class Nodes {
}
this.unregisterNode(node)
}
this.nextNodeId = 0
this.nodes.length = 0
}
}

View file

@ -18,4 +18,14 @@ export default class IFrameObserver extends Observer {
this.app.send(CreateIFrameDocument(hostID, docID))
})
}
syntheticObserve(selfId: number, doc: Document) {
this.observeRoot(doc, (docID) => {
if (docID === undefined) {
this.app.debug.log('OpenReplay: Iframe document not bound')
return
}
this.app.send(CreateIFrameDocument(selfId, docID))
})
}
}

View file

@ -1,5 +1,3 @@
// Le truc - for defining an absolute offset for (nested) iframe documents. (To track mouse movments)
export type Offset = [/*left:*/ number, /*top: */ number]
type OffsetState = {

View file

@ -140,6 +140,21 @@ export default class TopObserver extends Observer {
)
}
crossdomainObserve(selfId: number, frameOder: number) {
const observer = this
Element.prototype.attachShadow = function () {
// eslint-disable-next-line
const shadow = attachShadowNativeFn.apply(this, arguments)
observer.handleShadowRoot(shadow)
return shadow
}
this.app.nodes.clear()
this.app.nodes.syntheticMode(frameOder)
const iframeObserver = new IFrameObserver(this.app)
this.iframeObservers.push(iframeObserver)
iframeObserver.syntheticObserve(selfId, window.document)
}
disconnect() {
this.iframeOffsets.clear()
Element.prototype.attachShadow = attachShadowNativeFn

View file

@ -29,7 +29,7 @@ import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
import Selection from './modules/selection.js'
import Tabs from './modules/tabs.js'
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
import { IN_BROWSER, deprecationWarn, DOCS_HOST, inIframe } from './utils.js'
import FeatureFlags, { IFeatureFlag } from './modules/featureFlags.js'
import type { Options as AppOptions } from './app/index.js'
import type { Options as ConsoleOptions } from './modules/console.js'
@ -99,8 +99,10 @@ export default class API {
public featureFlags: FeatureFlags
private readonly app: App | null = null
private readonly crossdomainMode: boolean = false
constructor(private readonly options: Options) {
this.crossdomainMode = Boolean(inIframe() && options.crossdomain?.enabled)
if (!IN_BROWSER || !processOptions(options)) {
return
}
@ -158,25 +160,38 @@ export default class API {
return
}
const app = new App(options.projectKey, options.sessionToken, options, this.signalStartIssue)
const app = new App(
options.projectKey,
options.sessionToken,
options,
this.signalStartIssue,
this.crossdomainMode,
)
this.app = app
Viewport(app)
if (!this.crossdomainMode) {
// no need to send iframe viewport data since its a node for us
Viewport(app)
// calculated in main window
Connection(app)
// while we can calculate it here, trying to compute it for all parts is hard
Performance(app, options)
// no tabs in iframes yet
Tabs(app)
}
Mouse(app, options.mouse)
// inside iframe, we ignore viewport scroll
Scroll(app, this.crossdomainMode)
CSSRules(app)
ConstructedStyleSheets(app)
Connection(app)
Console(app, options)
Exception(app, options)
Img(app)
Input(app, options)
Mouse(app, options.mouse)
Timing(app, options)
Performance(app, options)
Scroll(app)
Focus(app)
Fonts(app)
Network(app, options.network)
Selection(app)
Tabs(app)
;(window as any).__OPENREPLAY__ = this
if (options.flags && options.flags.onFlagsLoad) {

View file

@ -105,6 +105,7 @@ export interface MouseHandlerOptions {
export default function (app: App, options?: MouseHandlerOptions): void {
const { disableClickmaps = false } = options || {}
function getTargetLabel(target: Element): string {
const dl = getLabelAttribute(target)
if (dl !== null) {
@ -203,7 +204,6 @@ export default function (app: App, options?: MouseHandlerOptions): 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)
@ -221,6 +221,15 @@ export default function (app: App, options?: MouseHandlerOptions): void {
}
const id = app.nodes.getID(target)
if (id !== undefined) {
const clickX = e.pageX
const clickY = e.pageY
const contentWidth = document.documentElement.scrollWidth
const contentHeight = document.documentElement.scrollHeight
const normalizedX = roundNumber(clickX / contentWidth)
const normalizedY = roundNumber(clickY / contentHeight)
sendMouseMove()
app.send(
MouseClick(
@ -228,6 +237,8 @@ export default function (app: App, options?: MouseHandlerOptions): void {
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
getTargetLabel(target),
isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : '',
normalizedX,
normalizedY,
),
true,
)
@ -245,3 +256,11 @@ export default function (app: App, options?: MouseHandlerOptions): void {
app.ticker.attach(sendMouseMove, options?.trackingOffset || 7)
}
/**
* we get 0 to 1 decimal number, convert and round it, then turn to %
* 0.39643 => 396.43 => 396 => 39.6%
* */
function roundNumber(num: number) {
return Math.round(num * 1e3) / 1e1
}

View file

@ -5,18 +5,18 @@ import { isNode, isElementNode, isRootNode, isDocument } from '../app/guards.js'
function getDocumentScroll(doc: Document): [number, number] {
const win = doc.defaultView
return [
(win && win.pageXOffset) ||
(win && win.scrollX) ||
(doc.documentElement && doc.documentElement.scrollLeft) ||
(doc.body && doc.body.scrollLeft) ||
0,
(win && win.pageYOffset) ||
(win && win.scrollY) ||
(doc.documentElement && doc.documentElement.scrollTop) ||
(doc.body && doc.body.scrollTop) ||
0,
]
}
export default function (app: App): void {
export default function (app: App, insideIframe: boolean | null): void {
let documentScroll = false
const nodeScroll: Map<Node, [number, number]> = new Map()
@ -32,9 +32,12 @@ export default function (app: App): void {
}
}
const sendSetViewportScroll = app.safe((): void =>
app.send(SetViewportScroll(...getDocumentScroll(document))),
)
const sendSetViewportScroll = app.safe((): void => {
if (insideIframe) {
return
}
app.send(SetViewportScroll(...getDocumentScroll(document)))
})
const sendSetNodeScroll = app.safe((s: [number, number], node: Node): void => {
const id = app.nodes.getID(node)

View file

@ -3,7 +3,7 @@ import { getTimeOrigin } from '../utils.js'
import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.gen.js'
export default function (app: App): void {
let url: string, width: number, height: number
let url: string | null, width: number, height: number
let navigationStart: number
let referrer = document.referrer
@ -11,7 +11,7 @@ export default function (app: App): void {
const { URL } = document
if (URL !== url) {
url = URL
app.send(SetPageLocation(url, referrer, navigationStart))
app.send(SetPageLocation(url, referrer, navigationStart, document.title))
navigationStart = 0
referrer = url
}
@ -32,7 +32,7 @@ export default function (app: App): void {
: app.safe(() => app.send(SetPageVisibility(document.hidden)))
app.attachStartCallback(() => {
url = ''
url = null
navigationStart = getTimeOrigin()
width = height = -1
sendSetPageLocation()

View file

@ -13,9 +13,11 @@ let timeOrigin: number = IN_BROWSER ? Date.now() - performance.now() : 0
export function adjustTimeOrigin() {
timeOrigin = Date.now() - performance.now()
}
export function getTimeOrigin() {
return timeOrigin
}
export const now: () => number =
IN_BROWSER && !!performance.now
? () => Math.round(performance.now() + timeOrigin)
@ -102,13 +104,17 @@ export function generateRandomId(len?: number) {
// msCrypto = IE11
// @ts-ignore
const safeCrypto = window.crypto || window.msCrypto
safeCrypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
if (safeCrypto) {
safeCrypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
} else {
return Array.from({ length: len || 40 }, () => dec2hex(Math.floor(Math.random() * 16))).join('')
}
}
export function inIframe() {
try {
return window.self !== window.top
return window.self && window.top && window.self !== window.top
} catch (e) {
return true
}
@ -141,9 +147,10 @@ export function createEventListener(
try {
target[safeAddEventListener](event, cb, capture)
} catch (e) {
const msg = e.message
console.debug(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Openreplay: ${e.messages}; if this error is caused by an IframeObserver, ignore it`,
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`,
)
}
}
@ -160,14 +167,14 @@ export function deleteEventListener(
try {
target[safeRemoveEventListener](event, cb, capture)
} catch (e) {
const msg = e.message
console.debug(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Openreplay: ${e.messages}; if this error is caused by an IframeObserver, ignore it`,
`Openreplay: ${msg}; if this error is caused by an IframeObserver, ignore it`,
)
}
}
class FIFOTaskScheduler {
taskQueue: any[]
isRunning: boolean
@ -234,3 +241,27 @@ export function requestIdleCb(callback: () => void) {
// })
// }
}
export function simpleMerge<T>(defaultObj: T, givenObj: Partial<T>): T {
const result = { ...defaultObj }
for (const key in givenObj) {
// eslint-disable-next-line no-prototype-builtins
if (givenObj.hasOwnProperty(key)) {
const userOptionValue = givenObj[key]
const defaultOptionValue = defaultObj[key]
if (
typeof userOptionValue === 'object' &&
!Array.isArray(userOptionValue) &&
userOptionValue !== null
) {
result[key] = simpleMerge(defaultOptionValue || {}, userOptionValue) as any
} else {
result[key] = userOptionValue as any
}
}
}
return result
}

View file

@ -0,0 +1,113 @@
// @ts-nocheck
import { describe, expect, test, jest, beforeEach, afterEach } from '@jest/globals'
import CanvasRecorder from '../main/app/canvas'
import App from '../main/app/index.js'
describe('CanvasRecorder', () => {
let appMock: jest.Mocked<App>
let canvasRecorder: CanvasRecorder
let nodeMock: Node
beforeEach(() => {
// @ts-ignore
appMock = {}
appMock.nodes = {
scanTree: jest.fn(),
attachNodeCallback: jest.fn(),
getID: jest.fn().mockReturnValue(1),
getNode: jest.fn(),
}
appMock.sanitizer = {
isObscured: jest.fn().mockReturnValue(false),
isHidden: jest.fn().mockReturnValue(false),
}
appMock.timestamp = jest.fn().mockReturnValue(1000)
appMock.send = jest.fn()
appMock.debug = {
log: jest.fn(),
error: jest.fn(),
}
appMock.session = {
getSessionToken: jest.fn().mockReturnValue('token'),
}
appMock.options = {
ingestPoint: 'http://example.com',
}
canvasRecorder = new CanvasRecorder(appMock, { fps: 30, quality: 'medium' })
nodeMock = document.createElement('canvas')
})
afterEach(() => {
jest.restoreAllMocks()
})
describe('startTracking', () => {
test('scans tree and attaches node callback after timeout', () => {
jest.useFakeTimers()
canvasRecorder.startTracking()
jest.advanceTimersByTime(500)
expect(appMock.nodes.scanTree).toHaveBeenCalled()
expect(appMock.nodes.attachNodeCallback).toHaveBeenCalled()
jest.useRealTimers()
})
})
describe('restartTracking', () => {
test('clears previous intervals and rescans the tree', () => {
const clearSpy = jest.spyOn(canvasRecorder, 'clear')
canvasRecorder.restartTracking()
expect(clearSpy).toHaveBeenCalled()
expect(appMock.nodes.scanTree).toHaveBeenCalled()
})
})
describe('captureCanvas', () => {
test('captures canvas and starts observing it', () => {
const observeMock = jest.fn()
window.IntersectionObserver = jest.fn().mockImplementation((callback) => {
return {
observe: observeMock,
disconnect: jest.fn(),
}
})
canvasRecorder.captureCanvas(nodeMock)
expect(observeMock).toHaveBeenCalledWith(nodeMock)
})
test('does not capture canvas if it is obscured', () => {
appMock.sanitizer.isObscured.mockReturnValue(true)
const observeMock = jest.fn()
window.IntersectionObserver = jest.fn().mockImplementation((callback) => {
return {
observe: observeMock,
disconnect: jest.fn(),
}
})
canvasRecorder.captureCanvas(nodeMock)
expect(observeMock).not.toHaveBeenCalled()
})
})
describe('recordCanvas', () => {
test('records canvas and sends snapshots at intervals', () => {
jest.useFakeTimers()
canvasRecorder.recordCanvas(nodeMock, 1)
jest.advanceTimersByTime(1000 / 30)
expect(appMock.send).toHaveBeenCalled()
jest.useRealTimers()
})
})
describe('clear', () => {
test('clears intervals and snapshots', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval')
canvasRecorder.recordCanvas(nodeMock, 1)
canvasRecorder.clear()
expect(clearIntervalSpy).toHaveBeenCalled()
expect(canvasRecorder['snapshots']).toEqual({})
})
})
})

View file

@ -5,8 +5,8 @@ import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globa
const Type = {
JSException: 78,
CustomEvent: 27,
MouseClick: 69,
SetPageLocation: 4,
MouseClick: 68,
SetPageLocation: 122,
NetworkRequest: 83,
}
@ -142,6 +142,7 @@ describe('ConditionsManager', () => {
'https://example.com',
'referrer',
Date.now(),
'this is a test title',
]
manager.processMessage(setPageLocationMessage)
expect(manager.hasStarted).toBeTruthy()

View file

@ -0,0 +1,42 @@
import connection from '../main/modules/connection'
import { describe, beforeEach, afterEach, it, expect, jest } from '@jest/globals'
describe('Connection module', () => {
const appMock = {
send: jest.fn(),
}
afterEach(() => {
jest.clearAllMocks()
})
it('should send connection information', () => {
const connectionInfo = {
downlink: 1,
type: 'wifi',
addEventListener: jest.fn(),
}
Object.assign(navigator, { connection: connectionInfo })
// @ts-ignore
connection(appMock)
expect(appMock.send).toHaveBeenCalledWith([54, 1000, 'wifi'])
})
it('should send unknown connection type', () => {
const connectionInfo = {
downlink: 1,
addEventListener: jest.fn(),
}
Object.assign(navigator, { connection: connectionInfo })
// @ts-ignore
connection(appMock)
expect(appMock.send).toHaveBeenCalledWith([54, 1000, 'unknown'])
})
it('should not send connection information if connection is undefined', () => {
Object.assign(navigator, { connection: undefined })
// @ts-ignore
connection(appMock)
expect(appMock.send).not.toHaveBeenCalled()
})
})

View file

@ -13,6 +13,7 @@ import {
generateRandomId,
ngSafeBrowserMethod,
requestIdleCb,
inIframe,
} from '../main/utils.js'
describe('adjustTimeOrigin', () => {
@ -185,6 +186,15 @@ describe('generateRandomId', () => {
expect(id).toHaveLength(40)
expect(/^[0-9a-f]+$/.test(id)).toBe(true)
})
test('falls back to Math.random if crypto api is not available', () => {
const originalCrypto = window.crypto
// @ts-ignore
window.crypto = undefined
const id = generateRandomId(20)
expect(id).toHaveLength(20)
expect(/^[0-9a-f]+$/.test(id)).toBe(true)
})
})
describe('ngSafeBrowserMethod', () => {
@ -207,7 +217,7 @@ describe('requestIdleCb', () => {
test('testing FIFO scheduler', async () => {
jest.useFakeTimers()
// @ts-ignore
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb())
const cb1 = jest.fn()
const cb2 = jest.fn()
@ -221,3 +231,18 @@ describe('requestIdleCb', () => {
expect(cb2).toBeCalledTimes(1)
})
})
describe('inIframe', () => {
test('returns true if the code is running inside an iframe', () => {
const originalSelf = window.self
const originalTop = window.top
Object.defineProperty(window, 'self', { value: {} })
Object.defineProperty(window, 'top', { value: {} })
expect(inIframe()).toBe(true)
Object.defineProperty(window, 'self', { value: originalSelf })
Object.defineProperty(window, 'top', { value: originalTop })
})
})

View file

@ -14,7 +14,7 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.uint(msg[1])
break
case Messages.Type.SetPageLocation:
case Messages.Type.SetPageLocationDeprecated:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3])
break
@ -211,6 +211,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
break
case Messages.Type.MouseClick:
return this.uint(msg[1]) && this.uint(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.uint(msg[5]) && this.uint(msg[6])
break
case Messages.Type.MouseClickDeprecated:
return this.uint(msg[1]) && this.uint(msg[2]) && this.string(msg[3]) && this.string(msg[4])
break
@ -302,6 +306,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.uint(msg[4])
break
case Messages.Type.SetPageLocation:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.string(msg[4])
break
}
}