diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index b5576edf3..2e6e8f899 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -63,7 +63,7 @@ func main() { messages.MsgUnbindNodes, // Mobile messages messages.MsgIOSSessionStart, messages.MsgIOSSessionEnd, messages.MsgIOSUserID, messages.MsgIOSUserAnonymousID, - messages.MsgIOSMetadata, messages.MsgIOSCustomEvent, messages.MsgIOSNetworkCall, + messages.MsgIOSMetadata, messages.MsgIOSEvent, messages.MsgIOSNetworkCall, messages.MsgIOSClickEvent, messages.MsgIOSSwipeEvent, messages.MsgIOSInputEvent, messages.MsgIOSCrash, messages.MsgIOSIssueEvent, } diff --git a/backend/internal/db/datasaver/saver.go b/backend/internal/db/datasaver/saver.go index 72ffa380e..a70b9ef14 100644 --- a/backend/internal/db/datasaver/saver.go +++ b/backend/internal/db/datasaver/saver.go @@ -72,21 +72,21 @@ func (s *saverImpl) handleMobileMessage(msg Message) error { case *IOSSessionEnd: return s.pg.InsertIOSSessionEnd(m.SessionID(), m) case *IOSUserID: - if err = s.sessions.UpdateUserID(session.SessionID, m.Value); err != nil { + if err = s.sessions.UpdateUserID(session.SessionID, m.ID); err != nil { return err } - s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERID_IOS", m.Value) + s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERID_IOS", m.ID) return nil case *IOSUserAnonymousID: - if err = s.sessions.UpdateAnonymousID(session.SessionID, m.Value); err != nil { + if err = s.sessions.UpdateAnonymousID(session.SessionID, m.ID); err != nil { return err } - s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSID_IOS", m.Value) + s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSID_IOS", m.ID) return nil case *IOSMetadata: return s.sessions.UpdateMetadata(m.SessionID(), m.Key, m.Value) - case *IOSCustomEvent: - return s.pg.InsertIOSCustomEvent(session, m) + case *IOSEvent: + return s.pg.InsertIOSEvent(session, m) case *IOSClickEvent: if err := s.pg.InsertIOSClickEvent(session, m); err != nil { return err diff --git a/backend/pkg/db/postgres/mobile.go b/backend/pkg/db/postgres/mobile.go index 3430cfe27..d7e2e63d6 100644 --- a/backend/pkg/db/postgres/mobile.go +++ b/backend/pkg/db/postgres/mobile.go @@ -18,7 +18,7 @@ func (conn *Conn) InsertIOSSessionEnd(sessionID uint64, e *messages.IOSSessionEn return nil } -func (conn *Conn) InsertIOSCustomEvent(session *sessions.Session, e *messages.IOSCustomEvent) error { +func (conn *Conn) InsertIOSEvent(session *sessions.Session, e *messages.IOSEvent) error { if err := conn.InsertCustomEvent(session.SessionID, e.Timestamp, truncSqIdx(e.Index), e.Name, e.Payload); err != nil { return err } diff --git a/backend/pkg/messages/filters.go b/backend/pkg/messages/filters.go index 881528c15..fca1a2065 100644 --- a/backend/pkg/messages/filters.go +++ b/backend/pkg/messages/filters.go @@ -2,7 +2,7 @@ package messages func IsReplayerType(id int) bool { - return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 104 != id && 107 != id && 110 != id && 111 != id + return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id } func IsIOSType(id int) bool { @@ -10,5 +10,5 @@ func IsIOSType(id int) bool { } func IsDOMType(id int) bool { - return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 105 == id || 106 == id + return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id } diff --git a/backend/pkg/messages/get-timestamp.go b/backend/pkg/messages/get-timestamp.go index 21d9aa57e..9ea84cd18 100644 --- a/backend/pkg/messages/get-timestamp.go +++ b/backend/pkg/messages/get-timestamp.go @@ -13,7 +13,7 @@ func GetTimestamp(message Message) uint64 { case *IOSMetadata: return msg.Timestamp - case *IOSCustomEvent: + case *IOSEvent: return msg.Timestamp case *IOSUserID: diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index d276d0645..720670c71 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -90,7 +90,7 @@ const ( MsgIOSSessionStart = 90 MsgIOSSessionEnd = 91 MsgIOSMetadata = 92 - MsgIOSCustomEvent = 93 + MsgIOSEvent = 93 MsgIOSUserID = 94 MsgIOSUserAnonymousID = 95 MsgIOSScreenChanges = 96 @@ -2411,7 +2411,7 @@ func (msg *IOSMetadata) TypeID() int { return 92 } -type IOSCustomEvent struct { +type IOSEvent struct { message Timestamp uint64 Length uint64 @@ -2419,7 +2419,7 @@ type IOSCustomEvent struct { Payload string } -func (msg *IOSCustomEvent) Encode() []byte { +func (msg *IOSEvent) Encode() []byte { buf := make([]byte, 41+len(msg.Name)+len(msg.Payload)) buf[0] = 93 p := 1 @@ -2430,11 +2430,11 @@ func (msg *IOSCustomEvent) Encode() []byte { return buf[:p] } -func (msg *IOSCustomEvent) Decode() Message { +func (msg *IOSEvent) Decode() Message { return msg } -func (msg *IOSCustomEvent) TypeID() int { +func (msg *IOSEvent) TypeID() int { return 93 } @@ -2442,16 +2442,16 @@ type IOSUserID struct { message Timestamp uint64 Length uint64 - Value string + ID string } func (msg *IOSUserID) Encode() []byte { - buf := make([]byte, 31+len(msg.Value)) + buf := make([]byte, 31+len(msg.ID)) buf[0] = 94 p := 1 p = WriteUint(msg.Timestamp, buf, p) p = WriteUint(msg.Length, buf, p) - p = WriteString(msg.Value, buf, p) + p = WriteString(msg.ID, buf, p) return buf[:p] } @@ -2467,16 +2467,16 @@ type IOSUserAnonymousID struct { message Timestamp uint64 Length uint64 - Value string + ID string } func (msg *IOSUserAnonymousID) Encode() []byte { - buf := make([]byte, 31+len(msg.Value)) + buf := make([]byte, 31+len(msg.ID)) buf[0] = 95 p := 1 p = WriteUint(msg.Timestamp, buf, p) p = WriteUint(msg.Length, buf, p) - p = WriteString(msg.Value, buf, p) + p = WriteString(msg.ID, buf, p) return buf[:p] } diff --git a/backend/pkg/messages/read-message.go b/backend/pkg/messages/read-message.go index e79d7706d..3260c0fed 100644 --- a/backend/pkg/messages/read-message.go +++ b/backend/pkg/messages/read-message.go @@ -1479,9 +1479,9 @@ func DecodeIOSMetadata(reader BytesReader) (Message, error) { return msg, err } -func DecodeIOSCustomEvent(reader BytesReader) (Message, error) { +func DecodeIOSEvent(reader BytesReader) (Message, error) { var err error = nil - msg := &IOSCustomEvent{} + msg := &IOSEvent{} if msg.Timestamp, err = reader.ReadUint(); err != nil { return nil, err } @@ -1506,7 +1506,7 @@ func DecodeIOSUserID(reader BytesReader) (Message, error) { if msg.Length, err = reader.ReadUint(); err != nil { return nil, err } - if msg.Value, err = reader.ReadString(); err != nil { + if msg.ID, err = reader.ReadString(); err != nil { return nil, err } return msg, err @@ -1521,7 +1521,7 @@ func DecodeIOSUserAnonymousID(reader BytesReader) (Message, error) { if msg.Length, err = reader.ReadUint(); err != nil { return nil, err } - if msg.Value, err = reader.ReadString(); err != nil { + if msg.ID, err = reader.ReadString(); err != nil { return nil, err } return msg, err @@ -2006,7 +2006,7 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) { case 92: return DecodeIOSMetadata(reader) case 93: - return DecodeIOSCustomEvent(reader) + return DecodeIOSEvent(reader) case 94: return DecodeIOSUserID(reader) case 95: diff --git a/ee/connectors/handler.py b/ee/connectors/handler.py index 3a7d0d27b..e6f786292 100644 --- a/ee/connectors/handler.py +++ b/ee/connectors/handler.py @@ -705,13 +705,13 @@ def handle_message(message: Message) -> Optional[DetailedEvent]: if isinstance(message, IOSUserID): n.iosuserid_timestamp = message.timestamp n.iosuserid_length = message.length - n.iosuserid_value = message.value + n.iosuserid_id = message.id return n if isinstance(message, IOSUserAnonymousID): n.iosuseranonymousid_timestamp = message.timestamp n.iosuseranonymousid_length = message.length - n.iosuseranonymousid_value = message.value + n.iosuseranonymousid_id = message.id return n if isinstance(message, IOSScreenEnter): @@ -779,11 +779,11 @@ def handle_message(message: Message) -> Optional[DetailedEvent]: n.iosissueevent_payload = message.payload return n - if isinstance(message, IOSCustomEvent): - n.ioscustomevent_timestamp = message.timestamp - n.ioscustomevent_length = message.length - n.ioscustomevent_name = message.name - n.ioscustomevent_payload = message.payload + if isinstance(message, IOSEvent): + n.iosevent_timestamp = message.timestamp + n.iosevent_length = message.length + n.iosevent_name = message.name + n.iosevent_payload = message.payload return n if isinstance(message, IOSInternalError): diff --git a/ee/connectors/msgcodec/messages.py b/ee/connectors/msgcodec/messages.py index 3f1a2112f..9e3091142 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -850,7 +850,7 @@ class IOSMetadata(Message): self.value = value -class IOSCustomEvent(Message): +class IOSEvent(Message): __id__ = 93 def __init__(self, timestamp, length, name, payload): @@ -863,19 +863,19 @@ class IOSCustomEvent(Message): class IOSUserID(Message): __id__ = 94 - def __init__(self, timestamp, length, value): + def __init__(self, timestamp, length, id): self.timestamp = timestamp self.length = length - self.value = value + self.id = id class IOSUserAnonymousID(Message): __id__ = 95 - def __init__(self, timestamp, length, value): + def __init__(self, timestamp, length, id): self.timestamp = timestamp self.length = length - self.value = value + self.id = id class IOSScreenChanges(Message): diff --git a/ee/connectors/msgcodec/messages.pyx b/ee/connectors/msgcodec/messages.pyx index 07d70bdd1..8c1b6206a 100644 --- a/ee/connectors/msgcodec/messages.pyx +++ b/ee/connectors/msgcodec/messages.pyx @@ -1258,7 +1258,7 @@ cdef class IOSMetadata(PyMessage): self.value = value -cdef class IOSCustomEvent(PyMessage): +cdef class IOSEvent(PyMessage): cdef public int __id__ cdef public unsigned long timestamp cdef public unsigned long length @@ -1277,26 +1277,26 @@ cdef class IOSUserID(PyMessage): cdef public int __id__ cdef public unsigned long timestamp cdef public unsigned long length - cdef public str value + cdef public str id - def __init__(self, unsigned long timestamp, unsigned long length, str value): + def __init__(self, unsigned long timestamp, unsigned long length, str id): self.__id__ = 94 self.timestamp = timestamp self.length = length - self.value = value + self.id = id cdef class IOSUserAnonymousID(PyMessage): cdef public int __id__ cdef public unsigned long timestamp cdef public unsigned long length - cdef public str value + cdef public str id - def __init__(self, unsigned long timestamp, unsigned long length, str value): + def __init__(self, unsigned long timestamp, unsigned long length, str id): self.__id__ = 95 self.timestamp = timestamp self.length = length - self.value = value + self.id = id cdef class IOSScreenChanges(PyMessage): diff --git a/ee/connectors/msgcodec/msgcodec.py b/ee/connectors/msgcodec/msgcodec.py index 9f98889b6..0040064c8 100644 --- a/ee/connectors/msgcodec/msgcodec.py +++ b/ee/connectors/msgcodec/msgcodec.py @@ -762,7 +762,7 @@ class MessageCodec(Codec): ) if message_id == 93: - return IOSCustomEvent( + return IOSEvent( timestamp=self.read_uint(reader), length=self.read_uint(reader), name=self.read_string(reader), @@ -773,14 +773,14 @@ class MessageCodec(Codec): return IOSUserID( timestamp=self.read_uint(reader), length=self.read_uint(reader), - value=self.read_string(reader) + id=self.read_string(reader) ) if message_id == 95: return IOSUserAnonymousID( timestamp=self.read_uint(reader), length=self.read_uint(reader), - value=self.read_string(reader) + id=self.read_string(reader) ) if message_id == 96: diff --git a/ee/connectors/msgcodec/msgcodec.pyx b/ee/connectors/msgcodec/msgcodec.pyx index 003273a30..6a6faf871 100644 --- a/ee/connectors/msgcodec/msgcodec.pyx +++ b/ee/connectors/msgcodec/msgcodec.pyx @@ -173,18 +173,18 @@ cdef class MessageCodec: return messages_list def handler(self, PyBytesIO reader, int mode = 0): - cdef unsigned long message_id = self.read_message_id(reader) + cdef unsigned long message_id = MessageCodec.read_message_id(reader) cdef int r_size if mode == 1: # We read the three bytes representing the length of message. It can be used to skip unwanted messages - r_size = self.read_size(reader) + r_size = MessageCodec.read_size(reader) if message_id not in self.msg_selector: reader.read(r_size) return None - return self.read_head_message(reader, message_id) + return MessageCodec.read_head_message(reader, message_id) elif mode == 0: # Old format with no bytes for message length - return self.read_head_message(reader, message_id) + return MessageCodec.read_head_message(reader, message_id) else: raise IOError() @@ -193,51 +193,51 @@ cdef class MessageCodec: if message_id == 0: return Timestamp( - timestamp=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader) ) if message_id == 1: return SessionStart( - timestamp=MessageCodec.read_uint(reader), - project_id=MessageCodec.read_uint(reader), - tracker_version=MessageCodec.read_string(reader), - rev_id=MessageCodec.read_string(reader), - user_uuid=MessageCodec.read_string(reader), - user_agent=MessageCodec.read_string(reader), - user_os=MessageCodec.read_string(reader), - user_os_version=MessageCodec.read_string(reader), - user_browser=MessageCodec.read_string(reader), - user_browser_version=MessageCodec.read_string(reader), - user_device=MessageCodec.read_string(reader), - user_device_type=MessageCodec.read_string(reader), - user_device_memory_size=MessageCodec.read_uint(reader), - user_device_heap_size=MessageCodec.read_uint(reader), - user_country=MessageCodec.read_string(reader), - user_id=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + project_id=self.read_uint(reader), + tracker_version=self.read_string(reader), + rev_id=self.read_string(reader), + user_uuid=self.read_string(reader), + user_agent=self.read_string(reader), + user_os=self.read_string(reader), + user_os_version=self.read_string(reader), + user_browser=self.read_string(reader), + user_browser_version=self.read_string(reader), + user_device=self.read_string(reader), + user_device_type=self.read_string(reader), + user_device_memory_size=self.read_uint(reader), + user_device_heap_size=self.read_uint(reader), + user_country=self.read_string(reader), + user_id=self.read_string(reader) ) if message_id == 3: return SessionEndDeprecated( - timestamp=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader) ) if message_id == 4: return SetPageLocation( - url=MessageCodec.read_string(reader), - referrer=MessageCodec.read_string(reader), - navigation_start=MessageCodec.read_uint(reader) + url=self.read_string(reader), + referrer=self.read_string(reader), + navigation_start=self.read_uint(reader) ) if message_id == 5: return SetViewportSize( - width=MessageCodec.read_uint(reader), - height=MessageCodec.read_uint(reader) + width=self.read_uint(reader), + height=self.read_uint(reader) ) if message_id == 6: return SetViewportScroll( - x=MessageCodec.read_int(reader), - y=MessageCodec.read_int(reader) + x=self.read_int(reader), + y=self.read_int(reader) ) if message_id == 7: @@ -247,763 +247,763 @@ cdef class MessageCodec: if message_id == 8: return CreateElementNode( - id=MessageCodec.read_uint(reader), - parent_id=MessageCodec.read_uint(reader), - index=MessageCodec.read_uint(reader), - tag=MessageCodec.read_string(reader), - svg=MessageCodec.read_boolean(reader) + id=self.read_uint(reader), + parent_id=self.read_uint(reader), + index=self.read_uint(reader), + tag=self.read_string(reader), + svg=self.read_boolean(reader) ) if message_id == 9: return CreateTextNode( - id=MessageCodec.read_uint(reader), - parent_id=MessageCodec.read_uint(reader), - index=MessageCodec.read_uint(reader) + id=self.read_uint(reader), + parent_id=self.read_uint(reader), + index=self.read_uint(reader) ) if message_id == 10: return MoveNode( - id=MessageCodec.read_uint(reader), - parent_id=MessageCodec.read_uint(reader), - index=MessageCodec.read_uint(reader) + id=self.read_uint(reader), + parent_id=self.read_uint(reader), + index=self.read_uint(reader) ) if message_id == 11: return RemoveNode( - id=MessageCodec.read_uint(reader) + id=self.read_uint(reader) ) if message_id == 12: return SetNodeAttribute( - id=MessageCodec.read_uint(reader), - name=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader) + id=self.read_uint(reader), + name=self.read_string(reader), + value=self.read_string(reader) ) if message_id == 13: return RemoveNodeAttribute( - id=MessageCodec.read_uint(reader), - name=MessageCodec.read_string(reader) + id=self.read_uint(reader), + name=self.read_string(reader) ) if message_id == 14: return SetNodeData( - id=MessageCodec.read_uint(reader), - data=MessageCodec.read_string(reader) + id=self.read_uint(reader), + data=self.read_string(reader) ) if message_id == 15: return SetCSSData( - id=MessageCodec.read_uint(reader), - data=MessageCodec.read_string(reader) + id=self.read_uint(reader), + data=self.read_string(reader) ) if message_id == 16: return SetNodeScroll( - id=MessageCodec.read_uint(reader), - x=MessageCodec.read_int(reader), - y=MessageCodec.read_int(reader) + id=self.read_uint(reader), + x=self.read_int(reader), + y=self.read_int(reader) ) if message_id == 17: return SetInputTarget( - id=MessageCodec.read_uint(reader), - label=MessageCodec.read_string(reader) + id=self.read_uint(reader), + label=self.read_string(reader) ) if message_id == 18: return SetInputValue( - id=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader), - mask=MessageCodec.read_int(reader) + id=self.read_uint(reader), + value=self.read_string(reader), + mask=self.read_int(reader) ) if message_id == 19: return SetInputChecked( - id=MessageCodec.read_uint(reader), - checked=MessageCodec.read_boolean(reader) + id=self.read_uint(reader), + checked=self.read_boolean(reader) ) if message_id == 20: return MouseMove( - x=MessageCodec.read_uint(reader), - y=MessageCodec.read_uint(reader) + x=self.read_uint(reader), + y=self.read_uint(reader) ) if message_id == 21: return NetworkRequestDeprecated( - type=MessageCodec.read_string(reader), - method=MessageCodec.read_string(reader), - url=MessageCodec.read_string(reader), - request=MessageCodec.read_string(reader), - response=MessageCodec.read_string(reader), - status=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader) + type=self.read_string(reader), + method=self.read_string(reader), + url=self.read_string(reader), + request=self.read_string(reader), + response=self.read_string(reader), + status=self.read_uint(reader), + timestamp=self.read_uint(reader), + duration=self.read_uint(reader) ) if message_id == 22: return ConsoleLog( - level=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader) + level=self.read_string(reader), + value=self.read_string(reader) ) if message_id == 23: return PageLoadTiming( - request_start=MessageCodec.read_uint(reader), - response_start=MessageCodec.read_uint(reader), - response_end=MessageCodec.read_uint(reader), - dom_content_loaded_event_start=MessageCodec.read_uint(reader), - dom_content_loaded_event_end=MessageCodec.read_uint(reader), - load_event_start=MessageCodec.read_uint(reader), - load_event_end=MessageCodec.read_uint(reader), - first_paint=MessageCodec.read_uint(reader), - first_contentful_paint=MessageCodec.read_uint(reader) + request_start=self.read_uint(reader), + response_start=self.read_uint(reader), + response_end=self.read_uint(reader), + dom_content_loaded_event_start=self.read_uint(reader), + dom_content_loaded_event_end=self.read_uint(reader), + load_event_start=self.read_uint(reader), + load_event_end=self.read_uint(reader), + first_paint=self.read_uint(reader), + first_contentful_paint=self.read_uint(reader) ) if message_id == 24: return PageRenderTiming( - speed_index=MessageCodec.read_uint(reader), - visually_complete=MessageCodec.read_uint(reader), - time_to_interactive=MessageCodec.read_uint(reader) + speed_index=self.read_uint(reader), + visually_complete=self.read_uint(reader), + time_to_interactive=self.read_uint(reader) ) if message_id == 25: return JSExceptionDeprecated( - name=MessageCodec.read_string(reader), - message=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + name=self.read_string(reader), + message=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 26: return IntegrationEvent( - timestamp=MessageCodec.read_uint(reader), - source=MessageCodec.read_string(reader), - name=MessageCodec.read_string(reader), - message=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + source=self.read_string(reader), + name=self.read_string(reader), + message=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 27: return CustomEvent( - name=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + name=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 28: return UserID( - id=MessageCodec.read_string(reader) + id=self.read_string(reader) ) if message_id == 29: return UserAnonymousID( - id=MessageCodec.read_string(reader) + id=self.read_string(reader) ) if message_id == 30: return Metadata( - key=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader) + key=self.read_string(reader), + value=self.read_string(reader) ) if message_id == 31: return PageEvent( - message_id=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - url=MessageCodec.read_string(reader), - referrer=MessageCodec.read_string(reader), - loaded=MessageCodec.read_boolean(reader), - request_start=MessageCodec.read_uint(reader), - response_start=MessageCodec.read_uint(reader), - response_end=MessageCodec.read_uint(reader), - dom_content_loaded_event_start=MessageCodec.read_uint(reader), - dom_content_loaded_event_end=MessageCodec.read_uint(reader), - load_event_start=MessageCodec.read_uint(reader), - load_event_end=MessageCodec.read_uint(reader), - first_paint=MessageCodec.read_uint(reader), - first_contentful_paint=MessageCodec.read_uint(reader), - speed_index=MessageCodec.read_uint(reader), - visually_complete=MessageCodec.read_uint(reader), - time_to_interactive=MessageCodec.read_uint(reader) + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + url=self.read_string(reader), + referrer=self.read_string(reader), + loaded=self.read_boolean(reader), + request_start=self.read_uint(reader), + response_start=self.read_uint(reader), + response_end=self.read_uint(reader), + dom_content_loaded_event_start=self.read_uint(reader), + dom_content_loaded_event_end=self.read_uint(reader), + load_event_start=self.read_uint(reader), + load_event_end=self.read_uint(reader), + first_paint=self.read_uint(reader), + first_contentful_paint=self.read_uint(reader), + speed_index=self.read_uint(reader), + visually_complete=self.read_uint(reader), + time_to_interactive=self.read_uint(reader) ) if message_id == 32: return InputEvent( - message_id=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader), - value_masked=MessageCodec.read_boolean(reader), - label=MessageCodec.read_string(reader) + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + value=self.read_string(reader), + value_masked=self.read_boolean(reader), + label=self.read_string(reader) ) if message_id == 37: return CSSInsertRule( - id=MessageCodec.read_uint(reader), - rule=MessageCodec.read_string(reader), - index=MessageCodec.read_uint(reader) + id=self.read_uint(reader), + rule=self.read_string(reader), + index=self.read_uint(reader) ) if message_id == 38: return CSSDeleteRule( - id=MessageCodec.read_uint(reader), - index=MessageCodec.read_uint(reader) + id=self.read_uint(reader), + index=self.read_uint(reader) ) if message_id == 39: return Fetch( - method=MessageCodec.read_string(reader), - url=MessageCodec.read_string(reader), - request=MessageCodec.read_string(reader), - response=MessageCodec.read_string(reader), - status=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader) + method=self.read_string(reader), + url=self.read_string(reader), + request=self.read_string(reader), + response=self.read_string(reader), + status=self.read_uint(reader), + timestamp=self.read_uint(reader), + duration=self.read_uint(reader) ) if message_id == 40: return Profiler( - name=MessageCodec.read_string(reader), - duration=MessageCodec.read_uint(reader), - args=MessageCodec.read_string(reader), - result=MessageCodec.read_string(reader) + name=self.read_string(reader), + duration=self.read_uint(reader), + args=self.read_string(reader), + result=self.read_string(reader) ) if message_id == 41: return OTable( - key=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader) + key=self.read_string(reader), + value=self.read_string(reader) ) if message_id == 42: return StateAction( - type=MessageCodec.read_string(reader) + type=self.read_string(reader) ) if message_id == 44: return Redux( - action=MessageCodec.read_string(reader), - state=MessageCodec.read_string(reader), - duration=MessageCodec.read_uint(reader) + action=self.read_string(reader), + state=self.read_string(reader), + duration=self.read_uint(reader) ) if message_id == 45: return Vuex( - mutation=MessageCodec.read_string(reader), - state=MessageCodec.read_string(reader) + mutation=self.read_string(reader), + state=self.read_string(reader) ) if message_id == 46: return MobX( - type=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + type=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 47: return NgRx( - action=MessageCodec.read_string(reader), - state=MessageCodec.read_string(reader), - duration=MessageCodec.read_uint(reader) + action=self.read_string(reader), + state=self.read_string(reader), + duration=self.read_uint(reader) ) if message_id == 48: return GraphQL( - operation_kind=MessageCodec.read_string(reader), - operation_name=MessageCodec.read_string(reader), - variables=MessageCodec.read_string(reader), - response=MessageCodec.read_string(reader) + operation_kind=self.read_string(reader), + operation_name=self.read_string(reader), + variables=self.read_string(reader), + response=self.read_string(reader) ) if message_id == 49: return PerformanceTrack( - frames=MessageCodec.read_int(reader), - ticks=MessageCodec.read_int(reader), - total_js_heap_size=MessageCodec.read_uint(reader), - used_js_heap_size=MessageCodec.read_uint(reader) + frames=self.read_int(reader), + ticks=self.read_int(reader), + total_js_heap_size=self.read_uint(reader), + used_js_heap_size=self.read_uint(reader) ) if message_id == 50: return StringDict( - key=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader) + key=self.read_uint(reader), + value=self.read_string(reader) ) if message_id == 51: return SetNodeAttributeDict( - id=MessageCodec.read_uint(reader), - name_key=MessageCodec.read_uint(reader), - value_key=MessageCodec.read_uint(reader) + id=self.read_uint(reader), + name_key=self.read_uint(reader), + value_key=self.read_uint(reader) ) if message_id == 53: return ResourceTimingDeprecated( - timestamp=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader), - ttfb=MessageCodec.read_uint(reader), - header_size=MessageCodec.read_uint(reader), - encoded_body_size=MessageCodec.read_uint(reader), - decoded_body_size=MessageCodec.read_uint(reader), - url=MessageCodec.read_string(reader), - initiator=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + duration=self.read_uint(reader), + ttfb=self.read_uint(reader), + header_size=self.read_uint(reader), + encoded_body_size=self.read_uint(reader), + decoded_body_size=self.read_uint(reader), + url=self.read_string(reader), + initiator=self.read_string(reader) ) if message_id == 54: return ConnectionInformation( - downlink=MessageCodec.read_uint(reader), - type=MessageCodec.read_string(reader) + downlink=self.read_uint(reader), + type=self.read_string(reader) ) if message_id == 55: return SetPageVisibility( - hidden=MessageCodec.read_boolean(reader) + hidden=self.read_boolean(reader) ) if message_id == 56: return PerformanceTrackAggr( - timestamp_start=MessageCodec.read_uint(reader), - timestamp_end=MessageCodec.read_uint(reader), - min_fps=MessageCodec.read_uint(reader), - avg_fps=MessageCodec.read_uint(reader), - max_fps=MessageCodec.read_uint(reader), - min_cpu=MessageCodec.read_uint(reader), - avg_cpu=MessageCodec.read_uint(reader), - max_cpu=MessageCodec.read_uint(reader), - min_total_js_heap_size=MessageCodec.read_uint(reader), - avg_total_js_heap_size=MessageCodec.read_uint(reader), - max_total_js_heap_size=MessageCodec.read_uint(reader), - min_used_js_heap_size=MessageCodec.read_uint(reader), - avg_used_js_heap_size=MessageCodec.read_uint(reader), - max_used_js_heap_size=MessageCodec.read_uint(reader) + timestamp_start=self.read_uint(reader), + timestamp_end=self.read_uint(reader), + min_fps=self.read_uint(reader), + avg_fps=self.read_uint(reader), + max_fps=self.read_uint(reader), + min_cpu=self.read_uint(reader), + avg_cpu=self.read_uint(reader), + max_cpu=self.read_uint(reader), + min_total_js_heap_size=self.read_uint(reader), + avg_total_js_heap_size=self.read_uint(reader), + max_total_js_heap_size=self.read_uint(reader), + min_used_js_heap_size=self.read_uint(reader), + avg_used_js_heap_size=self.read_uint(reader), + max_used_js_heap_size=self.read_uint(reader) ) if message_id == 57: return LoadFontFace( - parent_id=MessageCodec.read_uint(reader), - family=MessageCodec.read_string(reader), - source=MessageCodec.read_string(reader), - descriptors=MessageCodec.read_string(reader) + parent_id=self.read_uint(reader), + family=self.read_string(reader), + source=self.read_string(reader), + descriptors=self.read_string(reader) ) if message_id == 58: return SetNodeFocus( - id=MessageCodec.read_int(reader) + id=self.read_int(reader) ) if message_id == 59: return LongTask( - timestamp=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader), - context=MessageCodec.read_uint(reader), - container_type=MessageCodec.read_uint(reader), - container_src=MessageCodec.read_string(reader), - container_id=MessageCodec.read_string(reader), - container_name=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + duration=self.read_uint(reader), + context=self.read_uint(reader), + container_type=self.read_uint(reader), + container_src=self.read_string(reader), + container_id=self.read_string(reader), + container_name=self.read_string(reader) ) if message_id == 60: return SetNodeAttributeURLBased( - id=MessageCodec.read_uint(reader), - name=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader), - base_url=MessageCodec.read_string(reader) + id=self.read_uint(reader), + name=self.read_string(reader), + value=self.read_string(reader), + base_url=self.read_string(reader) ) if message_id == 61: return SetCSSDataURLBased( - id=MessageCodec.read_uint(reader), - data=MessageCodec.read_string(reader), - base_url=MessageCodec.read_string(reader) + id=self.read_uint(reader), + data=self.read_string(reader), + base_url=self.read_string(reader) ) if message_id == 62: return IssueEventDeprecated( - message_id=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - type=MessageCodec.read_string(reader), - context_string=MessageCodec.read_string(reader), - context=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + type=self.read_string(reader), + context_string=self.read_string(reader), + context=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 63: return TechnicalInfo( - type=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader) + type=self.read_string(reader), + value=self.read_string(reader) ) if message_id == 64: return CustomIssue( - name=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + name=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 66: return AssetCache( - url=MessageCodec.read_string(reader) + url=self.read_string(reader) ) if message_id == 67: return CSSInsertRuleURLBased( - id=MessageCodec.read_uint(reader), - rule=MessageCodec.read_string(reader), - index=MessageCodec.read_uint(reader), - base_url=MessageCodec.read_string(reader) + id=self.read_uint(reader), + rule=self.read_string(reader), + index=self.read_uint(reader), + base_url=self.read_string(reader) ) if message_id == 69: return MouseClick( - id=MessageCodec.read_uint(reader), - hesitation_time=MessageCodec.read_uint(reader), - label=MessageCodec.read_string(reader), - selector=MessageCodec.read_string(reader) + id=self.read_uint(reader), + hesitation_time=self.read_uint(reader), + label=self.read_string(reader), + selector=self.read_string(reader) ) if message_id == 70: return CreateIFrameDocument( - frame_id=MessageCodec.read_uint(reader), - id=MessageCodec.read_uint(reader) + frame_id=self.read_uint(reader), + id=self.read_uint(reader) ) if message_id == 71: return AdoptedSSReplaceURLBased( - sheet_id=MessageCodec.read_uint(reader), - text=MessageCodec.read_string(reader), - base_url=MessageCodec.read_string(reader) + sheet_id=self.read_uint(reader), + text=self.read_string(reader), + base_url=self.read_string(reader) ) if message_id == 72: return AdoptedSSReplace( - sheet_id=MessageCodec.read_uint(reader), - text=MessageCodec.read_string(reader) + sheet_id=self.read_uint(reader), + text=self.read_string(reader) ) if message_id == 73: return AdoptedSSInsertRuleURLBased( - sheet_id=MessageCodec.read_uint(reader), - rule=MessageCodec.read_string(reader), - index=MessageCodec.read_uint(reader), - base_url=MessageCodec.read_string(reader) + sheet_id=self.read_uint(reader), + rule=self.read_string(reader), + index=self.read_uint(reader), + base_url=self.read_string(reader) ) if message_id == 74: return AdoptedSSInsertRule( - sheet_id=MessageCodec.read_uint(reader), - rule=MessageCodec.read_string(reader), - index=MessageCodec.read_uint(reader) + sheet_id=self.read_uint(reader), + rule=self.read_string(reader), + index=self.read_uint(reader) ) if message_id == 75: return AdoptedSSDeleteRule( - sheet_id=MessageCodec.read_uint(reader), - index=MessageCodec.read_uint(reader) + sheet_id=self.read_uint(reader), + index=self.read_uint(reader) ) if message_id == 76: return AdoptedSSAddOwner( - sheet_id=MessageCodec.read_uint(reader), - id=MessageCodec.read_uint(reader) + sheet_id=self.read_uint(reader), + id=self.read_uint(reader) ) if message_id == 77: return AdoptedSSRemoveOwner( - sheet_id=MessageCodec.read_uint(reader), - id=MessageCodec.read_uint(reader) + sheet_id=self.read_uint(reader), + id=self.read_uint(reader) ) if message_id == 78: return JSException( - name=MessageCodec.read_string(reader), - message=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader), - metadata=MessageCodec.read_string(reader) + name=self.read_string(reader), + message=self.read_string(reader), + payload=self.read_string(reader), + metadata=self.read_string(reader) ) if message_id == 79: return Zustand( - mutation=MessageCodec.read_string(reader), - state=MessageCodec.read_string(reader) + mutation=self.read_string(reader), + state=self.read_string(reader) ) if message_id == 80: return BatchMeta( - page_no=MessageCodec.read_uint(reader), - first_index=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_int(reader) + page_no=self.read_uint(reader), + first_index=self.read_uint(reader), + timestamp=self.read_int(reader) ) if message_id == 81: return BatchMetadata( - version=MessageCodec.read_uint(reader), - page_no=MessageCodec.read_uint(reader), - first_index=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_int(reader), - location=MessageCodec.read_string(reader) + version=self.read_uint(reader), + page_no=self.read_uint(reader), + first_index=self.read_uint(reader), + timestamp=self.read_int(reader), + location=self.read_string(reader) ) if message_id == 82: return PartitionedMessage( - part_no=MessageCodec.read_uint(reader), - part_total=MessageCodec.read_uint(reader) + part_no=self.read_uint(reader), + part_total=self.read_uint(reader) ) if message_id == 83: return NetworkRequest( - type=MessageCodec.read_string(reader), - method=MessageCodec.read_string(reader), - url=MessageCodec.read_string(reader), - request=MessageCodec.read_string(reader), - response=MessageCodec.read_string(reader), - status=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader), - transferred_body_size=MessageCodec.read_uint(reader) + type=self.read_string(reader), + method=self.read_string(reader), + url=self.read_string(reader), + request=self.read_string(reader), + response=self.read_string(reader), + status=self.read_uint(reader), + timestamp=self.read_uint(reader), + duration=self.read_uint(reader), + transferred_body_size=self.read_uint(reader) ) if message_id == 112: return InputChange( - id=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader), - value_masked=MessageCodec.read_boolean(reader), - label=MessageCodec.read_string(reader), - hesitation_time=MessageCodec.read_int(reader), - input_duration=MessageCodec.read_int(reader) + id=self.read_uint(reader), + value=self.read_string(reader), + value_masked=self.read_boolean(reader), + label=self.read_string(reader), + hesitation_time=self.read_int(reader), + input_duration=self.read_int(reader) ) if message_id == 113: return SelectionChange( - selection_start=MessageCodec.read_uint(reader), - selection_end=MessageCodec.read_uint(reader), - selection=MessageCodec.read_string(reader) + selection_start=self.read_uint(reader), + selection_end=self.read_uint(reader), + selection=self.read_string(reader) ) if message_id == 114: return MouseThrashing( - timestamp=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader) ) if message_id == 115: return UnbindNodes( - total_removed_percent=MessageCodec.read_uint(reader) + total_removed_percent=self.read_uint(reader) ) if message_id == 116: return ResourceTiming( - timestamp=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader), - ttfb=MessageCodec.read_uint(reader), - header_size=MessageCodec.read_uint(reader), - encoded_body_size=MessageCodec.read_uint(reader), - decoded_body_size=MessageCodec.read_uint(reader), - url=MessageCodec.read_string(reader), - initiator=MessageCodec.read_string(reader), - transferred_size=MessageCodec.read_uint(reader), - cached=MessageCodec.read_boolean(reader) + timestamp=self.read_uint(reader), + duration=self.read_uint(reader), + ttfb=self.read_uint(reader), + header_size=self.read_uint(reader), + encoded_body_size=self.read_uint(reader), + decoded_body_size=self.read_uint(reader), + url=self.read_string(reader), + initiator=self.read_string(reader), + transferred_size=self.read_uint(reader), + cached=self.read_boolean(reader) ) if message_id == 117: return TabChange( - tab_id=MessageCodec.read_string(reader) + tab_id=self.read_string(reader) ) if message_id == 118: return TabData( - tab_id=MessageCodec.read_string(reader) + tab_id=self.read_string(reader) ) if message_id == 125: return IssueEvent( - message_id=MessageCodec.read_uint(reader), - timestamp=MessageCodec.read_uint(reader), - type=MessageCodec.read_string(reader), - context_string=MessageCodec.read_string(reader), - context=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader), - url=MessageCodec.read_string(reader) + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + type=self.read_string(reader), + context_string=self.read_string(reader), + context=self.read_string(reader), + payload=self.read_string(reader), + url=self.read_string(reader) ) if message_id == 126: return SessionEnd( - timestamp=MessageCodec.read_uint(reader), - encryption_key=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + encryption_key=self.read_string(reader) ) if message_id == 127: return SessionSearch( - timestamp=MessageCodec.read_uint(reader), - partition=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader), + partition=self.read_uint(reader) ) if message_id == 90: return IOSSessionStart( - timestamp=MessageCodec.read_uint(reader), - project_id=MessageCodec.read_uint(reader), - tracker_version=MessageCodec.read_string(reader), - rev_id=MessageCodec.read_string(reader), - user_uuid=MessageCodec.read_string(reader), - user_os=MessageCodec.read_string(reader), - user_os_version=MessageCodec.read_string(reader), - user_device=MessageCodec.read_string(reader), - user_device_type=MessageCodec.read_string(reader), - user_country=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + project_id=self.read_uint(reader), + tracker_version=self.read_string(reader), + rev_id=self.read_string(reader), + user_uuid=self.read_string(reader), + user_os=self.read_string(reader), + user_os_version=self.read_string(reader), + user_device=self.read_string(reader), + user_device_type=self.read_string(reader), + user_country=self.read_string(reader) ) if message_id == 91: return IOSSessionEnd( - timestamp=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader) ) if message_id == 92: return IOSMetadata( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - key=MessageCodec.read_string(reader), - value=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + key=self.read_string(reader), + value=self.read_string(reader) ) if message_id == 93: - return IOSCustomEvent( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - name=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + return IOSEvent( + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + name=self.read_string(reader), + payload=self.read_string(reader) ) if message_id == 94: return IOSUserID( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + id=self.read_string(reader) ) if message_id == 95: return IOSUserAnonymousID( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + id=self.read_string(reader) ) if message_id == 96: return IOSScreenChanges( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - x=MessageCodec.read_uint(reader), - y=MessageCodec.read_uint(reader), - width=MessageCodec.read_uint(reader), - height=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + x=self.read_uint(reader), + y=self.read_uint(reader), + width=self.read_uint(reader), + height=self.read_uint(reader) ) if message_id == 97: return IOSCrash( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - name=MessageCodec.read_string(reader), - reason=MessageCodec.read_string(reader), - stacktrace=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + name=self.read_string(reader), + reason=self.read_string(reader), + stacktrace=self.read_string(reader) ) if message_id == 98: return IOSViewComponentEvent( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - screen_name=MessageCodec.read_string(reader), - view_name=MessageCodec.read_string(reader), - visible=MessageCodec.read_boolean(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + screen_name=self.read_string(reader), + view_name=self.read_string(reader), + visible=self.read_boolean(reader) ) if message_id == 100: return IOSClickEvent( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - label=MessageCodec.read_string(reader), - x=MessageCodec.read_uint(reader), - y=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + label=self.read_string(reader), + x=self.read_uint(reader), + y=self.read_uint(reader) ) if message_id == 101: return IOSInputEvent( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - value=MessageCodec.read_string(reader), - value_masked=MessageCodec.read_boolean(reader), - label=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + value=self.read_string(reader), + value_masked=self.read_boolean(reader), + label=self.read_string(reader) ) if message_id == 102: return IOSPerformanceEvent( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - name=MessageCodec.read_string(reader), - value=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + name=self.read_string(reader), + value=self.read_uint(reader) ) if message_id == 103: return IOSLog( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - severity=MessageCodec.read_string(reader), - content=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + severity=self.read_string(reader), + content=self.read_string(reader) ) if message_id == 104: return IOSInternalError( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - content=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + content=self.read_string(reader) ) if message_id == 105: return IOSNetworkCall( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - type=MessageCodec.read_string(reader), - method=MessageCodec.read_string(reader), - url=MessageCodec.read_string(reader), - request=MessageCodec.read_string(reader), - response=MessageCodec.read_string(reader), - status=MessageCodec.read_uint(reader), - duration=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + type=self.read_string(reader), + method=self.read_string(reader), + url=self.read_string(reader), + request=self.read_string(reader), + response=self.read_string(reader), + status=self.read_uint(reader), + duration=self.read_uint(reader) ) if message_id == 106: return IOSSwipeEvent( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - label=MessageCodec.read_string(reader), - x=MessageCodec.read_uint(reader), - y=MessageCodec.read_uint(reader), - direction=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + label=self.read_string(reader), + x=self.read_uint(reader), + y=self.read_uint(reader), + direction=self.read_string(reader) ) if message_id == 107: return IOSBatchMeta( - timestamp=MessageCodec.read_uint(reader), - length=MessageCodec.read_uint(reader), - first_index=MessageCodec.read_uint(reader) + timestamp=self.read_uint(reader), + length=self.read_uint(reader), + first_index=self.read_uint(reader) ) if message_id == 110: return IOSPerformanceAggregated( - timestamp_start=MessageCodec.read_uint(reader), - timestamp_end=MessageCodec.read_uint(reader), - min_fps=MessageCodec.read_uint(reader), - avg_fps=MessageCodec.read_uint(reader), - max_fps=MessageCodec.read_uint(reader), - min_cpu=MessageCodec.read_uint(reader), - avg_cpu=MessageCodec.read_uint(reader), - max_cpu=MessageCodec.read_uint(reader), - min_memory=MessageCodec.read_uint(reader), - avg_memory=MessageCodec.read_uint(reader), - max_memory=MessageCodec.read_uint(reader), - min_battery=MessageCodec.read_uint(reader), - avg_battery=MessageCodec.read_uint(reader), - max_battery=MessageCodec.read_uint(reader) + timestamp_start=self.read_uint(reader), + timestamp_end=self.read_uint(reader), + min_fps=self.read_uint(reader), + avg_fps=self.read_uint(reader), + max_fps=self.read_uint(reader), + min_cpu=self.read_uint(reader), + avg_cpu=self.read_uint(reader), + max_cpu=self.read_uint(reader), + min_memory=self.read_uint(reader), + avg_memory=self.read_uint(reader), + max_memory=self.read_uint(reader), + min_battery=self.read_uint(reader), + avg_battery=self.read_uint(reader), + max_battery=self.read_uint(reader) ) if message_id == 111: return IOSIssueEvent( - timestamp=MessageCodec.read_uint(reader), - type=MessageCodec.read_string(reader), - context_string=MessageCodec.read_string(reader), - context=MessageCodec.read_string(reader), - payload=MessageCodec.read_string(reader) + timestamp=self.read_uint(reader), + type=self.read_string(reader), + context_string=self.read_string(reader), + context=self.read_string(reader), + payload=self.read_string(reader) ) diff --git a/ee/quickwit/msgcodec/codec.py b/ee/quickwit/msgcodec/codec.py index 7e1aabbf9..a51a44a68 100644 --- a/ee/quickwit/msgcodec/codec.py +++ b/ee/quickwit/msgcodec/codec.py @@ -646,7 +646,7 @@ class MessageCodec(Codec): ) if message_id == 93: - return IOSCustomEvent( + return IOSEvent( timestamp=self.read_uint(reader), length=self.read_uint(reader), name=self.read_string(reader), @@ -657,14 +657,14 @@ class MessageCodec(Codec): return IOSUserID( timestamp=self.read_uint(reader), length=self.read_uint(reader), - value=self.read_string(reader) + id=self.read_string(reader) ) if message_id == 95: return IOSUserAnonymousID( timestamp=self.read_uint(reader), length=self.read_uint(reader), - value=self.read_string(reader) + id=self.read_string(reader) ) if message_id == 96: diff --git a/ee/quickwit/msgcodec/messages.py b/ee/quickwit/msgcodec/messages.py index bc451b287..1463155bf 100644 --- a/ee/quickwit/msgcodec/messages.py +++ b/ee/quickwit/msgcodec/messages.py @@ -739,7 +739,7 @@ class IOSMetadata(Message): self.value = value -class IOSCustomEvent(Message): +class IOSEvent(Message): __id__ = 93 def __init__(self, timestamp, length, name: str, payload: str): @@ -752,19 +752,19 @@ class IOSCustomEvent(Message): class IOSUserID(Message): __id__ = 94 - def __init__(self, timestamp, length, value: str): + def __init__(self, timestamp, length, id: str): self.timestamp = timestamp self.length = length - self.value = value + self.id = id class IOSUserAnonymousID(Message): __id__ = 95 - def __init__(self, timestamp, length, value: str): + def __init__(self, timestamp, length, id: str): self.timestamp = timestamp self.length = length - self.value = value + self.id = id class IOSScreenChanges(Message): diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index 4f49c8e64..67dcba20c 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -113,7 +113,7 @@ function Integrations(props: Props) {
{filteredIntegrations.map((cat: any) => ( -
+
0 ? 'p-2' : '')}> {cat.integrations.map((integration: any) => ( + + + { + edit({ platform: value }); + }} + /> +
-
-
+
+
-
+
init(project)} />
diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index afe19a7ef..f8dda8b9d 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -79,7 +79,7 @@ export default class SiteDropdown extends React.PureComponent {
{sites.map((site) => (
  • this.switchSite(site.id)}> - + {site.host}
  • ))} diff --git a/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx b/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx index 38587d9b4..8f8cdc1ed 100644 --- a/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx +++ b/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx @@ -1,3 +1,4 @@ +import { Segmented } from 'antd'; import React from 'react'; import CircleNumber from '../CircleNumber'; import MetadataList from '../MetadataList/MetadataList'; @@ -10,6 +11,32 @@ import withPageTitle from 'App/components/hocs/withPageTitle'; interface Props extends WithOnboardingProps {} function IdentifyUsersTab(props: Props) { + const platforms = [ + { + label: ( +
    + Web +
    + ), + value: 'web', + } as const, + { + label: ( +
    + Mobile +
    + ), + value: 'mobile', + } as const, + ]; + const [platform, setPlatform] = React.useState(platforms[0]); + const { site } = props; + + React.useEffect(() => { + if (site.platform) + setPlatform(platforms.find(({ value }) => value === site.platform) || platforms[0]); + }, [site]); + return ( <>

    @@ -18,12 +45,25 @@ function IdentifyUsersTab(props: Props) {
    Identify Users

    - + +
    + Your platform + + setPlatform(platforms.find(({ value: v }) => v === value) || platforms[0]) + } + /> +
    @@ -34,12 +74,17 @@ function IdentifyUsersTab(props: Props) {
    -
    - - OpenReplay keeps the last communicated user ID. -
    - - + {platform.value === 'web' ? ( + + ) : ( + + )} + {platform.value === 'web' ? ( +
    + + OpenReplay keeps the last communicated user ID. +
    + ) : null}
    setMetadata method in your code to inject custom user data in the form of a key/value pair (string).
    - + {platform.value === 'web' ? ( + + ) : }
    @@ -109,4 +156,4 @@ function IdentifyUsersTab(props: Props) { ); } -export default withOnboarding(withPageTitle("Identify Users - OpenReplay")(IdentifyUsersTab)); +export default withOnboarding(withPageTitle('Identify Users - OpenReplay')(IdentifyUsersTab)); diff --git a/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx b/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx index e06ed4860..1b8277538 100644 --- a/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx +++ b/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx @@ -1,16 +1,27 @@ import React from 'react'; import OnboardingTabs from '../OnboardingTabs'; +import MobileOnboardingTabs from '../OnboardingTabs/OnboardingMobileTabs' import ProjectFormButton from '../ProjectFormButton'; -import { Button, Icon } from 'UI'; +import {Button, Icon } from 'UI'; import withOnboarding from '../withOnboarding'; import { WithOnboardingProps } from '../withOnboarding'; import { OB_TABS } from 'App/routes'; import withPageTitle from 'App/components/hocs/withPageTitle'; +import { Segmented } from "antd"; interface Props extends WithOnboardingProps {} function InstallOpenReplayTab(props: Props) { + const platforms = [ + { label:
    Web
    , value: 'web' } as const, + { label:
    Mobile
    , value: 'mobile' } as const, + ] + const [platform, setPlatform] = React.useState(platforms[0]); const { site } = props; + + React.useEffect(() => { + if (site.platform) setPlatform(platforms.find(({ value }) => value === site.platform) || platforms[0]) + }, [site]) return ( <>

    @@ -27,12 +38,18 @@ function InstallOpenReplayTab(props: Props) {

    +
    + Your platform + setPlatform(platforms.find(({ value: v }) => v === value) || platforms[0])} + /> +
    -
    - Setup OpenReplay through NPM package (recommended) or - script. -
    - + { + platform.value === 'web' ? : + }
    +
    + } + /> + + +
    + {/* @ts-ignore */} + +
    +
    +
    + + ); +} + +export default connect((state: any) => ({ + siteId: state.getIn(['site', 'siteId']), + integrations: state.getIn(['issues', 'list']), + modules: state.getIn(['user', 'account', 'modules']) || [], +}))(observer(SubHeader)); diff --git a/frontend/app/components/Session/Player/MobilePlayer/PerfWarnings.tsx b/frontend/app/components/Session/Player/MobilePlayer/PerfWarnings.tsx new file mode 100644 index 000000000..0fe38f492 --- /dev/null +++ b/frontend/app/components/Session/Player/MobilePlayer/PerfWarnings.tsx @@ -0,0 +1,110 @@ +import {IosPerformanceEvent} from "Player/web/messages"; +import React from 'react'; +import { MobilePlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; +import { Icon } from 'UI'; +import { mapIphoneModel } from 'Player/mobile/utils'; +import cn from 'classnames'; +import { connect } from 'react-redux'; +import { NONE } from 'Duck/components/player'; + +type warningsType = + | 'thermalState' + | 'memoryWarning' + | 'lowDiskSpace' + | 'isLowPowerModeEnabled' + | 'batteryLevel'; + +const elements = { + thermalState: { + title: 'Overheating', + icon: 'thermometer-sun', + }, + memoryWarning: { + title: 'High Memory Usage', + icon: 'memory-ios', + }, + lowDiskSpace: { + title: 'Low Disk Space', + icon: 'low-disc-space', + }, + isLowPowerModeEnabled: { + title: 'Low Power Mode', + icon: 'battery-charging', + }, + batteryLevel: { + title: 'Low Battery', + icon: 'battery', + }, +} as const; + +function PerfWarnings({ userDevice, bottomBlock }: { userDevice: string; bottomBlock: number }) { + const { store } = React.useContext(MobilePlayerContext); + const { scale, performanceListNow, performanceList } = store.get() + + const allElements = Object.keys(elements) as warningsType[]; + const list = React.useMemo(() => allElements + .filter(el => performanceList.findIndex((pw: IosPerformanceEvent & { techName: warningsType }) => pw.techName === el) !== -1) + , [performanceList.length]) + + const contStyles = { + left: '50%', + display: 'flex', + marginLeft: `${(mapIphoneModel(userDevice).styles.shell.width / 2 + 10) * scale}px`, + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'center', + gap: '6px', + position: 'absolute', + width: '200px', + top: '50%', + transform: 'translateY(-50%)', + zIndex: 0, + } as const; + + const activeWarnings = React.useMemo(() => { + const warnings: warningsType[] = [] + performanceListNow.forEach((warn: IosPerformanceEvent & { techName: warningsType }) => { + switch (warn.techName) { + case 'thermalState': + if (warn.value > 1) warnings.push(warn.techName) // 2 = serious 3 = overheating + break; + case 'memoryWarning': + warnings.push(warn.techName) + break; + case 'lowDiskSpace': + warnings.push(warn.techName) + break; + case 'isLowPowerModeEnabled': + if (warn.value === 1) warnings.push(warn.techName) + break; + case 'batteryLevel': + if (warn.value < 25) warnings.push(warn.techName) + break; + } + }) + + return warnings + }, [performanceListNow.length]); + if (bottomBlock !== NONE) return null; + + return ( +
    + {list.map((w) => ( +
    a === w) !== -1 ? 'opacity-100' : 'opacity-0' + )} + > + + {elements[w].title} +
    + ))} +
    + ); +} + +export default connect((state: any) => ({ + bottomBlock: state.getIn(['components', 'player', 'bottomBlock']), +}))(observer(PerfWarnings)); diff --git a/frontend/app/components/Session/Player/MobilePlayer/PlayerBlock.tsx b/frontend/app/components/Session/Player/MobilePlayer/PlayerBlock.tsx new file mode 100644 index 000000000..800fb4f97 --- /dev/null +++ b/frontend/app/components/Session/Player/MobilePlayer/PlayerBlock.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import cn from 'classnames'; +import { connect } from 'react-redux'; +import Player from './PlayerInst'; +import MobilePlayerSubheader from './MobilePlayerSubheader'; + +import styles from 'Components/Session_/playerBlock.module.css'; + +interface IProps { + fullscreen: boolean; + sessionId: string; + disabled: boolean; + activeTab: string; + jiraConfig: Record + fullView?: boolean +} + +function PlayerBlock(props: IProps) { + const { + fullscreen, + sessionId, + disabled, + activeTab, + jiraConfig, + fullView = false, + } = props; + + const shouldShowSubHeader = !fullscreen && !fullView + return ( +
    + {shouldShowSubHeader ? ( + + ) : null} + +
    + ); +} + +export default connect((state: any) => ({ + fullscreen: state.getIn(['components', 'player', 'fullscreen']), + sessionId: state.getIn(['sessions', 'current']).sessionId, + disabled: state.getIn(['components', 'targetDefiner', 'inspectorMode']), + jiraConfig: state.getIn(['issues', 'list'])[0], +}))(PlayerBlock) \ No newline at end of file diff --git a/frontend/app/components/Session/Player/MobilePlayer/PlayerContent.tsx b/frontend/app/components/Session/Player/MobilePlayer/PlayerContent.tsx new file mode 100644 index 000000000..4cdad1a0e --- /dev/null +++ b/frontend/app/components/Session/Player/MobilePlayer/PlayerContent.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import cn from 'classnames'; +import styles from 'Components/Session_/session.module.css'; +import { countDaysFrom } from 'App/date'; +import RightBlock from 'Components/Session/RightBlock'; +import { PlayerContext } from 'Components/Session/playerContext'; +import Session from 'Types/session' +import PlayerBlock from './PlayerBlock'; + +const TABS = { + EVENTS: 'User Events', + HEATMAPS: 'Click Map', +}; + +interface IProps { + fullscreen: boolean; + activeTab: string; + setActiveTab: (tab: string) => void; + session: Session +} + +function PlayerContent({ session, fullscreen, activeTab, setActiveTab }: IProps) { + const { store } = React.useContext(PlayerContext) + + const { + error, + } = store.get() + + const hasError = !!error + + const sessionDays = countDaysFrom(session.startedAt); + return ( +
    + {hasError ? ( +
    +
    +
    + {sessionDays > 2 ? 'Session not found.' : 'This session is still being processed.'} +
    +
    + {sessionDays > 2 + ? 'Please check your data retention policy.' + : 'Please check it again in a few minutes.'} +
    +
    +
    + ) : ( +
    +
    +
    + +
    +
    + {activeTab !== '' && ( + + )} +
    + )} +
    + ); +} + +function RightMenu({ tabs, activeTab, setActiveTab, fullscreen }: any) { + return ( + !fullscreen ? : null + ); +} + +export default observer(PlayerContent); diff --git a/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx new file mode 100644 index 000000000..52e3dd381 --- /dev/null +++ b/frontend/app/components/Session/Player/MobilePlayer/PlayerInst.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { findDOMNode } from 'react-dom'; +import cn from 'classnames'; +import { EscapeButton } from 'UI'; +import { + NONE, + CONSOLE, + NETWORK, + STACKEVENTS, + PERFORMANCE, + EXCEPTIONS, + OVERVIEW, + fullscreenOff, +} from 'Duck/components/player'; +import { MobileNetworkPanel } from 'Shared/DevTools/NetworkPanel'; +import { MobilePerformance } from 'Components/Session_/Performance'; +import { MobileExceptions } from 'Components/Session_/Exceptions/Exceptions'; +import MobileControls from './MobileControls'; +import Overlay from './MobileOverlay' +import stl from 'Components/Session_/Player/player.module.css'; +import { updateLastPlayedSession } from 'Duck/sessions'; +import { MobileOverviewPanel } from 'Components/Session_/OverviewPanel'; +import MobileConsolePanel from 'Shared/DevTools/ConsolePanel/MobileConsolePanel'; +import { MobilePlayerContext } from 'App/components/Session/playerContext'; +import { MobileStackEventPanel } from 'Shared/DevTools/StackEventPanel'; +import ReplayWindow from "Components/Session/Player/MobilePlayer/ReplayWindow"; +import PerfWarnings from "Components/Session/Player/MobilePlayer/PerfWarnings"; + +interface IProps { + fullView: boolean; + isMultiview?: boolean; + bottomBlock: number; + fullscreen: boolean; + fullscreenOff: () => any; + nextId: string; + sessionId: string; + activeTab: string; + updateLastPlayedSession: (id: string) => void + videoURL: string; + userDevice: string; +} + +function Player(props: IProps) { + const { + fullscreen, + fullscreenOff, + nextId, + bottomBlock, + activeTab, + fullView, + videoURL, + userDevice, + } = props; + const playerContext = React.useContext(MobilePlayerContext); + const isReady = playerContext.store.get().ready + const screenWrapper = React.useRef(null); + const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE; + const [isAttached, setAttached] = React.useState(false); + + React.useEffect(() => { + props.updateLastPlayedSession(props.sessionId); + const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture + if (parentElement && !isAttached) { + playerContext.player.attach(parentElement); + setAttached(true) + } + }, [isReady]); + + React.useEffect(() => { + playerContext.player.scale(); + }, [props.bottomBlock, props.fullscreen, playerContext.player, activeTab, fullView]); + + React.useEffect(() => { + playerContext.player.addFullscreenBoundary(props.fullscreen || fullView); + }, [props.fullscreen, fullView]) + if (!playerContext.player) return null; + + const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; + return ( +
    + {fullscreen && } +
    + + +
    + + +
    +
    + {!fullscreen && !!bottomBlock && ( +
    + {bottomBlock === OVERVIEW && } + {bottomBlock === CONSOLE && } + {bottomBlock === STACKEVENTS && } + {bottomBlock === NETWORK && } + {bottomBlock === PERFORMANCE && } + {bottomBlock === EXCEPTIONS && } +
    + )} + {!fullView ? ( + + ) : null} +
    + ); +} + +export default connect( + (state: any) => ({ + fullscreen: state.getIn(['components', 'player', 'fullscreen']), + nextId: state.getIn(['sessions', 'nextId']), + sessionId: state.getIn(['sessions', 'current']).sessionId, + userDevice: state.getIn(['sessions', 'current']).userDevice, + videoURL: state.getIn(['sessions', 'current']).videoURL, + bottomBlock: state.getIn(['components', 'player', 'bottomBlock']), + }), + { + fullscreenOff, + updateLastPlayedSession, + } +)(Player); diff --git a/frontend/app/components/Session/Player/MobilePlayer/ReplayWindow.tsx b/frontend/app/components/Session/Player/MobilePlayer/ReplayWindow.tsx new file mode 100644 index 000000000..7ef6cde97 --- /dev/null +++ b/frontend/app/components/Session/Player/MobilePlayer/ReplayWindow.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import { MobilePlayerContext, IOSPlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; +import { mapIphoneModel } from "Player/mobile/utils"; + +interface Props { + videoURL: string; + userDevice: string; +} + +const appleIcon = ` + + +` + +function ReplayWindow({ videoURL, userDevice }: Props) { + const playerContext = React.useContext(MobilePlayerContext); + const videoRef = React.useRef(); + + const time = playerContext.store.get().time + + React.useEffect(() => { + if (videoRef.current) { + const timeSecs = time / 1000 + if (videoRef.current.duration >= timeSecs) { + videoRef.current.currentTime = timeSecs + } + } + }, [time]) + + React.useEffect(() => { + if (playerContext.player.screen.document && videoURL) { + playerContext.player.pause() + const { svg, styles } = mapIphoneModel(userDevice) + + const host = document.createElement('div') + const videoEl = document.createElement('video') + const sourceEl = document.createElement('source') + const shell = document.createElement('div') + const icon = document.createElement('div') + const videoContainer = document.createElement('div') + + videoContainer.style.borderRadius = '10px' + videoContainer.style.overflow = 'hidden' + videoContainer.style.margin = styles.margin + videoContainer.style.display = 'none' + videoContainer.style.width = styles.screen.width + 'px' + videoContainer.style.height = styles.screen.height + 'px' + + videoContainer.appendChild(videoEl) + + shell.innerHTML = svg + + videoEl.width = styles.screen.width + videoEl.height = styles.screen.height + videoEl.style.backgroundColor = '#333' + + Object.assign(icon.style, { + backgroundColor: '#333', + borderRadius: '10px', + width: styles.screen.width + 'px', + height: styles.screen.height + 'px', + margin: styles.margin, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }) + const spacer = document.createElement('div') + spacer.style.width = '60px' + spacer.style.height = '60px' + + const loadingBar = document.createElement('div') + Object.assign(loadingBar.style, { + width: styles.screen.width/2 + 'px', + height: '6px', + borderRadius: '3px', + backgroundColor: 'white', + }) + icon.innerHTML = appleIcon + icon.appendChild(spacer) + icon.appendChild(loadingBar) + + shell.style.position = 'absolute' + shell.style.top = '0' + + sourceEl.setAttribute('src', videoURL) + sourceEl.setAttribute('type', 'video/mp4') + + host.appendChild(videoContainer) + host.appendChild(shell) + host.appendChild(icon) + videoEl.appendChild(sourceEl) + + videoEl.addEventListener("loadeddata", () => { + videoContainer.style.display = 'block' + icon.style.display = 'none' + host.removeChild(icon) + console.log('loaded') + playerContext.player.play() + }) + + videoRef.current = videoEl + playerContext.player.injectPlayer(host) + playerContext.player.customScale(styles.shell.width, styles.shell.height) + playerContext.player.updateDimensions({ + width: styles.screen.width, + height: styles.screen.height, + }) + playerContext.player.updateOverlayStyle({ + margin: styles.margin, + width: styles.screen.width + 'px', + height: styles.screen.height + 'px', + }) + } + }, [videoURL, playerContext.player.screen.document]) + return ( +
    + ) +} + +export default observer(ReplayWindow); \ No newline at end of file diff --git a/frontend/app/components/Session/Player/PlayerErrorBoundary.tsx b/frontend/app/components/Session/Player/PlayerErrorBoundary.tsx new file mode 100644 index 000000000..5170b1340 --- /dev/null +++ b/frontend/app/components/Session/Player/PlayerErrorBoundary.tsx @@ -0,0 +1,40 @@ +import React, { ErrorInfo } from 'react' +import { Button } from 'UI' + +class PlayerErrorBoundary extends React.Component { + state = { hasError: false, error: '' }; + constructor(props: any) { + super(props); + } + + componentDidCatch(error: Error, info: ErrorInfo) { + this.setState({ + hasError: true, + error: error + info.componentStack + }) + } + + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return ( +
    +

    Something went wrong during player rendering.

    +

    {this.state.error}

    + +
    + ); + } + + return this.props.children; + } +} + +export default PlayerErrorBoundary \ No newline at end of file diff --git a/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/UserCard/UserCard.js b/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/UserCard/UserCard.js index fdf0d5d9f..6619fb714 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/UserCard/UserCard.js +++ b/frontend/app/components/Session/Player/ReplayPlayer/EventsBlock/UserCard/UserCard.js @@ -12,6 +12,7 @@ import SessionInfoItem from 'Components/Session_/SessionInfoItem'; import { useModal } from 'App/components/Modal'; import UserSessionsModal from 'Shared/UserSessionsModal'; import { IFRAME } from 'App/constants/storageKeys'; +import { capitalize } from "App/utils"; function UserCard({ className, request, session, width, height, similarSessions, loading }) { const { settingsStore } = useStore(); @@ -48,70 +49,81 @@ function UserCard({ className, request, session, width, height, similarSessions, }; const avatarbgSize = '38px'; + + const safeOs = userOs === 'IOS' ? 'iOS' : userOs; return ( -
    -
    - -
    - - - +
    +
    + +
    + + + -
    +
    + + + {formatTimeOrDate(startedAt, timezone)} + + + · + {userCity && {userCity},} + {countries[userCountry]} + · + + {userBrowser ? `${capitalize(userBrowser)}, ` : ''} + {`${/ios/i.test(userOs) ? 'iOS ' : capitalize(userOs) + ','} `} + {capitalize(userDevice)} + + · + ( +
    + } + label={countries[userCountry]} + value={ - - {formatTimeOrDate(startedAt, timezone)} - - + { + <> + {userCity && {userCity},} + {userState && {userState}} + + } - · - {userCity && ( - {userCity}, - )} - {countries[userCountry]} - · - - {userBrowser}, {userOs}, {userDevice} - - · - ( -
    - } - label={countries[userCountry]} - value={{ - <> - {userCity && {userCity},} - {userState && {userState}} - - }} - /> - - - - {revId && } -
    - )} - > - More -
    -
    -
    + } + /> + {userBrowser && + + } + + + {revId && } +
    + )} + > + More +
    +
    +
    ); } diff --git a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx index 097a57694..70782b394 100644 --- a/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx +++ b/frontend/app/components/Session/Player/ReplayPlayer/PlayerInst.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { findDOMNode } from 'react-dom'; import cn from 'classnames'; +import { WebStackEventPanel } from 'Shared/DevTools/StackEventPanel/StackEventPanel'; import { EscapeButton } from 'UI'; import { NONE, @@ -17,22 +18,20 @@ import { OVERVIEW, fullscreenOff, } from 'Duck/components/player'; -import NetworkPanel from 'Shared/DevTools/NetworkPanel'; +import { WebNetworkPanel } from 'Shared/DevTools/NetworkPanel'; import Storage from 'Components/Session_/Storage'; import { ConnectedPerformance } from 'Components/Session_/Performance'; import GraphQL from 'Components/Session_/GraphQL'; -import Exceptions from 'Components/Session_/Exceptions/Exceptions'; +import { Exceptions } from 'Components/Session_/Exceptions/Exceptions'; import Inspector from 'Components/Session_/Inspector'; import Controls from 'Components/Session_/Player/Controls'; import Overlay from 'Components/Session_/Player/Overlay'; import stl from 'Components/Session_/Player/player.module.css'; import { updateLastPlayedSession } from 'Duck/sessions'; -import OverviewPanel from 'Components/Session_/OverviewPanel'; +import { OverviewPanel } from 'Components/Session_/OverviewPanel'; import ConsolePanel from 'Shared/DevTools/ConsolePanel'; import ProfilerPanel from 'Shared/DevTools/ProfilerPanel'; import { PlayerContext } from 'App/components/Session/playerContext'; -import StackEventPanel from 'Shared/DevTools/StackEventPanel'; - interface IProps { fullView: boolean; @@ -43,20 +42,13 @@ interface IProps { nextId: string; sessionId: string; activeTab: string; - updateLastPlayedSession: (id: string) => void + updateLastPlayedSession: (id: string) => void; } function Player(props: IProps) { - const { - fullscreen, - fullscreenOff, - nextId, - bottomBlock, - activeTab, - fullView, - } = props; + const { fullscreen, fullscreenOff, nextId, bottomBlock, activeTab, fullView } = props; const playerContext = React.useContext(PlayerContext); - const isReady = playerContext.store.get().ready + const isReady = playerContext.store.get().ready; const screenWrapper = React.useRef(null); const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE; const [isAttached, setAttached] = React.useState(false); @@ -66,7 +58,7 @@ function Player(props: IProps) { const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture if (parentElement && !isAttached) { playerContext.player.attach(parentElement); - setAttached(true) + setAttached(true); } }, [isReady]); @@ -83,7 +75,7 @@ function Player(props: IProps) { data-bottom-block={bottomBlockIsActive} > {fullscreen && } -
    +
    @@ -91,8 +83,8 @@ function Player(props: IProps) {
    {bottomBlock === OVERVIEW && } {bottomBlock === CONSOLE && } - {bottomBlock === NETWORK && } - {bottomBlock === STACKEVENTS && } + {bottomBlock === NETWORK && } + {bottomBlock === STACKEVENTS && } {bottomBlock === STORAGE && } {bottomBlock === PROFILER && } {bottomBlock === PERFORMANCE && } diff --git a/frontend/app/components/Session/Session.js b/frontend/app/components/Session/Session.tsx similarity index 82% rename from frontend/app/components/Session/Session.js rename to frontend/app/components/Session/Session.tsx index 75a96433b..ad152ad5e 100644 --- a/frontend/app/components/Session/Session.js +++ b/frontend/app/components/Session/Session.tsx @@ -11,15 +11,27 @@ import WebPlayer from './WebPlayer'; import { useStore } from 'App/mstore'; import { clearLogs } from 'App/dev/console'; +import MobilePlayer from "Components/Session/MobilePlayer"; + const SESSIONS_ROUTE = sessionsRoute(); +interface Props { + sessionId: string; + loading: boolean; + hasErrors: boolean; + fetchV2: (sessionId: string) => void; + clearCurrentSession: () => void; + session: Record; +} + function Session({ sessionId, loading, hasErrors, fetchV2, clearCurrentSession, - }) { + session, + }: Props) { usePageTitle("OpenReplay Session Player"); const [ initializing, setInitializing ] = useState(true) const { sessionStore } = useStore(); @@ -40,6 +52,7 @@ function Session({ sessionStore.resetUserFilter(); } ,[]) + const player = session.platform === 'ios' ? : return ( - + {player} ); } -export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state, props) => { +export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state: any, props: any) => { const { match: { params: { sessionId } } } = props; return { sessionId, diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 907a4f1bc..309072267 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -12,9 +12,9 @@ import ReadNote from '../Session_/Player/Controls/components/ReadNote'; import PlayerContent from './Player/ReplayPlayer/PlayerContent'; import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext'; import { observer } from 'mobx-react-lite'; -import { Note } from "App/services/NotesService"; -import { useParams } from 'react-router-dom' -import { toast } from 'react-toastify' +import { Note } from 'App/services/NotesService'; +import { useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; const TABS = { EVENTS: 'User Events', @@ -24,30 +24,24 @@ const TABS = { let playerInst: IPlayerContext['player'] | undefined; function WebPlayer(props: any) { - const { - session, - toggleFullscreen, - closeBottomBlock, - fullscreen, - fetchList, - } = props; + const { session, toggleFullscreen, closeBottomBlock, fullscreen, fetchList } = props; const { notesStore, sessionStore } = useStore(); const [activeTab, setActiveTab] = useState(''); const [noteItem, setNoteItem] = useState(undefined); const [visuallyAdjusted, setAdjusted] = useState(false); // @ts-ignore const [contextValue, setContextValue] = useState(defaultContextValue); - const params: { sessionId: string } = useParams() + const params: { sessionId: string } = useParams(); useEffect(() => { - playerInst = undefined + playerInst = undefined; if (!session.sessionId || contextValue.player !== undefined) return; fetchList('issues'); - sessionStore.setUserTimezone(session.timezone) + sessionStore.setUserTimezone(session.timezone); const [WebPlayerInst, PlayerStore] = createWebPlayer( session, (state) => makeAutoObservable(state), - toast, + toast ); setContextValue({ player: WebPlayerInst, store: PlayerStore }); playerInst = WebPlayerInst; @@ -58,55 +52,54 @@ function WebPlayer(props: any) { setNoteItem(notesStore.getNoteById(parseInt(note, 10), r)); WebPlayerInst.pause(); } - }) + }); - const freeze = props.query.get('freeze') + const freeze = props.query.get('freeze'); if (freeze) { - void WebPlayerInst.freeze() + void WebPlayerInst.freeze(); } }, [session.sessionId]); const { firstVisualEvent: visualOffset, messagesProcessed } = contextValue.store?.get() || {}; React.useEffect(() => { - if (messagesProcessed && session.events.length > 0 || session.errors.length > 0) { - contextValue.player?.updateLists?.(session) + if ((messagesProcessed && session.events.length > 0) || session.errors.length > 0) { + contextValue.player?.updateLists?.(session); } - }, [session.events, session.errors, contextValue.player, messagesProcessed]) - + }, [session.events, session.errors, contextValue.player, messagesProcessed]); React.useEffect(() => { if (noteItem !== undefined) { - contextValue.player.pause() + contextValue.player.pause(); } if (activeTab === '' && !noteItem !== undefined && messagesProcessed && contextValue.player) { const jumpToTime = props.query.get('jumpto'); - const shouldAdjustOffset = visualOffset !== 0 && !visuallyAdjusted + const shouldAdjustOffset = visualOffset !== 0 && !visuallyAdjusted; if (jumpToTime || shouldAdjustOffset) { if (jumpToTime > visualOffset) { contextValue.player.jump(parseInt(jumpToTime)); } else { - contextValue.player.jump(visualOffset) - setAdjusted(true) + contextValue.player.jump(visualOffset); + setAdjusted(true); } } - contextValue.player.play() + contextValue.player.play(); } - }, [activeTab, noteItem, visualOffset, messagesProcessed]) + }, [activeTab, noteItem, visualOffset, messagesProcessed]); React.useEffect(() => { if (activeTab === 'Click Map') { - contextValue.player?.pause() + contextValue.player?.pause(); } - }, [activeTab]) + }, [activeTab]); // LAYOUT (TODO: local layout state - useContext or something..) useEffect( () => () => { - console.debug('cleaning up player after', params.sessionId) + console.debug('cleaning up player after', params.sessionId); toggleFullscreen(false); closeBottomBlock(); playerInst?.clean(); @@ -117,11 +110,23 @@ function WebPlayer(props: any) { ); const onNoteClose = () => { - setNoteItem(undefined) + setNoteItem(undefined); contextValue.player.play(); }; - if (!session.sessionId) return ; + if (!session.sessionId) + return ( + + ); return ( @@ -133,19 +138,21 @@ function WebPlayer(props: any) { fullscreen={fullscreen} /> {/* @ts-ignore */} - {contextValue.player ? : } + {contextValue.player ? ( + + ) : ( + + )} {noteItem !== undefined ? ( - + ) : null} diff --git a/frontend/app/components/Session/playerContext.ts b/frontend/app/components/Session/playerContext.ts index 2f6c7c540..5a822e13d 100644 --- a/frontend/app/components/Session/playerContext.ts +++ b/frontend/app/components/Session/playerContext.ts @@ -1,11 +1,18 @@ -import { createContext } from 'react'; +import { createContext, Context } from 'react'; import { IWebPlayer, + IIosPlayer, + IIOSPlayerStore, IWebPlayerStore, IWebLivePlayer, IWebLivePlayerStore, } from 'Player' +export interface IOSPlayerContext { + player: IIosPlayer + store: IIOSPlayerStore +} + export interface IPlayerContext { player: IWebPlayer store: IWebPlayerStore, @@ -16,9 +23,15 @@ export interface ILivePlayerContext { store: IWebLivePlayerStore } -type ContextType = +type WebContextType = | IPlayerContext | ILivePlayerContext -export const defaultContextValue = { player: undefined, store: undefined} -// @ts-ignore -export const PlayerContext = createContext(defaultContextValue); + +type MobileContextType = IOSPlayerContext + +export const defaultContextValue = { player: undefined, store: undefined } + +const ContextProvider = createContext>(defaultContextValue); + +export const PlayerContext = ContextProvider as Context +export const MobilePlayerContext = ContextProvider as Context \ No newline at end of file diff --git a/frontend/app/components/Session_/EventsBlock/Event.tsx b/frontend/app/components/Session_/EventsBlock/Event.tsx index cb218441c..8b2f8b746 100644 --- a/frontend/app/components/Session_/EventsBlock/Event.tsx +++ b/frontend/app/components/Session_/EventsBlock/Event.tsx @@ -23,7 +23,7 @@ type Props = { }; const isFrustrationEvent = (evt: any): boolean => { - if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE) { + if (evt.type === 'mouse_thrashing' || evt.type === TYPES.CLICKRAGE || evt.type === TYPES.TAPRAGE) { return true; } if (evt.type === TYPES.CLICK || evt.type === TYPES.INPUT) { @@ -73,12 +73,22 @@ const Event: React.FC = ({ case TYPES.LOCATION: title = 'Visited'; body = event.url; - icon = 'location'; + icon = 'event/location'; + break; + case TYPES.SWIPE: + title = 'Swipe'; + body = event.direction; + icon = `chevron-${event.direction}` + break; + case TYPES.TOUCH: + title = 'Tapped'; + body = event.label; + icon = 'event/click'; break; case TYPES.CLICK: title = 'Clicked'; body = event.label; - icon = isFrustration ? 'click_hesitation' : 'click'; + icon = isFrustration ? 'event/click_hesitation' : 'event/click'; isFrustration ? Object.assign(tooltip, { disabled: false, @@ -89,7 +99,7 @@ const Event: React.FC = ({ case TYPES.INPUT: title = 'Input'; body = event.value; - icon = isFrustration ? 'input_hesitation' : 'input'; + icon = isFrustration ? 'event/input_hesitation' : 'event/input'; isFrustration ? Object.assign(tooltip, { disabled: false, @@ -98,18 +108,19 @@ const Event: React.FC = ({ : null; break; case TYPES.CLICKRAGE: - title = `${event.count} Clicks`; + case TYPES.TAPRAGE: + title = event.count ? `${event.count} Clicks` : 'Click Rage'; body = event.label; - icon = 'clickrage'; + icon = 'event/clickrage'; break; case TYPES.IOS_VIEW: title = 'View'; body = event.name; - icon = 'ios_view'; + icon = 'event/ios_view'; break; case 'mouse_thrashing': title = 'Mouse Thrashing'; - icon = 'mouse_thrashing'; + icon = 'event/mouse_thrashing'; break; } @@ -123,7 +134,7 @@ const Event: React.FC = ({ >
    - {event.type && } + {event.type && }
    @@ -160,6 +171,7 @@ const Event: React.FC = ({ const isFrustration = isFrustrationEvent(event); + const mobileTypes = [TYPES.TOUCH, TYPES.SWIPE, TYPES.TAPRAGE] return (
    = ({ [cls.selected]: selected, [cls.showSelection]: showSelection, [cls.red]: isRed, - [cls.clickType]: event.type === TYPES.CLICK, + [cls.clickType]: event.type === TYPES.CLICK || event.type === TYPES.SWIPE, [cls.inputType]: event.type === TYPES.INPUT, [cls.frustration]: isFrustration, [cls.highlight]: presentInSearch, [cls.lastInGroup]: whiteBg, - ['pl-4 pr-6 ml-4 py-2 border-l']: event.type !== TYPES.LOCATION + ['pl-4 pr-6 ml-4 py-2 border-l']: event.type !== TYPES.LOCATION, + ['border-0 border-l-0 ml-0']: mobileTypes.includes(event.type), })} onClick={onClick} onContextMenu={onContextMenu} diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx index c43f6c971..0b7ff0d59 100644 --- a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx +++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx @@ -38,7 +38,7 @@ function EventsBlock(props: IProps) { const { store, player } = React.useContext(PlayerContext); - const { playing, tabStates, tabChangeEvents } = store.get(); + const { playing, tabStates, tabChangeEvents = [] } = store.get(); const { filteredEvents, @@ -53,23 +53,30 @@ function EventsBlock(props: IProps) { const filteredLength = filteredEvents?.length || 0; const notesWithEvtsLength = notesWithEvents?.length || 0; const notesLength = notes.length; - const eventListNow = Object.values(tabStates)[0]?.eventListNow || []; + const eventListNow: any[] = [] + if (tabStates !== undefined) { + eventListNow.concat(Object.values(tabStates)[0]?.eventListNow || []) + } else { + eventListNow.concat(store.get().eventListNow) + } const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0; const usedEvents = React.useMemo(() => { - tabChangeEvents.forEach(ev => { - const urlsList = tabStates[ev.tabId].urlsList; - let found = false; - let i = urlsList.length - 1; - while (!found && i >= 0) { - const item = urlsList[i] - if (item.url && item.time <= ev.time) { - found = true; - ev.activeUrl = item.url.replace(/.*\/\/[^\/]*/, ''); + if (tabStates !== undefined) { + tabChangeEvents.forEach(ev => { + const urlsList = tabStates[ev.tabId].urlsList; + let found = false; + let i = urlsList.length - 1; + while (!found && i >= 0) { + const item = urlsList[i] + if (item.url && item.time <= ev.time) { + found = true; + ev.activeUrl = item.url.replace(/.*\/\/[^\/]*/, ''); + } + i--; } - i--; - } - }) + }) + } const eventsWithMobxNotes = [...notesWithEvents, ...notes].sort(sortEvents); return mergeEventLists(filteredLength > 0 ? filteredEvents : eventsWithMobxNotes, tabChangeEvents); }, [filteredLength, notesWithEvtsLength, notesLength]) @@ -133,7 +140,7 @@ function EventsBlock(props: IProps) { const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION; const event = usedEvents[index]; const isNote = 'noteId' in event; - const isTabChange = event.type === 'TABCHANGE'; + const isTabChange = 'type' in event && event.type === 'TABCHANGE'; const isCurrent = index === currentTimeEventIndex; return ( diff --git a/frontend/app/components/Session_/Exceptions/Exceptions.tsx b/frontend/app/components/Session_/Exceptions/Exceptions.tsx index 969a02271..0f97d921e 100644 --- a/frontend/app/components/Session_/Exceptions/Exceptions.tsx +++ b/frontend/app/components/Session_/Exceptions/Exceptions.tsx @@ -14,7 +14,7 @@ import { import { error as errorRoute } from 'App/routes'; import Autoscroll from '../Autoscroll'; import BottomBlock from '../BottomBlock'; -import { PlayerContext } from 'App/components/Session/playerContext'; +import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; interface IProps { @@ -23,7 +23,52 @@ interface IProps { errorStack: Record; } -function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) { +function MobileExceptionsCont() { + const { player, store } = React.useContext(MobilePlayerContext); + const { exceptionsList: exceptions = [] } = store.get(); + const [filter, setFilter] = React.useState(''); + + const onFilterChange = ({ target: { value } }: any) => setFilter(value); + + const filterRE = getRE(filter, 'i'); + const filtered = exceptions.filter((e: any) => filterRE.test(e.name) || filterRE.test(e.message)); + + return ( + <> + + +
    + Exceptions +
    + +
    + +
    +
    + + + + {filtered.map((e: any, index) => ( + + player.jump(e.time)} error={e} /> + + ))} + + + +
    + + ); +} + +function ExceptionsCont({ errorStack, sourcemapUploaded, loading }: IProps) { const { player, store } = React.useContext(PlayerContext); const { tabStates, currentTab } = store.get(); const { logListNow: logs = [], exceptionsList: exceptions = [] } = tabStates[currentTab] @@ -119,8 +164,10 @@ function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) { ); } -export default connect((state: any) => ({ +export const Exceptions = connect((state: any) => ({ errorStack: state.getIn(['sessions', 'errorStack']), sourcemapUploaded: state.getIn(['sessions', 'sourcemapUploaded']), loading: state.getIn(['sessions', 'fetchErrorStackList', 'loading']), -}))(observer(Exceptions)); +}))(observer(ExceptionsCont)); + +export const MobileExceptions = observer(MobileExceptionsCont) \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx index 53b44ea58..3500a10c0 100644 --- a/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx +++ b/frontend/app/components/Session_/OverviewPanel/OverviewPanel.tsx @@ -11,9 +11,72 @@ import cn from 'classnames'; import OverviewPanelContainer from './components/OverviewPanelContainer'; import { NoContent, Icon } from 'UI'; import { observer } from 'mobx-react-lite'; -import { PlayerContext } from 'App/components/Session/playerContext'; +import {MobilePlayerContext, PlayerContext} from 'App/components/Session/playerContext'; -function OverviewPanel({ issuesList }: { issuesList: Record[] }) { +function MobileOverviewPanelCont({ issuesList }: { issuesList: Record[] }) { + const { store, player } = React.useContext(MobilePlayerContext) + const [dataLoaded, setDataLoaded] = React.useState(false); + const [selectedFeatures, setSelectedFeatures] = React.useState([ + 'PERFORMANCE', + 'FRUSTRATIONS', + 'ERRORS', + 'NETWORK', + ]); + + const { + endTime, + eventList: eventsList, + frustrationsList, + exceptionsList, + fetchList, + performanceChartData, + performanceList, + } = store.get() + + const fetchPresented = fetchList.length > 0; + + const resources = { + NETWORK: fetchList.filter((r: any) => r.status >= 400 || r.isRed || r.isYellow), + ERRORS: exceptionsList, + EVENTS: eventsList, + PERFORMANCE: performanceChartData, + FRUSTRATIONS: frustrationsList, + }; + + useEffect(() => { + if (dataLoaded) { + return; + } + + if ( + exceptionsList.length > 0 || + eventsList.length > 0 || + issuesList.length > 0 || + performanceChartData.length > 0 || + frustrationsList.length > 0 + ) { + setDataLoaded(true); + } + }, [issuesList, exceptionsList, eventsList, performanceChartData, frustrationsList]); + + React.useEffect(() => { + player.scale() + }, [selectedFeatures]) + + return ( + + ) +} + +function WebOverviewPanelCont({ issuesList }: { issuesList: Record[] }) { const { store } = React.useContext(PlayerContext); const [dataLoaded, setDataLoaded] = React.useState(false); const [selectedFeatures, setSelectedFeatures] = React.useState([ @@ -28,7 +91,6 @@ function OverviewPanel({ issuesList }: { issuesList: Record[] }) { currentTab, tabStates, } = store.get(); - const states = Object.values(tabStates) const stackEventList = tabStates[currentTab]?.stackList || [] const eventsList = tabStates[currentTab]?.eventList || [] @@ -73,69 +135,101 @@ function OverviewPanel({ issuesList }: { issuesList: Record[] }) { } }, [resourceList, issuesList, exceptionsList, eventsList, stackEventList, performanceChartData, currentTab]); - return ( - - - - X-RAY -
    - -
    -
    - - - -
    - - - Select a debug option to visualize on timeline. -
    - } - > - - {selectedFeatures.map((feature: any, index: number) => ( -
    - ( - - )} - endTime={endTime} - message={HELP_MESSAGE[feature]} - /> -
    - ))} - -
    - - - - - ); + return } -export default connect( +function PanelComponent({ selectedFeatures, endTime, resources, fetchPresented, setSelectedFeatures, isMobile, performanceList }: any) { + return ( + + + + X-RAY +
    + +
    +
    + + + +
    + + + Select a debug option to visualize on timeline. +
    + } + > + + {selectedFeatures.map((feature: any, index: number) => ( +
    + ( + + )} + endTime={endTime} + message={HELP_MESSAGE[feature]} + /> + {isMobile && feature === 'PERFORMANCE' ? ( +
    + ( +
    + +
    + )} + endTime={endTime} + /> +
    + ) : null} +
    + ))} + +
    + + + + + ) +} + +export const OverviewPanel = connect( (state: any) => ({ issuesList: state.getIn(['sessions', 'current']).issues, }), { toggleBottomBlock, } -)(observer(OverviewPanel)); +)(observer(WebOverviewPanelCont)); + +export const MobileOverviewPanel = connect( + (state: any) => ({ + issuesList: state.getIn(['sessions', 'current']).issues, + }), + { + toggleBottomBlock, + } +)(observer(MobileOverviewPanelCont)); \ No newline at end of file diff --git a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx index 3a841d97c..a70c31e82 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/FeatureSelection/FeatureSelection.tsx @@ -8,9 +8,9 @@ const FRUSTRATIONS = 'FRUSTRATIONS'; const PERFORMANCE = 'PERFORMANCE'; export const HELP_MESSAGE: any = { - NETWORK: 'Network requests made in this session', + NETWORK: 'Network requests with issues in this session', EVENTS: 'Visualizes the events that takes place in the DOM', - ERRORS: 'Visualizes native JS errors like Type, URI, Syntax etc.', + ERRORS: 'Visualizes native errors like Type, URI, Syntax etc.', PERFORMANCE: 'Summary of this session’s memory, and CPU consumption on the timeline', FRUSTRATIONS: 'Indicates user frustrations in the session', }; diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index 73dffcfce..17be0b3ee 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -76,8 +76,9 @@ const TimelinePointer = React.memo((props: Props) => { const elData = { name: '', icon: ''} if (item.type === TYPES.CLICK) Object.assign(elData, { name: `User hesitated to click for ${Math.round(item.hesitation/1000)}s`, icon: 'click-hesitation' }) if (item.type === TYPES.INPUT) Object.assign(elData, { name: `User hesitated to enter a value for ${Math.round(item.hesitation/1000)}s`, icon: 'input-hesitation' }) - if (item.type === TYPES.CLICKRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' }) + if (item.type === TYPES.CLICKRAGE || item.type === TYPES.TAPRAGE) Object.assign(elData, { name: 'Click Rage', icon: 'click-rage' }) if (item.type === issueTypes.MOUSE_THRASHING) Object.assign(elData, { name: 'Mouse Thrashing', icon: 'cursor-trash' }) + if (item.type === 'ios_perf_event') Object.assign(elData, { name: item.name, icon: item.icon }) return ( { - {'Stack Event'} + {item.name || 'Stack Event'}
    } > diff --git a/frontend/app/components/Session_/OverviewPanel/index.ts b/frontend/app/components/Session_/OverviewPanel/index.ts index 328795cd7..e66a274d3 100644 --- a/frontend/app/components/Session_/OverviewPanel/index.ts +++ b/frontend/app/components/Session_/OverviewPanel/index.ts @@ -1 +1 @@ -export { default } from './OverviewPanel'; \ No newline at end of file +export { OverviewPanel, MobileOverviewPanel } from './OverviewPanel'; \ No newline at end of file diff --git a/frontend/app/components/Session_/Performance/Performance.tsx b/frontend/app/components/Session_/Performance/Performance.tsx index 9d81be5e9..33cbda4f9 100644 --- a/frontend/app/components/Session_/Performance/Performance.tsx +++ b/frontend/app/components/Session_/Performance/Performance.tsx @@ -1,6 +1,8 @@ +import {Timed} from "Player"; +import {PerformanceChartPoint} from "Player/mobile/managers/IOSPerformanceTrackManager"; import React from 'react'; import { connect } from 'react-redux'; -import { PlayerContext } from 'App/components/Session/playerContext'; +import {MobilePlayerContext, PlayerContext} from 'App/components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { AreaChart, @@ -96,6 +98,27 @@ const CPUTooltip = ({ active, payload }) => { ); }; +const MobileCpuTooltip = ({ active, payload }) => { + if (!payload) return null; + if (!active || payload.length < 1) { + return null; + } + if (payload[0].value === null) { + return ( +
    + {'App is in the background.'} +
    + ); + } + return ( +
    + {`${CPU}: `} + {payload[0].value} + {'%'} +
    + ); +} + const HeapTooltip = ({ active, payload }) => { if (!payload) return null; if (!active || payload.length < 2) return null; @@ -113,6 +136,19 @@ const HeapTooltip = ({ active, payload }) => { ); }; +const MobileMemoryTooltip = ({ active, payload }) => { + if (!payload) return null; + if (!active || payload.length < 1 || payload[1].value === null) return null; + return ( +
    +

    + Used Memory: + {formatBytes(payload[1].value)} +

    +
    + ); +} + const NodesCountTooltip = ({ active, payload }) => { if (!payload) return null; if (!active || !payload || payload.length === 0) return null; @@ -178,6 +214,202 @@ function addFpsMetadata(data) { }); } +function generateMobileChart(data: PerformanceChartPoint[], biggestMemSpike: number) { + return data.map(p => ({ + ...p, + isBackground: p.isBackground ? 50 : 0, + isMemBackground: p.isBackground ? biggestMemSpike : 0 + })) +} + +export const MobilePerformance = connect((state: any) => ({ + userDeviceMemorySize: state.getIn(['sessions', 'current']).userDeviceMemorySize || 0, +}))(observer(({ userDeviceMemorySize }: { userDeviceMemorySize: number }) => { + const { player, store } = React.useContext(MobilePlayerContext); + const [_timeTicks, setTicks] = React.useState([]) + const [_data, setData] = React.useState([]) + + const { + performanceChartTime = 0, + performanceChartData = [], + } = store.get(); + + + React.useEffect(() => { + // setTicks(generateTicks(performanceChartData)); + setTicks(performanceChartData.map(p => p.time)); + const biggestMemSpike = performanceChartData.reduce((acc, p) => { + if (p.memory && p.memory > acc) return p.memory; + return acc; + }, 0); + setData(generateMobileChart(performanceChartData, biggestMemSpike)); + }, []) + + + const onDotClick = ({ index: pointer }: { index: number }) => { + const point = _data[pointer]; + if (!!point) { + player.jump(point.time); + } + }; + + const onChartClick = (e: any) => { + if (e === null) return; + const { activeTooltipIndex } = e; + const point = _data[activeTooltipIndex]; + if (!!point) { + player.jump(point.time); + } + }; + + const availableCount = 2 + const height = `${100 / availableCount}%`; + + return ( + + +
    +
    Performance
    + + + +
    +
    + + + + + + + + {/* */} + ''} + domain={[0, 'dataMax']} + ticks={_timeTicks} + > + + + + + + + + + + + + + + ''} // tick={false} + _timeTicks to cartesian array + domain={[0, 'dataMax']} + ticks={_timeTicks} + > + + max * 1.2]} + /> + {/**/} + + + + + + + +
    + ); +})); + + function Performance({ userDeviceHeapSize, }: { @@ -226,6 +458,7 @@ function Performance({ const availableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0); const height = availableCount === 0 ? '0' : `${100 / availableCount}%`; + console.log(_data) return ( diff --git a/frontend/app/components/Session_/Player/Controls/EventsList.tsx b/frontend/app/components/Session_/Player/Controls/EventsList.tsx index 7b4b0fc24..860b6d0c5 100644 --- a/frontend/app/components/Session_/Player/Controls/EventsList.tsx +++ b/frontend/app/components/Session_/Player/Controls/EventsList.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; import stl from './timeline.module.css'; -import { PlayerContext } from 'Components/Session/playerContext'; +import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerContext'; import { observer } from 'mobx-react-lite'; import { getTimelinePosition } from './getTimelinePosition' @@ -25,4 +25,24 @@ function EventsList({ scale }: { scale: number }) { ); } -export default observer(EventsList); +function MobileEventsList({ scale }: { scale: number }) { + const { store } = useContext(MobilePlayerContext); + const { eventList } = store.get(); + const events = eventList.filter(e => e.type !== 'SWIPE') + + return ( + <> + {events.map((e) => ( +
    + ))} + + ); +} + +export const WebEventsList = observer(EventsList); +export const MobEventsList = observer(MobileEventsList); \ No newline at end of file diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx index ba3e2c558..41374daca 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx +++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx @@ -12,7 +12,7 @@ import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import { DateTime, Duration } from 'luxon'; import Issue from "Types/session/issue"; -import EventsList from './EventsList'; +import { WebEventsList, MobEventsList } from './EventsList'; import NotesList from './NotesList'; import SkipIntervalsList from './SkipIntervalsList' import TimelineTracker from "Components/Session_/Player/Controls/TimelineTracker"; @@ -23,6 +23,7 @@ interface IProps { startedAt: number tooltipVisible: boolean timezone?: string + isMobile?: boolean } function Timeline(props: IProps) { @@ -125,52 +126,51 @@ function Timeline(props: IProps) { return (
    +
    -
    - - - + + + - -
    - {devtoolsLoading || domLoading || !ready ?
    : null} -
    - - - - - - {/* TODO: refactor and make any sense out of this */} - - {/* {issues.map((i: Issue) => (*/} - {/* */} - {/*))}*/} +
    + {devtoolsLoading || domLoading || !ready ?
    : null}
    + + {props.isMobile ? : } + + + + {/* TODO: refactor and make any sense out of this */} + + {/* {issues.map((i: Issue) => (*/} + {/* */} + {/*))}*/}
    - ) +
    + ); } export default connect( diff --git a/frontend/app/components/Session_/Player/player.module.css b/frontend/app/components/Session_/Player/player.module.css index 0ee3ccd57..8251eaee0 100644 --- a/frontend/app/components/Session_/Player/player.module.css +++ b/frontend/app/components/Session_/Player/player.module.css @@ -19,6 +19,13 @@ 50% / 10px 10px; } +.mobileScreenWrapper { + width: 100%; + position: relative; + height: 100%; + background: #F6F6F6; +} + .disconnected { font-size: 40px; font-weight: 200; diff --git a/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx b/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx index a71eb7c65..8a352b014 100644 --- a/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx +++ b/frontend/app/components/Session_/SessionInfoItem/SessionInfoItem.tsx @@ -12,13 +12,17 @@ interface Props { export default function SessionInfoItem(props: Props) { const { label, icon, value, comp, isLast = false } = props return ( -
    -
    - { icon && } - { comp && comp } -
    -
    {label}
    -
    {value}
    +
    +
    + {icon && } + {comp && comp}
    - ) +
    + {label} +
    +
    + {value} +
    +
    + ); } diff --git a/frontend/app/components/Session_/Subheader.js b/frontend/app/components/Session_/Subheader.js index 901c7d67d..f0fb576f1 100644 --- a/frontend/app/components/Session_/Subheader.js +++ b/frontend/app/components/Session_/Subheader.js @@ -31,7 +31,7 @@ function SubHeader(props) { } return integrations.some((i) => i.token); - }); + }, [props.integrations]); const { showModal, hideModal } = useModal(); diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index a54dbeb3b..b56b4ce67 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -13,7 +13,6 @@ import { useModal } from 'App/components/Modal'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter' import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache' -import { toJS } from 'mobx' const ALL = 'ALL'; const INFO = 'INFO'; @@ -60,7 +59,7 @@ const getIconProps = (level: any) => { const INDEX_KEY = 'console'; -function ConsolePanel({ isLive }: { isLive: boolean }) { +function ConsolePanel({ isLive }: { isLive?: boolean }) { const { sessionStore: { devTools }, } = useStore() diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx new file mode 100644 index 000000000..d6b398d6c --- /dev/null +++ b/frontend/app/components/shared/DevTools/ConsolePanel/MobileConsolePanel.tsx @@ -0,0 +1,225 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { LogLevel, ILog } from 'Player'; +import BottomBlock from '../BottomBlock'; +import { Tabs, Input, Icon, NoContent } from 'UI'; +import cn from 'classnames'; +import ConsoleRow from '../ConsoleRow'; +import { IOSPlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext'; +import { observer } from 'mobx-react-lite'; +import { List, CellMeasurer, AutoSizer } from 'react-virtualized'; +import { useStore } from 'App/mstore'; +import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal'; +import { useModal } from 'App/components/Modal'; +import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; +import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; +import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'; + +const ALL = 'ALL'; +const INFO = 'INFO'; +const WARNINGS = 'WARNINGS'; +const ERRORS = 'ERRORS'; + +const LEVEL_TAB = { + [LogLevel.INFO]: INFO, + [LogLevel.LOG]: INFO, + [LogLevel.WARN]: WARNINGS, + [LogLevel.ERROR]: ERRORS, + [LogLevel.EXCEPTION]: ERRORS, +} as const; + +const TABS = [ALL, ERRORS, WARNINGS, INFO].map((tab) => ({ text: tab, key: tab })); + +function renderWithNL(s: string | null = '') { + if (typeof s !== 'string') return ''; + return s.split('\n').map((line, i) => ( +
    + {line} +
    + )); +} + +const getIconProps = (level: any) => { + switch (level) { + case LogLevel.INFO: + case LogLevel.LOG: + return { + name: 'console/info', + color: 'blue2', + }; + case LogLevel.WARN: + return { + name: 'console/warning', + color: 'red2', + }; + case LogLevel.ERROR: + return { + name: 'console/error', + color: 'red', + }; + } + return null; +}; + +const INDEX_KEY = 'console'; + +function MobileConsolePanel() { + const { + sessionStore: { devTools }, + } = useStore(); + + const filter = devTools[INDEX_KEY].filter; + const activeTab = devTools[INDEX_KEY].activeTab; + // Why do we need to keep index in the store? if we could get read of it it would simplify the code + const activeIndex = devTools[INDEX_KEY].index; + const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); + const { showModal } = useModal(); + + const { player, store } = React.useContext(MobilePlayerContext); + const jump = (t: number) => player.jump(t); + + const { + logList, + // exceptionsList, + logListNow, + exceptionsListNow, + } = store.get(); + + const list = logList as ILog[]; + // useMemo(() => logList.concat(exceptionsList).sort((a, b) => a.time - b.time), + // [ logList.length, exceptionsList.length ], + // ) as ILog[] + let filteredList = useRegExListFilterMemo(list, (l) => l.value, filter); + filteredList = useTabListFilterMemo(filteredList, (l) => LEVEL_TAB[l.level], ALL, activeTab); + + React.useEffect(() => { + setTimeout(() => { + cache.clearAll(); + _list.current?.recomputeRowHeights(); + }, 0); + }, [activeTab, filter]); + const onTabClick = (activeTab: any) => devTools.update(INDEX_KEY, { activeTab }); + const onFilterChange = ({ target: { value } }: any) => + devTools.update(INDEX_KEY, { filter: value }); + + // AutoScroll + const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( + filteredList, + getLastItemTime(logListNow, exceptionsListNow), + activeIndex, + (index) => devTools.update(INDEX_KEY, { index }) + ); + const onMouseEnter = stopAutoscroll; + const onMouseLeave = () => { + if (isDetailsModalActive) { + return; + } + timeoutStartAutoscroll(); + }; + + const _list = useRef(null); // TODO: fix react-virtualized types & encapsulate scrollToRow logic + useEffect(() => { + if (_list.current) { + // @ts-ignore + _list.current.scrollToRow(activeIndex); + } + }, [activeIndex]); + + const cache = useCellMeasurerCache(); + + const showDetails = (log: any) => { + setIsDetailsModalActive(true); + showModal(, { + right: true, + width: 1200, + onClose: () => { + setIsDetailsModalActive(false); + timeoutStartAutoscroll(); + }, + }); + devTools.update(INDEX_KEY, { index: filteredList.indexOf(log) }); + stopAutoscroll(); + }; + const _rowRenderer = ({ index, key, parent, style }: any) => { + const item = filteredList[index]; + + return ( + // @ts-ignore + + {({ measure, registerChild }) => ( +
    + showDetails(item)} + recalcHeight={measure} + /> +
    + )} +
    + ); + }; + + return ( + + {/* @ts-ignore */} + +
    + Console + +
    + + {/* @ts-ignore */} +
    + {/* @ts-ignore */} + + + + No Data +
    + } + size="small" + show={filteredList.length === 0} + > + {/* @ts-ignore */} + + {({ height, width }: any) => ( + // @ts-ignore + + )} + + + {/* @ts-ignore */} + + + ); +} + +export default observer(MobileConsolePanel); diff --git a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx index 7dc12da54..a8006a159 100644 --- a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx +++ b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx @@ -29,6 +29,7 @@ function ConsoleRow(props: Props) { const toggleExpand = () => { setExpanded(!expanded); }; + return (
    )} - {renderWithNL(lines.pop())} + {renderWithNL(lines.pop())}
    - {log.errorId && } + {log.errorId && }
    {canExpand && expanded && lines.map((l: string, i: number) => ( -
    +
    {l}
    ))} diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index 65b104826..ade07587c 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -1,22 +1,23 @@ +import WebPlayer from 'Player/web/WebPlayer'; +import MobilePlayer from 'Player/mobile/IOSPlayer'; import React, { useMemo, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { Duration } from 'luxon'; import { Tooltip, Tabs, Input, NoContent, Icon, Toggler } from 'UI'; -import { ResourceType } from 'Player'; +import { ResourceType, Timed } from 'Player'; import { formatBytes } from 'App/utils'; import { formatMs } from 'App/date'; import { useModal } from 'App/components/Modal'; import FetchDetailsModal from 'Shared/FetchDetailsModal'; -import { PlayerContext } from 'App/components/Session/playerContext'; +import { MobilePlayerContext, PlayerContext } from 'App/components/Session/playerContext'; import { useStore } from 'App/mstore'; -import { connect } from 'react-redux' +import { connect } from 'react-redux'; import TimeTable from '../TimeTable'; import BottomBlock from '../BottomBlock'; import InfoLine from '../BottomBlock/InfoLine'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; -import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter' -import { toJS } from 'mobx'; +import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; const INDEX_KEY = 'network'; @@ -36,7 +37,7 @@ const TYPE_TO_TAB = { [ResourceType.IMG]: IMG, [ResourceType.MEDIA]: MEDIA, [ResourceType.OTHER]: OTHER, -} +}; const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER] as const; const TABS = TAP_KEYS.map((tab) => ({ @@ -129,38 +130,109 @@ export function renderDuration(r: any) { ); } -function renderStatus({ status, cached }: { status: string, cached: boolean }) { +function renderStatus({ status, cached }: { status: string; cached: boolean }) { return ( <> {cached ? ( - +
    {status}
    - ) : status} + ) : ( + status + )} - ) + ); } -function NetworkPanel({ startedAt }: { startedAt: number }) { - const { player, store } = React.useContext(PlayerContext) +function NetworkPanelCont({ startedAt }: { startedAt: number }) { + const { player, store } = React.useContext(PlayerContext); - const { - domContentLoadedTime, - loadTime, - domBuildingTime, - tabStates, - currentTab - } = store.get() + const { domContentLoadedTime, loadTime, domBuildingTime, tabStates, currentTab } = store.get(); const { fetchList = [], resourceList = [], fetchListNow = [], - resourceListNow = [] - } = tabStates[currentTab] + resourceListNow = [], + } = tabStates[currentTab]; + return ( + + ); +} + +function MobileNetworkPanelCont({ startedAt }: { startedAt: number }) { + const { player, store } = React.useContext(MobilePlayerContext); + + const domContentLoadedTime = undefined; + const loadTime = undefined; + const domBuildingTime = undefined; + const { + fetchList = [], + resourceList = [], + fetchListNow = [], + resourceListNow = [], + } = store.get(); + + return ( + + ); +} + +interface Props { + domContentLoadedTime?: { + time: number; + value: number; + }; + loadTime?: { + time: number; + value: number; + }; + domBuildingTime?: number; + fetchList: Timed[]; + resourceList: Timed[]; + fetchListNow: Timed[]; + resourceListNow: Timed[]; + player: WebPlayer | MobilePlayer; + startedAt: number; + isMobile?: boolean; +} + +const NetworkPanelComp = observer(({ + loadTime, + domBuildingTime, + domContentLoadedTime, + fetchList, + resourceList, + fetchListNow, + resourceListNow, + player, + startedAt, + isMobile +}: Props) => { const { showModal } = useModal(); const [sortBy, setSortBy] = useState('time'); const [sortAscending, setSortAscending] = useState(true); @@ -174,70 +246,87 @@ function NetworkPanel({ startedAt }: { startedAt: number }) { const activeTab = devTools[INDEX_KEY].activeTab; const activeIndex = devTools[INDEX_KEY].index; - const list = useMemo(() => - // TODO: better merge (with body size info) - do it in player - resourceList.filter(res => !fetchList.some(ft => { - // res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player) - if (res.name === ft.name) { - if (res.time === ft.time) return true; - if (res.url.includes(ft.url)) { - return Math.abs(res.time - ft.time) < 350 - || Math.abs(res.timestamp - ft.timestamp) < 350; - } - } + const list = useMemo( + () => + // TODO: better merge (with body size info) - do it in player + resourceList + .filter( + (res) => + !fetchList.some((ft) => { + // res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player) + if (res.name === ft.name) { + if (res.time === ft.time) return true; + if (res.url.includes(ft.url)) { + return ( + Math.abs(res.time - ft.time) < 350 || + Math.abs(res.timestamp - ft.timestamp) < 350 + ); + } + } - if (res.name !== ft.name) { return false } - if (Math.abs(res.time - ft.time) > 250) { return false } // TODO: find good epsilons - if (Math.abs(res.duration - ft.duration) > 200) { return false } + if (res.name !== ft.name) { + return false; + } + if (Math.abs(res.time - ft.time) > 250) { + return false; + } // TODO: find good epsilons + if (Math.abs(res.duration - ft.duration) > 200) { + return false; + } - return true - })) - .concat(fetchList) - .sort((a, b) => a.time - b.time) - , [ resourceList.length, fetchList.length ]) + return true; + }) + ) + .concat(fetchList) + .sort((a, b) => a.time - b.time), + [resourceList.length, fetchList.length] + ); let filteredList = useMemo(() => { - if (!showOnlyErrors) { return list } - return list.filter(it => parseInt(it.status) >= 400 || !it.success) - }, [ showOnlyErrors, list ]) + if (!showOnlyErrors) { + return list; + } + return list.filter((it) => parseInt(it.status) >= 400 || !it.success); + }, [showOnlyErrors, list]); filteredList = useRegExListFilterMemo( filteredList, - it => [ it.status, it.name, it.type ], - filter, - ) - filteredList = useTabListFilterMemo(filteredList, it => TYPE_TO_TAB[it.type], ALL, activeTab) + (it) => [it.status, it.name, it.type], + filter + ); + filteredList = useTabListFilterMemo(filteredList, (it) => TYPE_TO_TAB[it.type], ALL, activeTab); - const onTabClick = (activeTab: typeof TAP_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab }) - const onFilterChange = ({ target: { value } }: React.ChangeEvent) => devTools.update(INDEX_KEY, { filter: value }) + const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) => + devTools.update(INDEX_KEY, { activeTab }); + const onFilterChange = ({ target: { value } }: React.ChangeEvent) => + devTools.update(INDEX_KEY, { filter: value }); // AutoScroll - const [ - timeoutStartAutoscroll, - stopAutoscroll, - ] = useAutoscroll( + const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( filteredList, getLastItemTime(fetchListNow, resourceListNow), activeIndex, - index => devTools.update(INDEX_KEY, { index }) - ) - const onMouseEnter = stopAutoscroll + (index) => devTools.update(INDEX_KEY, { index }) + ); + const onMouseEnter = stopAutoscroll; const onMouseLeave = () => { - if (isDetailsModalActive) { return } - timeoutStartAutoscroll() - } - - const resourcesSize = useMemo(() => - resourceList.reduce( - (sum, { decodedBodySize }) => sum + (decodedBodySize || 0), - 0, - ), [ resourceList.length ]) - const transferredSize = useMemo(() => - resourceList.reduce( - (sum, { headerSize, encodedBodySize }) => - sum + (headerSize || 0) + (encodedBodySize || 0), - 0, - ), [ resourceList.length ]) + if (isDetailsModalActive) { + return; + } + timeoutStartAutoscroll(); + }; + const resourcesSize = useMemo( + () => resourceList.reduce((sum, { decodedBodySize }) => sum + (decodedBodySize || 0), 0), + [resourceList.length] + ); + const transferredSize = useMemo( + () => + resourceList.reduce( + (sum, { headerSize, encodedBodySize }) => sum + (headerSize || 0) + (encodedBodySize || 0), + 0 + ), + [resourceList.length] + ); const referenceLines = useMemo(() => { const arr = []; @@ -256,24 +345,29 @@ function NetworkPanel({ startedAt }: { startedAt: number }) { } return arr; - }, [ domContentLoadedTime, loadTime ]) + }, [domContentLoadedTime, loadTime]); const showDetailsModal = (item: any) => { - setIsDetailsModalActive(true) + setIsDetailsModalActive(true); showModal( - 0} />, + 0} + />, { right: true, width: 500, onClose: () => { - setIsDetailsModalActive(false) - timeoutStartAutoscroll() - } + setIsDetailsModalActive(false); + timeoutStartAutoscroll(); + }, } - ) - devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }) - stopAutoscroll() - } + ); + devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }); + stopAutoscroll(); + }; return ( @@ -286,13 +380,15 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
    Network - + {isMobile ? null : + + }
    ); -} +}) -export default connect((state: any) => ({ +const WebNetworkPanel = connect((state: any) => ({ startedAt: state.getIn(['sessions', 'current']).startedAt, -}))(observer(NetworkPanel)); +}))(observer(NetworkPanelCont)); + +const MobileNetworkPanel = connect((state: any) => ({ + startedAt: state.getIn(['sessions', 'current']).startedAt, +}))(observer(MobileNetworkPanelCont)); + +export { + WebNetworkPanel, + MobileNetworkPanel +} \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/index.ts b/frontend/app/components/shared/DevTools/NetworkPanel/index.ts index 3014d5b0b..815543ee4 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/index.ts +++ b/frontend/app/components/shared/DevTools/NetworkPanel/index.ts @@ -1 +1 @@ -export { default } from './NetworkPanel' \ No newline at end of file +export { WebNetworkPanel, MobileNetworkPanel } from './NetworkPanel'; diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx index db974a2ef..bb77971d1 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx +++ b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx @@ -1,8 +1,9 @@ +import { Timed } from 'Player'; import React, { useEffect, useMemo, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { Tabs, Input, NoContent, Icon } from 'UI'; import { List, CellMeasurer, AutoSizer } from 'react-virtualized'; -import { PlayerContext } from 'App/components/Session/playerContext'; +import { PlayerContext, MobilePlayerContext } from 'App/components/Session/playerContext'; import BottomBlock from '../BottomBlock'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; @@ -14,20 +15,47 @@ import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'; import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'; +const mapNames = (type: string) => { + if (type === 'openreplay') return 'OpenReplay'; + return type +} + const INDEX_KEY = 'stackEvent'; const ALL = 'ALL'; const TAB_KEYS = [ALL, ...typeList] as const; const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab })); -function StackEventPanel() { +type EventsList = Array + +export const WebStackEventPanel = observer(() => { const { player, store } = React.useContext(PlayerContext); const jump = (t: number) => player.jump(t); const { currentTab, tabStates } = store.get(); const { stackList: list = [], stackListNow: listNow = [] } = tabStates[currentTab]; + return ; +}); + +export const MobileStackEventPanel = observer(() => { + const { player, store } = React.useContext(MobilePlayerContext); + const jump = (t: number) => player.jump(t); + const { eventList: list = [], eventListNow: listNow = [] } = store.get(); + + return ; +}); + +function EventsPanel({ + list, + listNow, + jump, +}: { + list: EventsList; + listNow: EventsList; + jump: (t: number) => void; +}) { const { - sessionStore: { devTools } + sessionStore: { devTools }, } = useStore(); const { showModal } = useModal(); const [isDetailsModalActive, setIsDetailsModalActive] = useState(false); // TODO:embed that into useModal @@ -35,24 +63,23 @@ function StackEventPanel() { const activeTab = devTools[INDEX_KEY].activeTab; const activeIndex = devTools[INDEX_KEY].index; - let filteredList = useRegExListFilterMemo(list, it => it.name, filter); - filteredList = useTabListFilterMemo(filteredList, it => it.source, ALL, activeTab); + let filteredList = useRegExListFilterMemo(list, (it) => it.name, filter); + filteredList = useTabListFilterMemo(filteredList, (it) => it.source, ALL, activeTab); - const onTabClick = (activeTab: typeof TAB_KEYS[number]) => devTools.update(INDEX_KEY, { activeTab }); - const onFilterChange = ({ target: { value } }: React.ChangeEvent) => devTools.update(INDEX_KEY, { filter: value }); - const tabs = useMemo(() => - TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)), + const onTabClick = (activeTab: (typeof TAB_KEYS)[number]) => + devTools.update(INDEX_KEY, { activeTab }); + const onFilterChange = ({ target: { value } }: React.ChangeEvent) => + devTools.update(INDEX_KEY, { filter: value }); + const tabs = useMemo( + () => TABS.filter(({ key }) => key === ALL || list.some(({ source }) => key === source)), [list.length] ); - const [ - timeoutStartAutoscroll, - stopAutoscroll - ] = useAutoscroll( + const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll( filteredList, getLastItemTime(listNow), activeIndex, - index => devTools.update(INDEX_KEY, { index }) + (index) => devTools.update(INDEX_KEY, { index }) ); const onMouseEnter = stopAutoscroll; const onMouseLeave = () => { @@ -66,17 +93,14 @@ function StackEventPanel() { const showDetails = (item: any) => { setIsDetailsModalActive(true); - showModal( - , - { - right: true, - width: 500, - onClose: () => { - setIsDetailsModalActive(false); - timeoutStartAutoscroll(); - } - } - ); + showModal(, { + right: true, + width: 500, + onClose: () => { + setIsDetailsModalActive(false); + timeoutStartAutoscroll(); + }, + }); devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) }); stopAutoscroll(); }; @@ -89,7 +113,6 @@ function StackEventPanel() { } }, [activeIndex]); - const _rowRenderer = ({ index, key, parent, style }: any) => { const item = filteredList[index]; @@ -121,29 +144,29 @@ function StackEventPanel() { onMouseLeave={onMouseLeave} > -
    - Stack Events - +
    + Stack Events +
    - + - +
    + No Data
    } - size='small' + size="small" show={filteredList.length === 0} > @@ -157,7 +180,7 @@ function StackEventPanel() { rowRenderer={_rowRenderer} width={width} height={height} - scrollToAlignment='center' + scrollToAlignment="center" /> )} @@ -166,5 +189,3 @@ function StackEventPanel() { ); } - -export default observer(StackEventPanel); diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/index.ts b/frontend/app/components/shared/DevTools/StackEventPanel/index.ts index bb0ca8cb6..c673678ee 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/index.ts +++ b/frontend/app/components/shared/DevTools/StackEventPanel/index.ts @@ -1 +1 @@ -export { default } from './StackEventPanel'; +export { WebStackEventPanel, MobileStackEventPanel } from './StackEventPanel'; diff --git a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js index e484c683e..e2686ef08 100644 --- a/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js +++ b/frontend/app/components/shared/NoSessionsMessage/NoSessionsMessage.js @@ -15,7 +15,7 @@ const NoSessionsMessage = (props) => { const activeSite = sites.find((s) => s.id === siteId); const showNoSessions = !!activeSite && !activeSite.recorded; const onboardingPath = withSiteId(onboardingRoute('installing'), siteId); - console.log('onboardingPath', onboardingPath, siteId); + return ( <> {showNoSessions && ( diff --git a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx index e5b23caec..b06169411 100644 --- a/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx +++ b/frontend/app/components/shared/ProjectDropdown/ProjectDropdown.tsx @@ -12,12 +12,14 @@ import { useModal } from 'Components/Modal'; import { init as initProject } from 'Duck/site'; import NewSiteForm from 'Components/Client/Sites/NewSiteForm'; import { withStore } from 'App/mstore'; +import { Icon } from 'UI' const { Text } = Typography; interface Site { id: string; host: string; + platform: 'web' | 'mobile'; } interface Props extends RouteComponentProps { @@ -66,7 +68,7 @@ function ProjectDropdown(props: Props) { {sites.map((site) => ( } + icon={} key={site.id} onClick={() => handleSiteChange(site.id)} className='!py-2' diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index bf01f823d..da269ce12 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -260,22 +260,21 @@ function SessionItem(props: RouteComponentProps & Props) { showLabel={true} />
    -
    - {userBrowser && ( - <> - - - - - - )} - - +
    + {userBrowser ? ( + + + + ) : null} + {userOs && userBrowser ? ( + + ) : null} + diff --git a/frontend/app/components/ui/SVG.tsx b/frontend/app/components/ui/SVG.tsx index 0cbc9792e..22de79bb7 100644 --- a/frontend/app/components/ui/SVG.tsx +++ b/frontend/app/components/ui/SVG.tsx @@ -1,7 +1,7 @@ import React from 'react'; -export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down-up' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_avatar1' | 'avatar/icn_avatar10' | 'avatar/icn_avatar11' | 'avatar/icn_avatar12' | 'avatar/icn_avatar13' | 'avatar/icn_avatar14' | 'avatar/icn_avatar15' | 'avatar/icn_avatar16' | 'avatar/icn_avatar17' | 'avatar/icn_avatar18' | 'avatar/icn_avatar19' | 'avatar/icn_avatar2' | 'avatar/icn_avatar20' | 'avatar/icn_avatar21' | 'avatar/icn_avatar22' | 'avatar/icn_avatar23' | 'avatar/icn_avatar3' | 'avatar/icn_avatar4' | 'avatar/icn_avatar5' | 'avatar/icn_avatar6' | 'avatar/icn_avatar7' | 'avatar/icn_avatar8' | 'avatar/icn_avatar9' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book-doc' | 'book' | 'bookmark' | 'broadcast' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'buildings' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-list' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-left-text' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock-history' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection-play' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'db-icons/icn-card-clickMap' | 'db-icons/icn-card-errors' | 'db-icons/icn-card-funnel' | 'db-icons/icn-card-funnels' | 'db-icons/icn-card-insights' | 'db-icons/icn-card-library' | 'db-icons/icn-card-mapchart' | 'db-icons/icn-card-pathAnalysis' | 'db-icons/icn-card-performance' | 'db-icons/icn-card-resources' | 'db-icons/icn-card-table' | 'db-icons/icn-card-timeseries' | 'db-icons/icn-card-webVitals' | 'desktop' | 'device' | 'diagram-3' | 'dice-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope-check' | 'envelope-x' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle-fill' | 'exclamation-circle' | 'exclamation-triangle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'fflag-multi' | 'fflag-single' | 'file-bar-graph' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filetype-js' | 'filetype-pdf' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'integrations/zustand' | 'journal-code' | 'key' | 'layer-group' | 'layers-half' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'people' | 'percent' | 'performance-icon' | 'person-border' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plug' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'pulse' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-btn' | 'record-circle' | 'record2' | 'redo-back' | 'redo' | 'redux' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'side_menu_closed' | 'side_menu_open' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sleep' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stickies' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'terminal' | 'text-paragraph' | 'toggles' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; +export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down-up' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_avatar1' | 'avatar/icn_avatar10' | 'avatar/icn_avatar11' | 'avatar/icn_avatar12' | 'avatar/icn_avatar13' | 'avatar/icn_avatar14' | 'avatar/icn_avatar15' | 'avatar/icn_avatar16' | 'avatar/icn_avatar17' | 'avatar/icn_avatar18' | 'avatar/icn_avatar19' | 'avatar/icn_avatar2' | 'avatar/icn_avatar20' | 'avatar/icn_avatar21' | 'avatar/icn_avatar22' | 'avatar/icn_avatar23' | 'avatar/icn_avatar3' | 'avatar/icn_avatar4' | 'avatar/icn_avatar5' | 'avatar/icn_avatar6' | 'avatar/icn_avatar7' | 'avatar/icn_avatar8' | 'avatar/icn_avatar9' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'battery-charging' | 'battery' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book-doc' | 'book' | 'bookmark' | 'broadcast' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'buildings' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-list' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-left-text' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock-history' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection-play' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'db-icons/icn-card-clickMap' | 'db-icons/icn-card-errors' | 'db-icons/icn-card-funnel' | 'db-icons/icn-card-funnels' | 'db-icons/icn-card-insights' | 'db-icons/icn-card-library' | 'db-icons/icn-card-mapchart' | 'db-icons/icn-card-pathAnalysis' | 'db-icons/icn-card-performance' | 'db-icons/icn-card-resources' | 'db-icons/icn-card-table' | 'db-icons/icn-card-timeseries' | 'db-icons/icn-card-webVitals' | 'desktop' | 'device' | 'diagram-3' | 'dice-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope-check' | 'envelope-x' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle-fill' | 'exclamation-circle' | 'exclamation-triangle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'fflag-multi' | 'fflag-single' | 'file-bar-graph' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filetype-js' | 'filetype-pdf' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'integrations/zustand' | 'journal-code' | 'key' | 'layer-group' | 'layers-half' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'low-disc-space' | 'magic' | 'map-marker-alt' | 'memory-ios' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'people' | 'percent' | 'performance-icon' | 'person-border' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plug' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'pulse' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-btn' | 'record-circle' | 'record2' | 'redo-back' | 'redo' | 'redux' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'side_menu_closed' | 'side_menu_open' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sleep' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stickies' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'terminal' | 'text-paragraph' | 'thermometer-sun' | 'toggles' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; interface Props { name: IconNames; @@ -61,6 +61,8 @@ const SVG = (props: Props) => { case 'ban': return ; case 'bar-chart-line': return ; case 'bar-pencil': return ; + case 'battery-charging': return ; + case 'battery': return ; case 'bell-fill': return ; case 'bell-plus': return ; case 'bell-slash': return ; @@ -343,8 +345,10 @@ const SVG = (props: Props) => { case 'list-ul': return ; case 'list': return ; case 'lock-alt': return ; + case 'low-disc-space': return ; case 'magic': return ; case 'map-marker-alt': return ; + case 'memory-ios': return ; case 'memory': return ; case 'mic-mute': return ; case 'mic': return ; @@ -451,6 +455,7 @@ const SVG = (props: Props) => { case 'telephone': return ; case 'terminal': return ; case 'text-paragraph': return ; + case 'thermometer-sun': return ; case 'toggles': return ; case 'tools': return ; case 'trash': return ; diff --git a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js b/frontend/app/components/ui/SegmentSelection/SegmentSelection.tsx similarity index 72% rename from frontend/app/components/ui/SegmentSelection/SegmentSelection.js rename to frontend/app/components/ui/SegmentSelection/SegmentSelection.tsx index 5a5cef77f..611fae9ce 100644 --- a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js +++ b/frontend/app/components/ui/SegmentSelection/SegmentSelection.tsx @@ -1,10 +1,29 @@ import React from 'react'; import { Icon, Tooltip } from 'UI'; import cn from 'classnames'; +import {IconNames} from "UI/SVG"; import styles from './segmentSelection.module.css'; -class SegmentSelection extends React.Component { - setActiveItem = (item) => { +type Entry = { value: string, name: string, disabled?: boolean, icon?: IconNames }; + +interface Props { + className?: string; + name: string; + value: T; + list: T[]; + onSelect: (_: null, data: { name: string, value: T['value']}) => void; + small?: boolean; + extraSmall?: boolean; + primary?: boolean; + size?: 'normal' | 'small' | 'extraSmall'; + icons?: boolean; + disabled?: boolean; + disabledMessage?: string; + outline?: boolean; +} + +class SegmentSelection extends React.Component> { + setActiveItem = (item: T) => { this.props.onSelect(null, { name: this.props.name, value: item.value }); }; @@ -49,7 +68,7 @@ class SegmentSelection extends React.Component { )}
    {item.name}
    diff --git a/frontend/app/components/ui/Tabs/Tabs.js b/frontend/app/components/ui/Tabs/Tabs.js index 248dae3dd..11a80bd1f 100644 --- a/frontend/app/components/ui/Tabs/Tabs.js +++ b/frontend/app/components/ui/Tabs/Tabs.js @@ -2,7 +2,7 @@ import React from 'react'; import cn from 'classnames'; import stl from './tabs.module.css'; -const Tabs = ({ tabs, active, onClick, border = true, className = '' }) => ( +const Tabs = ({ tabs, active, onClick, border = true, className = '', renameTab = (tab) => tab }) => (
    { tabs.map(({ key, text, hidden = false, disabled = false }) => (
    ( data-hidden={ hidden } onClick={ onClick && (() => onClick(key)) } role="tab" - data-openreplay-label={text} + data-openreplay-label={renameTab(text)} > - { text } + { renameTab(text) }
    ))}
    diff --git a/frontend/app/duck/sessions.ts b/frontend/app/duck/sessions.ts index 64d070deb..38bd60131 100644 --- a/frontend/app/duck/sessions.ts +++ b/frontend/app/duck/sessions.ts @@ -191,10 +191,11 @@ const reducer = (state = initialState, action: IAction) => { errors, events, issues, + crashes, resources, stackEvents, userEvents - } = action.data as { errors: any[], events: any[], issues: any[], resources: any[], stackEvents: any[], userEvents: EventData[] }; + } = action.data as { errors: any[], crashes: any[], events: any[], issues: any[], resources: any[], stackEvents: any[], userEvents: EventData[] }; const filterEvents = action.filter.events as Record[]; const session = state.get('current') as Session; const matching: number[] = []; @@ -228,6 +229,7 @@ const reducer = (state = initialState, action: IAction) => { const newSession = session.addEvents( events, + crashes, errors, issues, resources, diff --git a/frontend/app/player/common/ListWalker.ts b/frontend/app/player/common/ListWalker.ts index cc6decbfc..90a87bcca 100644 --- a/frontend/app/player/common/ListWalker.ts +++ b/frontend/app/player/common/ListWalker.ts @@ -1,7 +1,8 @@ -import type { Timed } from './types'; +import type { Timed } from 'Player'; export default class ListWalker { - private p = 0 /* Pointer to the "current" item */ + /* Pointer to the "current" item */ + private p = 0 constructor(private _list: Array = []) {} append(m: T): void { @@ -123,10 +124,12 @@ export default class ListWalker { } let changed = false; + // @ts-ignore while (this.p < this.length && this.list[this.p][key] <= val) { this.moveNext() changed = true; } + // @ts-ignore while (this.p > 0 && this.list[ this.p - 1 ][key] > val) { this.movePrev() changed = true; diff --git a/frontend/app/player/common/types.ts b/frontend/app/player/common/types.ts index 20cfa2a58..0289114da 100644 --- a/frontend/app/player/common/types.ts +++ b/frontend/app/player/common/types.ts @@ -1,5 +1,7 @@ export interface Timed { time: number + /** present in mobile events and in db events */ + timestamp?: number } export interface Indexed { diff --git a/frontend/app/player/create.ts b/frontend/app/player/create.ts index 0286a8017..2c99f92b5 100644 --- a/frontend/app/player/create.ts +++ b/frontend/app/player/create.ts @@ -1,50 +1,72 @@ -import SimpleStore from './common/SimpleStore' -import type { Store, SessionFilesInfo } from './common/types' +import SimpleStore from './common/SimpleStore'; +import type { Store, SessionFilesInfo } from './common/types'; -import WebPlayer from './web/WebPlayer' -import WebLivePlayer from './web/WebLivePlayer' +import WebPlayer from './web/WebPlayer'; +import WebLivePlayer from './web/WebLivePlayer'; -type WebState = typeof WebPlayer.INITIAL_STATE -type WebPlayerStore = Store -export type IWebPlayer = WebPlayer -export type IWebPlayerStore = WebPlayerStore +import IOSPlayer from 'Player/mobile/IOSPlayer'; -type WebLiveState = typeof WebLivePlayer.INITIAL_STATE -type WebLivePlayerStore = Store -export type IWebLivePlayer = WebLivePlayer -export type IWebLivePlayerStore = WebLivePlayerStore +type IosState = typeof IOSPlayer.INITIAL_STATE; +type IOSPlayerStore = Store; +export type IIosPlayer = IOSPlayer; +export type IIOSPlayerStore = IOSPlayerStore; -export function createWebPlayer( - session: SessionFilesInfo, - wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore, - uiErrorHandler?: { error: (msg: string) => void } -): [IWebPlayer, IWebPlayerStore] { - let store: WebPlayerStore = new SimpleStore({ - ...WebPlayer.INITIAL_STATE, - }) - if (wrapStore) { - store = wrapStore(store) - } +type WebState = typeof WebPlayer.INITIAL_STATE; +type WebPlayerStore = Store; +export type IWebPlayer = WebPlayer; +export type IWebPlayerStore = WebPlayerStore; - const player = new WebPlayer(store, session, false, false, uiErrorHandler) - return [player, store] +type WebLiveState = typeof WebLivePlayer.INITIAL_STATE; +type WebLivePlayerStore = Store; +export type IWebLivePlayer = WebLivePlayer; +export type IWebLivePlayerStore = WebLivePlayerStore; + +export function createIOSPlayer( + session: SessionFilesInfo, + wrapStore?: (s: IOSPlayerStore) => IOSPlayerStore, + uiErrorHandler?: { error: (msg: string) => void } +): [IIosPlayer, IOSPlayerStore] { + let store: IOSPlayerStore = new SimpleStore({ + ...IOSPlayer.INITIAL_STATE, + }); + if (wrapStore) { + store = wrapStore(store); + } + + const player = new IOSPlayer(store, session, uiErrorHandler); + return [player, store]; } +export function createWebPlayer( + session: SessionFilesInfo, + wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore, + uiErrorHandler?: { error: (msg: string) => void } +): [IWebPlayer, IWebPlayerStore] { + let store: WebPlayerStore = new SimpleStore({ + ...WebPlayer.INITIAL_STATE, + }); + if (wrapStore) { + store = wrapStore(store); + } + + const player = new WebPlayer(store, session, false, false, uiErrorHandler); + return [player, store]; +} export function createClickMapPlayer( - session: SessionFilesInfo, - wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore, - uiErrorHandler?: { error: (msg: string) => void } + session: SessionFilesInfo, + wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore, + uiErrorHandler?: { error: (msg: string) => void } ): [IWebPlayer, IWebPlayerStore] { - let store: WebPlayerStore = new SimpleStore({ - ...WebPlayer.INITIAL_STATE, - }) - if (wrapStore) { - store = wrapStore(store) - } + let store: WebPlayerStore = new SimpleStore({ + ...WebPlayer.INITIAL_STATE, + }); + if (wrapStore) { + store = wrapStore(store); + } - const player = new WebPlayer(store, session, false, true, uiErrorHandler) - return [player, store] + const player = new WebPlayer(store, session, false, true, uiErrorHandler); + return [player, store]; } export function createLiveWebPlayer( diff --git a/frontend/app/player/index.ts b/frontend/app/player/index.ts index 26a2270d5..0a342c64c 100644 --- a/frontend/app/player/index.ts +++ b/frontend/app/player/index.ts @@ -8,3 +8,7 @@ export * from './web/WebPlayer'; export * from './web/storageSelectors'; export * from './web/types'; export type { MarkedTarget } from './web/addons/TargetMarker' + +export * from './mobile/IOSPlayer' +export * from './mobile/IOSMessageManager' +export * from './mobile/managers/TouchManager' diff --git a/frontend/app/player/mobile/IOSLists.tsx b/frontend/app/player/mobile/IOSLists.tsx new file mode 100644 index 000000000..04c599633 --- /dev/null +++ b/frontend/app/player/mobile/IOSLists.tsx @@ -0,0 +1,92 @@ +import ListWalker from '../common/ListWalker'; +import ListWalkerWithMarks from '../common/ListWalkerWithMarks'; +import type { Timed } from '../common/types'; + + +const SIMPLE_LIST_NAMES = [ + "event", + "exceptions", + "profiles", + "frustrations", + "performance" +] as const +const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const + +const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ] as const + +type KeysList = `${typeof LIST_NAMES[number]}List` +type KeysListNow = `${typeof LIST_NAMES[number]}ListNow` +type KeysMarkedCountNow = `${typeof MARKED_LIST_NAMES[number]}MarkedCountNow` +type StateList = { + [key in KeysList]: Timed[] +} +type StateListNow = { + [key in KeysListNow]: Timed[] +} +type StateMarkedCountNow = { + [key in KeysMarkedCountNow]: number +} +type StateNow = StateListNow & StateMarkedCountNow +export type State = StateList & StateNow + +// maybe use list object itself inside the store + +export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => { + state[`${name}List`] = [] + state[`${name}ListNow`] = [] + return state + }, MARKED_LIST_NAMES.reduce((state, name) => { + state[`${name}MarkedCountNow`] = 0 + return state + }, {} as Partial) as Partial +) as State + + +type SimpleListsObject = { + [key in typeof SIMPLE_LIST_NAMES[number]]: ListWalker +} +type MarkedListsObject = { + [key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks +} +type ListsObject = SimpleListsObject & MarkedListsObject + +export type InitialLists = { + [key in typeof LIST_NAMES[number]]: any[] // .isRed()? +} + +export default class Lists { + lists: ListsObject + constructor(initialLists: Partial = {}) { + const lists: Partial = {} + for (const name of SIMPLE_LIST_NAMES) { + lists[name] = new ListWalker(initialLists[name]) + } + for (const name of MARKED_LIST_NAMES) { + // TODO: provide types + lists[name] = new ListWalkerWithMarks((el) => el.isRed, initialLists[name]) + } + this.lists = lists as ListsObject + } + + getFullListsState(): StateList { + return LIST_NAMES.reduce((state, name) => { + state[`${name}List`] = this.lists[name].list + return state + }, {} as Partial) as StateList + } + + moveGetState(t: number): StateNow { + return LIST_NAMES.reduce((state, name) => { + const lastMsg = this.lists[name].moveGetLast(t) // index: name === 'exceptions' ? undefined : index); + if (lastMsg != null) { + state[`${name}ListNow`] = this.lists[name].listNow + } + return state + }, MARKED_LIST_NAMES.reduce((state, name) => { + state[`${name}MarkedCountNow`] = this.lists[name].markedCountNow // Red --> Marked + return state + }, {} as Partial) as Partial + ) as State + } + +} \ No newline at end of file diff --git a/frontend/app/player/mobile/IOSMessageManager.ts b/frontend/app/player/mobile/IOSMessageManager.ts new file mode 100644 index 000000000..fe41a6f06 --- /dev/null +++ b/frontend/app/player/mobile/IOSMessageManager.ts @@ -0,0 +1,264 @@ +import logger from 'App/logger'; +import { getResourceFromNetworkRequest } from "Player"; + +import type { Store } from 'Player'; +import { IMessageManager } from 'Player/player/Animator'; + +import TouchManager from 'Player/mobile/managers/TouchManager'; +import ActivityManager from '../web/managers/ActivityManager'; +import Lists, { + InitialLists, + INITIAL_STATE as LISTS_INITIAL_STATE, + State as ListsState, +} from './IOSLists'; +import IOSPerformanceTrackManager, { PerformanceChartPoint } from "Player/mobile/managers/IOSPerformanceTrackManager"; +import { MType } from '../web/messages'; +import type { Message } from '../web/messages'; + +import Screen, { + INITIAL_STATE as SCREEN_INITIAL_STATE, + State as ScreenState, +} from '../web/Screen/Screen'; + +import { Log } from './types/log' + +import type { SkipInterval } from '../web/managers/ActivityManager'; + +export const performanceWarnings = ['thermalState', 'memoryWarning', 'lowDiskSpace', 'isLowPowerModeEnabled', 'batteryLevel'] + +const perfWarningFrustrations = { + thermalState: { + title: "Overheating", + icon: "thermometer-sun", + }, + memoryWarning: { + title: "High Memory Usage", + icon: "memory-ios" + }, + lowDiskSpace: { + title: "Low Disk Space", + icon: "low-disc-space" + }, + isLowPowerModeEnabled: { + title: "Low Power Mode", + icon: "battery-charging" + }, + batteryLevel: { + title: "Low Battery", + icon: "battery" + } +} + +export interface State extends ScreenState, ListsState { + skipIntervals: SkipInterval[]; + performanceChartData: PerformanceChartPoint[]; + performanceChartTime: number; + location?: string; + + error: boolean; + messagesLoading: boolean; + + cssLoading: boolean; + ready: boolean; + lastMessageTime: number; + messagesProcessed: boolean; + eventCount: number; + updateWarnings: number; +} + +const userEvents = [MType.IosSwipeEvent, MType.IosClickEvent, MType.IosInputEvent, MType.IosScreenChanges]; + +export default class IOSMessageManager implements IMessageManager { + static INITIAL_STATE: State = { + ...SCREEN_INITIAL_STATE, + ...LISTS_INITIAL_STATE, + updateWarnings: 0, + eventCount: 0, + performanceChartData: [], + performanceChartTime: 0, + skipIntervals: [], + error: false, + ready: false, + cssLoading: false, + lastMessageTime: 0, + messagesProcessed: false, + messagesLoading: false, + }; + + private activityManager: ActivityManager | null = null; + private performanceManager = new IOSPerformanceTrackManager(); + + private readonly sessionStart: number; + private lastMessageTime: number = 0; + private touchManager: TouchManager; + private lists: Lists; + + constructor( + private readonly session: Record, + private readonly state: Store, + private readonly screen: Screen, + private readonly uiErrorHandler?: { error: (error: string) => void }, + initialLists?: Partial + ) { + this.sessionStart = this.session.startedAt; + this.lists = new Lists(initialLists); + this.touchManager = new TouchManager(screen); + this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live + } + + public updateDimensions(dimensions: { width: number; height: number }) { + this.touchManager.updateDimensions(dimensions); + } + + public updateLists(lists: Partial) { + const exceptions = lists.exceptions + exceptions?.forEach(e => { + this.lists.lists.exceptions.insert(e); + this.lists.lists.log.insert(e) + }) + lists.frustrations?.forEach(f => { + this.lists.lists.frustrations.insert(f); + }) + + const eventCount = this.lists.lists.event.count //lists?.event?.length || 0; + const currentState = this.state.get(); + this.state.update({ + eventCount: currentState.eventCount + eventCount, + ...this.lists.getFullListsState(), + }); + } + + _sortMessagesHack() { + return; + } + + private waitingForFiles: boolean = false; + public onFileReadSuccess = () => { + let newState: Partial = { + ...this.state.get(), + eventCount: this.lists?.lists.event?.length || 0, + performanceChartData: this.performanceManager.chartData, + ...this.lists.getFullListsState(), + } + + if (this.activityManager) { + this.activityManager.end(); + newState['skipIntervals'] = this.activityManager.list + } + this.state.update(newState); + }; + + public onFileReadFailed = (e: any) => { + logger.error(e); + this.state.update({error: true}); + this.uiErrorHandler?.error('Error requesting a session file'); + }; + + public onFileReadFinally = () => { + this.waitingForFiles = false; + this.state.update({messagesProcessed: true}); + }; + + public startLoading = () => { + this.waitingForFiles = true; + this.state.update({messagesProcessed: false}); + this.setMessagesLoading(true); + }; + + resetMessageManagers() { + this.touchManager = new TouchManager(this.screen); + this.activityManager = new ActivityManager(this.session.duration.milliseconds); + } + + move(t: number): any { + const stateToUpdate: Record = {}; + + const lastPerformanceTrackMessage = this.performanceManager.moveGetLast(t); + if (lastPerformanceTrackMessage) { + Object.assign(stateToUpdate, { + performanceChartTime: lastPerformanceTrackMessage.time, + }) + } + + this.touchManager.move(t); + if ( + this.waitingForFiles && + this.lastMessageTime <= t && + t !== this.session.duration.milliseconds + ) { + this.setMessagesLoading(true); + } + + Object.assign(stateToUpdate, this.lists.moveGetState(t)) + Object.assign(stateToUpdate, { performanceListNow: this.lists.lists.performance.listNow }) + Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate); + } + + distributeMessage = (msg: Message & { tabId: string }): void => { + const lastMessageTime = Math.max(msg.time, this.lastMessageTime); + this.lastMessageTime = lastMessageTime; + this.state.update({lastMessageTime}); + if (userEvents.includes(msg.tp)) { + this.activityManager?.updateAcctivity(msg.time); + } + + switch (msg.tp) { + case MType.IosPerformanceEvent: + const performanceStats = ['background', 'memoryUsage', 'mainThreadCPU'] + if (performanceStats.includes(msg.name)) { + this.performanceManager.append(msg); + } + if (performanceWarnings.includes(msg.name)) { + // @ts-ignore + const item = perfWarningFrustrations[msg.name] + this.lists.lists.performance.append({ + ...msg, + name: item.title, + techName: msg.name, + icon: item.icon, + type: 'ios_perf_event' + } as any) + } + break; + // case MType.IosInputEvent: + // console.log('input', msg) + // break; + case MType.IosNetworkCall: + this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart)) + break; + case MType.IosEvent: + // @ts-ignore + this.lists.lists.event.insert({...msg, source: 'openreplay'}); + break; + case MType.IosSwipeEvent: + case MType.IosClickEvent: + this.touchManager.append(msg); + break; + case MType.IosLog: + const log = {...msg, level: msg.severity} + // @ts-ignore + this.lists.lists.log.append(Log(log)); + break; + default: + console.log(msg) + // stuff + break; + } + }; + + setMessagesLoading = (messagesLoading: boolean) => { + this.screen.display(!messagesLoading); + // @ts-ignore idk + this.state.update({messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading}); + }; + + private setSize({height, width}: { height: number; width: number }) { + this.screen.scale({height, width}); + this.state.update({width, height}); + } + + // TODO: clean managers? + clean() { + this.state.update(IOSMessageManager.INITIAL_STATE); + } +} diff --git a/frontend/app/player/mobile/IOSPlayer.ts b/frontend/app/player/mobile/IOSPlayer.ts new file mode 100644 index 000000000..bb975302e --- /dev/null +++ b/frontend/app/player/mobile/IOSPlayer.ts @@ -0,0 +1,131 @@ +import { Log, LogLevel, SessionFilesInfo } from 'Player' + +import type { Store } from 'Player' +import MessageLoader from "Player/web/MessageLoader"; +import Player from '../player/Player' +import Screen, { ScaleMode } from '../web/Screen/Screen' +import IOSMessageManager from "Player/mobile/IOSMessageManager"; + +export default class IOSPlayer extends Player { + static readonly INITIAL_STATE = { + ...Player.INITIAL_STATE, + ...MessageLoader.INITIAL_STATE, + ...IOSMessageManager.INITIAL_STATE, + scale: 1, + } + public screen: Screen + protected messageManager: IOSMessageManager + protected readonly messageLoader: MessageLoader + constructor( + protected wpState: Store, + session: SessionFilesInfo, + public readonly uiErrorHandler?: { error: (msg: string) => void } + ) { + const screen = new Screen(true, ScaleMode.Embed) + const messageManager = new IOSMessageManager(session, wpState, screen, uiErrorHandler) + const messageLoader = new MessageLoader( + session, + wpState, + messageManager, + false, + uiErrorHandler + ) + super(wpState, messageManager); + this.screen = screen + this.messageManager = messageManager + this.messageLoader = messageLoader + + void messageLoader.loadFiles() + const endTime = session.duration?.valueOf() || 0 + + wpState.update({ + session, + endTime, + }) + } + + attach = (parent: HTMLElement) => { + this.screen.attach(parent) + } + + public updateDimensions(dimensions: { width: number; height: number }) { + return this.messageManager.updateDimensions(dimensions) + } + + public updateLists(session: any) { + const exceptions = session.crashes.concat(session.errors || []) + const lists = { + event: session.events.map((e: Record) => { + if (e.name === 'Click') e.name = 'Touch' + return e + }) || [], + frustrations: session.frustrations || [], + stack: session.stackEvents || [], + exceptions: exceptions.map(({ name, ...rest }: any) => + Log({ + level: LogLevel.ERROR, + value: name, + name, + message: rest.reason, + errorId: rest.crashId || rest.errorId, + ...rest, + }) + ) || [], + } + + return this.messageManager.updateLists(lists) + } + + public updateOverlayStyle(style: Partial) { + this.screen.updateOverlayStyle(style) + } + + injectPlayer = (player: HTMLElement) => { + this.screen.addToBody(player) + this.screen.addMobileStyles() + window.addEventListener('resize', () => + this.customScale(this.customConstrains.width, this.customConstrains.height) + ) + } + + scale = () => { + // const { width, height } = this.wpState.get() + if (!this.screen) return; + console.debug("using customConstrains to scale player") + // sometimes happens in live assist sessions for some reason + this.screen?.scale?.(this.customConstrains) + } + + customConstrains = { + width: 0, + height: 0, + } + customScale = (width: number, height: number) => { + if (!this.screen) return; + this.screen?.scale?.({ width, height }) + this.customConstrains = { width, height } + this.wpState.update({ scale: this.screen.getScale() }) + } + + addFullscreenBoundary = (isFullscreen?: boolean) => { + if (isFullscreen) { + this.screen?.addFullscreenBoundary() + } else { + this.screen?.addMobileStyles() + } + } + + + clean = () => { + super.clean() + this.screen.clean() + // @ts-ignore + this.screen = undefined; + this.messageLoader.clean() + // @ts-ignore + this.messageManager = undefined; + window.removeEventListener('resize', this.scale) + } + + +} diff --git a/frontend/app/player/mobile/managers/IOSPerformanceTrackManager.ts b/frontend/app/player/mobile/managers/IOSPerformanceTrackManager.ts new file mode 100644 index 000000000..298e8c16d --- /dev/null +++ b/frontend/app/player/mobile/managers/IOSPerformanceTrackManager.ts @@ -0,0 +1,66 @@ +import ListWalker from "Player/common/ListWalker"; +import type { IosPerformanceEvent } from "Player/web/messages"; + +const performanceEvTypes = { + MemoryUsage: 'memoryUsage', + MainThreadCPU: 'mainThreadCPU', + Background: 'background', +} + +export type PerformanceChartPoint = { + time: number, + cpu: number | null, + memory: number | null, + isBackground: boolean, +} + +export default class IOSPerformanceTrackManager extends ListWalker { + private chart: Array = []; + private isInBg = false; + + lastData: { cpu: number | null, memory: number | null } = { cpu: null, memory: null }; + append(msg: IosPerformanceEvent): void { + if (!Object.values(performanceEvTypes).includes(msg.name)) { + return console.log('Unsupported performance event type', msg.name) + } + + let cpu: number | null = null; + let memory: number | null = null; + if (msg.time < 0) msg.time = 1; + if (msg.name === performanceEvTypes.Background) { + // @ts-ignore + const isBackground = msg.value === 1; + if (isBackground === this.isInBg) return; + this.isInBg = isBackground; + this.chart.push({ + time: msg.time, + cpu: null, + memory: null, + isBackground, + }) + return super.append(msg); + } + if (msg.name === performanceEvTypes.MemoryUsage) { + memory = msg.value; + cpu = this.lastData.cpu; + this.lastData.memory = memory; + } + if (msg.name === performanceEvTypes.MainThreadCPU) { + cpu = msg.value; + memory = this.lastData.memory; + this.lastData.cpu = cpu; + } + + this.chart.push({ + time: msg.time, + cpu, + memory, + isBackground: false, + }); + super.append(msg); + } + + get chartData(): Array { + return this.chart; + } +} diff --git a/frontend/app/player/mobile/managers/TouchManager.ts b/frontend/app/player/mobile/managers/TouchManager.ts new file mode 100644 index 000000000..2357c9c02 --- /dev/null +++ b/frontend/app/player/mobile/managers/TouchManager.ts @@ -0,0 +1,49 @@ +import {MOUSE_TRAIL} from "App/constants/storageKeys"; +import ListWalker from 'Player/common/ListWalker'; +import MouseTrail, { SwipeEvent } from 'Player/web/addons/MouseTrail'; +import type {IosClickEvent, IosSwipeEvent} from 'Player/web/messages'; +import {MType} from "Player/web/messages"; +import type Screen from 'Player/web/Screen/Screen'; + +export default class TouchManager extends ListWalker { + private touchTrail: MouseTrail | undefined; + private readonly removeTouchTrail: boolean = false; + + constructor(private screen: Screen) { + super(); + const canvas = document.createElement('canvas'); + canvas.id = 'openreplay-touch-trail'; + // canvas.className = styles.canvas; + + this.removeTouchTrail = localStorage.getItem(MOUSE_TRAIL) === 'false' + if (!this.removeTouchTrail) { + this.touchTrail = new MouseTrail(canvas, true) + } + + this.screen.overlay.appendChild(canvas); + this.touchTrail?.createContext(); + } + + public updateDimensions({ width, height }: { width: number; height: number; }) { + return this.touchTrail?.resizeCanvas(width, height); + } + + public move(t: number) { + const lastTouch = this.moveGetLast(t) + if (!!lastTouch) { + if (lastTouch.tp === MType.IosSwipeEvent) { + return + // not using swipe rn + // this.touchTrail?.createSwipeTrail({ + // x: lastTouch.x, + // y: lastTouch.y, + // direction: lastTouch.direction + // } as SwipeEvent) + } else { + this.screen.cursor.move(lastTouch) + this.screen.cursor.mobileClick() + } + } + } + +} diff --git a/frontend/app/player/mobile/types/log.ts b/frontend/app/player/mobile/types/log.ts new file mode 100644 index 000000000..144ac889a --- /dev/null +++ b/frontend/app/player/mobile/types/log.ts @@ -0,0 +1,49 @@ +export const enum LogLevel { + INFO = 'info', + LOG = 'log', + //ASSERT = 'assert', //? + WARN = 'warn', + ERROR = 'error', + EXCEPTION = 'exception', +} + +export interface ILog { + content: string; + severity: "info" | "log" | "warn" | "error" | "exception"; + time: number; + timestamp: number; + tp: number; + _index: number; +} + +export const Log = (log: ILog) => ({ + isRed: log.severity === LogLevel.EXCEPTION || log.severity === LogLevel.ERROR, + isYellow: log.severity === LogLevel.WARN, + value: log.content, + ...log +}) + +// content +// : +// ">>>POST:https://foss.openreplay.com/ingest/v1/mobile/i\n<<<\n" +// length +// : +// 65 +// severity +// : +// "info" +// tabId +// : +// "back-compatability" +// time +// : +// 10048 +// timestamp +// : +// 1692966743780 +// tp +// : +// 103 +// _index +// : +// 50 \ No newline at end of file diff --git a/frontend/app/player/mobile/utils.ts b/frontend/app/player/mobile/utils.ts new file mode 100644 index 000000000..1e434d466 --- /dev/null +++ b/frontend/app/player/mobile/utils.ts @@ -0,0 +1,103 @@ + +const iPhone12ProSvg2 = `` +const iPhone12ProMaxSvg2 = `` +const iphone14ProSvg2 = `` +const iphone14ProMaxSvg2 = `` +// old svg frames +// const iPhone12ProSvg = "" +// const iPhone12ProMaxSvg = "" +// const iphone14ProSvg = "" +// const iphone14ProMaxSvg = "" + +const screenResolutions = { + iPhone12Pro: { + margin: '18px 0 0 21px', + screen: { + width: 391, + height: 845, + }, + shell: { + width: 438, + height: 883, + }, + }, + iPhone12ProMax: { + margin: '18px 0 0 21px', + screen: { + width: 429, + height: 927, + }, + shell: { + width: 476, + height: 965, + }, + }, + iPhone14Pro: { + margin: '21px 0 0 23px', + screen: { + width: 394, + height: 853, + }, + shell: { + width: 436, + height: 891, + }, + }, + iPhone14ProMax: { + margin: '21px 0 0 23px', + screen: { + width: 431, + height: 933, + }, + shell: { + width: 473, + height: 971, + }, + }, +}; + +/** + * @param modelName - device.safeDescription from DeviceKit + * @returns - returns a phone shell svg and styles for inner screen dimensions + * plus margins from shell + * _______ + * currently mapped: 12, 12pro, map, 13, 13pro, max, 14, 14 pro, 14 max/plus + * + * everything else is considered as 12 pro + * */ +export function mapIphoneModel(modelName: string) { + + const iPhone12Pro = [ + "iPhone 12", + "iPhone 12 Pro", + "iPhone 13", + "iPhone 13 Pro", + "iPhone 14" + ]; + + const iPhone12ProMax = [ + "iPhone 12 Pro Max", + "iPhone 13 Pro Max", + "iPhone 14 Plus" + ]; + + const iPhone14Pro = [ + "iPhone 14 Pro" + ]; + + const iPhone14ProMax = [ + "iPhone 14 Pro Max" + ]; + + if (iPhone12Pro.includes(modelName)) { + return { svg: iPhone12ProSvg2, styles: screenResolutions.iPhone12Pro } as const; + } else if (iPhone12ProMax.includes(modelName)) { + return { svg: iPhone12ProMaxSvg2, styles: screenResolutions.iPhone12ProMax } as const; + } else if (iPhone14Pro.includes(modelName)) { + return { svg: iphone14ProSvg2, styles: screenResolutions.iPhone14Pro } as const; + } else if (iPhone14ProMax.includes(modelName)) { + return { svg: iphone14ProMaxSvg2, styles: screenResolutions.iPhone14ProMax } as const; + } else { + return { svg: iPhone12ProSvg2, styles: screenResolutions.iPhone12Pro} as const; // Default fallback + } +} diff --git a/frontend/app/player/player/Animator.ts b/frontend/app/player/player/Animator.ts index 6199f0c96..bfa537600 100644 --- a/frontend/app/player/player/Animator.ts +++ b/frontend/app/player/player/Animator.ts @@ -1,5 +1,5 @@ -import type { Store, Interval } from '../common/types'; -import MessageManager from 'App/player/web/MessageManager' +import {Message} from "Player/web/messages"; +import type { Store, Interval } from 'Player'; const fps = 60 @@ -21,6 +21,18 @@ const cancelAnimationFrame = window.mozCancelAnimationFrame || window.clearTimeout +export interface IMessageManager { + onFileReadSuccess(): void; + onFileReadFailed(e: any): void; + onFileReadFinally(): void; + startLoading(): void; + resetMessageManagers(): void; + move(t: number): any; + distributeMessage(msg: Message): void; + setMessagesLoading(messagesLoading: boolean): void; + clean(): void; + _sortMessagesHack: (msgs: Message[]) => void; +} export interface SetState { time: number @@ -56,7 +68,7 @@ export default class Animator { private animationFrameRequestId: number = 0 - constructor(private store: Store, private mm: MessageManager) { + constructor(private store: Store, private mm: IMessageManager) { // @ts-ignore window.playerJump = this.jump.bind(this) diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts index e3520191a..d80e679db 100644 --- a/frontend/app/player/player/Player.ts +++ b/frontend/app/player/player/Player.ts @@ -1,11 +1,10 @@ import * as typedLocalStorage from './localStorage'; import type { Store } from '../common/types'; -import Animator from './Animator'; +import Animator, { IMessageManager } from './Animator'; import type { GetState as AnimatorGetState } from './Animator'; -import MessageManager from "Player/web/MessageManager"; export const SPEED_OPTIONS = [0.5, 1, 2, 4, 8, 16] - +import type { Message } from "Player/web/messages"; /* == separate this == */ const HIGHEST_SPEED = 16 @@ -35,7 +34,7 @@ export default class Player extends Animator { speed: initialSpeed, } as const - constructor(private pState: Store, private manager: MessageManager) { + constructor(private pState: Store, private manager: IMessageManager) { super(pState, manager) // Autoplay @@ -114,4 +113,4 @@ export default class Player extends Animator { this.manager.clean() } -} \ No newline at end of file +} diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts index 9c30d1890..c935d1017 100644 --- a/frontend/app/player/web/MessageLoader.ts +++ b/frontend/app/player/web/MessageLoader.ts @@ -1,4 +1,5 @@ import type { Store, SessionFilesInfo } from 'Player'; +import {IMessageManager} from "Player/player/Animator"; import { decryptSessionBytes } from './network/crypto'; import MFileReader from './messages/MFileReader'; import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles'; @@ -6,7 +7,6 @@ import type { Message, } from './messages'; import logger from 'App/logger'; -import MessageManager from "Player/web/MessageManager"; interface State { @@ -27,7 +27,7 @@ export default class MessageLoader { constructor( private readonly session: SessionFilesInfo, private store: Store, - private messageManager: MessageManager, + private messageManager: IMessageManager, private isClickmap: boolean, private uiErrorHandler?: { error: (msg: string) => void } ) {} diff --git a/frontend/app/player/web/Screen/Cursor.ts b/frontend/app/player/web/Screen/Cursor.ts index 19283d9d4..49c332be3 100644 --- a/frontend/app/player/web/Screen/Cursor.ts +++ b/frontend/app/player/web/Screen/Cursor.ts @@ -74,7 +74,7 @@ export default class Cursor { } click() { - const styleList = this.isMobile ? styles.clickedMobile : styles.clicked + const styleList = styles.clicked this.cursor.classList.add(styleList) this.onClick?.() setTimeout(() => { @@ -82,6 +82,22 @@ export default class Cursor { }, 600) } + clickTimeout?: NodeJS.Timeout + mobileClick() { + const styleList = styles.mobileTouch + if (this.clickTimeout) { + clearTimeout(this.clickTimeout) + this.cursor.classList.remove(styleList) + this.clickTimeout = undefined + } + this.cursor.classList.add(styleList) + this.onClick?.() + this.clickTimeout = setTimeout(() => { + this.cursor.classList.remove(styleList) + this.clickTimeout = undefined + }, 600) + } + setOnClickHook(callback: () => void) { this.onClick = callback } diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index a7edc82e2..2956940e3 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -84,6 +84,15 @@ export default class Screen { this.cursor = new Cursor(this.overlay, isMobile) // TODO: move outside } + addMobileStyles() { + this.iframe.className = styles.mobileIframe + this.screen.className = styles.mobileScreen + } + + addFullscreenBoundary() { + this.screen.className = styles.mobileScreenFullview + } + clean() { this.iframe?.remove?.(); this.overlay?.remove?.(); @@ -100,6 +109,13 @@ export default class Screen { this.parentElement = parentElement; } + addToBody(el: HTMLElement) { + if (this.document) { + this.document.body.style.margin = '0'; + this.document.body.appendChild(el) + } + } + getParentElement(): HTMLElement | null { return this.parentElement } @@ -236,7 +252,7 @@ export default class Screen { }) this.boundingRect = this.screen.getBoundingClientRect(); - this.onUpdateHook(width, height) + this.onUpdateHook?.(width, height) } setOnUpdate(cb: any) { @@ -255,6 +271,10 @@ export default class Screen { }, 750) } + public updateOverlayStyle(style: Partial) { + Object.assign(this.overlay.style, style) + } + public clearSelection() { if (this.selectionTargets.start && this.selectionTargets.end) { this.overlay.removeChild(this.selectionTargets.start); diff --git a/frontend/app/player/web/Screen/cursor.module.css b/frontend/app/player/web/Screen/cursor.module.css index 5da51ab0f..bebe9b346 100644 --- a/frontend/app/player/web/Screen/cursor.module.css +++ b/frontend/app/player/web/Screen/cursor.module.css @@ -68,9 +68,41 @@ } } -.cursor.clickedMobile::after { - -webkit-animation: anim-effect-sanja 1s ease-out forwards; - animation: anim-effect-sanja 1s ease-out forwards; +.mobileTouch { + width: 25px; + height: 25px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 42 42'%3E%3Cstyle%3E @keyframes x2menhl6cupbu94auppeu01e_t %7B 0%25 %7B transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); %7D 62.5%25 %7B transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 75%25 %7B transform: translate(23px, 23px) scale(0.9, 0.9) translate(-2px, -2px); animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 100%25 %7B transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); %7D %7D @keyframes x2menhl6cupbu94auppeu01e_sw %7B 0%25 %7B stroke-width: 2px; %7D 62.5%25 %7B stroke-width: 2px; animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 75%25 %7B stroke-width: 5px; animation-timing-function: cubic-bezier(0, 0, 0.6, 1); %7D 100%25 %7B stroke-width: 2px; %7D %7D %3C/style%3E%3Cellipse rx='20' ry='20' fill='rgba(0,0,0,0.50)' stroke='%23fff' stroke-width='2' transform='translate(23,23) translate(-2,-2)' style='animation: 0.6s infinite linear both x2menhl6cupbu94auppeu01e_t, 0.6s infinite linear both x2menhl6cupbu94auppeu01e_sw;' /%3E%3C/svg%3E")!important; +} + +@keyframes touch-animation { + 0% { + transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); + } + 31% { + transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); + animation-timing-function: cubic-bezier(0, 0, 0.6, 1); + } + 47.5% { + transform: translate(23px, 23px) scale(0.9, 0.9) translate(-2px, -2px); + animation-timing-function: cubic-bezier(0, 0, 0.6, 1); + } + 50% { + transform: translate(23px, 23px) scale(1, 1) translate(-2px, -2px); + } + 51% { + stroke-width: 2px; + } + 62.5% { + stroke-width: 2px; + animation-timing-function: cubic-bezier(0, 0, 0.6, 1); + } + 75% { + stroke-width: 5px; + animation-timing-function: cubic-bezier(0, 0, 0.6, 1); + } + 100% { + stroke-width: 2px; + } } @-webkit-keyframes anim-effect-sanja { diff --git a/frontend/app/player/web/Screen/screen.module.css b/frontend/app/player/web/Screen/screen.module.css index b69579935..84b856a12 100644 --- a/frontend/app/player/web/Screen/screen.module.css +++ b/frontend/app/player/web/Screen/screen.module.css @@ -24,4 +24,33 @@ .highlightoff { opacity: 0; transition: all 0.25s cubic-bezier(0, 0, 0.4, 1.0); +} + +.mobileScreen { + user-select: none; + position: absolute; + transform-origin: left top; + top: 50%; + left: 50%; + background-color: rgb(223, 223, 223); + border-radius: 70px; + box-shadow: 0 0 30px 0 rgba(0,0,0,0.2); /* 0 0 70px 30px rgba(0,0,0,0.1); */ + margin-top: -20px; +} +.mobileScreenFullview { + user-select: none; + position: absolute; + transform-origin: left top; + top: 50%; + left: 50%; + background-color: rgb(223, 223, 223); + border-radius: 70px; + box-shadow: 0 0 30px 0 rgba(0,0,0,0.2); + margin-top: 0!important; +} + +.mobileIframe { + position: absolute; + border: none; + background: none; } \ No newline at end of file diff --git a/frontend/app/player/web/addons/MouseTrail.ts b/frontend/app/player/web/addons/MouseTrail.ts index 803dd4ab7..a8893ac1f 100644 --- a/frontend/app/player/web/addons/MouseTrail.ts +++ b/frontend/app/player/web/addons/MouseTrail.ts @@ -5,6 +5,8 @@ const LINE_DURATION = 3.5; const LINE_WIDTH_START = 5; +export type SwipeEvent = { x: number; y: number; direction: 'up' | 'down' | 'left' | 'right' } + export default class MouseTrail { public isActive = true; public context: CanvasRenderingContext2D; @@ -13,13 +15,15 @@ export default class MouseTrail { * 1 - every frame, * 2 - every 2nd frame * and so on, doesn't always work properly - * but 1 doesnt affect performance so we fine + * but 1 doesnt affect performance so we're fine * */ private drawOnFrame = 1; private currentFrame = 0; + private lineDuration = LINE_DURATION; private points: Point[] = []; + private swipePoints: Point[] = [] - constructor(private readonly canvas: HTMLCanvasElement) { + constructor(private readonly canvas: HTMLCanvasElement, isNativeMobile: boolean = false) { // @ts-ignore patching window window.requestAnimFrame = window.requestAnimationFrame || @@ -34,6 +38,10 @@ export default class MouseTrail { function (callback: any) { window.setTimeout(callback, 1000 / 60); }; + + if (isNativeMobile) { + this.lineDuration = 5 + } } resizeCanvas = (w: number, h: number) => { @@ -73,10 +81,31 @@ export default class MouseTrail { } }; + createSwipeTrail = ({ x, y, direction }: SwipeEvent) => { + const startCoords = this.calculateTrail({ x, y, direction }); + this.addPoint(startCoords.x, startCoords.y); + this.addPoint(x, y); + } + + calculateTrail = ({ x, y, direction }: SwipeEvent) => { + switch (direction) { + case 'up': + return { x, y: y - 20 }; + case 'down': + return { x, y: y + 20 }; + case 'left': + return { x: x - 20, y }; + case 'right': + return { x: x + 20, y }; + default: + return { x, y }; + } + } + animatePoints = () => { this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); - const duration = (LINE_DURATION * 1000) / 60; + const duration = (this.lineDuration * 1000) / 60; let point, lastPoint; for (let i = 0; i < this.points.length; i++) { diff --git a/frontend/app/player/web/managers/ActivityManager.ts b/frontend/app/player/web/managers/ActivityManager.ts index 383f29c15..224345933 100644 --- a/frontend/app/player/web/managers/ActivityManager.ts +++ b/frontend/app/player/web/managers/ActivityManager.ts @@ -14,10 +14,9 @@ class SkipIntervalCls { export type SkipInterval = InstanceType; - export default class ActivityManager extends ListWalker { - private endTime: number = 0; - private minInterval: number = 0; + private readonly endTime: number = 0; + private readonly minInterval: number = 0; private lastActivity: number = 0; constructor(duration: number) { super(); diff --git a/frontend/app/player/web/managers/MouseMoveManager.ts b/frontend/app/player/web/managers/MouseMoveManager.ts index 3374f6024..84da68a90 100644 --- a/frontend/app/player/web/managers/MouseMoveManager.ts +++ b/frontend/app/player/web/managers/MouseMoveManager.ts @@ -9,7 +9,7 @@ import { MOUSE_TRAIL } from "App/constants/storageKeys"; export default class MouseMoveManager extends ListWalker { private hoverElements: Array = [] private mouseTrail: MouseTrail | undefined - private removeMouseTrail = false + private readonly removeMouseTrail: boolean = false constructor(private screen: Screen) { super() diff --git a/frontend/app/player/web/messages/MFileReader.ts b/frontend/app/player/web/messages/MFileReader.ts index 0a5e590c5..1fdeb3cbe 100644 --- a/frontend/app/player/web/messages/MFileReader.ts +++ b/frontend/app/player/web/messages/MFileReader.ts @@ -101,7 +101,7 @@ export default class MFileReader extends RawMessageReader { const index = this.noIndexes ? 0 : this.getLastMessageID() const msg = Object.assign(rewriteMessage(rMsg), { - time: this.currentTime, + time: this.currentTime || rMsg.timestamp - this.startTime!, tabId: this.currentTab, }, !this.noIndexes ? { _index: index } : {}) diff --git a/frontend/app/player/web/messages/RawMessageReader.gen.ts b/frontend/app/player/web/messages/RawMessageReader.gen.ts index 18b01eaba..6edd13fce 100644 --- a/frontend/app/player/web/messages/RawMessageReader.gen.ts +++ b/frontend/app/player/web/messages/RawMessageReader.gen.ts @@ -719,7 +719,7 @@ export default class RawMessageReader extends PrimitiveReader { const name = this.readString(); if (name === null) { return resetPointer() } const payload = this.readString(); if (payload === null) { return resetPointer() } return { - tp: MType.IosCustomEvent, + tp: MType.IosEvent, timestamp, length, name, @@ -805,6 +805,18 @@ export default class RawMessageReader extends PrimitiveReader { }; } + case 104: { + const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() } + const length = this.readUint(); if (length === null) { return resetPointer() } + const content = this.readString(); if (content === null) { return resetPointer() } + return { + tp: MType.IosInternalError, + timestamp, + length, + content, + }; + } + case 105: { const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() } const length = this.readUint(); if (length === null) { return resetPointer() } @@ -847,6 +859,22 @@ export default class RawMessageReader extends PrimitiveReader { }; } + case 111: { + const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() } + const type = this.readString(); if (type === null) { return resetPointer() } + const contextString = this.readString(); if (contextString === null) { return resetPointer() } + const context = this.readString(); if (context === null) { return resetPointer() } + const payload = this.readString(); if (payload === null) { return resetPointer() } + return { + tp: MType.IosIssueEvent, + timestamp, + type, + contextString, + context, + payload, + }; + } + default: throw new Error(`Unrecognizable message type: ${ tp }; Pointer at the position ${this.p} of ${this.buf.length}`) return null; diff --git a/frontend/app/player/web/messages/filters.gen.ts b/frontend/app/player/web/messages/filters.gen.ts index 12c16e6bd..1c8a091c6 100644 --- a/frontend/app/player/web/messages/filters.gen.ts +++ b/frontend/app/player/web/messages/filters.gen.ts @@ -3,7 +3,8 @@ import { MType } from './raw.gen' -const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,117,118,93,96,100,101,102,103,105,106] +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] export function isDOMType(t: MType) { return DOM_TYPES.includes(t) } \ No newline at end of file diff --git a/frontend/app/player/web/messages/message.gen.ts b/frontend/app/player/web/messages/message.gen.ts index 06148bd0d..ef7922b71 100644 --- a/frontend/app/player/web/messages/message.gen.ts +++ b/frontend/app/player/web/messages/message.gen.ts @@ -61,14 +61,16 @@ import type { RawResourceTiming, RawTabChange, RawTabData, - RawIosCustomEvent, + RawIosEvent, RawIosScreenChanges, RawIosClickEvent, RawIosInputEvent, RawIosPerformanceEvent, RawIosLog, + RawIosInternalError, RawIosNetworkCall, RawIosSwipeEvent, + RawIosIssueEvent, } from './raw.gen' export type Message = RawMessage & Timed @@ -188,7 +190,7 @@ export type TabChange = RawTabChange & Timed export type TabData = RawTabData & Timed -export type IosCustomEvent = RawIosCustomEvent & Timed +export type IosEvent = RawIosEvent & Timed export type IosScreenChanges = RawIosScreenChanges & Timed @@ -200,7 +202,11 @@ export type IosPerformanceEvent = RawIosPerformanceEvent & Timed export type IosLog = RawIosLog & Timed +export type IosInternalError = RawIosInternalError & Timed + export type IosNetworkCall = RawIosNetworkCall & Timed export type IosSwipeEvent = RawIosSwipeEvent & Timed +export type IosIssueEvent = RawIosIssueEvent & Timed + diff --git a/frontend/app/player/web/messages/raw.gen.ts b/frontend/app/player/web/messages/raw.gen.ts index 02d5397d3..d910ee949 100644 --- a/frontend/app/player/web/messages/raw.gen.ts +++ b/frontend/app/player/web/messages/raw.gen.ts @@ -59,14 +59,16 @@ export const enum MType { ResourceTiming = 116, TabChange = 117, TabData = 118, - IosCustomEvent = 93, + IosEvent = 93, IosScreenChanges = 96, IosClickEvent = 100, IosInputEvent = 101, IosPerformanceEvent = 102, IosLog = 103, + IosInternalError = 104, IosNetworkCall = 105, IosSwipeEvent = 106, + IosIssueEvent = 111, } @@ -474,8 +476,8 @@ export interface RawTabData { tabId: string, } -export interface RawIosCustomEvent { - tp: MType.IosCustomEvent, +export interface RawIosEvent { + tp: MType.IosEvent, timestamp: number, length: number, name: string, @@ -526,6 +528,13 @@ export interface RawIosLog { content: string, } +export interface RawIosInternalError { + tp: MType.IosInternalError, + timestamp: number, + length: number, + content: string, +} + export interface RawIosNetworkCall { tp: MType.IosNetworkCall, timestamp: number, @@ -549,5 +558,14 @@ export interface RawIosSwipeEvent { direction: string, } +export interface RawIosIssueEvent { + tp: MType.IosIssueEvent, + timestamp: number, + type: string, + contextString: string, + context: string, + payload: string, +} -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 | RawRedux | 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 | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall | RawIosSwipeEvent; + +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 | RawRedux | 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 | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent; diff --git a/frontend/app/player/web/messages/tracker-legacy.gen.ts b/frontend/app/player/web/messages/tracker-legacy.gen.ts index 0508a313a..10eae3770 100644 --- a/frontend/app/player/web/messages/tracker-legacy.gen.ts +++ b/frontend/app/player/web/messages/tracker-legacy.gen.ts @@ -60,12 +60,14 @@ export const TP_MAP = { 116: MType.ResourceTiming, 117: MType.TabChange, 118: MType.TabData, - 93: MType.IosCustomEvent, + 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, } as const diff --git a/frontend/app/player/web/types/resource.ts b/frontend/app/player/web/types/resource.ts index 26e8bb8c4..03b7ade26 100644 --- a/frontend/app/player/web/types/resource.ts +++ b/frontend/app/player/web/types/resource.ts @@ -1,4 +1,4 @@ -import type { ResourceTiming, NetworkRequest, Fetch } from '../messages' +import type {ResourceTiming, NetworkRequest, Fetch, IosNetworkCall} from '../messages' export const enum ResourceType { XHR = 'xhr', @@ -103,7 +103,7 @@ export function getResourceFromResourceTiming(msg: ResourceTiming, sessStart: nu }) } -export function getResourceFromNetworkRequest(msg: NetworkRequest | Fetch, sessStart: number) { +export function getResourceFromNetworkRequest(msg: NetworkRequest | Fetch | IosNetworkCall, sessStart: number) { return Resource({ ...msg, // @ts-ignore diff --git a/frontend/app/svg/icons/battery-charging.svg b/frontend/app/svg/icons/battery-charging.svg new file mode 100644 index 000000000..ceb36c6b6 --- /dev/null +++ b/frontend/app/svg/icons/battery-charging.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/battery.svg b/frontend/app/svg/icons/battery.svg new file mode 100644 index 000000000..2f45978b4 --- /dev/null +++ b/frontend/app/svg/icons/battery.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/low-disc-space.svg b/frontend/app/svg/icons/low-disc-space.svg new file mode 100644 index 000000000..e43417717 --- /dev/null +++ b/frontend/app/svg/icons/low-disc-space.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/app/svg/icons/memory-ios.svg b/frontend/app/svg/icons/memory-ios.svg new file mode 100644 index 000000000..81d908498 --- /dev/null +++ b/frontend/app/svg/icons/memory-ios.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/thermometer-sun.svg b/frontend/app/svg/icons/thermometer-sun.svg new file mode 100644 index 000000000..cdda750f3 --- /dev/null +++ b/frontend/app/svg/icons/thermometer-sun.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts index a20bcc768..0dd1864c7 100644 --- a/frontend/app/types/session/event.ts +++ b/frontend/app/types/session/event.ts @@ -4,8 +4,13 @@ const INPUT = 'INPUT'; const LOCATION = 'LOCATION'; const CUSTOM = 'CUSTOM'; const CLICKRAGE = 'CLICKRAGE'; +const TAPRAGE = 'tap_rage' const IOS_VIEW = 'VIEW'; -export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW }; + +const TOUCH = 'TAP'; +const SWIPE = 'SWIPE'; + +export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW, TOUCH, SWIPE, TAPRAGE }; export type EventType = | typeof CONSOLE @@ -13,7 +18,10 @@ export type EventType = | typeof INPUT | typeof LOCATION | typeof CUSTOM - | typeof CLICKRAGE; + | typeof CLICKRAGE + | typeof IOS_VIEW + | typeof TOUCH + | typeof SWIPE; interface IEvent { time: number; @@ -41,6 +49,16 @@ interface ClickEvent extends IEvent { hesitation: number; } +interface TouchEvent extends IEvent { + targetContent: string; + count: number; +} + +interface SwipeEvent extends IEvent { + direction: 'left' | 'right' | 'up' | 'down'; + label: string +} + interface InputEvent extends IEvent { value: string; hesitation: number; @@ -86,6 +104,19 @@ class Event { } } +class Swipe extends Event { + readonly type = SWIPE; + readonly name = 'Swipe'; + readonly label: string; + readonly direction: string; + + constructor(evt: SwipeEvent) { + super(evt); + this.label = evt.label; + this.direction = evt.direction; + } +} + class Console extends Event { readonly type = CONSOLE; readonly name = 'Console'; @@ -117,6 +148,20 @@ export class Click extends Event { } } +export class Touch extends Event { + readonly type: typeof TOUCH = TOUCH; + readonly name = 'Tap'; + targetContent = ''; + count: number; + hesitation: number = 0; + + constructor(evt: TouchEvent) { + super(evt); + this.targetContent = evt.targetContent; + this.count = evt.count; + } +} + class Input extends Event { readonly type = INPUT; readonly name = 'Input'; @@ -156,27 +201,28 @@ export class Location extends Event { } } -export type InjectedEvent = Console | Click | Input | Location; +export type InjectedEvent = Console | Click | Input | Location | Touch | Swipe; export default function (event: EventData) { - if (event.type && event.type === CONSOLE) { - return new Console(event as ConsoleEvent); + if (!event.type) { + return console.error('Unknown event type: ', event) } - if (event.type && event.type === CLICK) { - return new Click(event as ClickEvent); + switch (event.type) { + case CONSOLE: + return new Console(event as ConsoleEvent); + case TOUCH: + return new Touch(event as TouchEvent) + case CLICK: + return new Click(event as ClickEvent); + case INPUT: + return new Input(event as InputEvent); + case LOCATION: + return new Location(event as LocationEvent); + case CLICKRAGE: + return new Click(event as ClickEvent, true); + case SWIPE: + return new Swipe(event as SwipeEvent); + default: + return console.error(`Unknown event type: ${event.type}`); } - if (event.type && event.type === INPUT) { - return new Input(event as InputEvent); - } - if (event.type && event.type === LOCATION) { - return new Location(event as LocationEvent); - } - if (event.type && event.type === CLICKRAGE) { - return new Click(event as ClickEvent, true); - } - // not used right now? - // if (event.type === CUSTOM || !event.type) { - // return new Event(event) - // } - console.error(`Unknown event type: ${event.type}`); } diff --git a/frontend/app/types/session/issue.ts b/frontend/app/types/session/issue.ts index 85b5d4236..303b0571d 100644 --- a/frontend/app/types/session/issue.ts +++ b/frontend/app/types/session/issue.ts @@ -7,6 +7,7 @@ export const types = { CRASH: 'crash', CLICK_RAGE: 'click_rage', MOUSE_THRASHING: 'mouse_thrashing', + TAP_RAGE: 'tap_rage', } as const type TypeKeys = keyof typeof types @@ -23,6 +24,7 @@ export const issues_types = [ { 'type': types.CLICK_RAGE, 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' }, { 'type': types.CRASH, 'visible': true, 'order': 4, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' }, { 'type': types.MOUSE_THRASHING, 'visible': true, 'order': 5, 'name': 'Mouse Thrashing', 'icon': 'cursor-trash' }, + { 'type': types.TAP_RAGE, 'visible': true, 'order': 6, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' }, // { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' }, // { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' }, // { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' }, diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index 0301d54ce..67eff4f8f 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -51,6 +51,18 @@ function hashString(s: string): number { return hash; } +interface IosCrash { + crashId: string + name: string + projectId: number + reason: string + seqIndex: number + sessionId: string + stacktrace: string + time: number + timestamp: number +} + export interface ISession { sessionId: string; pageTitle: string; @@ -62,6 +74,7 @@ export interface ISession { startedAt: number; duration: number; events: InjectedEvent[]; + crashes: IosCrash[] stackEvents: StackEvent[]; metadata: []; favorite: boolean; @@ -98,7 +111,6 @@ export interface ISession { userDeviceHeapSize: number; userDeviceMemorySize: number; errors: SessionError[]; - crashes?: []; socket: string; isIOS: boolean; revId: string | null; @@ -108,7 +120,7 @@ export interface ISession { notes: Note[]; notesWithEvents: Array; fileKey: string; - platform: string; + platform: "web" | "ios" | "android"; projectId: string; startTs: number; timestamp: number; @@ -138,7 +150,8 @@ const emptyValues = { notes: [], metadata: {}, startedAt: 0, -}; + platform: 'web', +} as const export default class Session { sessionId: ISession['sessionId']; @@ -198,6 +211,7 @@ export default class Session { notesWithEvents: ISession['notesWithEvents']; frustrations: Array timezone?: ISession['timezone']; + platform: ISession['platform']; fileKey: ISession['fileKey']; durationSeconds: number; @@ -218,6 +232,7 @@ export default class Session { domURL = [], devtoolsURL = [], mobsUrl = [], + crashes = [], notes = [], ...session } = sessionData; @@ -253,7 +268,7 @@ export default class Session { stackEventsList.push(...mergedArrays); } - const exceptions = (errors as IError[]).map((e) => new SessionError(e)) || []; + const exceptions = (errors as IError[])?.map((e) => new SessionError(e)) || []; const issuesList = (issues as IIssue[]).map( @@ -291,6 +306,7 @@ export default class Session { isMobile, startedAt, duration, + crashes, durationSeconds, userNumericHash: hashString( session.userId || @@ -316,13 +332,14 @@ export default class Session { addEvents( sessionEvents: EventData[], + crashes: IosCrash[], errors: any[], issues: any[], resources: any[], - userEvents: any[], - stackEvents: any[] + userEvents: any[] = [], + stackEvents: any[] = [] ) { - const exceptions = (errors as IError[]).map((e) => new SessionError(e)) || []; + const exceptions = (errors as IError[])?.map((e) => new SessionError(e)) || []; const issuesList = (issues as IIssue[]).map( (i, k) => new Issue({ ...i, time: i.timestamp - this.startedAt, key: k }) @@ -356,11 +373,11 @@ export default class Session { // @ts-ignore return ev.hesitation > 1000 } - return ev.type === TYPES.CLICKRAGE + return ev.type === TYPES.CLICKRAGE || ev.type === TYPES.TAPRAGE } ) - const frustrationIssues = issuesList.filter(i => i.type === issueTypes.MOUSE_THRASHING) + const frustrationIssues = issuesList.filter(i => i.type === issueTypes.MOUSE_THRASHING || i.type === issueTypes.TAP_RAGE) const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || []; const mixedEventsWithIssues = mergeEventLists( @@ -378,6 +395,7 @@ export default class Session { this.stackEvents = stackEventsList; // @ts-ignore this.frustrations = frustrationList; + this.crashes = crashes || []; return this; } diff --git a/frontend/app/types/site/site.js b/frontend/app/types/site/site.js index c5c0e4f1e..2d4a208ca 100644 --- a/frontend/app/types/site/site.js +++ b/frontend/app/types/site/site.js @@ -15,6 +15,7 @@ export default Record({ id: undefined, name: '', host: '', + platform: 'web', status: RED, lastRecordedSessionAt: undefined, gdpr: GDPR(), @@ -33,7 +34,7 @@ export default Record({ toData() { const js = this.toJS(); - + console.log(js, this) delete js.key; delete js.gdpr; return js; diff --git a/frontend/app/utils/index.ts b/frontend/app/utils/index.ts index 6f57e6f29..50314c956 100644 --- a/frontend/app/utils/index.ts +++ b/frontend/app/utils/index.ts @@ -181,7 +181,7 @@ export const camelCased = (val) => }); export function capitalize(s: string) { - if (!s || s.length === 0) return s; + if (!s || !s.length) return s; return s[0].toUpperCase() + s.slice(1); } diff --git a/mobs/ios_messages.rb b/mobs/ios_messages.rb index 0b9775d98..e3159d557 100644 --- a/mobs/ios_messages.rb +++ b/mobs/ios_messages.rb @@ -22,7 +22,7 @@ message 92, 'IOSMetadata' do string 'Value' end -message 93, 'IOSCustomEvent', :replayer => true do +message 93, 'IOSEvent', :replayer => true do uint 'Timestamp' uint 'Length' string 'Name' @@ -32,13 +32,13 @@ end message 94, 'IOSUserID' do uint 'Timestamp' uint 'Length' - string 'Value' + string 'ID' end message 95, 'IOSUserAnonymousID' do uint 'Timestamp' uint 'Length' - string 'Value' + string 'ID' end message 96, 'IOSScreenChanges', :replayer => true do @@ -86,15 +86,16 @@ end Name/Value may be : "physicalMemory": Total memory in bytes "processorCount": Total processors in device -?"activeProcessorCount": Number of currently used processors +"activeProcessorCount": Number of currently used processors "systemUptime": Elapsed time (in seconds) since last boot -?"isLowPowerModeEnabled": Possible values (1 or 0) -2/3!"thermalState": Possible values (0:nominal 1:fair 2:serious 3:critical) -!"batteryLevel": Possible values (0 .. 100) +"isLowPowerModeEnabled": Possible values (1 or 0) +"thermalState": Possible values (0:nominal 1:fair 2:serious 3:critical) +"batteryLevel": Possible values (0 .. 100) "batteryState": Possible values (0:unknown 1:unplugged 2:charging 3:full) "orientation": Possible values (0unknown 1:portrait 2:portraitUpsideDown 3:landscapeLeft 4:landscapeRight 5:faceUp 6:faceDown) "mainThreadCPU": Possible values (0 .. 100) "memoryUsage": Used memory in bytes +"fps": Frames per second =end message 102, 'IOSPerformanceEvent', :replayer => true do uint 'Timestamp' @@ -110,22 +111,22 @@ message 103, 'IOSLog', :replayer => true do string 'Content' end -message 104, 'IOSInternalError' do +message 104, 'IOSInternalError', :replayer => true do uint 'Timestamp' uint 'Length' string 'Content' end message 105, 'IOSNetworkCall', :replayer => true do - uint 'Timestamp' - uint 'Length' - string 'Type' - string 'Method' - string 'URL' - string 'Request' - string 'Response' - uint 'Status' - uint 'Duration' + uint 'Timestamp' + uint 'Length' + string 'Type' + string 'Method' + string 'URL' + string 'Request' + string 'Response' + uint 'Status' + uint 'Duration' end message 106, 'IOSSwipeEvent', :replayer => true do @@ -137,13 +138,13 @@ message 106, 'IOSSwipeEvent', :replayer => true do string 'Direction' end -message 107, 'IOSBatchMeta', :replayer => false do +message 107, 'IOSBatchMeta' do uint 'Timestamp' uint 'Length' uint 'FirstIndex' end -message 110, 'IOSPerformanceAggregated', :swift => false do +message 110, 'IOSPerformanceAggregated', :swift => false do uint 'TimestampStart' uint 'TimestampEnd' uint 'MinFPS' @@ -160,7 +161,7 @@ message 110, 'IOSPerformanceAggregated', :swift => false do uint 'MaxBattery' end -message 111, 'IOSIssueEvent' do +message 111, 'IOSIssueEvent', :replayer => true do uint 'Timestamp' string 'Type' string 'ContextString' diff --git a/mobs/templates/frontend~app~player~web~messages~filters.gen.ts.erb b/mobs/templates/frontend~app~player~web~messages~filters.gen.ts.erb index 370d27798..8d383bf7f 100644 --- a/mobs/templates/frontend~app~player~web~messages~filters.gen.ts.erb +++ b/mobs/templates/frontend~app~player~web~messages~filters.gen.ts.erb @@ -3,7 +3,8 @@ import { MType } from './raw.gen' -const DOM_TYPES = [<%= $messages.select { |msg| msg.replayer == true }.map { |msg| "#{msg.id}" }.join "," %>] +const IOS_TYPES = [<%= $messages.select { |msg| msg.context == :ios }.map{ |msg| "#{msg.id}" }.join(",") %>] +const DOM_TYPES = [<%= $messages.select { |msg| msg.replayer == true && msg.context != :ios }.map { |msg| "#{msg.id}" }.join "," %>] export function isDOMType(t: MType) { return DOM_TYPES.includes(t) } \ No newline at end of file diff --git a/tracker/tracker-testing-playground/package.json b/tracker/tracker-testing-playground/package.json index a8386d93a..8137d1c76 100644 --- a/tracker/tracker-testing-playground/package.json +++ b/tracker/tracker-testing-playground/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@openreplay/tracker": "file:../../../openreplay/tracker/tracker", + "@openreplay/tracker": "file:../tracker", "@openreplay/tracker-redux": "^3.5.1", "@tanstack/react-table": "^8.2.6", "@types/node": "^12.0.0",