From 97a08853e8420368d8e34fa01b5fe99cca7b48a5 Mon Sep 17 00:00:00 2001 From: Delirium Date: Mon, 30 Sep 2024 16:08:42 +0200 Subject: [PATCH] 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 --- backend/cmd/db/main.go | 2 +- backend/pkg/db/postgres/bulks.go | 6 +- backend/pkg/db/postgres/events.go | 3 +- .../pkg/handlers/custom/pageEventBuilder.go | 35 +++-- backend/pkg/messages/filters.go | 2 +- .../pkg/messages/legacy-message-transform.go | 21 +++ backend/pkg/messages/messages.go | 90 ++++++++++++- backend/pkg/messages/read-message.go | 82 +++++++++++- ee/connectors/msgcodec/messages.py | 34 ++++- ee/connectors/msgcodec/messages.pyx | 56 +++++++- ee/connectors/msgcodec/msgcodec.py | 30 ++++- ee/connectors/msgcodec/msgcodec.pyx | 30 ++++- .../components/Session_/EventsBlock/Event.tsx | 41 +++--- .../Session_/EventsBlock/EventGroupWrapper.js | 14 -- .../Session_/EventsBlock/LoadInfo.js | 121 ++++++++++++------ .../app/player/web/messages/tracker.gen.ts | 8 +- frontend/app/types/session/event.ts | 8 ++ frontend/app/types/session/session.ts | 8 +- mobs/messages.rb | 29 ++++- tracker/tracker/CHANGELOG.md | 4 + tracker/tracker/bun.lockb | Bin 207697 -> 208055 bytes tracker/tracker/package.json | 5 +- tracker/tracker/src/common/messages.gen.ts | 9 +- tracker/tracker/src/main/app/messages.gen.ts | 11 ++ tracker/tracker/src/main/modules/timing.ts | 21 ++- .../src/webworker/MessageEncoder.gen.ts | 4 + 26 files changed, 563 insertions(+), 111 deletions(-) diff --git a/backend/cmd/db/main.go b/backend/cmd/db/main.go index 811ecdaf0..8585b8f66 100644 --- a/backend/cmd/db/main.go +++ b/backend/cmd/db/main.go @@ -60,7 +60,7 @@ func main() { messages.MsgFetch, messages.MsgNetworkRequest, messages.MsgGraphQL, messages.MsgStateAction, messages.MsgMouseClick, messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated, messages.MsgPageLoadTiming, messages.MsgPageRenderTiming, - messages.MsgPageEvent, messages.MsgMouseThrashing, messages.MsgInputChange, + messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange, messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger, // Mobile messages messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID, diff --git a/backend/pkg/db/postgres/bulks.go b/backend/pkg/db/postgres/bulks.go index cd73fc6a9..0ba9e0ccd 100644 --- a/backend/pkg/db/postgres/bulks.go +++ b/backend/pkg/db/postgres/bulks.go @@ -126,11 +126,11 @@ func (conn *BulkSet) initBulks() { "events.pages", "(session_id, message_id, timestamp, referrer, base_referrer, host, path, query, dom_content_loaded_time, "+ "load_time, response_end, first_paint_time, first_contentful_paint_time, speed_index, visually_complete, "+ - "time_to_interactive, response_time, dom_building_time)", + "time_to_interactive, response_time, dom_building_time, web_vitals)", "($%d, $%d, $%d, LEFT($%d, 8000), LEFT($%d, 8000), LEFT($%d, 300), LEFT($%d, 2000), LEFT($%d, 8000), "+ "NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0),"+ - " NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0))", - 18, 200) + " NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, 0), NULLIF($%d, ''))", + 19, 200) if err != nil { conn.log.Fatal(conn.ctx, "can't create webPageEvents bulk: %s", err) } diff --git a/backend/pkg/db/postgres/events.go b/backend/pkg/db/postgres/events.go index a6be4c99a..47b9cb566 100644 --- a/backend/pkg/db/postgres/events.go +++ b/backend/pkg/db/postgres/events.go @@ -116,7 +116,8 @@ func (conn *Conn) InsertWebPageEvent(sess *sessions.Session, e *messages.PageEve // base_path is deprecated if err = conn.bulks.Get("webPageEvents").Append(sess.SessionID, truncSqIdx(e.MessageID), e.Timestamp, e.Referrer, url.DiscardURLQuery(e.Referrer), host, path, query, e.DomContentLoadedEventEnd, e.LoadEventEnd, e.ResponseEnd, e.FirstPaint, e.FirstContentfulPaint, - e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e)); err != nil { + e.SpeedIndex, e.VisuallyComplete, e.TimeToInteractive, calcResponseTime(e), calcDomBuildingTime(e), + e.WebVitals); err != nil { sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID) conn.log.Error(sessCtx, "insert web page event in bulk err: %s", err) } diff --git a/backend/pkg/handlers/custom/pageEventBuilder.go b/backend/pkg/handlers/custom/pageEventBuilder.go index 5bab7d4cc..589551b79 100644 --- a/backend/pkg/handlers/custom/pageEventBuilder.go +++ b/backend/pkg/handlers/custom/pageEventBuilder.go @@ -1,6 +1,8 @@ package custom import ( + "encoding/json" + "fmt" . "openreplay/backend/pkg/messages" ) @@ -9,6 +11,7 @@ const PageEventTimeout = 1 * 60 * 1000 type pageEventBuilder struct { pageEvent *PageEvent firstTimingHandled bool + webVitals map[string]string } func NewPageEventBuilder() *pageEventBuilder { @@ -69,7 +72,7 @@ func (b *pageEventBuilder) Handle(message Message, timestamp uint64) Message { if msg.FirstContentfulPaint <= 30000 { b.pageEvent.FirstContentfulPaint = msg.FirstContentfulPaint } - return b.buildIfTimingsComplete() + return nil //b.buildIfTimingsComplete() case *PageRenderTiming: if b.pageEvent == nil { break @@ -77,8 +80,12 @@ func (b *pageEventBuilder) Handle(message Message, timestamp uint64) Message { b.pageEvent.SpeedIndex = msg.SpeedIndex b.pageEvent.VisuallyComplete = msg.VisuallyComplete b.pageEvent.TimeToInteractive = msg.TimeToInteractive - return b.buildIfTimingsComplete() - + return nil //b.buildIfTimingsComplete() + case *WebVitals: + if b.webVitals == nil { + b.webVitals = make(map[string]string) + } + b.webVitals[msg.Name] = msg.Value } if b.pageEvent != nil && b.pageEvent.Timestamp+PageEventTimeout < timestamp { @@ -94,13 +101,21 @@ func (b *pageEventBuilder) Build() Message { pageEvent := b.pageEvent b.pageEvent = nil b.firstTimingHandled = false + if b.webVitals != nil { + if vitals, err := json.Marshal(b.webVitals); err == nil { + pageEvent.WebVitals = string(vitals) + } else { + // DEBUG + fmt.Printf("Error marshalling web vitals: %v\n", err) + } + } return pageEvent } -func (b *pageEventBuilder) buildIfTimingsComplete() Message { - if b.firstTimingHandled { - return b.Build() - } - b.firstTimingHandled = true - return nil -} +//func (b *pageEventBuilder) buildIfTimingsComplete() Message { +// if b.firstTimingHandled { +// return b.Build() +// } +// b.firstTimingHandled = true +// return nil +//} diff --git a/backend/pkg/messages/filters.go b/backend/pkg/messages/filters.go index eea77cf9d..7a363f3d8 100644 --- a/backend/pkg/messages/filters.go +++ b/backend/pkg/messages/filters.go @@ -2,7 +2,7 @@ package messages func IsReplayerType(id int) bool { - return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id + return 1 != id && 3 != id && 17 != id && 23 != id && 24 != id && 25 != id && 26 != id && 27 != id && 28 != id && 29 != id && 30 != id && 31 != id && 32 != id && 33 != id && 42 != id && 56 != id && 62 != id && 63 != id && 64 != id && 66 != id && 78 != id && 80 != id && 81 != id && 82 != id && 112 != id && 115 != id && 124 != id && 125 != id && 126 != id && 127 != id && 90 != id && 91 != id && 92 != id && 94 != id && 95 != id && 97 != id && 98 != id && 107 != id && 110 != id } func IsMobileType(id int) bool { diff --git a/backend/pkg/messages/legacy-message-transform.go b/backend/pkg/messages/legacy-message-transform.go index fbb7a8222..23ab1e52d 100644 --- a/backend/pkg/messages/legacy-message-transform.go +++ b/backend/pkg/messages/legacy-message-transform.go @@ -59,6 +59,27 @@ func transformDeprecated(msg Message) Message { NavigationStart: m.NavigationStart, DocumentTitle: "", } + case *PageEventDeprecated: + return &PageEvent{ + MessageID: m.MessageID, + Timestamp: m.Timestamp, + URL: m.URL, + Referrer: m.Referrer, + Loaded: m.Loaded, + RequestStart: m.RequestStart, + ResponseStart: m.ResponseStart, + ResponseEnd: m.ResponseEnd, + DomContentLoadedEventStart: m.DomContentLoadedEventStart, + DomContentLoadedEventEnd: m.DomContentLoadedEventEnd, + LoadEventStart: m.LoadEventStart, + LoadEventEnd: m.LoadEventEnd, + FirstPaint: m.FirstPaint, + FirstContentfulPaint: m.FirstContentfulPaint, + SpeedIndex: m.SpeedIndex, + VisuallyComplete: m.VisuallyComplete, + TimeToInteractive: m.TimeToInteractive, + WebVitals: "", + } } return msg } diff --git a/backend/pkg/messages/messages.go b/backend/pkg/messages/messages.go index 5b0c4acee..19be1a0c7 100644 --- a/backend/pkg/messages/messages.go +++ b/backend/pkg/messages/messages.go @@ -32,8 +32,9 @@ const ( MsgUserID = 28 MsgUserAnonymousID = 29 MsgMetadata = 30 - MsgPageEvent = 31 + MsgPageEventDeprecated = 31 MsgInputEvent = 32 + MsgPageEvent = 33 MsgCSSInsertRule = 37 MsgCSSDeleteRule = 38 MsgFetch = 39 @@ -91,6 +92,7 @@ const ( MsgRedux = 121 MsgSetPageLocation = 122 MsgGraphQL = 123 + MsgWebVitals = 124 MsgIssueEvent = 125 MsgSessionEnd = 126 MsgSessionSearch = 127 @@ -874,7 +876,7 @@ func (msg *Metadata) TypeID() int { return 30 } -type PageEvent struct { +type PageEventDeprecated struct { message MessageID uint64 Timestamp uint64 @@ -895,7 +897,7 @@ type PageEvent struct { TimeToInteractive uint64 } -func (msg *PageEvent) Encode() []byte { +func (msg *PageEventDeprecated) Encode() []byte { buf := make([]byte, 171+len(msg.URL)+len(msg.Referrer)) buf[0] = 31 p := 1 @@ -919,11 +921,11 @@ func (msg *PageEvent) Encode() []byte { return buf[:p] } -func (msg *PageEvent) Decode() Message { +func (msg *PageEventDeprecated) Decode() Message { return msg } -func (msg *PageEvent) TypeID() int { +func (msg *PageEventDeprecated) TypeID() int { return 31 } @@ -956,6 +958,61 @@ func (msg *InputEvent) TypeID() int { return 32 } +type PageEvent struct { + message + MessageID uint64 + Timestamp uint64 + URL string + Referrer string + Loaded bool + RequestStart uint64 + ResponseStart uint64 + ResponseEnd uint64 + DomContentLoadedEventStart uint64 + DomContentLoadedEventEnd uint64 + LoadEventStart uint64 + LoadEventEnd uint64 + FirstPaint uint64 + FirstContentfulPaint uint64 + SpeedIndex uint64 + VisuallyComplete uint64 + TimeToInteractive uint64 + WebVitals string +} + +func (msg *PageEvent) Encode() []byte { + buf := make([]byte, 181+len(msg.URL)+len(msg.Referrer)+len(msg.WebVitals)) + buf[0] = 33 + p := 1 + p = WriteUint(msg.MessageID, buf, p) + p = WriteUint(msg.Timestamp, buf, p) + p = WriteString(msg.URL, buf, p) + p = WriteString(msg.Referrer, buf, p) + p = WriteBoolean(msg.Loaded, buf, p) + p = WriteUint(msg.RequestStart, buf, p) + p = WriteUint(msg.ResponseStart, buf, p) + p = WriteUint(msg.ResponseEnd, buf, p) + p = WriteUint(msg.DomContentLoadedEventStart, buf, p) + p = WriteUint(msg.DomContentLoadedEventEnd, buf, p) + p = WriteUint(msg.LoadEventStart, buf, p) + p = WriteUint(msg.LoadEventEnd, buf, p) + p = WriteUint(msg.FirstPaint, buf, p) + p = WriteUint(msg.FirstContentfulPaint, buf, p) + p = WriteUint(msg.SpeedIndex, buf, p) + p = WriteUint(msg.VisuallyComplete, buf, p) + p = WriteUint(msg.TimeToInteractive, buf, p) + p = WriteString(msg.WebVitals, buf, p) + return buf[:p] +} + +func (msg *PageEvent) Decode() Message { + return msg +} + +func (msg *PageEvent) TypeID() int { + return 33 +} + type CSSInsertRule struct { message ID uint64 @@ -2443,6 +2500,29 @@ func (msg *GraphQL) TypeID() int { return 123 } +type WebVitals struct { + message + Name string + Value string +} + +func (msg *WebVitals) Encode() []byte { + buf := make([]byte, 21+len(msg.Name)+len(msg.Value)) + buf[0] = 124 + p := 1 + p = WriteString(msg.Name, buf, p) + p = WriteString(msg.Value, buf, p) + return buf[:p] +} + +func (msg *WebVitals) Decode() Message { + return msg +} + +func (msg *WebVitals) TypeID() int { + return 124 +} + type IssueEvent struct { message MessageID uint64 diff --git a/backend/pkg/messages/read-message.go b/backend/pkg/messages/read-message.go index 4ddde9c3b..3521a0e29 100644 --- a/backend/pkg/messages/read-message.go +++ b/backend/pkg/messages/read-message.go @@ -468,9 +468,9 @@ func DecodeMetadata(reader BytesReader) (Message, error) { return msg, err } -func DecodePageEvent(reader BytesReader) (Message, error) { +func DecodePageEventDeprecated(reader BytesReader) (Message, error) { var err error = nil - msg := &PageEvent{} + msg := &PageEventDeprecated{} if msg.MessageID, err = reader.ReadUint(); err != nil { return nil, err } @@ -546,6 +546,66 @@ func DecodeInputEvent(reader BytesReader) (Message, error) { return msg, err } +func DecodePageEvent(reader BytesReader) (Message, error) { + var err error = nil + msg := &PageEvent{} + if msg.MessageID, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.Timestamp, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.URL, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Referrer, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Loaded, err = reader.ReadBoolean(); err != nil { + return nil, err + } + if msg.RequestStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.ResponseStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.ResponseEnd, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.DomContentLoadedEventStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.DomContentLoadedEventEnd, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.LoadEventStart, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.LoadEventEnd, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.FirstPaint, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.FirstContentfulPaint, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.SpeedIndex, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.VisuallyComplete, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.TimeToInteractive, err = reader.ReadUint(); err != nil { + return nil, err + } + if msg.WebVitals, err = reader.ReadString(); err != nil { + return nil, err + } + return msg, err +} + func DecodeCSSInsertRule(reader BytesReader) (Message, error) { var err error = nil msg := &CSSInsertRule{} @@ -1494,6 +1554,18 @@ func DecodeGraphQL(reader BytesReader) (Message, error) { return msg, err } +func DecodeWebVitals(reader BytesReader) (Message, error) { + var err error = nil + msg := &WebVitals{} + if msg.Name, err = reader.ReadString(); err != nil { + return nil, err + } + if msg.Value, err = reader.ReadString(); err != nil { + return nil, err + } + return msg, err +} + func DecodeIssueEvent(reader BytesReader) (Message, error) { var err error = nil msg := &IssueEvent{} @@ -2019,9 +2091,11 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) { case 30: return DecodeMetadata(reader) case 31: - return DecodePageEvent(reader) + return DecodePageEventDeprecated(reader) case 32: return DecodeInputEvent(reader) + case 33: + return DecodePageEvent(reader) case 37: return DecodeCSSInsertRule(reader) case 38: @@ -2136,6 +2210,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) { return DecodeSetPageLocation(reader) case 123: return DecodeGraphQL(reader) + case 124: + return DecodeWebVitals(reader) case 125: return DecodeIssueEvent(reader) case 126: diff --git a/ee/connectors/msgcodec/messages.py b/ee/connectors/msgcodec/messages.py index f14b05f92..0ad3a15be 100644 --- a/ee/connectors/msgcodec/messages.py +++ b/ee/connectors/msgcodec/messages.py @@ -281,7 +281,7 @@ class Metadata(Message): self.value = value -class PageEvent(Message): +class PageEventDeprecated(Message): __id__ = 31 def __init__(self, message_id, timestamp, url, referrer, loaded, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint, speed_index, visually_complete, time_to_interactive): @@ -315,6 +315,30 @@ class InputEvent(Message): self.label = label +class PageEvent(Message): + __id__ = 33 + + def __init__(self, message_id, timestamp, url, referrer, loaded, request_start, response_start, response_end, dom_content_loaded_event_start, dom_content_loaded_event_end, load_event_start, load_event_end, first_paint, first_contentful_paint, speed_index, visually_complete, time_to_interactive, web_vitals): + self.message_id = message_id + self.timestamp = timestamp + self.url = url + self.referrer = referrer + self.loaded = loaded + self.request_start = request_start + self.response_start = response_start + self.response_end = response_end + self.dom_content_loaded_event_start = dom_content_loaded_event_start + self.dom_content_loaded_event_end = dom_content_loaded_event_end + self.load_event_start = load_event_start + self.load_event_end = load_event_end + self.first_paint = first_paint + self.first_contentful_paint = first_contentful_paint + self.speed_index = speed_index + self.visually_complete = visually_complete + self.time_to_interactive = time_to_interactive + self.web_vitals = web_vitals + + class CSSInsertRule(Message): __id__ = 37 @@ -859,6 +883,14 @@ class GraphQL(Message): self.duration = duration +class WebVitals(Message): + __id__ = 124 + + def __init__(self, name, value): + self.name = name + self.value = value + + class IssueEvent(Message): __id__ = 125 diff --git a/ee/connectors/msgcodec/messages.pyx b/ee/connectors/msgcodec/messages.pyx index d495bfaf4..37183b755 100644 --- a/ee/connectors/msgcodec/messages.pyx +++ b/ee/connectors/msgcodec/messages.pyx @@ -410,7 +410,7 @@ cdef class Metadata(PyMessage): self.value = value -cdef class PageEvent(PyMessage): +cdef class PageEventDeprecated(PyMessage): cdef public int __id__ cdef public unsigned long message_id cdef public unsigned long timestamp @@ -468,6 +468,49 @@ cdef class InputEvent(PyMessage): self.label = label +cdef class PageEvent(PyMessage): + cdef public int __id__ + cdef public unsigned long message_id + cdef public unsigned long timestamp + cdef public str url + cdef public str referrer + cdef public bint loaded + cdef public unsigned long request_start + cdef public unsigned long response_start + cdef public unsigned long response_end + cdef public unsigned long dom_content_loaded_event_start + cdef public unsigned long dom_content_loaded_event_end + cdef public unsigned long load_event_start + cdef public unsigned long load_event_end + cdef public unsigned long first_paint + cdef public unsigned long first_contentful_paint + cdef public unsigned long speed_index + cdef public unsigned long visually_complete + cdef public unsigned long time_to_interactive + cdef public str web_vitals + + def __init__(self, unsigned long message_id, unsigned long timestamp, str url, str referrer, bint loaded, unsigned long request_start, unsigned long response_start, unsigned long response_end, unsigned long dom_content_loaded_event_start, unsigned long dom_content_loaded_event_end, unsigned long load_event_start, unsigned long load_event_end, unsigned long first_paint, unsigned long first_contentful_paint, unsigned long speed_index, unsigned long visually_complete, unsigned long time_to_interactive, str web_vitals): + self.__id__ = 33 + self.message_id = message_id + self.timestamp = timestamp + self.url = url + self.referrer = referrer + self.loaded = loaded + self.request_start = request_start + self.response_start = response_start + self.response_end = response_end + self.dom_content_loaded_event_start = dom_content_loaded_event_start + self.dom_content_loaded_event_end = dom_content_loaded_event_end + self.load_event_start = load_event_start + self.load_event_end = load_event_end + self.first_paint = first_paint + self.first_contentful_paint = first_contentful_paint + self.speed_index = speed_index + self.visually_complete = visually_complete + self.time_to_interactive = time_to_interactive + self.web_vitals = web_vitals + + cdef class CSSInsertRule(PyMessage): cdef public int __id__ cdef public unsigned long id @@ -1271,6 +1314,17 @@ cdef class GraphQL(PyMessage): self.duration = duration +cdef class WebVitals(PyMessage): + cdef public int __id__ + cdef public str name + cdef public str value + + def __init__(self, str name, str value): + self.__id__ = 124 + self.name = name + self.value = value + + cdef class IssueEvent(PyMessage): cdef public int __id__ cdef public unsigned long message_id diff --git a/ee/connectors/msgcodec/msgcodec.py b/ee/connectors/msgcodec/msgcodec.py index 6a4a73bdc..6745ee99a 100644 --- a/ee/connectors/msgcodec/msgcodec.py +++ b/ee/connectors/msgcodec/msgcodec.py @@ -309,7 +309,7 @@ class MessageCodec(Codec): ) if message_id == 31: - return PageEvent( + return PageEventDeprecated( message_id=self.read_uint(reader), timestamp=self.read_uint(reader), url=self.read_string(reader), @@ -338,6 +338,28 @@ class MessageCodec(Codec): label=self.read_string(reader) ) + if message_id == 33: + return PageEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + url=self.read_string(reader), + referrer=self.read_string(reader), + loaded=self.read_boolean(reader), + request_start=self.read_uint(reader), + response_start=self.read_uint(reader), + response_end=self.read_uint(reader), + dom_content_loaded_event_start=self.read_uint(reader), + dom_content_loaded_event_end=self.read_uint(reader), + load_event_start=self.read_uint(reader), + load_event_end=self.read_uint(reader), + first_paint=self.read_uint(reader), + first_contentful_paint=self.read_uint(reader), + speed_index=self.read_uint(reader), + visually_complete=self.read_uint(reader), + time_to_interactive=self.read_uint(reader), + web_vitals=self.read_string(reader) + ) + if message_id == 37: return CSSInsertRule( id=self.read_uint(reader), @@ -768,6 +790,12 @@ class MessageCodec(Codec): duration=self.read_uint(reader) ) + if message_id == 124: + return WebVitals( + name=self.read_string(reader), + value=self.read_string(reader) + ) + if message_id == 125: return IssueEvent( message_id=self.read_uint(reader), diff --git a/ee/connectors/msgcodec/msgcodec.pyx b/ee/connectors/msgcodec/msgcodec.pyx index b7f9e105e..1936ffea5 100644 --- a/ee/connectors/msgcodec/msgcodec.pyx +++ b/ee/connectors/msgcodec/msgcodec.pyx @@ -407,7 +407,7 @@ cdef class MessageCodec: ) if message_id == 31: - return PageEvent( + return PageEventDeprecated( message_id=self.read_uint(reader), timestamp=self.read_uint(reader), url=self.read_string(reader), @@ -436,6 +436,28 @@ cdef class MessageCodec: label=self.read_string(reader) ) + if message_id == 33: + return PageEvent( + message_id=self.read_uint(reader), + timestamp=self.read_uint(reader), + url=self.read_string(reader), + referrer=self.read_string(reader), + loaded=self.read_boolean(reader), + request_start=self.read_uint(reader), + response_start=self.read_uint(reader), + response_end=self.read_uint(reader), + dom_content_loaded_event_start=self.read_uint(reader), + dom_content_loaded_event_end=self.read_uint(reader), + load_event_start=self.read_uint(reader), + load_event_end=self.read_uint(reader), + first_paint=self.read_uint(reader), + first_contentful_paint=self.read_uint(reader), + speed_index=self.read_uint(reader), + visually_complete=self.read_uint(reader), + time_to_interactive=self.read_uint(reader), + web_vitals=self.read_string(reader) + ) + if message_id == 37: return CSSInsertRule( id=self.read_uint(reader), @@ -866,6 +888,12 @@ cdef class MessageCodec: duration=self.read_uint(reader) ) + if message_id == 124: + return WebVitals( + name=self.read_string(reader), + value=self.read_string(reader) + ) + if message_id == 125: return IssueEvent( message_id=self.read_uint(reader), diff --git a/frontend/app/components/Session_/EventsBlock/Event.tsx b/frontend/app/components/Session_/EventsBlock/Event.tsx index 3823699d8..8d212e578 100644 --- a/frontend/app/components/Session_/EventsBlock/Event.tsx +++ b/frontend/app/components/Session_/EventsBlock/Event.tsx @@ -25,7 +25,6 @@ type Props = { isCurrent?: boolean; onClick?: () => void; showSelection?: boolean; - showLoadInfo?: boolean; toggleLoadInfo?: () => void; isRed?: boolean; presentInSearch?: boolean; @@ -52,7 +51,6 @@ const Event: React.FC = ({ isCurrent = false, onClick, showSelection = false, - showLoadInfo, toggleLoadInfo, isRed = false, presentInSearch = false, @@ -251,25 +249,26 @@ const Event: React.FC = ({ {renderBody()} {isLocation && - (event.fcpTime || - event.visuallyComplete || - event.timeToInteractive) && ( - elements / 1.2, - divisorFn: (elements, parts) => elements / (2 * parts + 1), - })} - /> - )} + (event.fcpTime || + event.visuallyComplete || + event.timeToInteractive || + event.webvitals) ? ( + elements / 1.2, + divisorFn: (elements, parts) => elements / (2 * parts + 1), + })} + /> + ) : null} ); }; diff --git a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js index e638ad339..67a130dc9 100644 --- a/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js +++ b/frontend/app/components/Session_/EventsBlock/EventGroupWrapper.js @@ -2,14 +2,11 @@ import UxtEvent from "Components/Session_/EventsBlock/UxtEvent"; import React from 'react'; import { connect } from 'react-redux'; import { TextEllipsis, Icon } from 'UI'; -import withToggle from 'HOCs/withToggle'; import { TYPES } from 'Types/session/event'; import Event from './Event'; import stl from './eventGroupWrapper.module.css'; import NoteEvent from './NoteEvent'; -// TODO: incapsulate toggler in LocationEvent -@withToggle('showLoadInfo', 'toggleLoadInfo') @connect( (state) => ({ members: state.getIn(['members', 'list']), @@ -17,14 +14,6 @@ import NoteEvent from './NoteEvent'; }), ) class EventGroupWrapper extends React.Component { - toggleLoadInfo = (e) => { - e.stopPropagation(); - this.props.toggleLoadInfo(); - }; - - componentDidMount() { - this.props.toggleLoadInfo(this.props.isFirst); - } onEventClick = (e) => this.props.onEventClick(e, this.props.event); @@ -39,7 +28,6 @@ class EventGroupWrapper extends React.Component { isCurrent, isEditing, showSelection, - showLoadInfo, isFirst, presentInSearch, isNote, @@ -77,8 +65,6 @@ class EventGroupWrapper extends React.Component { event={event} onClick={this.onEventClick} selected={isSelected} - showLoadInfo={showLoadInfo} - toggleLoadInfo={this.toggleLoadInfo} isCurrent={isCurrent} presentInSearch={presentInSearch} isLastInGroup={isLastInGroup} diff --git a/frontend/app/components/Session_/EventsBlock/LoadInfo.js b/frontend/app/components/Session_/EventsBlock/LoadInfo.js index 3227ec55d..deea1fc17 100644 --- a/frontend/app/components/Session_/EventsBlock/LoadInfo.js +++ b/frontend/app/components/Session_/EventsBlock/LoadInfo.js @@ -1,49 +1,98 @@ import React from 'react'; -import styles from './loadInfo.module.css'; -import { numberWithCommas } from 'App/utils' -const LoadInfo = ({ showInfo = false, onClick, event: { fcpTime, visuallyComplete, timeToInteractive }, prorata: { a, b, c } }) => ( +import { numberWithCommas } from 'App/utils'; + +import styles from './loadInfo.module.css'; + +const LoadInfo = ({ + webvitals, + event: { fcpTime, visuallyComplete, timeToInteractive }, + prorata: { a, b, c }, +}) => (
-
- { typeof fcpTime === 'number' &&
} - { typeof visuallyComplete === 'number' &&
} - { typeof timeToInteractive === 'number' &&
} +
+ {typeof fcpTime === 'number' &&
} + {typeof visuallyComplete === 'number' && ( +
+ )} + {typeof timeToInteractive === 'number' && ( +
+ )}
-
- { typeof fcpTime === 'number' && -
-
-
{ 'Time to Render' }
-
{ `${ numberWithCommas(fcpTime || 0) }ms` }
+
+ {typeof fcpTime === 'number' && ( +
+
+
{'Time to Render'}
+
{`${numberWithCommas( + fcpTime || 0 + )}ms`}
- } - { typeof visuallyComplete === 'number' && -
-
-
{ 'Visually Complete' }
-
{ `${ numberWithCommas(visuallyComplete || 0) }ms` }
+ )} + {typeof visuallyComplete === 'number' && ( +
+
+
{'Visually Complete'}
+
{`${numberWithCommas( + visuallyComplete || 0 + )}ms`}
- } - { typeof timeToInteractive === 'number' && -
-
-
{ 'Time To Interactive' }
-
{ `${ numberWithCommas(timeToInteractive || 0) }ms` }
+ )} + {typeof timeToInteractive === 'number' && ( +
+
+
{'Time To Interactive'}
+
{`${numberWithCommas( + timeToInteractive || 0 + )}ms`}
- } - {/*
- - - { '.HAR' } - -
- { new Date().toString() } -
-
*/} + )} + {webvitals + ? Object.keys(webvitals).map((key) => ( + + )) + : null}
); +function WebVitalsValue({ name, value }) { + const valInt = Number(value); + const valDisplay = + name !== 'CLS' + ? Math.round(valInt) + : valInt > 1 + ? Math.round(valInt) + : valInt.toExponential(1).split('e'); + + const unit = { + CLS: 'score', + FCP: 'ms', + INP: 'ms', + LCP: 'ms', + TTFB: 'ms', + }; + return ( +
+
+
{name}
+
+ {Array.isArray(valDisplay) ? ( + <> + {valDisplay[0]}× 10{valDisplay[1]} + + ) : ( + <> + {valDisplay} {unit[name]} + + )} +
+
+ ); +} + +const WebVitalsValueMemo = React.memo(WebVitalsValue); + LoadInfo.displayName = 'LoadInfo'; -export default LoadInfo; +export default React.memo(LoadInfo); diff --git a/frontend/app/player/web/messages/tracker.gen.ts b/frontend/app/player/web/messages/tracker.gen.ts index cdf0be6c9..c23167cec 100644 --- a/frontend/app/player/web/messages/tracker.gen.ts +++ b/frontend/app/player/web/messages/tracker.gen.ts @@ -550,8 +550,14 @@ type TrGraphQL = [ duration: number, ] +type TrWebVitals = [ + type: 124, + name: string, + value: string, +] -export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL + +export type TrackerMessage = TrTimestamp | TrSetPageLocationDeprecated | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals export default function translate(tMsg: TrackerMessage): RawMessage | null { switch(tMsg[0]) { diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts index 10b1ed23e..dd207e04a 100644 --- a/frontend/app/types/session/event.ts +++ b/frontend/app/types/session/event.ts @@ -80,6 +80,7 @@ export interface LocationEvent extends IEvent { referrer: string; firstContentfulPaintTime: number; firstPaintTime: number; + webVitals: string | null; } export type EventData = ConsoleEvent | ClickEvent | InputEvent | LocationEvent | IEvent; @@ -192,12 +193,19 @@ export class Location extends Event { visuallyComplete: LocationEvent['visuallyComplete']; timeToInteractive: LocationEvent['timeToInteractive']; referrer: LocationEvent['referrer']; + webvitals: { + cls?: number; + lcp?: number; + inp?: number; + ttfb?: number; + } | null; constructor(evt: LocationEvent) { super(evt); Object.assign(this, { ...evt, fcpTime: evt.firstContentfulPaintTime || evt.firstPaintTime, + webvitals: evt.webVitals ? JSON.parse(evt.webVitals) : null, }); } } diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index f109315e4..11d976dde 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -261,7 +261,6 @@ export default class Session { const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType); const events: InjectedEvent[] = []; - const rawEvents: (EventData & { key: number })[] = []; if (session.events?.length) { (session.events as EventData[]).forEach((event: EventData, k) => { @@ -271,7 +270,6 @@ export default class Session { if (EventClass) { events.push(EventClass); } - rawEvents.push({ ...event, time, key: k }); } }); } @@ -306,7 +304,7 @@ export default class Session { const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || []; const mixedEventsWithIssues = mergeEventLists( - mergeEventLists(rawEvents, rawNotes), + mergeEventLists(events, rawNotes), frustrationIssues ).sort(sortEvents) @@ -377,7 +375,6 @@ export default class Session { const events: InjectedEvent[] = []; const uxtDoneEvents = userTestingEvents.filter(e => e.status === 'done' && e.title).map(e => ({ ...e, type: 'UXT_EVENT', key: e.signal_id })) - const rawEvents: (EventData & { key: number })[] = []; let uxtIndexNum = 0; if (sessionEvents.length) { @@ -394,7 +391,6 @@ export default class Session { if (EventClass) { events.push(EventClass); } - rawEvents.push({ ...event, time, key: k, }); } }); } @@ -412,7 +408,7 @@ export default class Session { const frustrationList = [...frustrationEvents, ...frustrationIssues].sort(sortEvents) || []; const mixedEventsWithIssues = mergeEventLists( - rawEvents, + events, frustrationIssues.filter(i => i.type !== issueTypes.DEAD_CLICK) ) diff --git a/mobs/messages.rb b/mobs/messages.rb index f5dfaed52..b36de9041 100644 --- a/mobs/messages.rb +++ b/mobs/messages.rb @@ -163,7 +163,7 @@ message 30, 'Metadata', :replayer => false do string 'Key' string 'Value' end -message 31, 'PageEvent', :tracker => false, :replayer => false do +message 31, 'PageEventDeprecated', :tracker => false, :replayer => false do uint 'MessageID' uint 'Timestamp' string 'URL' @@ -182,6 +182,7 @@ message 31, 'PageEvent', :tracker => false, :replayer => false do uint 'VisuallyComplete' uint 'TimeToInteractive' end + message 32, 'InputEvent', :tracker => false, :replayer => false do uint 'MessageID' uint 'Timestamp' @@ -190,6 +191,27 @@ message 32, 'InputEvent', :tracker => false, :replayer => false do string 'Label' end +message 33, 'PageEvent', :tracker => false, :replayer => false do + uint 'MessageID' + uint 'Timestamp' + string 'URL' + string 'Referrer' + boolean 'Loaded' + uint 'RequestStart' + uint 'ResponseStart' + uint 'ResponseEnd' + uint 'DomContentLoadedEventStart' + uint 'DomContentLoadedEventEnd' + uint 'LoadEventStart' + uint 'LoadEventEnd' + uint 'FirstPaint' + uint 'FirstContentfulPaint' + uint 'SpeedIndex' + uint 'VisuallyComplete' + uint 'TimeToInteractive' + string 'WebVitals' +end + # DEPRECATED since 4.0.2 in favor of AdoptedSSInsertRule + AdoptedSSAddOwner message 37, 'CSSInsertRule' do uint 'ID' @@ -556,6 +578,11 @@ message 123, 'GraphQL', :replayer => :devtools do uint 'Duration' end +message 124, 'WebVitals', :replayer => false do + string 'Name' + string 'Value' +end + ## Backend-only message 125, 'IssueEvent', :replayer => false, :tracker => false do uint 'MessageID' diff --git a/tracker/tracker/CHANGELOG.md b/tracker/tracker/CHANGELOG.md index 88923305a..36093537d 100644 --- a/tracker/tracker/CHANGELOG.md +++ b/tracker/tracker/CHANGELOG.md @@ -1,3 +1,7 @@ +# 15.0.0 + +- new webvitals messages source + # 14.0.8 - use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy)) diff --git a/tracker/tracker/bun.lockb b/tracker/tracker/bun.lockb index 4f71fe9575a53a4723f43c743b83fb1f7aa74884..6db3834ebca4de11f13afa6e37264b0dd3d20722 100755 GIT binary patch delta 37891 zcmeI5dwh@e|Nr0DWpDPz7?I5^Mh?TqHnYuaZ|Bp@G3GQ3W5Y0;!yMX3Ih4eeUWB5f zRSWegd`eO(l#mo5bYiheQRMWyKd%GRSD)|qd;8tK|MjlNp0DTQb$mWw$Lo4uZ`Xk@ zDsDbf@v-pG-}B<0D8HioN0B4CTpe(DcB4P4{j_vsy%FKt`b??S{oHERuxS~We9A3N zsoiv(rkjfO=PsA4prht;l}C;qJw7cr-Q|ixuZmtlU;1QtL_Hsut0LOJkrj|-+?GCR z+?cRYnUh_kGbfA+n=;AuA-u%Dwe)iB{9RNiN?JLWs}`~wvIergugg^vnSkzxJb+#W z8G$a94E4BN(xe9HlK)q9;YVhqjUPvgXVK0|=ox7_VYGHkHoBziz#l2kKtkmg1kzfu z$pagR{E$*ABj+L|;j@%d2T5gS0lR5M2>KKZzXv%IDLzQyk2K&l65fpru4a{+o-;CS zeA=k==i#NirAVR*9z<4j<+}=ckdek0cxl)xNb%%I@{1v%xG@-62`TCCv-!!$D)|ui!}=rlBc&mK z=faMZDlPvTR~E!kpja47L#4cjagTVkJXT1>zoARl=Gw9%ZZ)cvxi@erEtUk!DWE#? z0i;;e$>?3CM*e=vx)-f|J!?d~gf9NbM>atYKsH21+Psczj(z~+Baj=B;+Mye(vjnk z^^v`hQfm`ipGULB-{X-1$U#V2kzcUd9tn=!lhe~i=7weF)FOe{TMj9Hd#y2jb*M&!txaJe3eGwU@ryQ@@b z-O4I+8dF|k-!qQ58@By}nxx&6!p3K2W#+mDw6;5u%gz15Z{(ZySfky36=%9Yxyb~I&v!6zLo zB7Hg*@>yax!0CqZ=~L4uz-Pkqe?doEUg%`GHyzL9rccgGwfT#kt%f9ZvF7-TNNLdT zNa?26h?j2p&P>Ou^*I4au+x^^x>*UB+h&XRpi9NQ?F!P8tP1)d#Y2BtDc#+nr`6JNwmuXot_$vE z71&HNqMtyQRqPX_bj1#&@W*>w4f`A^mVRu@QR$=7C*b3e6UEOi*Jje^OG|V5S{C(3 z%KDlK|?z0L!$VirSH_)Yl^ZHpE!vtHVAZ1?tM7)fFef=%{RVt7Mj7`tU z4I4Q=b7P8C-lWMBvnJ){xY`YHO)v`{H$Hs|qu!O5HhD}Aay1oVWI;!44-#|-0 zgp`W*AjQ(F1FQ;5kkS>@F(s4q?FL&FFS8py4_S%$0YbWq)T&BAfoF*V0 z%CI%p0;l6UrCZHT8#!^Z*xekR4W(cS>8l}M9&MR@!Io2v-sNj}4vevk%QUjf*U$zU z3(MEe|2JL`&p$ZU%8{89W-g6BNY-*aEJ|!n}be?M=@iLUUjkg^B z1X3KEZl`CWn+1)?vdZ5;J<<`+*|H|hrd+*Xrx9AAw${_=TA_CS#z|Hpr&s>61Qa;E z>-6ebS|q)7bC()DY9wuRji4MEhRqlvBAwAt-`30JTKWF6LUb$WM@I2?ccgfw4N`_z zCnkdE|I_vKpb=5Aw#Hswv4&^ubgPUXjO>cF^AF9iX2vuby?j1KmnN>V`P|7Z*gagX zNmC}|jhr|t{cU*hYmqH4&a%=c&9+Ki1TQn`ex$TC?0&1I`AD%O8!0XdK@wlSU{Ve} z#V5xI^sAlUmuz*2Zf6%$d5%?lB)UKR8gwZ>7`-~O8d4nb&|E8BR%RB9`UuxFbZKU{ z2dwJSa`GmO9G96}!A_r^GbJ~3e2yz_o|W%Lp52^_1jPPx#ty%Ro)c)&>KVp)zZzXT zEU->3p-4HkG+t;;4}Wx-e$Jud+I(v)nx{MX5_D<&aa)cudRMA3u+k&e;F5EEZu%(K zUOV;$qyH2vl_k~3*?I=oZsG-F4l+W8MGv1Z;olqQaN z%`WvxTRv(_#{nTrEl19S7yD<}GJ2WSb~ZDa*sdn%5+7j8x=87OQR(TEIFGqlA;++< z7{e-uggV3h`s0>GuOp=`X=A5KQLgRFEk8YA6jrVs=cBBWt5LHom7kzX$@G}%-6bom zMXkz8Ytc$U4bmh#NTSx;D29642(9bYzBe-KdOaa5_%UP& zHnKxvv@=FAF*A(NdS1^5J}y@xF=dSN5iy?1ZkMYKn%mTRqA^XWD57qR=fPWAy%_fa zv_zvMBHmNEtW|=QCmAi)%2O{!d(a52@AbR|7YV0JIa-NPSl_G7Hj4TCg%R4o>+$-! zTx^-rhL9Nd<7f#+QT=%B8>6^^SFh(W8aGVzWaGasB&sfnw5>*AL$7w-C~oN0z2%L@ zjS@YJ7(kqz3d%_>?oZKT&GwaLK^6L!h%X!o8DFdx`YCPtRnSnIb}Cv6&tXn=$~ zI|+3syDlB;_GfI4GE?Ld8f1n(CDg_Yb*kxd4KYK{5Hj<-|0HC^wqlLyV8%X5sGU(1 z-`XgS@alKfG8#uFvfpM#;@?`1iBzW@GKwR;di`Ldaa5u<(#VYRYR?;mQC|1&!T7~2 zEAk#GtE6#jBeMlbYa1`MNc2pv?Q(Uy>8SUN&}c?-9ha*UoMuEs#b}d^!f3Ddj8RO? zSH#FDqo1Q=JQYJ+uBN8JIN2SGW*PDrp-x6v{dmt8gl@T{E+-epbK`hzxKSA6)m}7; z`Fq3&jrF=yIoBQnECZaM&tNIcUB{(XZI57WBR6csLPdXgeA20 zAz%%~4TNM&yUm$df)aW`lzsbuJ85{fs+^=pJW8bu>p`w-}CQi)Amt|TLC zSZiMb9ZgTk>^qBwAMj5o?(1do^kJS+oapszfwPyMNX8^uD>DW4dSb$?5o*q2&lEIC zS4rWjgmI;?i4~TjFNWoo`(s^bY=Bw5aW5@)->aM{TR;`H0cL4S`ZgywZ~;_ii+{% zq4g&Yn;EI!8JQiuo|a7EK2~k4jvI_(xVxfnuClbTtC88s>v;rDyr&tPCdc}sNE0yv z7x=R#h?x|N)4QQ{Lt`&!7o)vo6n6G{&cI2dT;}4|fOWs4xKcBW%r0K{VRWzQ+Im%S9C*@nyfB<)W}ToY9AYgNnUr^c+=sL@t&51Xt-FyGCkhN?Cy21g^M;v z>mfqPW~eSzwKT%&#d}5)vh2h!?saI9Mp0P2Cpf|7>Oo1SC7y9;?MzL)SA-^3vPdux zOVI3Avc%S>Fe#6L5fbBhgdn|ZTl~%~jgI~WO%@@mhW1{o2GcsvgJ|N7vgUx=f!4|h z3yt?&Atd(EZ;fI+5i~+-#)BPWeNdR4{aX7Hkd`wT=g0cAm3>flrS62R*>Hi-0225Z z*-K-M%syVvGHmM)!yKSv&Y(#F%mGGo%l1@a?tv2sNe9qH92s6S3j2CJWg)~BR+n}{ zlN26vMOj3UqO4)%`2y{xNuF9P7NVJR**!osbDlg-NCpMNlofX;nzdGy!&5R~aY+3b zcU!bb={|Q3A=4`DUq)enuXfES?(fxOI~$Et65ZLIC9%T)phtKDaP$Zv(~*Cgsj}#2}y2T%NkjZIb*GneF@P)v}Z9P%imuRvhr6+ zay&no5VXv6R*kpOXujn;-LJdRcyOX;CcDXKD+y!d6q>k+Dcm{66P!##%nT7JF`f}< zvYA`1e-=%gP1731=-($BKSm_FYtxP9G<82msIQCz?GGa})$8fm)3Vh}?p};$O^mMy znI3mH=|x|fHccdCmGT>*Bq_y{*xMN+oGaXqpjpeoIYO3AGEpK~rtDs0a*ao`mV+0K z%;8?`C!=t<*Hg3a&5^*lZk`dE=Jk9EH^MTM<*E67)-cBj{bM{iXfnYVAyF~zO=zaS z^(y_0u_F?-Cyl}pUOAkO@aob1jm9Gr-SZgQN#Nfj-@3T@$TqhF4sUa^e7>VD<;GMA-45! zmn+o_tt6CbhV(R-E6ogzB$R4ojc@HkV2nvMq^r`+&?AI~nW3{cL)}I?#l1+#iY+_J zX}~Z-DQ1p02{Ae%OpJRlS}${5dXtch z2=-RS+zneZcT7*h82XdAnnrf_7|$#;DS;`&@&ANTJkjf^o?)$7cz$?{XWDJrTWGOX z$&AC_(5wk3S27JVow6bp#rmSK!LqD~zuz|sv%Q`_;3T=V$2J*jbsLN7v{+vhX|c78 ze~Q);jZwncsnR$ZZDHM78=*O3e`b!?vky*EFk2hM=SNXn ztec4@aqJ}=E4HFFN5hRYx)|-|zT}C^aWuK$nS#a%J|f=zvG8W7daleoxq=-?$f{q` zu0-QPs!hD-AR+M{?Fx<2t4}e;&PvpKO)(nROY}T8#WId=>J;mX5=la<3$CKEX4{KJ zx2eunDvf*u%{G1m(O61OLhENP_pcC=UZ`d+L6_0kT4lg8y4vJfeM!4HljNesnsM30y@D2H=98g) zdIlFu;w(LCrt#7ouFYn$dJ}C`{stQRowc%gv{_caSgU0OniOjdtxPl->{i?cTf>W( zeFLpEaSSwe_>|d}tH{TBLZNjtd+U_Vv)6~j=q>Lz#?DLhJaWIYhe|=8qIIyUWYAR2 zw+ga`P#?5#;@Fb9#&`VnT=lTGt|nxeeEzXmqwrx)i4R#W;bMY85sM}>f&s(vc^;Zn!36}% z#JgzsnH6&6*E`NPetaa+levJK>5xoOG8{$giDnhnX`##2+lpf{E3`pQO80p*Yl~>R z$eK;2joK2U@KLYlb2xD=mbQuU)Oy&`tP}SHG!|hFIxi7wK`NZg%Ko)cyo4?Pk(>P; z8taQ9)67~A7o*A4Wb`eH@$9uV;+Re~7u)Xh3?d{Runby_CRJD$E&Gk)rC#0lQKRv) zL{IxiZ_X{|eF2)R31nkve~Kpk<~AKycZua88aevHbJDDcAPmsv|~o`a<9AN zGJI+diN%DP->iEZnl(km^OtQ6tLXA*!)UA$JrfOQyI_8Lo=3As6xwlHGsnHV`s1WD zyDCXSRKoo@M%f&+NHjW>rRO~~bH(#3xynm9qwEp5=G6}Xwp!KrBd(X8Bu(8OhC2kMnq z7(cE`bickrZu*t1ig!0(N#RD(YV-Ers(8;HbZH3Jzl&n@fG3T{YZ5(&pR|@#N@QoN zu*y1kRFL+#+n}*SQPdMRLn*7xNdWgEA?_V(h zrKc0!X-}D%xP`HakhPJWC6q#IDw`KCk3a30!ouYK6s?;P78UPlxYik0GDfq|WM#zQ zWig(Y?KoDGlBNYh;AeQ*B$XIY8(hQJM)0v&_{$=?jf zN2KTsJ~N&X;c^+rpQ#ZjY_w>6M3x2dq8NV9)-akrTPI&qv=N3+8zAMk1H!il@)0R| z2U~VT%12}{7y%^TC?Fq^)xdP%2UsfQdj}W8)3k#gH$TX#PQc|uh#qs5F|N_HiqH8v zFq^o}Oy_cymSW6{K$^B5lmRaT`P@lLx>tZS^;J+FYyrY=1;W1tq?~tb{awE3Hk-B+ zLOvoT!AC$c?gH{DErs6=MD79dDJ{j+Pk~gpAIPV)l=2UntVs4&`P?F`fnI+q|_#&{6)%0jIeo;5{$HUq}78$NC7SEh|*FjjI()>q9@wv z+t|EF3AVNMJ4q?Oy&Zo`Qh!Av%|{&@O`%4Pe?u@#gf6cE>eO+Y?*5FrKMCj z%;rT(aJVhgY(8z26DTFc!qMpB;!LDOjkV)NN=N6|dTA+CuFZ>-{L_${k-WaP>F7K= zR-^=H*t$qD`+i#&Ddx^Y1|ruWrL?tnyhv%~^ESU8DN%*;$ChIL%eIcpHw$dK{ zNGT^BDe5c6yOg;B}jq!bI1kw!e{*wM>}`W;_9w5ySmzL*w380|rXg z%68V$QWly3c!>(K<3&pPTDC4S2tCHuMM_n1B8@e#)`&xKwYFnLO6~+(CfdA6;k`&1 zdF_$9;kTiNQEP**gRYXIlbz#EQtItum(&$0NxR$WMN0l;;f>S{*2$xf9a~yT{rzpe zw3OT_HZM|wgKS--#1FRh(o$NLYV#sxl^t2e)!5aDz{7TgNC`e_>$gcGcw>#WM7ov` zE7d$^r#5W;aa%4&%BQrHPJ9YpH>PZ~hW|5mDv=U=-quA*=`Yy2NO8tXHvh8C-$_co zSBMv5U$xVT6n=xPBdeO$zhM(1bt7R@4RdkWVzZ^CWS7UsGou8gaH zE^+_4#Qo;F}L$uj|c@_zYl zOLUdx-jjDHUCujrbU~9R-pFd~+i%@(*H*poT<(eB25ny`zx#^siRa3^+yzZ`!K~ggSO_wB+bt#MqBZbpOO4gl2+MR`B93|WtX3E4XujNbytdU39V>Xl6IGI z8EyS;KO=Q_l2+X)+?`?!*yCsT?Mc#V7z6jD7``9-8QahT4bR6Z#ul`!kCU_@V=G$5 zCw@loCrMhck@-oA5wO?K*o#)%2;7@u>_VHpH%SXIcB4)I)Xxa}G)b#x+qK zz7e_)`_LBeOVSz|N6;4R$G-hZTBxybKlUBKKC~uA+yU%ETXP^u3p0w*R(ytipCxI{ zjFq2Z-$Cp{i!iz##6Gm5gGpMHaT#s>=h*jok``?gevW;Iut;*G6n8DC=Gmq}WJk@+R|9mYO1uMv0{`_N_|PSV;MyV0f}!M-C&T6-h! z2=;x2eP|tx(66u$ZShx0T4&=3+Jd9lcQlEgDl9yTeaEm5Ey;*GhJ9#jjwNZyMlsro z>0~u@9}c(e-QWLo50^N$YD|Mq7UZ`%WZj{fxpB*mn~9P9|w7#=w)< zcMAK^1{$7I*oT&NDoGn`Y(>lX2K&BA(o&7gZ?NxM>_Z!71b&NsXtTdf($b9GXw$#L zzVDK>kw)Hk*!Mm5p`{z4-(w%z;_s8RF~$+J1wUZl4@p|4vG51%JB@v4wm<)ACt5xM&XaxcNY83 zCTY`*foHMr9QL8*8J=_4hn96NNt9(S;;_RdN|^{YC7% zn4~Q?3NK>c@7VWy5|5`0{2lxLz&^B^M zP0Z>g<9 zWR!sjE(5VmWtM>m@PXJX#C8?v1F=hp***|E)NUcByCK5d5bvuzH$nW?tpIUI6;^;4P!YnfBE*+!U_}UDKZtEY98n%W zh%G{7`9U02TZPD|1QA>b;<(DJ1QAdfVy_S5LRsh5mwN;3WK#1T#h-)e{5F(%^#9kq8sKA;KyM&ls6GBtF zg_s@$5f%hdM&$)TG^z!0TnM)ctp#ydh{d%a%BdqlEC_~32!`Nk%V3Dudmu`LsG#ER zfjA??ntLGpRIv~%Y7@k!S_N0tIT^L0_sET6{5BZ ztPinEh}rcaLey>{rZ<2HYXDJCb!^u)wKy(FEt@+Q4_L8smnsFZwisx z6e3y`HiZ}v2H_V55u*l%LHLG4Y!f0*dBP#K2$2;I5wErikUi~ z5MCA79AcLcvztS-Rl9|l9sv;+0nuLNML;x)gg7olM->_gaaf4Okr18L5g`^tK_o;$ zbX5zZAYxlUln9Ze;#xqQ5n@dXh-6hP#ENK$ti5NV<7se!Wf7Fu@HW-5GiV4EPs6CAhroHP=I&jCx|&}w-D1iLxgpPctGWKhG^6U;KA~hLexhhPC7|;X4uLp!u1A9RD_Jr6b z#7f1#eSklN$m$8PN^KP)qZdSQFNifNvlm1_Z-~7@JgowIL+lb_c5jGvYPS&6`#^;C zfp}Ks^?_*A7vi`O&#TbB5Ql|W+!x|Sbwr2-_dz7w2T`aN-Ukue528efmsMOph%-X0 z=?77yiiKFwA0oLw#0IspKSY-lh-*S@Qe9IZE(uYT0`a=KEX4W&5UB$o-cW@DAO;MC z@EZuRSq&Tr;X4Ron-E)-XAr~|A+iQRyrs4Zkuew|cre5^l{pw9UHu-$aN%)WC@lzLOxf32|9@CP8cwB5M-F6}44} zjBJSDY=~sZ0}+-3QAXwEKs3sQI4*=+h32v$ zmQ{0dwU3Nn>Qqx*rf4myALko+9+@eSZ?;mnU(Azj1>-g^oua+2S9ktJMgD!N@Uc18 zw_mTRfz!1YwO=-CdD{D$`&2Oh6pyMhi|d&>AwH_tOwHFlrVii!3-_#Wp0PO-C&=U9 zYc~7M(uVnH>o#WgKeEwZWQa^2G7u`S&ed&u&hASR141=SQhZv$Sxt4(DH|`f{_j zSTpp%q6yZ&Z6k&s0^)>=a8;${O(cstwghW-P2^!-X-iKZwQ;$2Qav_T%ia9Qa?M}U zW9M&v>Iv;vP5W~5(iK{JU7z&CW-5F}SMScyd^XQoqy0KUoy^rD%5Sv)DbL|$mrvJQ z*P*`aH>?jq*FpQMN4Z}wpM%Q}=y&CR4lB<~2<)etTwmH>LQ2XiKt4z8-0}bfKXfra zU)h}8!a8F=baB+?lW*8u zJ;KhDqdMuK<=>Ta`4ARIm60A2vOefeK)fN5!ZiSk35Yk`HrJ5wN{QrC*5>31v0$4k zXLF%&@R>DLWio zvbnqLbj{!{+gx=!U30i!2!|l=wz&wx=LzQvS;HnH3GW1TfjnX?l|+FLZ7$H}TA;rm zk$mJ4WC_ah7-DllaQrX7a5A4s7t7^QWQmRe7YR2--lH?5gp38wW0SS*gmG}YfHbO( z&9$<1ge%16!Fo^#UIPCDZ-6(!X0Qi*0``J;z;>_>EC){j1y%s* zKXJ8m58aV(KJ+dNSUDW!3*@;sdEi~<>YqR!z`F>32ePQiBDNkB0(rLZP0)tI+Jd(T zzYVqldGPUBAWsyo1M(PgBIS93Jj2@vxv@+N-wqS`FCae`kY6-R0aJl|4fGUv8Y}@1 z10EwZA1#!}%qN4XU>e8+GeBn=(FOESnn$k@){o!-Fc1s^gTW9m6bu8yfed7sowZa) zk3K9vni{;QHIcGgl|lLdHz)^u0qdTtJg5Lkq((;3KdL z>;dwG_PbyQkjJ~_`}B=qzx2leD*qJh0BgZY@FZ9TR)a*40A${bpc46UNjeyVJ_Sq# z(}4Uq;UyZ_o^U&`lkmG>2au=j+YlcL8iVOz8dzH<-+JzKDT(Bt0F}k}Aut~-0E@u= zpdP3XLO}$G1g}xR>tGhR4rCFRb+R&$T|+jB55PXKA8Z4E(lB|LsRQT+dgSxxF|Z66 zK!FuN7VRg&T_6a^f-MVlc_3@%5E|PL$dkuCKu^#WbOZ8Lb4MW0J+}qzKx-gRPs`!M z5BUd;l;2B5%j4=tfIJPq7c_xv3c^4$&>Xx6|0a;<8f5*upbk~go8>=%Cc9Y&q&zGv zzkQeohJaLXALs}AgVrDsv;kc}Ci!Y1<+n4#Kq?pv3V|%u&j4AT?*+0S$ok)#I(mWj zpo4712?UOlK%RV;<+w88gUHW86yZ$d2rv?i0x4iP{16ZiWU2N?ehGgF90syid`G#N zU@VviHyiAeL*=X z1GoY0bV)5l%MR$OP2d6Kvmh1RgB}d%vV2!H0@4V%0+4g4NU2Z?l!OG0aORn1@(Yza#BbE zcn(O*X8;yPvoTV59=HU~gTvr7I10W5J3&M6IhX*R0x}Hdf(GC{AZz5NQT(Y7#4^bs zky7zSAR~G*kjlkF;svQR4rBus-!#;Op1y-EI1f>KO>AiW?x zCwq_-mkb7jcEAgyxb|QW7zkp395EzM3n2TH>{m^Ij4YY3vJ!*?SrwWANfQlZ-%FI9 zmd&CShy~IRskk+W2f~XkkOoL2``NlQ&S_9v!fk+*CFMAIq~Q{lw9;^CKnKtXbToB( zS}N%Vx`NI?Hs~bK9oz?`fIgrn=nZ;-zP1!D1vm|K(uppW4+CP<-%9HL&k6pQ@iIze zbT}hn65)v;07#s}=h)$VAq&6^;0&c{geL%T*;Ftc%mOpPY;Zr2hC6v*B`m{H$`EUv za-JeALw6Mz1m*$p*aJX3^0*YUl)y6ZAdv7P@DP{}7J>!fF|Y(Y3MB3k@Gw|xheb-o z($7+c0c21<2`Yh=U^%D=6nFxx0BgjI)dXG!g7BAnq@nU_ZK(#_@s_whw#?=7LYa z$KX96VW58j;`+zj`2v{Wj?^t^hLK6Q1oD5>|Y4AO02%Z9Bv5bRn z!8hO(I0i~ve-gcP#p0FY;A?QAbR6}&P6DSwDd;;O6^q3ufyDg)q@s;L8saqiN5WE( zr2ia91B<~Ka2k|=bKora3H%HsuZ%D8PJZcDN+D~3WIPJK1k%^P11VT6mqs|2UL^b* z_!XQ4rOR@>asmD$;B?C$giDRFd~>?^X;$X4$QdNikPHsfa%t2Ra2`m*q#&_W`dGT* zPjDGXBV>TpLKXnYCykcI%9%r~E&w-xSbrTnEB$|sfVfyPP6OgsTh0w}tgaKN-XUxmfT3Um&`}OIWVg8X?6i zqPGK5h@_QiE0xH_hcxP5AO^|#`Vz7{5RZug(nu%0oTYE}e;$#NxGH27AXhQX(Q6_D z!QG%bkb_wOr~zaO3NNx2GNhFL0AcZl9Nc4p9Ps6I7zt#RjX*ZnTyBu%Zfi}Pk+JcrOyALU6>)xOfkipjx z*#Q&~?j`>&fLz>lwn-U)T?lst$sh^HX`h6y9-t@a1Ns4J&@ALkFayW|JsZd+iVCp&tV>fC|vX za>+Lbyac5CZa6NbtJO`cvYrrb76086UNLl&I`SUn<6f6c0fkj{;co573 z4*;p$`OHU`bPK@4;1M8c3?SSxummgxkAWw^lR!2g!RlLjzFFC`5bMCxU@dqCya1jD zPmnPczkt8_1M-2Pu|FI%&N$RxFT8rNNR{3V$E{J@6gj50QJoZmb)uX}<&Cf>YpbARY<<-w>_= zq{1J-_dqI?Zut=ugL43b^3AU%E~5MdB!O6Y0V(|>UGgjV1^f)o+u`4k<%sW#>L&{b6kBAU-op2=5jhtz7iNORUlZ;$RB%90@xlzt1aiLmtkKUjR!Oa`) z(Npwjv4t(AzABJq-!n0r<*k|Q_qIzQQB(tWr*IyBlmQxkZ!B0 zP*)EOl3O+vP=C$7=jyE2Y8F=0hDOGPHxF;_I;gyL^)Y&&j~YHy572h09i-Rm@m*zI zJ)l}QAM3pqPd!t0>V|EZcURMz`S2K*UZ(eME|AE5(OuX+3?8-k_X%s*N6?N0d|H_aYO@sphe?p_@v(*R;NJ zH~sE=nsaUMYu<$@xV9!HziHoQO?0iXX)1C}x>MH&C?+)oVIK%khMNCG{9d z^jejae+o(M7F%tMc3y;6-G9?tGnPzPK|3PCTS&J!?;<=qcIHE#RxO_wD8c7M>m$L?D(k^bubCVD`S^De~w=@-^6`|-D{w^HP*O%$%Zq&^n0 zUwz+1H_A6<()%&d@veV!yc<_VZH%CpPVlmBbpB?1$HqH0^Kdd=i);}dVk@avC`y~G=EUjM%9@!TP!r>+bh+vsW;S7*h~4VJFg+^T zdHdxrJ7-T?{pBYIDJhy6#e{Jl|CqR9U*-E3bXUZ%6h_i_BUK3{L_2TbJo|F`^G|*D z^qaRDVZB?^RS>mo33O{E>-d2bb|9T&dmWI9NWMD`gdeCr)dkIEuA-mw%YpeeOIcFdhJ#d|Gx2dt8dohRBsK8 zo3vfMJq)aBgR80TkqnLd)wKxrp8YB!5;r)kWfo6F=EDX#ud{k|-D~}>&ED6~_J6c2 zw690rd7+I6XOohJR*TZ3oR(<0s%I2ldsv+q#lUsmK{~wa$O?5A_WO!*qpStId~Nkq zlpav4Vjc5VNWrP=dtYtT!p~YuBBkf86nB?t?lKFjq4XAdEvKHKzn>DNTTsKNh11iT z?Xvdu)MG7m#!w6W9v$VE7JB5Jq?u@K^zDiMdy*7)EN*y=#Ur~rEAZ{hYgDZ}Y*ONH!i#-=NEh7R(oRcnzj4=WsTYSgTr) z^Rm??tLq%US`hsh30hhUm^n)B_KAq);IWMbFsOY!>tr(f?Y;%^JLZdb*sNvD9#rRL zh5SiXXw4v%djmD_VwvKYcOAI> zC->^FOB#_67qDu(uBsgrte1~a$0Yksb+NUc%#zVQfr+?JO-s<{1Uawot=zuT(aH%W za&(CakB~**s*QAEi6md3(nKs(%Mm)~w>^n^_n^5d2Yxm27iJu-< zy!R=PXKa(R^guPrtM|CQ6P#gSAAZ;;gWAwmd*|1ts-CF!F{IWUjHyaB6NhBAd2Q^p z6dEZ9Fm_KTyLE`Td0ea!tG;fJ$+MKZtv*J(pt9OhntgEn%Xv$E-j?C;kouV<(ay_d zXO&G|mGH>(6R62PvfgnNn%(oBN@#~`>?Sy8l{=j@)UWOI7Is2yfNI;GCGB8qYjl6n zqtlCLmz-)rjj|~*7>=nW?Kv6zKms|J_qu=l+s8J&{)*W{&Bb`0v4<2gj+r{wRFw`gHWF0R4tjS6MP3K?k(OSytAieCd+I-& zBwtm{I^xkZCFgH7tfL;|n0338?W3c*-ciqRJoKNBzjoLEhq@Q5U=F20PqwvIol3tx z{m9_3F>*GO%N1<1&#>#&I7zpKbXD-*)e){g+uf+~IO!On;-%Y9x3)>URSCJGo%bHE zDnD)O?$ICYvwcgq++m)!bUG})3r?|CfM8aDzdsMFPiN>ggZ{pf?kc1ojy>2#54O)| z)!b3xaWW~pQiVAwoBGCtN6Y%KPYvwK05mT~0@Vv$8P0#I9g@y6#%_^ML?mkx9Tvy~ zPI5M@<-AWgJ7E70i0DO4eB;ek)>IdFN*$79UuYE zQE!))+;O1X*2=%EE%vYva$cNv=vp~M6e&&)Y$a&LpmvjGmcF2z} zwvlV~=3J9PyFJo1KECcsNY`7ocBO~ ze(0H39*J+{H61O7dRK2%)EhrIFMxh_?9?fF*9QliStDq@tB*QG0&R%8(wi~Y(@6qG#7`!znsGK;9hD{KfOSERh{ogO;#0Z z=uq9aT4?<90~!puXoy#>TLt!6jGvA4$E)^ma(Z=#+R>lA!M^`s-*>oK-R*Y|>;seY zUhSSm2d0&L85m7Xa>8aYKB1PT&>YKtwKGMpDZBK^6waYeeYSrskE-DV^tnOKd$@;u zf9>pyM=E}OtL2tmLCy=i>p%F>^V{p}dxr$}D*TtZVV^Ip5&j=;p4iKUwSEOT?*)H< z)fWdI+FJCI)PLK`aC`R;QauOVJOViGynRXJ2PbC-;Rx#zSp^Q(Ye)C*E04yw3gQ<% zb1X9C@(!~W8T2maHRCgm*7pBoWSATV{|AL-4tEiB_#_b53~HRa`Zv&(4d51gS zx)(N2Z6|?!;Zv=SbqgIzukZ*hI4Q#QOUz`yz_ST?~eEWD6=GZsM(X|`1c#A<__mp=W1@RR-~^J z#_S(8YyQ}KC53cpglmu5M-gl?5d}Ed+Ig$HnL1>>hx>#Mbq`d#!S=P#!E^V{nCL&y z>|k@p%2wC%X^8WJaDFJXaZ=q5a;aegk{KTh;Ean!04=TpxC(h*;@d=N;{D+&K4Z`I>v`-->CHs@@sFoh0Y2?qzd7 z8#VsZ-}GB4GSvAIR5f2!7|CdJ-U|Qll549s*Q@g5t#rGTmjo@H*U10Sw!ZJh_8oTK zO5nU`KBi$&Y~+>woo>a14poniq!G@m>F@h``?~oT=G45EVw~DR;o3ZPR2t#D+P?BP z2O^qY{rKvwblX&A?jGpP(^cb99Ev-sRMhDIy2jrxi+p5r*MGNS-+ou3rSppV&)0f? zY!>aDqu|K_)C+>lOJJ;z|X^KSd?O;>#t-!GsW6-8KQ;|J9#E-uyTF)~%n z+tzBognh=Tb7Ppc3za)VPY!ZkcK`6G$2YzG)$S9zCd(vOg|2QYD}x%I7;oJ(jIZ5g z=!|9ElI$AT`A4XAB+zS4P;X~2$ZgBiPZ=CDIw?;kP4A>qGUcXDMfFG~9ne#4M-6gb zNq;W4(Tuo<>nBsBeK)7O@*7K?&O7X1Tv(FcAgvLLn0Y15^ckVrj-|kMgVofrdgFV? zPqOCZMxRSIwEy!*WEFmGp{mskCKE5+ZJiX*N{!|rg)$KFj3sb=HprE zb$%l0FRBWYWIN>%4srThse7L~uzH<8X4;n?6=tY5lW=-99!cPM(ihM5J8t-A{}(Y*zBQ2~U)}e9WwtnadZ;Dm5DeR;ihj5k+dLXm6{R zMSQH*O<_)doy`n8q%LQ(91K@sl5VW(JQ>Z*6@~R1AcK--Ta{e)tNTXvHfw*;wRjkR zsy(E>ovbI<3c24(aP`Y>ddK@dT|g`B;T(3q>YgLxiWG8Z@cb|Jw?7lraxW>ksVY+< zU*+d86D!YAwQ}`H^+OJATBEdFI@adw#;c56u9Temi9}sHbec4 zd-Q7aRr#r$l$y-noH$kIBF4JUR%^z5tH?T2yKS6Yt=qe%wQ<(Ox4zGtr;6l;+~166 zI>%a6xxZ=N8+%j@dl270shZ`nt-L_-(xqR&`EuoveV$)S@%DIm&5oIHIqp*O*giqT zw7?e1dq>SCYtY|+5w=%tA^|^b{#eqLP>^)r+|AGZ(L1~KVG5GfnuFJ5b%O*guaV#` z5}bZDZ)wUm*?-sxSY-a@d`}E_X?x6ICipKff67#F;!6EbU;5z9c=AdQ;qg#nf+#hi z$E3XA2j6Q=jBJ?X?XWOjp?Y%$OM`t!^L|oGJGW;yJbEs_n|$BODOOG@qg3!r zz7?+Yh*~>Se^Prv#m}Pet!yp-rj4|~Z?W|~(90nMp84~^8*=uNw$TD^WWFgBxEa zp?xKkrWVhpGagVc&Svu5!Pjyeq)j19mH+*GClNQ#8gSzh9$%1s@YqL`WbaDWC=bvB zmZ_ol>yfqUF0&f;O#gk~uRCvBzD9lqMkhNZa+_LA{Hy=0}6mYt$*$Tdr)TadunNO59har%nYR|>D6V)l1QcF~o2lXbp?-KRI9DRWu8B}GZ`IuXQ ze(a%HD;7p^K^-05(%gNosb&w6%_>AaG*@rx>G`DjtaQQ6W7E%b*(IzLdX<%G@&kH7 z(79EX-**md{zv&o-dtj~()8j*c zdhGGF-AV_vTCIl66R~1X1nWx_u^!MeNCMRpwni5??>GBotQ&Zm#%lyne znbKoqv_`uAB&I4cyNX-9GG*h}as-h>N*v$3nl22gxW;_0svzg>elNeOSC+9Py-CAv zbFRl3@22mX)SE>%ndI%Bww93hzwB^s&gq#l)$C2JpPDUBwr@|2RGUQ0RUbd3cTAW~ zF8S^wV68RS-apbaqDrO7`^e3Op;Uf)h-FdYQ2p)CfA-~|=?sYTDtSI7T~OQtZsNI4 z3AHKd6}1XB=ixp+=rVV{&7GU+KdS3d`SFFUn$M3L{P5_`ZEJo0fEAy&cf-}W(@#f# zjb4TLnOR?SI5{S=LIpN&IWuhess1kGyl>{u#7p^=hxM+}Xho;brps^c_|$I7B_q!W w#Vq6-EYGy`5n)p^bJNDFbrs6g3c9K0Gz)8XO9`W|)TS5okj+~b=_}X%KRw5*1poj5 delta 37612 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.0" diff --git a/tracker/tracker/src/common/messages.gen.ts b/tracker/tracker/src/common/messages.gen.ts index 7ec02abe7..9982ef8e9 100644 --- a/tracker/tracker/src/common/messages.gen.ts +++ b/tracker/tracker/src/common/messages.gen.ts @@ -78,6 +78,7 @@ export declare const enum Type { Redux = 121, SetPageLocation = 122, GraphQL = 123, + WebVitals = 124, } @@ -626,6 +627,12 @@ export type GraphQL = [ /*duration:*/ number, ] +export type WebVitals = [ + /*type:*/ Type.WebVitals, + /*name:*/ string, + /*value:*/ string, +] -type Message = Timestamp | SetPageLocationDeprecated | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL + +type Message = Timestamp | SetPageLocationDeprecated | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL | WebVitals export default Message diff --git a/tracker/tracker/src/main/app/messages.gen.ts b/tracker/tracker/src/main/app/messages.gen.ts index a951a6148..7d3af2af0 100644 --- a/tracker/tracker/src/main/app/messages.gen.ts +++ b/tracker/tracker/src/main/app/messages.gen.ts @@ -1019,3 +1019,14 @@ export function GraphQL( ] } +export function WebVitals( + name: string, + value: string, +): Messages.WebVitals { + return [ + Messages.Type.WebVitals, + name, + value, + ] +} + diff --git a/tracker/tracker/src/main/modules/timing.ts b/tracker/tracker/src/main/modules/timing.ts index a45bfb570..12d6c8bb2 100644 --- a/tracker/tracker/src/main/modules/timing.ts +++ b/tracker/tracker/src/main/modules/timing.ts @@ -1,7 +1,8 @@ import type App from '../app/index.js' import { hasTag } from '../app/guards.js' import { isURL, getTimeOrigin } from '../utils.js' -import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js' +import { ResourceTiming, PageLoadTiming, PageRenderTiming, WebVitals } from '../app/messages.gen.js' +import { onCLS, onINP, onLCP, onTTFB, Metric } from 'web-vitals' // Inspired by https://github.com/WPO-Foundation/RUM-SpeedIndex/blob/master/src/rum-speedindex.js @@ -139,6 +140,12 @@ export default function (app: App, opts: Partial): void { const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming)) + function onVitalsSignal(msg: T) { + if (app.active()) { + return app.send(WebVitals(msg.name, String(msg.value))) + } + } + let prevSessionID: string | undefined app.attachStartCallback(function ({ sessionID }) { if (sessionID !== prevSessionID) { @@ -147,6 +154,18 @@ export default function (app: App, opts: Partial): void { prevSessionID = sessionID } observer.observe({ entryTypes: ['resource'] }) + // browser support: + // onCLS(): Chromium + // onFCP(): Chromium, Firefox, Safari + // onFID(): Chromium, Firefox (Deprecated) + // onINP(): Chromium + // onLCP(): Chromium, Firefox + // onTTFB(): Chromium, Firefox, Safari + + onCLS(onVitalsSignal) + onINP(onVitalsSignal) + onLCP(onVitalsSignal) + onTTFB(onVitalsSignal) }) app.attachStopCallback(function () { diff --git a/tracker/tracker/src/webworker/MessageEncoder.gen.ts b/tracker/tracker/src/webworker/MessageEncoder.gen.ts index bec1496be..1fbf883a1 100644 --- a/tracker/tracker/src/webworker/MessageEncoder.gen.ts +++ b/tracker/tracker/src/webworker/MessageEncoder.gen.ts @@ -314,6 +314,10 @@ export default class MessageEncoder extends PrimitiveEncoder { return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.uint(msg[5]) break + case Messages.Type.WebVitals: + return this.string(msg[1]) && this.string(msg[2]) + break + } }