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.MsgFetch, messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick,
|
||||||
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
|
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
|
||||||
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
||||||
messages.MsgPageEvent, messages.MsgMouseThrashing, messages.MsgInputChange,
|
messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange,
|
||||||
messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger,
|
messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger,
|
||||||
// Mobile messages
|
// Mobile messages
|
||||||
messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID,
|
messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID,
|
||||||
|
|
|
||||||
|
|
@ -126,11 +126,11 @@ func (conn *BulkSet) initBulks() {
|
||||||
"events.pages",
|
"events.pages",
|
||||||
"(session_id, message_id, timestamp, referrer, base_referrer, host, path, query, dom_content_loaded_time, "+
|
"(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, "+
|
"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), "+
|
"($%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), 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), NULLIF($%d, ''))",
|
||||||
18, 200)
|
19, 200)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.log.Fatal(conn.ctx, "can't create webPageEvents bulk: %s", err)
|
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
|
// base_path is deprecated
|
||||||
if err = conn.bulks.Get("webPageEvents").Append(sess.SessionID, truncSqIdx(e.MessageID), e.Timestamp, e.Referrer, url.DiscardURLQuery(e.Referrer),
|
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,
|
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)
|
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
|
||||||
conn.log.Error(sessCtx, "insert web page event in bulk err: %s", err)
|
conn.log.Error(sessCtx, "insert web page event in bulk err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package custom
|
package custom
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
. "openreplay/backend/pkg/messages"
|
. "openreplay/backend/pkg/messages"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -9,6 +11,7 @@ const PageEventTimeout = 1 * 60 * 1000
|
||||||
type pageEventBuilder struct {
|
type pageEventBuilder struct {
|
||||||
pageEvent *PageEvent
|
pageEvent *PageEvent
|
||||||
firstTimingHandled bool
|
firstTimingHandled bool
|
||||||
|
webVitals map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPageEventBuilder() *pageEventBuilder {
|
func NewPageEventBuilder() *pageEventBuilder {
|
||||||
|
|
@ -69,7 +72,7 @@ func (b *pageEventBuilder) Handle(message Message, timestamp uint64) Message {
|
||||||
if msg.FirstContentfulPaint <= 30000 {
|
if msg.FirstContentfulPaint <= 30000 {
|
||||||
b.pageEvent.FirstContentfulPaint = msg.FirstContentfulPaint
|
b.pageEvent.FirstContentfulPaint = msg.FirstContentfulPaint
|
||||||
}
|
}
|
||||||
return b.buildIfTimingsComplete()
|
return nil //b.buildIfTimingsComplete()
|
||||||
case *PageRenderTiming:
|
case *PageRenderTiming:
|
||||||
if b.pageEvent == nil {
|
if b.pageEvent == nil {
|
||||||
break
|
break
|
||||||
|
|
@ -77,8 +80,12 @@ func (b *pageEventBuilder) Handle(message Message, timestamp uint64) Message {
|
||||||
b.pageEvent.SpeedIndex = msg.SpeedIndex
|
b.pageEvent.SpeedIndex = msg.SpeedIndex
|
||||||
b.pageEvent.VisuallyComplete = msg.VisuallyComplete
|
b.pageEvent.VisuallyComplete = msg.VisuallyComplete
|
||||||
b.pageEvent.TimeToInteractive = msg.TimeToInteractive
|
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 {
|
if b.pageEvent != nil && b.pageEvent.Timestamp+PageEventTimeout < timestamp {
|
||||||
|
|
@ -94,13 +101,21 @@ func (b *pageEventBuilder) Build() Message {
|
||||||
pageEvent := b.pageEvent
|
pageEvent := b.pageEvent
|
||||||
b.pageEvent = nil
|
b.pageEvent = nil
|
||||||
b.firstTimingHandled = false
|
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
|
return pageEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *pageEventBuilder) buildIfTimingsComplete() Message {
|
//func (b *pageEventBuilder) buildIfTimingsComplete() Message {
|
||||||
if b.firstTimingHandled {
|
// if b.firstTimingHandled {
|
||||||
return b.Build()
|
// return b.Build()
|
||||||
}
|
// }
|
||||||
b.firstTimingHandled = true
|
// b.firstTimingHandled = true
|
||||||
return nil
|
// return nil
|
||||||
}
|
//}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
package messages
|
package messages
|
||||||
|
|
||||||
func IsReplayerType(id int) bool {
|
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 {
|
func IsMobileType(id int) bool {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,27 @@ func transformDeprecated(msg Message) Message {
|
||||||
NavigationStart: m.NavigationStart,
|
NavigationStart: m.NavigationStart,
|
||||||
DocumentTitle: "",
|
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
|
return msg
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,9 @@ const (
|
||||||
MsgUserID = 28
|
MsgUserID = 28
|
||||||
MsgUserAnonymousID = 29
|
MsgUserAnonymousID = 29
|
||||||
MsgMetadata = 30
|
MsgMetadata = 30
|
||||||
MsgPageEvent = 31
|
MsgPageEventDeprecated = 31
|
||||||
MsgInputEvent = 32
|
MsgInputEvent = 32
|
||||||
|
MsgPageEvent = 33
|
||||||
MsgCSSInsertRule = 37
|
MsgCSSInsertRule = 37
|
||||||
MsgCSSDeleteRule = 38
|
MsgCSSDeleteRule = 38
|
||||||
MsgFetch = 39
|
MsgFetch = 39
|
||||||
|
|
@ -91,6 +92,7 @@ const (
|
||||||
MsgRedux = 121
|
MsgRedux = 121
|
||||||
MsgSetPageLocation = 122
|
MsgSetPageLocation = 122
|
||||||
MsgGraphQL = 123
|
MsgGraphQL = 123
|
||||||
|
MsgWebVitals = 124
|
||||||
MsgIssueEvent = 125
|
MsgIssueEvent = 125
|
||||||
MsgSessionEnd = 126
|
MsgSessionEnd = 126
|
||||||
MsgSessionSearch = 127
|
MsgSessionSearch = 127
|
||||||
|
|
@ -874,7 +876,7 @@ func (msg *Metadata) TypeID() int {
|
||||||
return 30
|
return 30
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageEvent struct {
|
type PageEventDeprecated struct {
|
||||||
message
|
message
|
||||||
MessageID uint64
|
MessageID uint64
|
||||||
Timestamp uint64
|
Timestamp uint64
|
||||||
|
|
@ -895,7 +897,7 @@ type PageEvent struct {
|
||||||
TimeToInteractive uint64
|
TimeToInteractive uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *PageEvent) Encode() []byte {
|
func (msg *PageEventDeprecated) Encode() []byte {
|
||||||
buf := make([]byte, 171+len(msg.URL)+len(msg.Referrer))
|
buf := make([]byte, 171+len(msg.URL)+len(msg.Referrer))
|
||||||
buf[0] = 31
|
buf[0] = 31
|
||||||
p := 1
|
p := 1
|
||||||
|
|
@ -919,11 +921,11 @@ func (msg *PageEvent) Encode() []byte {
|
||||||
return buf[:p]
|
return buf[:p]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *PageEvent) Decode() Message {
|
func (msg *PageEventDeprecated) Decode() Message {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg *PageEvent) TypeID() int {
|
func (msg *PageEventDeprecated) TypeID() int {
|
||||||
return 31
|
return 31
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -956,6 +958,61 @@ func (msg *InputEvent) TypeID() int {
|
||||||
return 32
|
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 {
|
type CSSInsertRule struct {
|
||||||
message
|
message
|
||||||
ID uint64
|
ID uint64
|
||||||
|
|
@ -2443,6 +2500,29 @@ func (msg *GraphQL) TypeID() int {
|
||||||
return 123
|
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 {
|
type IssueEvent struct {
|
||||||
message
|
message
|
||||||
MessageID uint64
|
MessageID uint64
|
||||||
|
|
|
||||||
|
|
@ -468,9 +468,9 @@ func DecodeMetadata(reader BytesReader) (Message, error) {
|
||||||
return msg, err
|
return msg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodePageEvent(reader BytesReader) (Message, error) {
|
func DecodePageEventDeprecated(reader BytesReader) (Message, error) {
|
||||||
var err error = nil
|
var err error = nil
|
||||||
msg := &PageEvent{}
|
msg := &PageEventDeprecated{}
|
||||||
if msg.MessageID, err = reader.ReadUint(); err != nil {
|
if msg.MessageID, err = reader.ReadUint(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -546,6 +546,66 @@ func DecodeInputEvent(reader BytesReader) (Message, error) {
|
||||||
return msg, err
|
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) {
|
func DecodeCSSInsertRule(reader BytesReader) (Message, error) {
|
||||||
var err error = nil
|
var err error = nil
|
||||||
msg := &CSSInsertRule{}
|
msg := &CSSInsertRule{}
|
||||||
|
|
@ -1494,6 +1554,18 @@ func DecodeGraphQL(reader BytesReader) (Message, error) {
|
||||||
return msg, err
|
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) {
|
func DecodeIssueEvent(reader BytesReader) (Message, error) {
|
||||||
var err error = nil
|
var err error = nil
|
||||||
msg := &IssueEvent{}
|
msg := &IssueEvent{}
|
||||||
|
|
@ -2019,9 +2091,11 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
||||||
case 30:
|
case 30:
|
||||||
return DecodeMetadata(reader)
|
return DecodeMetadata(reader)
|
||||||
case 31:
|
case 31:
|
||||||
return DecodePageEvent(reader)
|
return DecodePageEventDeprecated(reader)
|
||||||
case 32:
|
case 32:
|
||||||
return DecodeInputEvent(reader)
|
return DecodeInputEvent(reader)
|
||||||
|
case 33:
|
||||||
|
return DecodePageEvent(reader)
|
||||||
case 37:
|
case 37:
|
||||||
return DecodeCSSInsertRule(reader)
|
return DecodeCSSInsertRule(reader)
|
||||||
case 38:
|
case 38:
|
||||||
|
|
@ -2136,6 +2210,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
||||||
return DecodeSetPageLocation(reader)
|
return DecodeSetPageLocation(reader)
|
||||||
case 123:
|
case 123:
|
||||||
return DecodeGraphQL(reader)
|
return DecodeGraphQL(reader)
|
||||||
|
case 124:
|
||||||
|
return DecodeWebVitals(reader)
|
||||||
case 125:
|
case 125:
|
||||||
return DecodeIssueEvent(reader)
|
return DecodeIssueEvent(reader)
|
||||||
case 126:
|
case 126:
|
||||||
|
|
|
||||||
|
|
@ -281,7 +281,7 @@ class Metadata(Message):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
class PageEvent(Message):
|
class PageEventDeprecated(Message):
|
||||||
__id__ = 31
|
__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):
|
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
|
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):
|
class CSSInsertRule(Message):
|
||||||
__id__ = 37
|
__id__ = 37
|
||||||
|
|
||||||
|
|
@ -859,6 +883,14 @@ class GraphQL(Message):
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
|
||||||
|
|
||||||
|
class WebVitals(Message):
|
||||||
|
__id__ = 124
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
class IssueEvent(Message):
|
class IssueEvent(Message):
|
||||||
__id__ = 125
|
__id__ = 125
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,7 @@ cdef class Metadata(PyMessage):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
|
||||||
|
|
||||||
cdef class PageEvent(PyMessage):
|
cdef class PageEventDeprecated(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public unsigned long message_id
|
cdef public unsigned long message_id
|
||||||
cdef public unsigned long timestamp
|
cdef public unsigned long timestamp
|
||||||
|
|
@ -468,6 +468,49 @@ cdef class InputEvent(PyMessage):
|
||||||
self.label = label
|
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 class CSSInsertRule(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public unsigned long id
|
cdef public unsigned long id
|
||||||
|
|
@ -1271,6 +1314,17 @@ cdef class GraphQL(PyMessage):
|
||||||
self.duration = duration
|
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 class IssueEvent(PyMessage):
|
||||||
cdef public int __id__
|
cdef public int __id__
|
||||||
cdef public unsigned long message_id
|
cdef public unsigned long message_id
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ class MessageCodec(Codec):
|
||||||
)
|
)
|
||||||
|
|
||||||
if message_id == 31:
|
if message_id == 31:
|
||||||
return PageEvent(
|
return PageEventDeprecated(
|
||||||
message_id=self.read_uint(reader),
|
message_id=self.read_uint(reader),
|
||||||
timestamp=self.read_uint(reader),
|
timestamp=self.read_uint(reader),
|
||||||
url=self.read_string(reader),
|
url=self.read_string(reader),
|
||||||
|
|
@ -338,6 +338,28 @@ class MessageCodec(Codec):
|
||||||
label=self.read_string(reader)
|
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:
|
if message_id == 37:
|
||||||
return CSSInsertRule(
|
return CSSInsertRule(
|
||||||
id=self.read_uint(reader),
|
id=self.read_uint(reader),
|
||||||
|
|
@ -768,6 +790,12 @@ class MessageCodec(Codec):
|
||||||
duration=self.read_uint(reader)
|
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:
|
if message_id == 125:
|
||||||
return IssueEvent(
|
return IssueEvent(
|
||||||
message_id=self.read_uint(reader),
|
message_id=self.read_uint(reader),
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ cdef class MessageCodec:
|
||||||
)
|
)
|
||||||
|
|
||||||
if message_id == 31:
|
if message_id == 31:
|
||||||
return PageEvent(
|
return PageEventDeprecated(
|
||||||
message_id=self.read_uint(reader),
|
message_id=self.read_uint(reader),
|
||||||
timestamp=self.read_uint(reader),
|
timestamp=self.read_uint(reader),
|
||||||
url=self.read_string(reader),
|
url=self.read_string(reader),
|
||||||
|
|
@ -436,6 +436,28 @@ cdef class MessageCodec:
|
||||||
label=self.read_string(reader)
|
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:
|
if message_id == 37:
|
||||||
return CSSInsertRule(
|
return CSSInsertRule(
|
||||||
id=self.read_uint(reader),
|
id=self.read_uint(reader),
|
||||||
|
|
@ -866,6 +888,12 @@ cdef class MessageCodec:
|
||||||
duration=self.read_uint(reader)
|
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:
|
if message_id == 125:
|
||||||
return IssueEvent(
|
return IssueEvent(
|
||||||
message_id=self.read_uint(reader),
|
message_id=self.read_uint(reader),
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ type Props = {
|
||||||
isCurrent?: boolean;
|
isCurrent?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
showSelection?: boolean;
|
showSelection?: boolean;
|
||||||
showLoadInfo?: boolean;
|
|
||||||
toggleLoadInfo?: () => void;
|
toggleLoadInfo?: () => void;
|
||||||
isRed?: boolean;
|
isRed?: boolean;
|
||||||
presentInSearch?: boolean;
|
presentInSearch?: boolean;
|
||||||
|
|
@ -52,7 +51,6 @@ const Event: React.FC<Props> = ({
|
||||||
isCurrent = false,
|
isCurrent = false,
|
||||||
onClick,
|
onClick,
|
||||||
showSelection = false,
|
showSelection = false,
|
||||||
showLoadInfo,
|
|
||||||
toggleLoadInfo,
|
toggleLoadInfo,
|
||||||
isRed = false,
|
isRed = false,
|
||||||
presentInSearch = false,
|
presentInSearch = false,
|
||||||
|
|
@ -251,25 +249,26 @@ const Event: React.FC<Props> = ({
|
||||||
{renderBody()}
|
{renderBody()}
|
||||||
</div>
|
</div>
|
||||||
{isLocation &&
|
{isLocation &&
|
||||||
(event.fcpTime ||
|
(event.fcpTime ||
|
||||||
event.visuallyComplete ||
|
event.visuallyComplete ||
|
||||||
event.timeToInteractive) && (
|
event.timeToInteractive ||
|
||||||
<LoadInfo
|
event.webvitals) ? (
|
||||||
showInfo={showLoadInfo}
|
<LoadInfo
|
||||||
onClick={toggleLoadInfo}
|
onClick={toggleLoadInfo}
|
||||||
event={event}
|
event={event}
|
||||||
prorata={prorata({
|
webvitals={event.webvitals}
|
||||||
parts: 100,
|
prorata={prorata({
|
||||||
elements: {
|
parts: 100,
|
||||||
a: event.fcpTime,
|
elements: {
|
||||||
b: event.visuallyComplete,
|
a: event.fcpTime,
|
||||||
c: event.timeToInteractive,
|
b: event.visuallyComplete,
|
||||||
},
|
c: event.timeToInteractive,
|
||||||
startDivisorFn: (elements) => elements / 1.2,
|
},
|
||||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
startDivisorFn: (elements) => elements / 1.2,
|
||||||
})}
|
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||||
/>
|
})}
|
||||||
)}
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,11 @@ import UxtEvent from "Components/Session_/EventsBlock/UxtEvent";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { TextEllipsis, Icon } from 'UI';
|
import { TextEllipsis, Icon } from 'UI';
|
||||||
import withToggle from 'HOCs/withToggle';
|
|
||||||
import { TYPES } from 'Types/session/event';
|
import { TYPES } from 'Types/session/event';
|
||||||
import Event from './Event';
|
import Event from './Event';
|
||||||
import stl from './eventGroupWrapper.module.css';
|
import stl from './eventGroupWrapper.module.css';
|
||||||
import NoteEvent from './NoteEvent';
|
import NoteEvent from './NoteEvent';
|
||||||
|
|
||||||
// TODO: incapsulate toggler in LocationEvent
|
|
||||||
@withToggle('showLoadInfo', 'toggleLoadInfo')
|
|
||||||
@connect(
|
@connect(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
members: state.getIn(['members', 'list']),
|
members: state.getIn(['members', 'list']),
|
||||||
|
|
@ -17,14 +14,6 @@ import NoteEvent from './NoteEvent';
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
class EventGroupWrapper extends React.Component {
|
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);
|
onEventClick = (e) => this.props.onEventClick(e, this.props.event);
|
||||||
|
|
||||||
|
|
@ -39,7 +28,6 @@ class EventGroupWrapper extends React.Component {
|
||||||
isCurrent,
|
isCurrent,
|
||||||
isEditing,
|
isEditing,
|
||||||
showSelection,
|
showSelection,
|
||||||
showLoadInfo,
|
|
||||||
isFirst,
|
isFirst,
|
||||||
presentInSearch,
|
presentInSearch,
|
||||||
isNote,
|
isNote,
|
||||||
|
|
@ -77,8 +65,6 @@ class EventGroupWrapper extends React.Component {
|
||||||
event={event}
|
event={event}
|
||||||
onClick={this.onEventClick}
|
onClick={this.onEventClick}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
showLoadInfo={showLoadInfo}
|
|
||||||
toggleLoadInfo={this.toggleLoadInfo}
|
|
||||||
isCurrent={isCurrent}
|
isCurrent={isCurrent}
|
||||||
presentInSearch={presentInSearch}
|
presentInSearch={presentInSearch}
|
||||||
isLastInGroup={isLastInGroup}
|
isLastInGroup={isLastInGroup}
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,98 @@
|
||||||
import React from 'react';
|
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>
|
||||||
<div className={ styles.bar } onClick={ onClick }>
|
<div className={styles.bar}>
|
||||||
{ typeof fcpTime === 'number' && <div style={ { width: `${ a }%` } } /> }
|
{typeof fcpTime === 'number' && <div style={{ width: `${a}%` }} />}
|
||||||
{ typeof visuallyComplete === 'number' && <div style={ { width: `${ b }%` } } /> }
|
{typeof visuallyComplete === 'number' && (
|
||||||
{ typeof timeToInteractive === 'number' && <div style={ { width: `${ c }%` } } /> }
|
<div style={{ width: `${b}%` }} />
|
||||||
|
)}
|
||||||
|
{typeof timeToInteractive === 'number' && (
|
||||||
|
<div style={{ width: `${c}%` }} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.bottomBlock } data-hidden={ !showInfo }>
|
<div className={styles.bottomBlock}>
|
||||||
{ typeof fcpTime === 'number' &&
|
{typeof fcpTime === 'number' && (
|
||||||
<div className={ styles.wrapper }>
|
<div className={styles.wrapper}>
|
||||||
<div className={ styles.lines } />
|
<div className={styles.lines} />
|
||||||
<div className={ styles.label } >{ 'Time to Render' }</div>
|
<div className={styles.label}>{'Time to Render'}</div>
|
||||||
<div className={ styles.value }>{ `${ numberWithCommas(fcpTime || 0) }ms` }</div>
|
<div className={styles.value}>{`${numberWithCommas(
|
||||||
|
fcpTime || 0
|
||||||
|
)}ms`}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{ typeof visuallyComplete === 'number' &&
|
{typeof visuallyComplete === 'number' && (
|
||||||
<div className={ styles.wrapper }>
|
<div className={styles.wrapper}>
|
||||||
<div className={ styles.lines } />
|
<div className={styles.lines} />
|
||||||
<div className={ styles.label } >{ 'Visually Complete' }</div>
|
<div className={styles.label}>{'Visually Complete'}</div>
|
||||||
<div className={ styles.value }>{ `${ numberWithCommas(visuallyComplete || 0) }ms` }</div>
|
<div className={styles.value}>{`${numberWithCommas(
|
||||||
|
visuallyComplete || 0
|
||||||
|
)}ms`}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{ typeof timeToInteractive === 'number' &&
|
{typeof timeToInteractive === 'number' && (
|
||||||
<div className={ styles.wrapper }>
|
<div className={styles.wrapper}>
|
||||||
<div className={ styles.lines } />
|
<div className={styles.lines} />
|
||||||
<div className={ styles.label } >{ 'Time To Interactive' }</div>
|
<div className={styles.label}>{'Time To Interactive'}</div>
|
||||||
<div className={ styles.value }>{ `${ numberWithCommas(timeToInteractive || 0) }ms` }</div>
|
<div className={styles.value}>{`${numberWithCommas(
|
||||||
|
timeToInteractive || 0
|
||||||
|
)}ms`}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{/* <div className={ styles.download }>
|
{webvitals
|
||||||
<a>
|
? Object.keys(webvitals).map((key) => (
|
||||||
<Icon name="download" />
|
<WebVitalsValueMemo name={key.toUpperCase()} value={webvitals[key]} />
|
||||||
{ '.HAR' }
|
))
|
||||||
</a>
|
: null}
|
||||||
<div>
|
|
||||||
{ new Date().toString() }
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
</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';
|
LoadInfo.displayName = 'LoadInfo';
|
||||||
|
|
||||||
export default LoadInfo;
|
export default React.memo(LoadInfo);
|
||||||
|
|
|
||||||
|
|
@ -550,8 +550,14 @@ type TrGraphQL = [
|
||||||
duration: number,
|
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 {
|
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||||
switch(tMsg[0]) {
|
switch(tMsg[0]) {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ export interface LocationEvent extends IEvent {
|
||||||
referrer: string;
|
referrer: string;
|
||||||
firstContentfulPaintTime: number;
|
firstContentfulPaintTime: number;
|
||||||
firstPaintTime: number;
|
firstPaintTime: number;
|
||||||
|
webVitals: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventData = ConsoleEvent | ClickEvent | InputEvent | LocationEvent | IEvent;
|
export type EventData = ConsoleEvent | ClickEvent | InputEvent | LocationEvent | IEvent;
|
||||||
|
|
@ -192,12 +193,19 @@ export class Location extends Event {
|
||||||
visuallyComplete: LocationEvent['visuallyComplete'];
|
visuallyComplete: LocationEvent['visuallyComplete'];
|
||||||
timeToInteractive: LocationEvent['timeToInteractive'];
|
timeToInteractive: LocationEvent['timeToInteractive'];
|
||||||
referrer: LocationEvent['referrer'];
|
referrer: LocationEvent['referrer'];
|
||||||
|
webvitals: {
|
||||||
|
cls?: number;
|
||||||
|
lcp?: number;
|
||||||
|
inp?: number;
|
||||||
|
ttfb?: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
constructor(evt: LocationEvent) {
|
constructor(evt: LocationEvent) {
|
||||||
super(evt);
|
super(evt);
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
...evt,
|
...evt,
|
||||||
fcpTime: evt.firstContentfulPaintTime || evt.firstPaintTime,
|
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 isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType);
|
||||||
|
|
||||||
const events: InjectedEvent[] = [];
|
const events: InjectedEvent[] = [];
|
||||||
const rawEvents: (EventData & { key: number })[] = [];
|
|
||||||
|
|
||||||
if (session.events?.length) {
|
if (session.events?.length) {
|
||||||
(session.events as EventData[]).forEach((event: EventData, k) => {
|
(session.events as EventData[]).forEach((event: EventData, k) => {
|
||||||
|
|
@ -271,7 +270,6 @@ export default class Session {
|
||||||
if (EventClass) {
|
if (EventClass) {
|
||||||
events.push(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 frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || [];
|
||||||
|
|
||||||
const mixedEventsWithIssues = mergeEventLists(
|
const mixedEventsWithIssues = mergeEventLists(
|
||||||
mergeEventLists(rawEvents, rawNotes),
|
mergeEventLists(events, rawNotes),
|
||||||
frustrationIssues
|
frustrationIssues
|
||||||
).sort(sortEvents)
|
).sort(sortEvents)
|
||||||
|
|
||||||
|
|
@ -377,7 +375,6 @@ export default class Session {
|
||||||
|
|
||||||
const events: InjectedEvent[] = [];
|
const events: InjectedEvent[] = [];
|
||||||
const uxtDoneEvents = userTestingEvents.filter(e => e.status === 'done' && e.title).map(e => ({ ...e, type: 'UXT_EVENT', key: e.signal_id }))
|
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;
|
let uxtIndexNum = 0;
|
||||||
if (sessionEvents.length) {
|
if (sessionEvents.length) {
|
||||||
|
|
@ -394,7 +391,6 @@ export default class Session {
|
||||||
if (EventClass) {
|
if (EventClass) {
|
||||||
events.push(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 frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || [];
|
||||||
|
|
||||||
const mixedEventsWithIssues = mergeEventLists(
|
const mixedEventsWithIssues = mergeEventLists(
|
||||||
rawEvents,
|
events,
|
||||||
frustrationIssues.filter(i => i.type !== issueTypes.DEAD_CLICK)
|
frustrationIssues.filter(i => i.type !== issueTypes.DEAD_CLICK)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ message 30, 'Metadata', :replayer => false do
|
||||||
string 'Key'
|
string 'Key'
|
||||||
string 'Value'
|
string 'Value'
|
||||||
end
|
end
|
||||||
message 31, 'PageEvent', :tracker => false, :replayer => false do
|
message 31, 'PageEventDeprecated', :tracker => false, :replayer => false do
|
||||||
uint 'MessageID'
|
uint 'MessageID'
|
||||||
uint 'Timestamp'
|
uint 'Timestamp'
|
||||||
string 'URL'
|
string 'URL'
|
||||||
|
|
@ -182,6 +182,7 @@ message 31, 'PageEvent', :tracker => false, :replayer => false do
|
||||||
uint 'VisuallyComplete'
|
uint 'VisuallyComplete'
|
||||||
uint 'TimeToInteractive'
|
uint 'TimeToInteractive'
|
||||||
end
|
end
|
||||||
|
|
||||||
message 32, 'InputEvent', :tracker => false, :replayer => false do
|
message 32, 'InputEvent', :tracker => false, :replayer => false do
|
||||||
uint 'MessageID'
|
uint 'MessageID'
|
||||||
uint 'Timestamp'
|
uint 'Timestamp'
|
||||||
|
|
@ -190,6 +191,27 @@ message 32, 'InputEvent', :tracker => false, :replayer => false do
|
||||||
string 'Label'
|
string 'Label'
|
||||||
end
|
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
|
# DEPRECATED since 4.0.2 in favor of AdoptedSSInsertRule + AdoptedSSAddOwner
|
||||||
message 37, 'CSSInsertRule' do
|
message 37, 'CSSInsertRule' do
|
||||||
uint 'ID'
|
uint 'ID'
|
||||||
|
|
@ -556,6 +578,11 @@ message 123, 'GraphQL', :replayer => :devtools do
|
||||||
uint 'Duration'
|
uint 'Duration'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
message 124, 'WebVitals', :replayer => false do
|
||||||
|
string 'Name'
|
||||||
|
string 'Value'
|
||||||
|
end
|
||||||
|
|
||||||
## Backend-only
|
## Backend-only
|
||||||
message 125, 'IssueEvent', :replayer => false, :tracker => false do
|
message 125, 'IssueEvent', :replayer => false, :tracker => false do
|
||||||
uint 'MessageID'
|
uint 'MessageID'
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
# 15.0.0
|
||||||
|
|
||||||
|
- new webvitals messages source
|
||||||
|
|
||||||
# 14.0.8
|
# 14.0.8
|
||||||
|
|
||||||
- use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy))
|
- 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",
|
"name": "@openreplay/tracker",
|
||||||
"description": "The OpenReplay tracker main package",
|
"description": "The OpenReplay tracker main package",
|
||||||
"version": "14.0.8",
|
"version": "15.0.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"logging",
|
"logging",
|
||||||
"replay"
|
"replay"
|
||||||
|
|
@ -52,7 +52,8 @@
|
||||||
"@medv/finder": "^3.2.0",
|
"@medv/finder": "^3.2.0",
|
||||||
"@openreplay/network-proxy": "^1.0.3",
|
"@openreplay/network-proxy": "^1.0.3",
|
||||||
"error-stack-parser": "^2.0.6",
|
"error-stack-parser": "^2.0.6",
|
||||||
"fflate": "^0.8.2"
|
"fflate": "^0.8.2",
|
||||||
|
"web-vitals": "^4.2.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0"
|
"node": ">=14.0"
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ export declare const enum Type {
|
||||||
Redux = 121,
|
Redux = 121,
|
||||||
SetPageLocation = 122,
|
SetPageLocation = 122,
|
||||||
GraphQL = 123,
|
GraphQL = 123,
|
||||||
|
WebVitals = 124,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -626,6 +627,12 @@ export type GraphQL = [
|
||||||
/*duration:*/ number,
|
/*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
|
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 type App from '../app/index.js'
|
||||||
import { hasTag } from '../app/guards.js'
|
import { hasTag } from '../app/guards.js'
|
||||||
import { isURL, getTimeOrigin } from '../utils.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
|
// 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))
|
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
|
let prevSessionID: string | undefined
|
||||||
app.attachStartCallback(function ({ sessionID }) {
|
app.attachStartCallback(function ({ sessionID }) {
|
||||||
if (sessionID !== prevSessionID) {
|
if (sessionID !== prevSessionID) {
|
||||||
|
|
@ -147,6 +154,18 @@ export default function (app: App, opts: Partial<Options>): void {
|
||||||
prevSessionID = sessionID
|
prevSessionID = sessionID
|
||||||
}
|
}
|
||||||
observer.observe({ entryTypes: ['resource'] })
|
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 () {
|
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])
|
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.uint(msg[5])
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case Messages.Type.WebVitals:
|
||||||
|
return this.string(msg[1]) && this.string(msg[2])
|
||||||
|
break
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue