diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 811ecdaf0..8585b8f66 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -60,7 +60,7 @@ func main() { messages.MsgFetch, messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick, messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated, messages.MsgPageLoadTiming, messages.MsgPageRenderTiming, - messages.MsgPageEvent, messages.MsgMouseThrashing, messages.MsgInputChange, + messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange, messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger, // Mobile messages messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID, diff --git a/backend/pkg/db/postgres/bulks.go b/backend/pkg/db/postgres/bulks.go index cd73fc6a9..0ba9e0ccd 100644 --- a/backend/pkg/db/postgres/bulks.go +++ b/backend/pkg/db/postgres/bulks.go @@ -126,11 +126,11 @@ func (conn *BulkSet) initBulks() { "events.pages", "(session_id, message_id, timestamp, referrer, base_referrer, host, path, query, dom_content_loaded_time, "+ "load_time, response_end, first_paint_time, first_contentful_paint_time, speed_index, visually_complete, "+ - "time_to_interactive, response_time, dom_building_time)", + "time_to_interactive, response_time, dom_building_time, web_vitals)", "($%d, $%d, $%d, LEFT($%d, 8000), LEFT($%d, 8000), LEFT($%d, 300), LEFT($%d, 2000), LEFT($%d, 8000), "+ "NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0),"+ - " NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0))", - 18, 200) + " NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, ''))", + 19, 200) if err != nil { conn.log.Fatal(conn.ctx, "can't create webPageEvents bulk: %s", err) } diff --git a/backend/pkg/db/postgres/events.go b/backend/pkg/db/postgres/events.go index a6be4c99a..47b9cb566 100644 --- a/backend/pkg/db/postgres/events.go +++ b/backend/pkg/db/postgres/events.go @@ -116,7 +116,8 @@ func (conn *Conn) InsertWebPageEvent(sess *sessions.Session, e *messages.PageEve // base_path is deprecated if err = conn.bulks.Get("webPageEvents").Append(sess.SessionID, truncSqIdx(e.MessageID), e.Timestamp, e.Referrer, url.DiscardURLQuery(e.Referrer), host, path, query, e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint, - e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e)); err != nil { + e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e), + e.WebVitals); err != nil { sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID) conn.log.Error(sessCtx, "insert web page event in bulk err: %s", err) } diff --git a/backend/pkg/handlers/custom/pageEventBuilder.go b/backend/pkg/handlers/custom/pageEventBuilder.go index 5bab7d4cc..589551b79 100644 --- a/backend/pkg/handlers/custom/pageEventBuilder.go +++ b/backend/pkg/handlers/custom/pageEventBuilder.go @@ -1,6 +1,8 @@ package custom import ( + "encoding/json" + "fmt" . "openreplay/backend/pkg/messages" ) @@ -9,6 +11,7 @@ const PageEventTimeout = 1 * 60 * 1000 type pageEventBuilder struct { pageEvent *PageEvent firstTimingHandled bool + webVitals map[string]string } func NewPageEventBuilder() *pageEventBuilder { @@ -69,7 +72,7 @@ func (b *pageEventBuilder) Handle(message Message, timestamp uint64) Message { if msg.FirstContentfulPaint <= 30000 { b.pageEvent.FirstContentfulPaint = msg.FirstContentfulPaint } - return b.buildIfTimingsComplete() + return nil //b.buildIfTimingsComplete() case *PageRenderTiming: if b.pageEvent == nil { break @@ -77,8 +80,12 @@ func (b *pageEventBuilder) Handle(message Message, timestamp uint64) Message { b.pageEvent.SpeedIndex = msg.SpeedIndex b.pageEvent.VisuallyComplete = msg.VisuallyComplete b.pageEvent.TimeToInteractive = msg.TimeToInteractive - return b.buildIfTimingsComplete() - + return nil //b.buildIfTimingsComplete() + case *WebVitals: + if b.webVitals == nil { + b.webVitals = make(map[string]string) + } + b.webVitals[msg.Name] = msg.Value } if b.pageEvent != nil && b.pageEvent.Timestamp+PageEventTimeout < timestamp { @@ -94,13 +101,21 @@ func (b *pageEventBuilder) Build() Message { pageEvent := b.pageEvent b.pageEvent = nil b.firstTimingHandled = false + if b.webVitals != nil { + if vitals, err := json.Marshal(b.webVitals); err == nil { + pageEvent.WebVitals = string(vitals) + } else { + // DEBUG + fmt.Printf("Error marshalling web vitals: %v\n", err) + } + } return pageEvent } -func (b *pageEventBuilder) buildIfTimingsComplete() Message { - if b.firstTimingHandled { - return b.Build() - } - b.firstTimingHandled = true - return nil -} +//func (b *pageEventBuilder) buildIfTimingsComplete() Message { +// if b.firstTimingHandled { +// return b.Build() +// } +// b.firstTimingHandled = true +// return nil +//} diff --git a/backend/pkg/messages/filters.go b/backend/pkg/messages/filters.go index eea77cf9d..7a363f3d8 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 && 107 != id && 110 != id + return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 124 != 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 IsMobileType(id int) bool { diff --git a/backend/pkg/messages/legacy-message-transform.go b/backend/pkg/messages/legacy-message-transform.go index fbb7a8222..23ab1e52d 100644 --- a/backend/pkg/messages/legacy-message-transform.go +++ b/backend/pkg/messages/legacy-message-transform.go @@ -59,6 +59,27 @@ func transformDeprecated(msg Message) Message { NavigationStart: m.NavigationStart, DocumentTitle: "", } + case *PageEventDeprecated: + return &PageEvent{ + MessageID: m.MessageID, + Timestamp: m.Timestamp, + URL: m.URL, + Referrer: m.Referrer, + Loaded: m.Loaded, + RequestStart: m.RequestStart, + ResponseStart: m.ResponseStart, + ResponseEnd: m.ResponseEnd, + DomContentLoadedEventStart: m.DomContentLoadedEventStart, + DomContentLoadedEventEnd: m.DomContentLoadedEventEnd, + LoadEventStart: m.LoadEventStart, + LoadEventEnd: m.LoadEventEnd, + FirstPaint: m.FirstPaint, + FirstContentfulPaint: m.FirstContentfulPaint, + SpeedIndex: m.SpeedIndex, + VisuallyComplete: m.VisuallyComplete, + TimeToInteractive: m.TimeToInteractive, + WebVitals: "", + } } return msg } diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index 5b0c4acee..19be1a0c7 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -32,8 +32,9 @@ const ( MsgUserID = 28 MsgUserAnonymousID = 29 MsgMetadata = 30 - MsgPageEvent = 31 + MsgPageEventDeprecated = 31 MsgInputEvent = 32 + MsgPageEvent = 33 MsgCSSInsertRule = 37 MsgCSSDeleteRule = 38 MsgFetch = 39 @@ -91,6 +92,7 @@ const ( MsgRedux = 121 MsgSetPageLocation = 122 MsgGraphQL = 123 + MsgWebVitals = 124 MsgIssueEvent = 125 MsgSessionEnd = 126 MsgSessionSearch = 127 @@ -874,7 +876,7 @@ func (msg *Metadata) TypeID() int { return 30 } -type PageEvent struct { +type PageEventDeprecated struct { message MessageID uint64 Timestamp uint64 @@ -895,7 +897,7 @@ type PageEvent struct { TimeToInteractive uint64 } -func (msg *PageEvent) Encode() []byte { +func (msg *PageEventDeprecated) Encode() []byte { buf := make([]byte, 171+len(msg.URL)+len(msg.Referrer)) buf[0] = 31 p := 1 @@ -919,11 +921,11 @@ func (msg *PageEvent) Encode() []byte { return buf[:p] } -func (msg *PageEvent) Decode() Message { +func (msg *PageEventDeprecated) Decode() Message { return msg } -func (msg *PageEvent) TypeID() int { +func (msg *PageEventDeprecated) TypeID() int { return 31 } @@ -956,6 +958,61 @@ func (msg *InputEvent) TypeID() int { return 32 } +type PageEvent struct { + message + MessageID uint64 + Timestamp uint64 + URL string + Referrer string + Loaded bool + RequestStart uint64 + ResponseStart uint64 + ResponseEnd uint64 + DomContentLoadedEventStart uint64 + DomContentLoadedEventEnd uint64 + LoadEventStart uint64 + LoadEventEnd uint64 + FirstPaint uint64 + FirstContentfulPaint uint64 + SpeedIndex uint64 + VisuallyComplete uint64 + TimeToInteractive uint64 + WebVitals string +} + +func (msg *PageEvent) Encode() []byte { + buf := make([]byte, 181+len(msg.URL)+len(msg.Referrer)+len(msg.WebVitals)) + buf[0] = 33 + p := 1 + p = WriteUint(msg.MessageID, buf, p) + p = WriteUint(msg.Timestamp, buf, p) + p = WriteString(msg.URL, buf, p) + p = WriteString(msg.Referrer, buf, p) + p = WriteBoolean(msg.Loaded, buf, p) + p = WriteUint(msg.RequestStart, buf, p) + p = WriteUint(msg.ResponseStart, buf, p) + p = WriteUint(msg.ResponseEnd, buf, p) + p = WriteUint(msg.DomContentLoadedEventStart, buf, p) + p = WriteUint(msg.DomContentLoadedEventEnd, buf, p) + p = WriteUint(msg.LoadEventStart, buf, p) + p = WriteUint(msg.LoadEventEnd, buf, p) + p = WriteUint(msg.FirstPaint, buf, p) + p = WriteUint(msg.FirstContentfulPaint, buf, p) + p = WriteUint(msg.SpeedIndex, buf, p) + p = WriteUint(msg.VisuallyComplete, buf, p) + p = WriteUint(msg.TimeToInteractive, buf, p) + p = WriteString(msg.WebVitals, buf, p) + return buf[:p] +} + +func (msg *PageEvent) Decode() Message { + return msg +} + +func (msg *PageEvent) TypeID() int { + return 33 +} + type CSSInsertRule struct { message ID uint64 @@ -2443,6 +2500,29 @@ func (msg *GraphQL) TypeID() int { return 123 } +type WebVitals struct { + message + Name string + Value string +} + +func (msg *WebVitals) Encode() []byte { + buf := make([]byte, 21+len(msg.Name)+len(msg.Value)) + buf[0] = 124 + p := 1 + p = WriteString(msg.Name, buf, p) + p = WriteString(msg.Value, buf, p) + return buf[:p] +} + +func (msg *WebVitals) Decode() Message { + return msg +} + +func (msg *WebVitals) TypeID() int { + return 124 +} + type IssueEvent struct { message MessageID uint64 diff --git a/backend/pkg/messages/read-message.go b/backend/pkg/messages/read-message.go index 4ddde9c3b..3521a0e29 100644 --- a/backend/pkg/messages/read-message.go +++ b/backend/pkg/messages/read-message.go @@ -468,9 +468,9 @@ func DecodeMetadata(reader BytesReader) (Message, error) { return msg, err } -func DecodePageEvent(reader BytesReader) (Message, error) { +func DecodePageEventDeprecated(reader BytesReader) (Message, error) { var err error = nil - msg := &PageEvent{} + msg := &PageEventDeprecated{} if msg.MessageID, err = reader.ReadUint(); err != nil { return nil, err } @@ -546,6 +546,66 @@ func DecodeInputEvent(reader BytesReader) (Message, error) { return msg, err } +func DecodePageEvent(reader BytesReader) (Message, error) { + var err error = nil + msg := &PageEvent{} + if msg.MessageID, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.Timestamp, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.URL, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Referrer, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Loaded, err = reader.ReadBoolean(); err != nil { + return nil, err + } + if msg.RequestStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.ResponseStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.ResponseEnd, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.DomContentLoadedEventStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.DomContentLoadedEventEnd, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.LoadEventStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.LoadEventEnd, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.FirstPaint, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.FirstContentfulPaint, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.SpeedIndex, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.VisuallyComplete, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.TimeToInteractive, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.WebVitals, err = reader.ReadString(); err != nil { + return nil, err + } + return msg, err +} + func DecodeCSSInsertRule(reader BytesReader) (Message, error) { var err error = nil msg := &CSSInsertRule{} @@ -1494,6 +1554,18 @@ func DecodeGraphQL(reader BytesReader) (Message, error) { return msg, err } +func DecodeWebVitals(reader BytesReader) (Message, error) { + var err error = nil + msg := &WebVitals{} + if msg.Name, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Value, err = reader.ReadString(); err != nil { + return nil, err + } + return msg, err +} + func DecodeIssueEvent(reader BytesReader) (Message, error) { var err error = nil msg := &IssueEvent{} @@ -2019,9 +2091,11 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) { case 30: return DecodeMetadata(reader) case 31: - return DecodePageEvent(reader) + return DecodePageEventDeprecated(reader) case 32: return DecodeInputEvent(reader) + case 33: + return DecodePageEvent(reader) case 37: return DecodeCSSInsertRule(reader) case 38: @@ -2136,6 +2210,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) { return DecodeSetPageLocation(reader) case 123: return DecodeGraphQL(reader) + case 124: + return DecodeWebVitals(reader) case 125: return DecodeIssueEvent(reader) case 126: diff --git a/ee/connectors/msgcodec/messages.py b/ee/connectors/msgcodec/messages.py index f14b05f92..0ad3a15be 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -281,7 +281,7 @@ class Metadata(Message): self.value = value -class PageEvent(Message): +class PageEventDeprecated(Message): __id__ = 31 def __init__(self, message_id, timestamp, url, referrer, loaded, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint, speed_index, visually_complete, time_to_interactive): @@ -315,6 +315,30 @@ class InputEvent(Message): self.label = label +class PageEvent(Message): + __id__ = 33 + + def __init__(self, message_id, timestamp, url, referrer, loaded, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint, speed_index, visually_complete, time_to_interactive, web_vitals): + self.message_id = message_id + self.timestamp = timestamp + self.url = url + self.referrer = referrer + self.loaded = loaded + self.request_start = request_start + self.response_start = response_start + self.response_end = response_end + self.dom_content_loaded_event_start = dom_content_loaded_event_start + self.dom_content_loaded_event_end = dom_content_loaded_event_end + self.load_event_start = load_event_start + self.load_event_end = load_event_end + self.first_paint = first_paint + self.first_contentful_paint = first_contentful_paint + self.speed_index = speed_index + self.visually_complete = visually_complete + self.time_to_interactive = time_to_interactive + self.web_vitals = web_vitals + + class CSSInsertRule(Message): __id__ = 37 @@ -859,6 +883,14 @@ class GraphQL(Message): self.duration = duration +class WebVitals(Message): + __id__ = 124 + + def __init__(self, name, value): + self.name = name + self.value = value + + class IssueEvent(Message): __id__ = 125 diff --git a/ee/connectors/msgcodec/messages.pyx b/ee/connectors/msgcodec/messages.pyx index d495bfaf4..37183b755 100644 --- a/ee/connectors/msgcodec/messages.pyx +++ b/ee/connectors/msgcodec/messages.pyx @@ -410,7 +410,7 @@ cdef class Metadata(PyMessage): self.value = value -cdef class PageEvent(PyMessage): +cdef class PageEventDeprecated(PyMessage): cdef public int __id__ cdef public unsigned long message_id cdef public unsigned long timestamp @@ -468,6 +468,49 @@ cdef class InputEvent(PyMessage): self.label = label +cdef class PageEvent(PyMessage): + cdef public int __id__ + cdef public unsigned long message_id + cdef public unsigned long timestamp + cdef public str url + cdef public str referrer + cdef public bint loaded + cdef public unsigned long request_start + cdef public unsigned long response_start + cdef public unsigned long response_end + cdef public unsigned long dom_content_loaded_event_start + cdef public unsigned long dom_content_loaded_event_end + cdef public unsigned long load_event_start + cdef public unsigned long load_event_end + cdef public unsigned long first_paint + cdef public unsigned long first_contentful_paint + cdef public unsigned long speed_index + cdef public unsigned long visually_complete + cdef public unsigned long time_to_interactive + cdef public str web_vitals + + def __init__(self, unsigned long message_id, unsigned long timestamp, str url, str referrer, bint loaded, unsigned long request_start, unsigned long response_start, unsigned long response_end, unsigned long dom_content_loaded_event_start, unsigned long dom_content_loaded_event_end, unsigned long load_event_start, unsigned long load_event_end, unsigned long first_paint, unsigned long first_contentful_paint, unsigned long speed_index, unsigned long visually_complete, unsigned long time_to_interactive, str web_vitals): + self.__id__ = 33 + self.message_id = message_id + self.timestamp = timestamp + self.url = url + self.referrer = referrer + self.loaded = loaded + self.request_start = request_start + self.response_start = response_start + self.response_end = response_end + self.dom_content_loaded_event_start = dom_content_loaded_event_start + self.dom_content_loaded_event_end = dom_content_loaded_event_end + self.load_event_start = load_event_start + self.load_event_end = load_event_end + self.first_paint = first_paint + self.first_contentful_paint = first_contentful_paint + self.speed_index = speed_index + self.visually_complete = visually_complete + self.time_to_interactive = time_to_interactive + self.web_vitals = web_vitals + + cdef class CSSInsertRule(PyMessage): cdef public int __id__ cdef public unsigned long id @@ -1271,6 +1314,17 @@ cdef class GraphQL(PyMessage): self.duration = duration +cdef class WebVitals(PyMessage): + cdef public int __id__ + cdef public str name + cdef public str value + + def __init__(self, str name, str value): + self.__id__ = 124 + self.name = name + self.value = value + + cdef class IssueEvent(PyMessage): cdef public int __id__ cdef public unsigned long message_id diff --git a/ee/connectors/msgcodec/msgcodec.py b/ee/connectors/msgcodec/msgcodec.py index 6a4a73bdc..6745ee99a 100644 --- a/ee/connectors/msgcodec/msgcodec.py +++ b/ee/connectors/msgcodec/msgcodec.py @@ -309,7 +309,7 @@ class MessageCodec(Codec): ) if message_id == 31: - return PageEvent( + return PageEventDeprecated( message_id=self.read_uint(reader), timestamp=self.read_uint(reader), url=self.read_string(reader), @@ -338,6 +338,28 @@ class MessageCodec(Codec): label=self.read_string(reader) ) + if message_id == 33: + return PageEvent( + 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), + web_vitals=self.read_string(reader) + ) + if message_id == 37: return CSSInsertRule( id=self.read_uint(reader), @@ -768,6 +790,12 @@ class MessageCodec(Codec): duration=self.read_uint(reader) ) + if message_id == 124: + return WebVitals( + name=self.read_string(reader), + value=self.read_string(reader) + ) + if message_id == 125: return IssueEvent( message_id=self.read_uint(reader), diff --git a/ee/connectors/msgcodec/msgcodec.pyx b/ee/connectors/msgcodec/msgcodec.pyx index b7f9e105e..1936ffea5 100644 --- a/ee/connectors/msgcodec/msgcodec.pyx +++ b/ee/connectors/msgcodec/msgcodec.pyx @@ -407,7 +407,7 @@ cdef class MessageCodec: ) if message_id == 31: - return PageEvent( + return PageEventDeprecated( message_id=self.read_uint(reader), timestamp=self.read_uint(reader), url=self.read_string(reader), @@ -436,6 +436,28 @@ cdef class MessageCodec: label=self.read_string(reader) ) + if message_id == 33: + return PageEvent( + 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), + web_vitals=self.read_string(reader) + ) + if message_id == 37: return CSSInsertRule( id=self.read_uint(reader), @@ -866,6 +888,12 @@ cdef class MessageCodec: duration=self.read_uint(reader) ) + if message_id == 124: + return WebVitals( + name=self.read_string(reader), + value=self.read_string(reader) + ) + if message_id == 125: return IssueEvent( message_id=self.read_uint(reader), diff --git a/frontend/app/components/Session_/EventsBlock/Event.tsx b/frontend/app/components/Session_/EventsBlock/Event.tsx index 3823699d8..8d212e578 100644 --- a/frontend/app/components/Session_/EventsBlock/Event.tsx +++ b/frontend/app/components/Session_/EventsBlock/Event.tsx @@ -25,7 +25,6 @@ type Props = { isCurrent?: boolean; onClick?: () => void; showSelection?: boolean; - showLoadInfo?: boolean; toggleLoadInfo?: () => void; isRed?: boolean; presentInSearch?: boolean; @@ -52,7 +51,6 @@ const Event: React.FC = ({ isCurrent = false, onClick, showSelection = false, - showLoadInfo, toggleLoadInfo, isRed = false, presentInSearch = false, @@ -251,25 +249,26 @@ const Event: React.FC = ({ {renderBody()} {isLocation && - (event.fcpTime || - event.visuallyComplete || - event.timeToInteractive) && ( - elements / 1.2, - divisorFn: (elements, parts) => elements / (2 * parts + 1), - })} - /> - )} + (event.fcpTime || + event.visuallyComplete || + event.timeToInteractive || + event.webvitals) ? ( + elements / 1.2, + divisorFn: (elements, parts) => elements / (2 * parts + 1), + })} + /> + ) : null} ); }; diff --git a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js index e638ad339..67a130dc9 100644 --- a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js @@ -2,14 +2,11 @@ import UxtEvent from "Components/Session_/EventsBlock/UxtEvent"; import React from 'react'; import { connect } from 'react-redux'; import { TextEllipsis, Icon } from 'UI'; -import withToggle from 'HOCs/withToggle'; import { TYPES } from 'Types/session/event'; import Event from './Event'; import stl from './eventGroupWrapper.module.css'; import NoteEvent from './NoteEvent'; -// TODO: incapsulate toggler in LocationEvent -@withToggle('showLoadInfo', 'toggleLoadInfo') @connect( (state) => ({ members: state.getIn(['members', 'list']), @@ -17,14 +14,6 @@ import NoteEvent from './NoteEvent'; }), ) class EventGroupWrapper extends React.Component { - toggleLoadInfo = (e) => { - e.stopPropagation(); - this.props.toggleLoadInfo(); - }; - - componentDidMount() { - this.props.toggleLoadInfo(this.props.isFirst); - } onEventClick = (e) => this.props.onEventClick(e, this.props.event); @@ -39,7 +28,6 @@ class EventGroupWrapper extends React.Component { isCurrent, isEditing, showSelection, - showLoadInfo, isFirst, presentInSearch, isNote, @@ -77,8 +65,6 @@ class EventGroupWrapper extends React.Component { event={event} onClick={this.onEventClick} selected={isSelected} - showLoadInfo={showLoadInfo} - toggleLoadInfo={this.toggleLoadInfo} isCurrent={isCurrent} presentInSearch={presentInSearch} isLastInGroup={isLastInGroup} diff --git a/frontend/app/components/Session_/EventsBlock/LoadInfo.js b/frontend/app/components/Session_/EventsBlock/LoadInfo.js index 3227ec55d..deea1fc17 100644 --- a/frontend/app/components/Session_/EventsBlock/LoadInfo.js +++ b/frontend/app/components/Session_/EventsBlock/LoadInfo.js @@ -1,49 +1,98 @@ import React from 'react'; -import styles from './loadInfo.module.css'; -import { numberWithCommas } from 'App/utils' -const LoadInfo = ({ showInfo = false, onClick, event: { fcpTime, visuallyComplete, timeToInteractive }, prorata: { a, b, c } }) => ( +import { numberWithCommas } from 'App/utils'; + +import styles from './loadInfo.module.css'; + +const LoadInfo = ({ + webvitals, + event: { fcpTime, visuallyComplete, timeToInteractive }, + prorata: { a, b, c }, +}) => (
-
- { typeof fcpTime === 'number' &&
} - { typeof visuallyComplete === 'number' &&
} - { typeof timeToInteractive === 'number' &&
} +
+ {typeof fcpTime === 'number' &&
} + {typeof visuallyComplete === 'number' && ( +
+ )} + {typeof timeToInteractive === 'number' && ( +
+ )}
-
- { typeof fcpTime === 'number' && -
-
-
{ 'Time to Render' }
-
{ `${ numberWithCommas(fcpTime || 0) }ms` }
+
+ {typeof fcpTime === 'number' && ( +
+
+
{'Time to Render'}
+
{`${numberWithCommas( + fcpTime || 0 + )}ms`}
- } - { typeof visuallyComplete === 'number' && -
-
-
{ 'Visually Complete' }
-
{ `${ numberWithCommas(visuallyComplete || 0) }ms` }
+ )} + {typeof visuallyComplete === 'number' && ( +
+
+
{'Visually Complete'}
+
{`${numberWithCommas( + visuallyComplete || 0 + )}ms`}
- } - { typeof timeToInteractive === 'number' && -
-
-
{ 'Time To Interactive' }
-
{ `${ numberWithCommas(timeToInteractive || 0) }ms` }
+ )} + {typeof timeToInteractive === 'number' && ( +
+
+
{'Time To Interactive'}
+
{`${numberWithCommas( + timeToInteractive || 0 + )}ms`}
- } - {/*
- - - { '.HAR' } - -
- { new Date().toString() } -
-
*/} + )} + {webvitals + ? Object.keys(webvitals).map((key) => ( + + )) + : null}
); +function WebVitalsValue({ name, value }) { + const valInt = Number(value); + const valDisplay = + name !== 'CLS' + ? Math.round(valInt) + : valInt > 1 + ? Math.round(valInt) + : valInt.toExponential(1).split('e'); + + const unit = { + CLS: 'score', + FCP: 'ms', + INP: 'ms', + LCP: 'ms', + TTFB: 'ms', + }; + return ( +
+
+
{name}
+
+ {Array.isArray(valDisplay) ? ( + <> + {valDisplay[0]}× 10{valDisplay[1]} + + ) : ( + <> + {valDisplay} {unit[name]} + + )} +
+
+ ); +} + +const WebVitalsValueMemo = React.memo(WebVitalsValue); + LoadInfo.displayName = 'LoadInfo'; -export default LoadInfo; +export default React.memo(LoadInfo); diff --git a/frontend/app/player/web/messages/tracker.gen.ts b/frontend/app/player/web/messages/tracker.gen.ts index cdf0be6c9..c23167cec 100644 --- a/frontend/app/player/web/messages/tracker.gen.ts +++ b/frontend/app/player/web/messages/tracker.gen.ts @@ -550,8 +550,14 @@ type TrGraphQL = [ duration: number, ] +type TrWebVitals = [ + type: 124, + name: string, + value: string, +] -export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL + +export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals export default function translate(tMsg: TrackerMessage): RawMessage | null { switch(tMsg[0]) { diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts index 10b1ed23e..dd207e04a 100644 --- a/frontend/app/types/session/event.ts +++ b/frontend/app/types/session/event.ts @@ -80,6 +80,7 @@ export interface LocationEvent extends IEvent { referrer: string; firstContentfulPaintTime: number; firstPaintTime: number; + webVitals: string | null; } export type EventData = ConsoleEvent | ClickEvent | InputEvent | LocationEvent | IEvent; @@ -192,12 +193,19 @@ export class Location extends Event { visuallyComplete: LocationEvent['visuallyComplete']; timeToInteractive: LocationEvent['timeToInteractive']; referrer: LocationEvent['referrer']; + webvitals: { + cls?: number; + lcp?: number; + inp?: number; + ttfb?: number; + } | null; constructor(evt: LocationEvent) { super(evt); Object.assign(this, { ...evt, fcpTime: evt.firstContentfulPaintTime || evt.firstPaintTime, + webvitals: evt.webVitals ? JSON.parse(evt.webVitals) : null, }); } } diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index f109315e4..11d976dde 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -261,7 +261,6 @@ export default class Session { const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType); const events: InjectedEvent[] = []; - const rawEvents: (EventData & { key: number })[] = []; if (session.events?.length) { (session.events as EventData[]).forEach((event: EventData, k) => { @@ -271,7 +270,6 @@ export default class Session { if (EventClass) { events.push(EventClass); } - rawEvents.push({ ...event, time, key: k }); } }); } @@ -306,7 +304,7 @@ export default class Session { const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || []; const mixedEventsWithIssues = mergeEventLists( - mergeEventLists(rawEvents, rawNotes), + mergeEventLists(events, rawNotes), frustrationIssues ).sort(sortEvents) @@ -377,7 +375,6 @@ export default class Session { const events: InjectedEvent[] = []; const uxtDoneEvents = userTestingEvents.filter(e => e.status === 'done' && e.title).map(e => ({ ...e, type: 'UXT_EVENT', key: e.signal_id })) - const rawEvents: (EventData & { key: number })[] = []; let uxtIndexNum = 0; if (sessionEvents.length) { @@ -394,7 +391,6 @@ export default class Session { if (EventClass) { events.push(EventClass); } - rawEvents.push({ ...event, time, key: k, }); } }); } @@ -412,7 +408,7 @@ export default class Session { const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || []; const mixedEventsWithIssues = mergeEventLists( - rawEvents, + events, frustrationIssues.filter(i => i.type !== issueTypes.DEAD_CLICK) ) diff --git a/mobs/messages.rb b/mobs/messages.rb index f5dfaed52..b36de9041 100644 --- a/mobs/messages.rb +++ b/mobs/messages.rb @@ -163,7 +163,7 @@ message 30, 'Metadata', :replayer => false do string 'Key' string 'Value' end -message 31, 'PageEvent', :tracker => false, :replayer => false do +message 31, 'PageEventDeprecated', :tracker => false, :replayer => false do uint 'MessageID' uint 'Timestamp' string 'URL' @@ -182,6 +182,7 @@ message 31, 'PageEvent', :tracker => false, :replayer => false do uint 'VisuallyComplete' uint 'TimeToInteractive' end + message 32, 'InputEvent', :tracker => false, :replayer => false do uint 'MessageID' uint 'Timestamp' @@ -190,6 +191,27 @@ message 32, 'InputEvent', :tracker => false, :replayer => false do string 'Label' end +message 33, 'PageEvent', :tracker => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'URL' + string 'Referrer' + boolean 'Loaded' + uint 'RequestStart' + uint 'ResponseStart' + uint 'ResponseEnd' + uint 'DomContentLoadedEventStart' + uint 'DomContentLoadedEventEnd' + uint 'LoadEventStart' + uint 'LoadEventEnd' + uint 'FirstPaint' + uint 'FirstContentfulPaint' + uint 'SpeedIndex' + uint 'VisuallyComplete' + uint 'TimeToInteractive' + string 'WebVitals' +end + # DEPRECATED since 4.0.2 in favor of AdoptedSSInsertRule + AdoptedSSAddOwner message 37, 'CSSInsertRule' do uint 'ID' @@ -556,6 +578,11 @@ message 123, 'GraphQL', :replayer => :devtools do uint 'Duration' end +message 124, 'WebVitals', :replayer => false do + string 'Name' + string 'Value' +end + ## Backend-only message 125, 'IssueEvent', :replayer => false, :tracker => false do uint 'MessageID' diff --git a/tracker/tracker/CHANGELOG.md b/tracker/tracker/CHANGELOG.md index 88923305a..36093537d 100644 --- a/tracker/tracker/CHANGELOG.md +++ b/tracker/tracker/CHANGELOG.md @@ -1,3 +1,7 @@ +# 15.0.0 + +- new webvitals messages source + # 14.0.8 - use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy)) diff --git a/tracker/tracker/bun.lockb b/tracker/tracker/bun.lockb index 4f71fe957..6db3834eb 100755 Binary files a/tracker/tracker/bun.lockb and b/tracker/tracker/bun.lockb differ diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 50306094c..c06370bfb 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "14.0.8", + "version": "15.0.0", "keywords": [ "logging", "replay" @@ -52,7 +52,8 @@ "@medv/finder": "^3.2.0", "@openreplay/network-proxy": "^1.0.3", "error-stack-parser": "^2.0.6", - "fflate": "^0.8.2" + "fflate": "^0.8.2", + "web-vitals": "^4.2.3" }, "engines": { "node": ">=14.0" diff --git a/tracker/tracker/src/common/messages.gen.ts b/tracker/tracker/src/common/messages.gen.ts index 7ec02abe7..9982ef8e9 100644 --- a/tracker/tracker/src/common/messages.gen.ts +++ b/tracker/tracker/src/common/messages.gen.ts @@ -78,6 +78,7 @@ export declare const enum Type { Redux = 121, SetPageLocation = 122, GraphQL = 123, + WebVitals = 124, } @@ -626,6 +627,12 @@ export type GraphQL = [ /*duration:*/ number, ] +export type WebVitals = [ + /*type:*/ Type.WebVitals, + /*name:*/ string, + /*value:*/ string, +] -type Message = Timestamp | SetPageLocationDeprecated | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL + +type Message = Timestamp | SetPageLocationDeprecated | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL | WebVitals export default Message diff --git a/tracker/tracker/src/main/app/messages.gen.ts b/tracker/tracker/src/main/app/messages.gen.ts index a951a6148..7d3af2af0 100644 --- a/tracker/tracker/src/main/app/messages.gen.ts +++ b/tracker/tracker/src/main/app/messages.gen.ts @@ -1019,3 +1019,14 @@ export function GraphQL( ] } +export function WebVitals( + name: string, + value: string, +): Messages.WebVitals { + return [ + Messages.Type.WebVitals, + name, + value, + ] +} + diff --git a/tracker/tracker/src/main/modules/timing.ts b/tracker/tracker/src/main/modules/timing.ts index a45bfb570..12d6c8bb2 100644 --- a/tracker/tracker/src/main/modules/timing.ts +++ b/tracker/tracker/src/main/modules/timing.ts @@ -1,7 +1,8 @@ import type App from '../app/index.js' import { hasTag } from '../app/guards.js' import { isURL, getTimeOrigin } from '../utils.js' -import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js' +import { ResourceTiming, PageLoadTiming, PageRenderTiming, WebVitals } from '../app/messages.gen.js' +import { onCLS, onINP, onLCP, onTTFB, Metric } from 'web-vitals' // Inspired by https://github.com/WPO-Foundation/RUM-SpeedIndex/blob/master/src/rum-speedindex.js @@ -139,6 +140,12 @@ export default function (app: App, opts: Partial): void { const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming)) + function onVitalsSignal(msg: T) { + if (app.active()) { + return app.send(WebVitals(msg.name, String(msg.value))) + } + } + let prevSessionID: string | undefined app.attachStartCallback(function ({ sessionID }) { if (sessionID !== prevSessionID) { @@ -147,6 +154,18 @@ export default function (app: App, opts: Partial): void { prevSessionID = sessionID } observer.observe({ entryTypes: ['resource'] }) + // browser support: + // onCLS(): Chromium + // onFCP(): Chromium, Firefox, Safari + // onFID(): Chromium, Firefox (Deprecated) + // onINP(): Chromium + // onLCP(): Chromium, Firefox + // onTTFB(): Chromium, Firefox, Safari + + onCLS(onVitalsSignal) + onINP(onVitalsSignal) + onLCP(onVitalsSignal) + onTTFB(onVitalsSignal) }) app.attachStopCallback(function () { diff --git a/tracker/tracker/src/webworker/MessageEncoder.gen.ts b/tracker/tracker/src/webworker/MessageEncoder.gen.ts index bec1496be..1fbf883a1 100644 --- a/tracker/tracker/src/webworker/MessageEncoder.gen.ts +++ b/tracker/tracker/src/webworker/MessageEncoder.gen.ts @@ -314,6 +314,10 @@ export default class MessageEncoder extends PrimitiveEncoder { return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.uint(msg[5]) break + case Messages.Type.WebVitals: + return this.string(msg[1]) && this.string(msg[2]) + break + } }