Webvitals for replays (#2627)
* adding new web vitals track * adding new web vitals track * update vitals message * feat(heuristics): added web vitals support to the page event builder * update mtype * feat(heuristics): applied a new value type * feat(heuristics): fixed if err case * feat(heuristics): fixed the sql issue * new event display * tracker v 15.0.0 start --------- Co-authored-by: Alexander <zavorotynskiy@pm.me>
This commit is contained in:
parent
702bad06b9
commit
97a08853e8
26 changed files with 563 additions and 111 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<Props> = ({
|
|||
isCurrent = false,
|
||||
onClick,
|
||||
showSelection = false,
|
||||
showLoadInfo,
|
||||
toggleLoadInfo,
|
||||
isRed = false,
|
||||
presentInSearch = false,
|
||||
|
|
@ -251,25 +249,26 @@ const Event: React.FC<Props> = ({
|
|||
{renderBody()}
|
||||
</div>
|
||||
{isLocation &&
|
||||
(event.fcpTime ||
|
||||
event.visuallyComplete ||
|
||||
event.timeToInteractive) && (
|
||||
<LoadInfo
|
||||
showInfo={showLoadInfo}
|
||||
onClick={toggleLoadInfo}
|
||||
event={event}
|
||||
prorata={prorata({
|
||||
parts: 100,
|
||||
elements: {
|
||||
a: event.fcpTime,
|
||||
b: event.visuallyComplete,
|
||||
c: event.timeToInteractive,
|
||||
},
|
||||
startDivisorFn: (elements) => elements / 1.2,
|
||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
(event.fcpTime ||
|
||||
event.visuallyComplete ||
|
||||
event.timeToInteractive ||
|
||||
event.webvitals) ? (
|
||||
<LoadInfo
|
||||
onClick={toggleLoadInfo}
|
||||
event={event}
|
||||
webvitals={event.webvitals}
|
||||
prorata={prorata({
|
||||
parts: 100,
|
||||
elements: {
|
||||
a: event.fcpTime,
|
||||
b: event.visuallyComplete,
|
||||
c: event.timeToInteractive,
|
||||
},
|
||||
startDivisorFn: (elements) => elements / 1.2,
|
||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
}) => (
|
||||
<div>
|
||||
<div className={ styles.bar } onClick={ onClick }>
|
||||
{ typeof fcpTime === 'number' && <div style={ { width: `${ a }%` } } /> }
|
||||
{ typeof visuallyComplete === 'number' && <div style={ { width: `${ b }%` } } /> }
|
||||
{ typeof timeToInteractive === 'number' && <div style={ { width: `${ c }%` } } /> }
|
||||
<div className={styles.bar}>
|
||||
{typeof fcpTime === 'number' && <div style={{ width: `${a}%` }} />}
|
||||
{typeof visuallyComplete === 'number' && (
|
||||
<div style={{ width: `${b}%` }} />
|
||||
)}
|
||||
{typeof timeToInteractive === 'number' && (
|
||||
<div style={{ width: `${c}%` }} />
|
||||
)}
|
||||
</div>
|
||||
<div className={ styles.bottomBlock } data-hidden={ !showInfo }>
|
||||
{ typeof fcpTime === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Time to Render' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(fcpTime || 0) }ms` }</div>
|
||||
<div className={styles.bottomBlock}>
|
||||
{typeof fcpTime === 'number' && (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.lines} />
|
||||
<div className={styles.label}>{'Time to Render'}</div>
|
||||
<div className={styles.value}>{`${numberWithCommas(
|
||||
fcpTime || 0
|
||||
)}ms`}</div>
|
||||
</div>
|
||||
}
|
||||
{ typeof visuallyComplete === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Visually Complete' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(visuallyComplete || 0) }ms` }</div>
|
||||
)}
|
||||
{typeof visuallyComplete === 'number' && (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.lines} />
|
||||
<div className={styles.label}>{'Visually Complete'}</div>
|
||||
<div className={styles.value}>{`${numberWithCommas(
|
||||
visuallyComplete || 0
|
||||
)}ms`}</div>
|
||||
</div>
|
||||
}
|
||||
{ typeof timeToInteractive === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Time To Interactive' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(timeToInteractive || 0) }ms` }</div>
|
||||
)}
|
||||
{typeof timeToInteractive === 'number' && (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.lines} />
|
||||
<div className={styles.label}>{'Time To Interactive'}</div>
|
||||
<div className={styles.value}>{`${numberWithCommas(
|
||||
timeToInteractive || 0
|
||||
)}ms`}</div>
|
||||
</div>
|
||||
}
|
||||
{/* <div className={ styles.download }>
|
||||
<a>
|
||||
<Icon name="download" />
|
||||
{ '.HAR' }
|
||||
</a>
|
||||
<div>
|
||||
{ new Date().toString() }
|
||||
</div>
|
||||
</div> */}
|
||||
)}
|
||||
{webvitals
|
||||
? Object.keys(webvitals).map((key) => (
|
||||
<WebVitalsValueMemo name={key.toUpperCase()} value={webvitals[key]} />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.lines} />
|
||||
<div className={styles.label}>{name}</div>
|
||||
<div className={styles.value}>
|
||||
{Array.isArray(valDisplay) ? (
|
||||
<>
|
||||
{valDisplay[0]}× 10<sup>{valDisplay[1]}</sup>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{valDisplay} {unit[name]}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const WebVitalsValueMemo = React.memo(WebVitalsValue);
|
||||
|
||||
LoadInfo.displayName = 'LoadInfo';
|
||||
|
||||
export default LoadInfo;
|
||||
export default React.memo(LoadInfo);
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1019,3 +1019,14 @@ export function GraphQL(
|
|||
]
|
||||
}
|
||||
|
||||
export function WebVitals(
|
||||
name: string,
|
||||
value: string,
|
||||
): Messages.WebVitals {
|
||||
return [
|
||||
Messages.Type.WebVitals,
|
||||
name,
|
||||
value,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Options>): void {
|
|||
|
||||
const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming))
|
||||
|
||||
function onVitalsSignal<T extends Metric>(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<Options>): 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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue