Add incident event (#3380)
* add incindent messages * feat(proto): regenerated proto files * feat(proto): insert incident message to CH * added incidents * genereate mobs * feat(db): added incident event insertion to PG * add incidents to session config * fixed activity incident highlight * fixed incidents search --------- Co-authored-by: Андрей Бабушкин <andreybabushkin2000@gmail.com>
This commit is contained in:
parent
82599f4afd
commit
3c249b2b5a
36 changed files with 2339 additions and 1955 deletions
|
|
@ -70,7 +70,7 @@ func main() {
|
|||
messages.MsgMouseClickDeprecated, messages.MsgSetPageLocation, messages.MsgSetPageLocationDeprecated,
|
||||
messages.MsgPageLoadTiming, messages.MsgPageRenderTiming,
|
||||
messages.MsgPageEvent, messages.MsgPageEventDeprecated, messages.MsgMouseThrashing, messages.MsgInputChange,
|
||||
messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger,
|
||||
messages.MsgUnbindNodes, messages.MsgCanvasNode, messages.MsgTagTrigger, messages.MsgIncident,
|
||||
// Mobile messages
|
||||
messages.MsgMobileSessionStart, messages.MsgMobileSessionEnd, messages.MsgMobileUserID, messages.MsgMobileUserAnonymousID,
|
||||
messages.MsgMobileMetadata, messages.MsgMobileEvent, messages.MsgMobileNetworkCall,
|
||||
|
|
|
|||
|
|
@ -140,6 +140,11 @@ func (s *saverImpl) handleWebMessage(sessCtx context.Context, session *sessions.
|
|||
return err
|
||||
}
|
||||
return s.ch.InsertWebPerformanceTrackAggr(session, m)
|
||||
case *messages.Incident:
|
||||
if err := s.pg.InsertIncident(session, m); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ch.InsertIncident(session, m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type Connector interface {
|
|||
InsertIssue(session *sessions.Session, msg *messages.IssueEvent) error
|
||||
InsertWebInputDuration(session *sessions.Session, msg *messages.InputChange) error
|
||||
InsertMouseThrashing(session *sessions.Session, msg *messages.MouseThrashing) error
|
||||
InsertIncident(session *sessions.Session, msg *messages.Incident) error
|
||||
InsertMobileSession(session *sessions.Session) error
|
||||
InsertMobileCustom(session *sessions.Session, msg *messages.MobileEvent) error
|
||||
InsertMobileClick(session *sessions.Session, msg *messages.MobileClickEvent) error
|
||||
|
|
@ -111,8 +112,8 @@ var batches = map[string]string{
|
|||
"pages": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"clicks": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"inputs": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$duration_s", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"errors": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", error_id, "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"performance": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"errors": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", error_id, "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"performance": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"requests": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$duration_s", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"custom": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties", properties) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"graphql": `INSERT INTO product_analytics.events (session_id, project_id, event_id, "$event_name", created_at, "$time", distinct_id, "$auto_captured", "$device", "$os_version", "$os", "$browser", "$referrer", "$country", "$state", "$city", "$current_url", "$properties") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
|
|
@ -809,6 +810,45 @@ func (c *connectorImpl) InsertGraphQL(session *sessions.Session, msg *messages.G
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *connectorImpl) InsertIncident(session *sessions.Session, msg *messages.Incident) error {
|
||||
jsonString, err := json.Marshal(map[string]interface{}{
|
||||
"label": msg.Label,
|
||||
"start_time": msg.StartTime,
|
||||
"end_time": msg.EndTime,
|
||||
"user_device": session.UserDevice,
|
||||
"user_device_type": session.UserDeviceType,
|
||||
"page_title ": msg.PageTitle,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marshal custom event: %s", err)
|
||||
}
|
||||
eventTime := datetime(msg.Timestamp)
|
||||
if err := c.batches["custom"].Append(
|
||||
session.SessionID,
|
||||
uint16(session.ProjectID),
|
||||
getUUID(msg),
|
||||
"INCIDENT",
|
||||
eventTime,
|
||||
eventTime.Unix(),
|
||||
session.UserUUID,
|
||||
true,
|
||||
session.Platform,
|
||||
session.UserOSVersion,
|
||||
session.UserOS,
|
||||
session.UserBrowser,
|
||||
session.Referrer,
|
||||
session.UserCountry,
|
||||
session.UserState,
|
||||
session.UserCity,
|
||||
cropString(msg.Url),
|
||||
jsonString,
|
||||
); err != nil {
|
||||
c.checkError("custom", err)
|
||||
return fmt.Errorf("can't append to custom batch: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mobile events
|
||||
|
||||
func (c *connectorImpl) InsertMobileSession(session *sessions.Session) error {
|
||||
|
|
|
|||
|
|
@ -270,3 +270,15 @@ func (conn *Conn) InsertWebStatsPerformance(p *messages.PerformanceTrackAggr) er
|
|||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *Conn) InsertIncident(sess *sessions.Session, e *messages.Incident) error {
|
||||
sessCtx := context.WithValue(context.Background(), "sessionID", sess.SessionID)
|
||||
issueID := hashid.MobileIncidentID(sess.ProjectID, sess.SessionID, e.Timestamp)
|
||||
if err := conn.bulks.Get("webIssues").Append(sess.ProjectID, issueID, "incident", e.Url); err != nil {
|
||||
conn.log.Error(sessCtx, "insert incident issue err: %s", err)
|
||||
}
|
||||
if err := conn.bulks.Get("webIssueEvents").Append(sess.SessionID, issueID, e.Timestamp, truncSqIdx(e.MsgID()), nil); err != nil {
|
||||
conn.log.Error(sessCtx, "insert incident issue event err: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,3 +38,11 @@ func MouseThrashingID(projectID uint32, sessID, ts uint64) string {
|
|||
hash.Write([]byte(strconv.FormatUint(ts, 10)))
|
||||
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func MobileIncidentID(projectID uint32, sessID, ts uint64) string {
|
||||
hash := fnv.New128a()
|
||||
hash.Write([]byte("mobile_incident"))
|
||||
hash.Write([]byte(strconv.FormatUint(sessID, 10)))
|
||||
hash.Write([]byte(strconv.FormatUint(ts, 10)))
|
||||
return strconv.FormatUint(uint64(projectID), 16) + hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ func IsMobileType(id int) bool {
|
|||
|
||||
func IsDOMType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 34 == id || 35 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 43 == id || 52 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 68 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 119 == id || 122 == id || 93 == id || 96 == id || 100 == id || 101 == id || 102 == id || 103 == id || 104 == id || 105 == id || 106 == id || 111 == id
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -829,6 +829,15 @@ class ResourceTiming(Message):
|
|||
self.stalled = stalled
|
||||
|
||||
|
||||
class Incident(Message):
|
||||
__id__ = 87
|
||||
|
||||
def __init__(self, label, start_time, end_time):
|
||||
self.label = label
|
||||
self.start_time = start_time
|
||||
self.end_time = end_time
|
||||
|
||||
|
||||
class LongAnimationTask(Message):
|
||||
__id__ = 89
|
||||
|
||||
|
|
|
|||
|
|
@ -1241,6 +1241,19 @@ cdef class ResourceTiming(PyMessage):
|
|||
self.stalled = stalled
|
||||
|
||||
|
||||
cdef class Incident(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public str label
|
||||
cdef public long start_time
|
||||
cdef public long end_time
|
||||
|
||||
def __init__(self, str label, long start_time, long end_time):
|
||||
self.__id__ = 87
|
||||
self.label = label
|
||||
self.start_time = start_time
|
||||
self.end_time = end_time
|
||||
|
||||
|
||||
cdef class LongAnimationTask(PyMessage):
|
||||
cdef public int __id__
|
||||
cdef public str name
|
||||
|
|
|
|||
|
|
@ -750,6 +750,13 @@ class MessageCodec(Codec):
|
|||
stalled=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 87:
|
||||
return Incident(
|
||||
label=self.read_string(reader),
|
||||
start_time=self.read_int(reader),
|
||||
end_time=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 89:
|
||||
return LongAnimationTask(
|
||||
name=self.read_string(reader),
|
||||
|
|
|
|||
|
|
@ -848,6 +848,13 @@ cdef class MessageCodec:
|
|||
stalled=self.read_uint(reader)
|
||||
)
|
||||
|
||||
if message_id == 87:
|
||||
return Incident(
|
||||
label=self.read_string(reader),
|
||||
start_time=self.read_int(reader),
|
||||
end_time=self.read_int(reader)
|
||||
)
|
||||
|
||||
if message_id == 89:
|
||||
return LongAnimationTask(
|
||||
name=self.read_string(reader),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { TYPES } from 'Types/session/event';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import UxtEvent from 'Components/Session_/EventsBlock/UxtEvent';
|
||||
|
|
@ -32,6 +32,7 @@ function EventGroupWrapper(props) {
|
|||
presentInSearch,
|
||||
isNote,
|
||||
isTabChange,
|
||||
isIncident,
|
||||
filterOutNote,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -57,6 +58,15 @@ function EventGroupWrapper(props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (isIncident) {
|
||||
return (
|
||||
<Incident
|
||||
isCurrent={isCurrent}
|
||||
label={event.label}
|
||||
onClick={onEventClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (isLocation) {
|
||||
return (
|
||||
<Event
|
||||
|
|
@ -100,11 +110,19 @@ function EventGroupWrapper(props) {
|
|||
);
|
||||
};
|
||||
|
||||
const shadowColor = isSearched ? '#F0A930' : props.isPrev
|
||||
? '#A7BFFF'
|
||||
: props.isCurrent
|
||||
? '#394EFF'
|
||||
: 'transparent';
|
||||
const shadowColor = useMemo(() => {
|
||||
if (isSearched) {
|
||||
return '#F0A930';
|
||||
}
|
||||
if (props.isPrev) {
|
||||
return '#A7BFFF';
|
||||
}
|
||||
if (props.isCurrent) {
|
||||
return '#394EFF';
|
||||
}
|
||||
return 'transparent';
|
||||
}, [isSearched, props.isPrev, props.isCurrent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
|
|
@ -172,4 +190,22 @@ function TabChange({ from, to, activeUrl, onClick }) {
|
|||
);
|
||||
};
|
||||
|
||||
function Incident({ label, onClick }: { label: string; onClick: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="pr-6 pl-4 py-2 relative user-select-none transition-all duration-200 cursor-pointer rounded-[3px] hover:bg-[var(--color-active-blue)] bg-[var(--color-white)]"
|
||||
>
|
||||
<div className='flex items-center py-2 gap-[10.5px]'>
|
||||
<Icon name="console/warning" size={18} color="gray-dark" />
|
||||
<div className="flex flex-col">
|
||||
<span style={{ fontWeight: 500 }}>{t('Incident')}</span>
|
||||
<span className="text-ellipsis overflow-hidden whitespace-nowrap max-w-full text-sm text-[var(--color-gray-medium)]">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(EventGroupWrapper);
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ function EventsBlock(props: IProps) {
|
|||
const { notesStore, uxtestingStore, uiPlayerStore, sessionStore } =
|
||||
useStore();
|
||||
const session = sessionStore.current;
|
||||
const { notesWithEvents } = session;
|
||||
const { uxtVideo } = session;
|
||||
const notesWithEvents = session.notesWithEvents;
|
||||
const incidents = session.incidents;
|
||||
const uxtVideo = session.uxtVideo;
|
||||
const { filteredEvents } = sessionStore;
|
||||
const query = sessionStore.eventsQuery;
|
||||
const { eventsIndex } = sessionStore;
|
||||
|
|
@ -86,26 +87,28 @@ function EventsBlock(props: IProps) {
|
|||
}
|
||||
});
|
||||
}
|
||||
const eventsWithMobxNotes = [...notesWithEvents, ...notes]
|
||||
.sort(sortEvents);
|
||||
const eventsWithMobxNotes = [...incidents, ...notesWithEvents, ...notes, ].sort(sortEvents);
|
||||
const filteredTabEvents = query.length
|
||||
? tabChangeEvents
|
||||
.filter((e => (e.activeUrl as string).includes(query)))
|
||||
: tabChangeEvents;
|
||||
const list = mergeEventLists(
|
||||
query.length > 0 ? filteredEvents : eventsWithMobxNotes,
|
||||
filteredTabEvents
|
||||
? tabChangeEvents.filter((e) => (e.activeUrl as string).includes(query))
|
||||
: tabChangeEvents;
|
||||
return mergeEventLists(
|
||||
filteredLength > 0 ? filteredEvents : eventsWithMobxNotes,
|
||||
tabChangeEvents,
|
||||
)
|
||||
if (zoomEnabled) {
|
||||
return list.filter((e) =>
|
||||
.filter((e) =>
|
||||
zoomEnabled
|
||||
? 'time' in e
|
||||
? e.time >= zoomStartTs && e.time <= zoomEndTs
|
||||
: false
|
||||
: true
|
||||
).filter((e: any) => !e.noteId && e.type !== 'TABCHANGE' && uiPlayerStore.showOnlySearchEvents ? e.isHighlighted : true);
|
||||
}
|
||||
return list;
|
||||
? 'time' in e
|
||||
? e.time >= zoomStartTs && e.time <= zoomEndTs
|
||||
: false
|
||||
: true,
|
||||
)
|
||||
.filter((e: any) =>
|
||||
!e.noteId &&
|
||||
e.type !== 'TABCHANGE' &&
|
||||
uiPlayerStore.showOnlySearchEvents
|
||||
? e.isHighlighted
|
||||
: true,
|
||||
);
|
||||
}, [
|
||||
filteredLength,
|
||||
query,
|
||||
|
|
@ -114,15 +117,17 @@ function EventsBlock(props: IProps) {
|
|||
zoomEnabled,
|
||||
zoomStartTs,
|
||||
zoomEndTs,
|
||||
uiPlayerStore.showOnlySearchEvents
|
||||
uiPlayerStore.showOnlySearchEvents,
|
||||
]);
|
||||
|
||||
const findLastFitting = React.useCallback(
|
||||
(time: number) => {
|
||||
if (!usedEvents.length) return 0;
|
||||
let i = usedEvents.length - 1;
|
||||
const allEvents = usedEvents.concat(incidents);
|
||||
if (!allEvents.length) return 0;
|
||||
let i = allEvents.length - 1;
|
||||
if (time > endTime / 2) {
|
||||
while (i >= 0) {
|
||||
const event = usedEvents[i];
|
||||
while (i > 0) {
|
||||
const event = allEvents[i];
|
||||
if ('time' in event && event.time <= time) break;
|
||||
i--;
|
||||
}
|
||||
|
|
@ -130,18 +135,18 @@ function EventsBlock(props: IProps) {
|
|||
}
|
||||
let l = 0;
|
||||
while (l < i) {
|
||||
const event = usedEvents[l];
|
||||
const event = allEvents[l];
|
||||
if ('time' in event && event.time >= time) break;
|
||||
l++;
|
||||
}
|
||||
return l;
|
||||
},
|
||||
[usedEvents, time, endTime],
|
||||
[usedEvents, incidents, time, endTime],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentTimeEventIndex(findLastFitting(time));
|
||||
}, [])
|
||||
}, [time]);
|
||||
|
||||
const write = ({
|
||||
target: { value },
|
||||
|
|
@ -195,9 +200,10 @@ function EventsBlock(props: IProps) {
|
|||
const event = usedEvents[index];
|
||||
const isNote = 'noteId' in event;
|
||||
const isTabChange = 'type' in event && event.type === 'TABCHANGE';
|
||||
const isIncident = 'type' in event && event.type === 'INCIDENT';
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
const isPrev = index < currentTimeEventIndex;
|
||||
const isSearched = event.isHighlighted
|
||||
const isSearched = event.isHighlighted;
|
||||
|
||||
return (
|
||||
<EventGroupWrapper
|
||||
|
|
@ -213,6 +219,7 @@ function EventsBlock(props: IProps) {
|
|||
showSelection={!playing}
|
||||
isNote={isNote}
|
||||
isTabChange={isTabChange}
|
||||
isIncident={isIncident}
|
||||
isPrev={isPrev}
|
||||
filterOutNote={filterOutNote}
|
||||
setActiveTab={setActiveTab}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ import {
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { getTimelinePosition } from './getTimelinePosition';
|
||||
import { useStore } from '@/mstore';
|
||||
import { getTimelineEventWidth } from './getTimelineEventWidth';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
function EventsList() {
|
||||
const { store } = useContext(PlayerContext);
|
||||
const { uiPlayerStore } = useStore();
|
||||
const { uiPlayerStore, sessionStore } = useStore();
|
||||
const { eventCount, endTime, tabStates, sessionStart } = store.get();
|
||||
const { incidents } = sessionStore.current;
|
||||
|
||||
const { eventCount, endTime } = store.get();
|
||||
const { tabStates } = store.get();
|
||||
const scale = 100 / endTime;
|
||||
const events = React.useMemo(
|
||||
() => Object.values(tabStates)[0]?.eventList.filter((e) => {
|
||||
|
|
@ -39,10 +41,25 @@ function EventsList() {
|
|||
<div
|
||||
/* @ts-ignore TODO */
|
||||
key={`${e.key}_${e.time}`}
|
||||
className={`absolute w-[2px] h-[10px] z-[3] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
|
||||
className={`absolute w-[2px] h-[10px] z-[4] pointer-events-none ${e.isHighlighted ? 'bg-[#f0a930]' : 'bg-[#394eff]'}`}
|
||||
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
|
||||
/>
|
||||
))}
|
||||
{incidents.map((i) => {
|
||||
const width = getTimelineEventWidth(endTime, (i as any).time, (i as any).endTime - sessionStart);
|
||||
return (
|
||||
<Tooltip title={i.label} key={(i as any).startTime}>
|
||||
<div
|
||||
/* @ts-ignore TODO */
|
||||
className={`absolute h-[10px] z-[3] bg-[#ff5454]`}
|
||||
style={{
|
||||
left: `${getTimelinePosition((i as any).time, scale)}%`,
|
||||
width: typeof width === 'string' ? width : `${width}%`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { getTimelinePosition } from '@/utils';
|
||||
|
||||
export function getTimelineEventWidth(
|
||||
sessionDuration: number,
|
||||
eventStart: number,
|
||||
eventEnd: number,
|
||||
): number | string {
|
||||
if (eventStart < 0) {
|
||||
eventStart = 0;
|
||||
}
|
||||
if (eventEnd > sessionDuration) {
|
||||
eventEnd = sessionDuration;
|
||||
}
|
||||
if (eventStart === eventEnd) {
|
||||
return '2px';
|
||||
}
|
||||
|
||||
const width = ((eventEnd - eventStart) / sessionDuration) * 100;
|
||||
|
||||
return width < 1 ? '4px' : width;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { issues_types, types } from 'Types/session/issue';
|
||||
import { Grid, Segmented } from 'antd';
|
||||
import { Angry, CircleAlert, Skull, WifiOff, ChevronDown } from 'lucide-react';
|
||||
import { Angry, CircleAlert, Skull, WifiOff, ChevronDown, MessageCircleWarning } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
|
@ -15,6 +15,7 @@ const tagIcons = {
|
|||
[types.CLICK_RAGE]: <Angry size={14} />,
|
||||
[types.CRASH]: <Skull size={14} />,
|
||||
[types.TAP_RAGE]: <Angry size={14} />,
|
||||
[types.INCIDENT]: <MessageCircleWarning size={14} />,
|
||||
} as Record<string, any>;
|
||||
|
||||
function SessionTags() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/* Auto-generated, do not edit */
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
function Funnel_message_circle_warning(props: Props) {
|
||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22ZM12 8v4M12 16h.01"/></svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Funnel_message_circle_warning;
|
||||
|
|
@ -289,6 +289,7 @@ export { default as Funnel_hdd_fill } from './funnel_hdd_fill';
|
|||
export { default as Funnel_hourglass_top } from './funnel_hourglass_top';
|
||||
export { default as Funnel_image_fill } from './funnel_image_fill';
|
||||
export { default as Funnel_image } from './funnel_image';
|
||||
export { default as Funnel_message_circle_warning } from './funnel_message_circle_warning';
|
||||
export { default as Funnel_microchip } from './funnel_microchip';
|
||||
export { default as Funnel_mouse } from './funnel_mouse';
|
||||
export { default as Funnel_patch_exclamation_fill } from './funnel_patch_exclamation_fill';
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ import {
|
|||
Funnel_hourglass_top,
|
||||
Funnel_image_fill,
|
||||
Funnel_image,
|
||||
Funnel_message_circle_warning,
|
||||
Funnel_microchip,
|
||||
Funnel_mouse,
|
||||
Funnel_patch_exclamation_fill,
|
||||
|
|
@ -1371,6 +1372,9 @@ const SVG = (props: Props) => {
|
|||
// case 'funnel/image':
|
||||
case 'funnel/image': return <Funnel_image width={ width } height={ height } fill={ fill } />;
|
||||
|
||||
// case 'funnel/message-circle-warning':
|
||||
case 'funnel/message-circle-warning': return <Funnel_message_circle_warning width={ width } height={ height } fill={ fill } />;
|
||||
|
||||
// case 'funnel/microchip':
|
||||
case 'funnel/microchip': return <Funnel_microchip width={ width } height={ height } fill={ fill } />;
|
||||
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ export default class SessionStore {
|
|||
events: evData.events.map((e) => ({
|
||||
...e,
|
||||
isHighlighted: checkEventWithFilters(e, searchStore.instance.filters)
|
||||
}))
|
||||
})),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch events', e);
|
||||
|
|
@ -359,6 +359,7 @@ export default class SessionStore {
|
|||
stackEvents = [],
|
||||
userEvents = [],
|
||||
userTesting = [],
|
||||
incidents = [],
|
||||
} = eventsData;
|
||||
|
||||
const filterEvents = filter.events as Record<string, any>[];
|
||||
|
|
@ -399,6 +400,7 @@ export default class SessionStore {
|
|||
userEvents,
|
||||
stackEvents,
|
||||
userTesting,
|
||||
incidents,
|
||||
);
|
||||
this.current = session;
|
||||
this.eventsIndex = matching;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ const SIMPLE_LIST_NAMES = [
|
|||
'exceptions',
|
||||
'profiles',
|
||||
'frustrations',
|
||||
'incidents',
|
||||
] as const;
|
||||
const MARKED_LIST_NAMES = [
|
||||
'log',
|
||||
|
|
|
|||
|
|
@ -773,6 +773,18 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 87: {
|
||||
const label = this.readString(); if (label === null) { return resetPointer() }
|
||||
const startTime = this.readInt(); if (startTime === null) { return resetPointer() }
|
||||
const endTime = this.readInt(); if (endTime === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.Incident,
|
||||
label,
|
||||
startTime,
|
||||
endTime,
|
||||
};
|
||||
}
|
||||
|
||||
case 89: {
|
||||
const name = this.readString(); if (name === null) { return resetPointer() }
|
||||
const duration = this.readInt(); if (duration === null) { return resetPointer() }
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import type {
|
|||
RawNetworkRequest,
|
||||
RawWsChannel,
|
||||
RawResourceTiming,
|
||||
RawIncident,
|
||||
RawLongAnimationTask,
|
||||
RawSelectionChange,
|
||||
RawMouseThrashing,
|
||||
|
|
@ -207,6 +208,8 @@ export type WsChannel = RawWsChannel & Timed
|
|||
|
||||
export type ResourceTiming = RawResourceTiming & Timed
|
||||
|
||||
export type Incident = RawIncident & Timed
|
||||
|
||||
export type LongAnimationTask = RawLongAnimationTask & Timed
|
||||
|
||||
export type SelectionChange = RawSelectionChange & Timed
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export const enum MType {
|
|||
NetworkRequest = 83,
|
||||
WsChannel = 84,
|
||||
ResourceTiming = 85,
|
||||
Incident = 87,
|
||||
LongAnimationTask = 89,
|
||||
SelectionChange = 113,
|
||||
MouseThrashing = 114,
|
||||
|
|
@ -521,6 +522,13 @@ export interface RawResourceTiming {
|
|||
stalled: number,
|
||||
}
|
||||
|
||||
export interface RawIncident {
|
||||
tp: MType.Incident,
|
||||
label: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
}
|
||||
|
||||
export interface RawLongAnimationTask {
|
||||
tp: MType.LongAnimationTask,
|
||||
name: string,
|
||||
|
|
@ -695,4 +703,4 @@ export interface RawMobileIssueEvent {
|
|||
}
|
||||
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawStringDictGlobal | RawSetNodeAttributeDictGlobal | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQlDeprecated | RawPerformanceTrack | RawStringDictDeprecated | RawSetNodeAttributeDictDeprecated | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecatedDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawResourceTiming | RawLongAnimationTask | RawSelectionChange | RawMouseThrashing | RawResourceTimingDeprecated | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawGraphQl | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocationDeprecated | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawStringDictGlobal | RawSetNodeAttributeDictGlobal | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawReduxDeprecated | RawVuex | RawMobX | RawNgRx | RawGraphQlDeprecated | RawPerformanceTrack | RawStringDictDeprecated | RawSetNodeAttributeDictDeprecated | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecatedDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawMouseClickDeprecated | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawResourceTiming | RawIncident | RawLongAnimationTask | RawSelectionChange | RawMouseThrashing | RawResourceTimingDeprecated | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | RawRedux | RawSetPageLocation | RawGraphQl | RawMobileEvent | RawMobileScreenChanges | RawMobileClickEvent | RawMobileInputEvent | RawMobilePerformanceEvent | RawMobileLog | RawMobileInternalError | RawMobileNetworkCall | RawMobileSwipeEvent | RawMobileIssueEvent;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export const TP_MAP = {
|
|||
83: MType.NetworkRequest,
|
||||
84: MType.WsChannel,
|
||||
85: MType.ResourceTiming,
|
||||
87: MType.Incident,
|
||||
89: MType.LongAnimationTask,
|
||||
113: MType.SelectionChange,
|
||||
114: MType.MouseThrashing,
|
||||
|
|
|
|||
|
|
@ -510,6 +510,13 @@ type TrResourceTiming = [
|
|||
stalled: number,
|
||||
]
|
||||
|
||||
type TrIncident = [
|
||||
type: 87,
|
||||
label: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
]
|
||||
|
||||
type TrLongAnimationTask = [
|
||||
type: 89,
|
||||
name: string,
|
||||
|
|
@ -614,7 +621,7 @@ type TrWebVitals = [
|
|||
]
|
||||
|
||||
|
||||
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 | TrStringDictGlobal | TrSetNodeAttributeDictGlobal | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDictDeprecated | TrSetNodeAttributeDictDeprecated | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecatedDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrResourceTiming | TrLongAnimationTask | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTimingDeprecated | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals
|
||||
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 | TrStringDictGlobal | TrSetNodeAttributeDictGlobal | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrReduxDeprecated | TrVuex | TrMobX | TrNgRx | TrGraphQLDeprecated | TrPerformanceTrack | TrStringDictDeprecated | TrSetNodeAttributeDictDeprecated | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecatedDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrMouseClickDeprecated | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrResourceTiming | TrIncident | TrLongAnimationTask | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTimingDeprecated | TrTabChange | TrTabData | TrCanvasNode | TrTagTrigger | TrRedux | TrSetPageLocation | TrGraphQL | TrWebVitals
|
||||
|
||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||
switch(tMsg[0]) {
|
||||
|
|
@ -1148,6 +1155,15 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 87: {
|
||||
return {
|
||||
tp: MType.Incident,
|
||||
label: tMsg[1],
|
||||
startTime: tMsg[2],
|
||||
endTime: tMsg[3],
|
||||
}
|
||||
}
|
||||
|
||||
case 89: {
|
||||
return {
|
||||
tp: MType.LongAnimationTask,
|
||||
|
|
|
|||
1
frontend/app/svg/icons/funnel/message-circle-warning.svg
Normal file
1
frontend/app/svg/icons/funnel/message-circle-warning.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-warning-icon lucide-message-circle-warning"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
|
||||
|
After Width: | Height: | Size: 350 B |
|
|
@ -10,6 +10,7 @@ const IOS_VIEW = 'VIEW';
|
|||
const UXT_EVENT = 'UXT_EVENT';
|
||||
const TOUCH = 'TAP';
|
||||
const SWIPE = 'SWIPE';
|
||||
const INCIDENT = 'INCIDENT';
|
||||
|
||||
export const TYPES = {
|
||||
CONSOLE,
|
||||
|
|
@ -24,6 +25,7 @@ export const TYPES = {
|
|||
SWIPE,
|
||||
TAPRAGE,
|
||||
UXT_EVENT,
|
||||
INCIDENT,
|
||||
};
|
||||
|
||||
export type EventType =
|
||||
|
|
@ -35,7 +37,8 @@ export type EventType =
|
|||
| typeof CLICKRAGE
|
||||
| typeof IOS_VIEW
|
||||
| typeof TOUCH
|
||||
| typeof SWIPE;
|
||||
| typeof SWIPE
|
||||
| typeof INCIDENT
|
||||
|
||||
interface IEvent {
|
||||
time: number;
|
||||
|
|
@ -99,6 +102,12 @@ export interface LocationEvent extends IEvent {
|
|||
webVitals: string | null;
|
||||
}
|
||||
|
||||
export interface IncidentEvent extends IEvent {
|
||||
label: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export type EventData =
|
||||
| ConsoleEvent
|
||||
| ClickEvent
|
||||
|
|
@ -276,6 +285,21 @@ export class Location extends Event {
|
|||
}
|
||||
}
|
||||
|
||||
export class Incident extends Event {
|
||||
readonly name = 'Incident';
|
||||
|
||||
readonly type = CUSTOM;
|
||||
|
||||
constructor(evt: IncidentEvent) {
|
||||
super(evt);
|
||||
Object.assign(this, {
|
||||
...evt,
|
||||
label: evt.label || 'User signaled an incident',
|
||||
type: 'INCIDENT',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type InjectedEvent =
|
||||
| Console
|
||||
| Click
|
||||
|
|
@ -283,7 +307,8 @@ export type InjectedEvent =
|
|||
| Location
|
||||
| Touch
|
||||
| Swipe
|
||||
| UxtEvent;
|
||||
| UxtEvent
|
||||
| Incident;
|
||||
|
||||
export default function (event: EventData) {
|
||||
if ('allow_typing' in event) {
|
||||
|
|
@ -307,6 +332,8 @@ export default function (event: EventData) {
|
|||
return new Click(event as ClickEvent, true);
|
||||
case SWIPE:
|
||||
return new Swipe(event as SwipeEvent);
|
||||
case INCIDENT:
|
||||
return new Incident(event as IncidentEvent);
|
||||
default:
|
||||
return console.error(`Unknown event type: ${event.type}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import i18next, { TFunction } from 'i18next';
|
||||
import Record from 'Types/Record';
|
||||
|
||||
export const types = {
|
||||
|
|
@ -10,6 +9,7 @@ export const types = {
|
|||
MOUSE_THRASHING: 'mouse_thrashing',
|
||||
TAP_RAGE: 'tap_rage',
|
||||
DEAD_CLICK: 'dead_click',
|
||||
INCIDENT: 'incident',
|
||||
} as const;
|
||||
|
||||
type TypeKeys = keyof typeof types;
|
||||
|
|
@ -75,6 +75,14 @@ export const issues_types = [
|
|||
name: 'Mouse Thrashing',
|
||||
icon: 'cursor-trash',
|
||||
},
|
||||
{
|
||||
type: types.INCIDENT,
|
||||
visible: true,
|
||||
order: 7,
|
||||
name: 'Incidents',
|
||||
icon: 'funnel/message-circle-warning',
|
||||
// isEvent: false,
|
||||
}
|
||||
// { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' },
|
||||
// { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' },
|
||||
// { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Duration } from 'luxon';
|
||||
import { Note } from 'App/services/NotesService';
|
||||
import { toJS } from 'mobx';
|
||||
import SessionEvent, { TYPES, EventData, InjectedEvent } from './event';
|
||||
import SessionEvent, { TYPES, EventData, InjectedEvent, Incident } from './event';
|
||||
import StackEvent from './stackEvent';
|
||||
import SessionError, { IError } from './error';
|
||||
import Issue, { IIssue, types as issueTypes } from './issue';
|
||||
|
|
@ -145,6 +145,7 @@ export interface ISession {
|
|||
isMobileNative?: boolean;
|
||||
audio?: string;
|
||||
assistOnly?: boolean;
|
||||
incidents?: Array<Incident>;
|
||||
}
|
||||
|
||||
const emptyValues = {
|
||||
|
|
@ -284,6 +285,8 @@ export default class Session {
|
|||
|
||||
frustrations: Array<IIssue | InjectedEvent>;
|
||||
|
||||
incidents: Array<Incident>
|
||||
|
||||
timezone?: ISession['timezone'];
|
||||
|
||||
platform: ISession['platform'];
|
||||
|
|
@ -329,6 +332,7 @@ export default class Session {
|
|||
canvasURL = [],
|
||||
uxtVideo = [],
|
||||
videoURL = [],
|
||||
incidents = [],
|
||||
...session
|
||||
} = sessionData;
|
||||
const duration = Duration.fromMillis(
|
||||
|
|
@ -446,6 +450,7 @@ export default class Session {
|
|||
userEvents: any[] = [],
|
||||
stackEvents: any[] = [],
|
||||
userTestingEvents: any[] = [],
|
||||
incidents: any[] = [],
|
||||
) {
|
||||
const exceptions =
|
||||
(errors as IError[])?.map((e) => new SessionError(e)) || [];
|
||||
|
|
@ -506,6 +511,11 @@ export default class Session {
|
|||
const frustrationList =
|
||||
[...frustrationEvents, ...frustrationIssues].sort(sortEvents) || [];
|
||||
|
||||
const incidentsList = incidents.sort((a, b) => a.startTime - b.startTime).map((i) => ({
|
||||
...i,
|
||||
time: i.startTime - this.startedAt,
|
||||
})).map((i) => new Incident(i));
|
||||
|
||||
const mixedEventsWithIssues = mergeEventLists(
|
||||
events,
|
||||
frustrationIssues.filter((i) => i.type !== issueTypes.DEAD_CLICK),
|
||||
|
|
@ -524,6 +534,7 @@ export default class Session {
|
|||
this.frustrations = frustrationList;
|
||||
this.crashes = crashes || [];
|
||||
this.addedEvents = true;
|
||||
this.incidents = incidentsList;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -542,6 +542,12 @@ message 85, 'ResourceTiming', :replayer => :devtools do
|
|||
uint 'Stalled'
|
||||
end
|
||||
|
||||
message 87, 'Incident', :replayer => :devtools do
|
||||
string 'Label'
|
||||
int 'StartTime'
|
||||
int 'EndTime'
|
||||
end
|
||||
|
||||
message 89, 'LongAnimationTask', :replayer => :devtools do
|
||||
string 'Name'
|
||||
int 'Duration'
|
||||
|
|
@ -653,4 +659,4 @@ message 127, 'SessionSearch', :tracker => false, :replayer => false do
|
|||
uint 'Partition'
|
||||
end
|
||||
|
||||
# FREE 2, 35, 36, 65, 85, 86, 87, 88, 89
|
||||
# FREE 2, 35, 36, 65, 87, 88, 89
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export declare const enum Type {
|
|||
NetworkRequest = 83,
|
||||
WSChannel = 84,
|
||||
ResourceTiming = 85,
|
||||
Incident = 87,
|
||||
LongAnimationTask = 89,
|
||||
InputChange = 112,
|
||||
SelectionChange = 113,
|
||||
|
|
@ -593,6 +594,13 @@ export type ResourceTiming = [
|
|||
/*stalled:*/ number,
|
||||
]
|
||||
|
||||
export type Incident = [
|
||||
/*type:*/ Type.Incident,
|
||||
/*label:*/ string,
|
||||
/*startTime:*/ number,
|
||||
/*endTime:*/ number,
|
||||
]
|
||||
|
||||
export type LongAnimationTask = [
|
||||
/*type:*/ Type.LongAnimationTask,
|
||||
/*name:*/ string,
|
||||
|
|
@ -697,5 +705,5 @@ export type WebVitals = [
|
|||
]
|
||||
|
||||
|
||||
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 | StringDictGlobal | SetNodeAttributeDictGlobal | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDictDeprecated | SetNodeAttributeDictDeprecated | StringDict | SetNodeAttributeDict | ResourceTimingDeprecatedDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | ResourceTiming | LongAnimationTask | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTimingDeprecated | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL | WebVitals
|
||||
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 | StringDictGlobal | SetNodeAttributeDictGlobal | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | ReduxDeprecated | Vuex | MobX | NgRx | GraphQLDeprecated | PerformanceTrack | StringDictDeprecated | SetNodeAttributeDictDeprecated | StringDict | SetNodeAttributeDict | ResourceTimingDeprecatedDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | MouseClickDeprecated | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | ResourceTiming | Incident | LongAnimationTask | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTimingDeprecated | TabChange | TabData | CanvasNode | TagTrigger | Redux | SetPageLocation | GraphQL | WebVitals
|
||||
export default Message
|
||||
|
|
|
|||
|
|
@ -946,6 +946,19 @@ export function ResourceTiming(
|
|||
]
|
||||
}
|
||||
|
||||
export function Incident(
|
||||
label: string,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
): Messages.Incident {
|
||||
return [
|
||||
Messages.Type.Incident,
|
||||
label,
|
||||
startTime,
|
||||
endTime,
|
||||
]
|
||||
}
|
||||
|
||||
export function LongAnimationTask(
|
||||
name: string,
|
||||
duration: number,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import App from './app/index.js'
|
|||
|
||||
export { default as App } from './app/index.js'
|
||||
|
||||
import { UserAnonymousID, CustomEvent, CustomIssue } from './app/messages.gen.js'
|
||||
import { UserAnonymousID, CustomEvent, CustomIssue, Incident } from './app/messages.gen.js'
|
||||
import * as _Messages from './app/messages.gen.js'
|
||||
|
||||
export const Messages = _Messages
|
||||
|
|
@ -539,4 +539,15 @@ export default class API {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
incident = (options: {
|
||||
label?: string;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
}) => {
|
||||
if (this.app === null) {
|
||||
return
|
||||
}
|
||||
this.app.send(Incident(options.label ?? '', options.startTime, options.endTime ?? options.startTime))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,6 +286,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
|||
return this.uint(msg[1]) && this.uint(msg[2]) && this.uint(msg[3]) && this.uint(msg[4]) && this.uint(msg[5]) && this.uint(msg[6]) && this.string(msg[7]) && this.string(msg[8]) && this.uint(msg[9]) && this.boolean(msg[10]) && this.uint(msg[11]) && this.uint(msg[12]) && this.uint(msg[13]) && this.uint(msg[14]) && this.uint(msg[15]) && this.uint(msg[16]) && this.uint(msg[17])
|
||||
break
|
||||
|
||||
case Messages.Type.Incident:
|
||||
return this.string(msg[1]) && this.int(msg[2]) && this.int(msg[3])
|
||||
break
|
||||
|
||||
case Messages.Type.LongAnimationTask:
|
||||
return this.string(msg[1]) && this.int(msg[2]) && this.int(msg[3]) && this.int(msg[4]) && this.int(msg[5]) && this.string(msg[6])
|
||||
break
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue