@@ -172,4 +190,22 @@ function TabChange({ from, to, activeUrl, onClick }) {
);
};
+function Incident({ label, onClick }: { label: string; onClick: () => void }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+ {t('Incident')}
+ {label}
+
+
+
+ );
+};
+
export default observer(EventGroupWrapper);
diff --git a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
index b9240c12b..19988dc73 100644
--- a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
+++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
@@ -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 (
Object.values(tabStates)[0]?.eventList.filter((e) => {
@@ -39,10 +41,25 @@ function EventsList() {
))}
+ {incidents.map((i) => {
+ const width = getTimelineEventWidth(endTime, (i as any).time, (i as any).endTime - sessionStart);
+ return (
+
+
+
+ )
+ })}
>
);
}
diff --git a/frontend/app/components/Session_/Player/Controls/getTimelineEventWidth.ts b/frontend/app/components/Session_/Player/Controls/getTimelineEventWidth.ts
new file mode 100644
index 000000000..954eaaf33
--- /dev/null
+++ b/frontend/app/components/Session_/Player/Controls/getTimelineEventWidth.ts
@@ -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;
+}
diff --git a/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx
index 018e0b389..a84f5cae4 100644
--- a/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx
+++ b/frontend/app/components/shared/SessionsTabOverview/components/SessionTags/SessionTags.tsx
@@ -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]: ,
[types.CRASH]: ,
[types.TAP_RAGE]: ,
+ [types.INCIDENT]: ,
} as Record;
function SessionTags() {
diff --git a/frontend/app/components/ui/Icons/funnel_message_circle_warning.tsx b/frontend/app/components/ui/Icons/funnel_message_circle_warning.tsx
new file mode 100644
index 000000000..2789a8f3d
--- /dev/null
+++ b/frontend/app/components/ui/Icons/funnel_message_circle_warning.tsx
@@ -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 (
+
+ );
+}
+
+export default Funnel_message_circle_warning;
diff --git a/frontend/app/components/ui/Icons/index.ts b/frontend/app/components/ui/Icons/index.ts
index 849046c42..225deb1b0 100644
--- a/frontend/app/components/ui/Icons/index.ts
+++ b/frontend/app/components/ui/Icons/index.ts
@@ -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';
diff --git a/frontend/app/components/ui/SVG.tsx b/frontend/app/components/ui/SVG.tsx
index a9ebb6839..4cec3d10f 100644
--- a/frontend/app/components/ui/SVG.tsx
+++ b/frontend/app/components/ui/SVG.tsx
@@ -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 ;
+ // case 'funnel/message-circle-warning':
+ case 'funnel/message-circle-warning': return ;
+
// case 'funnel/microchip':
case 'funnel/microchip': return ;
diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts
index c283a465f..42b4161ce 100644
--- a/frontend/app/mstore/sessionStore.ts
+++ b/frontend/app/mstore/sessionStore.ts
@@ -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[];
@@ -399,6 +400,7 @@ export default class SessionStore {
userEvents,
stackEvents,
userTesting,
+ incidents,
);
this.current = session;
this.eventsIndex = matching;
diff --git a/frontend/app/player/web/Lists.ts b/frontend/app/player/web/Lists.ts
index 0d4914c7c..4cfdab840 100644
--- a/frontend/app/player/web/Lists.ts
+++ b/frontend/app/player/web/Lists.ts
@@ -62,6 +62,7 @@ const SIMPLE_LIST_NAMES = [
'exceptions',
'profiles',
'frustrations',
+ 'incidents',
] as const;
const MARKED_LIST_NAMES = [
'log',
diff --git a/frontend/app/player/web/messages/RawMessageReader.gen.ts b/frontend/app/player/web/messages/RawMessageReader.gen.ts
index 54c9d29c2..9d364ae34 100644
--- a/frontend/app/player/web/messages/RawMessageReader.gen.ts
+++ b/frontend/app/player/web/messages/RawMessageReader.gen.ts
@@ -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() }
diff --git a/frontend/app/player/web/messages/message.gen.ts b/frontend/app/player/web/messages/message.gen.ts
index 32d4d3df1..2a2bc73ee 100644
--- a/frontend/app/player/web/messages/message.gen.ts
+++ b/frontend/app/player/web/messages/message.gen.ts
@@ -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
diff --git a/frontend/app/player/web/messages/raw.gen.ts b/frontend/app/player/web/messages/raw.gen.ts
index e6ca247e2..b04064c8d 100644
--- a/frontend/app/player/web/messages/raw.gen.ts
+++ b/frontend/app/player/web/messages/raw.gen.ts
@@ -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;
diff --git a/frontend/app/player/web/messages/tracker-legacy.gen.ts b/frontend/app/player/web/messages/tracker-legacy.gen.ts
index cef13cce6..ccbdc11a0 100644
--- a/frontend/app/player/web/messages/tracker-legacy.gen.ts
+++ b/frontend/app/player/web/messages/tracker-legacy.gen.ts
@@ -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,
diff --git a/frontend/app/player/web/messages/tracker.gen.ts b/frontend/app/player/web/messages/tracker.gen.ts
index 0c6958268..4607f9860 100644
--- a/frontend/app/player/web/messages/tracker.gen.ts
+++ b/frontend/app/player/web/messages/tracker.gen.ts
@@ -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,
diff --git a/frontend/app/svg/icons/funnel/message-circle-warning.svg b/frontend/app/svg/icons/funnel/message-circle-warning.svg
new file mode 100644
index 000000000..723cfce0b
--- /dev/null
+++ b/frontend/app/svg/icons/funnel/message-circle-warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts
index 2eeb568c4..b3435de85 100644
--- a/frontend/app/types/session/event.ts
+++ b/frontend/app/types/session/event.ts
@@ -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}`);
}
diff --git a/frontend/app/types/session/issue.ts b/frontend/app/types/session/issue.ts
index 6aaf1245b..3972eecef 100644
--- a/frontend/app/types/session/issue.ts
+++ b/frontend/app/types/session/issue.ts
@@ -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' },
diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts
index 8e578b99c..8c0b90d7e 100644
--- a/frontend/app/types/session/session.ts
+++ b/frontend/app/types/session/session.ts
@@ -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;
}
const emptyValues = {
@@ -284,6 +285,8 @@ export default class Session {
frustrations: Array;
+ incidents: Array
+
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;
}
diff --git a/mobs/messages.rb b/mobs/messages.rb
index 2dddad994..c0a9bb0ea 100644
--- a/mobs/messages.rb
+++ b/mobs/messages.rb
@@ -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
diff --git a/tracker/tracker/src/common/messages.gen.ts b/tracker/tracker/src/common/messages.gen.ts
index 6ed1f3f82..e0b2251b9 100644
--- a/tracker/tracker/src/common/messages.gen.ts
+++ b/tracker/tracker/src/common/messages.gen.ts
@@ -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
diff --git a/tracker/tracker/src/main/app/messages.gen.ts b/tracker/tracker/src/main/app/messages.gen.ts
index 1421824ce..3d4aaace2 100644
--- a/tracker/tracker/src/main/app/messages.gen.ts
+++ b/tracker/tracker/src/main/app/messages.gen.ts
@@ -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,
diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts
index d0df3edf3..fb7c3e72f 100644
--- a/tracker/tracker/src/main/index.ts
+++ b/tracker/tracker/src/main/index.ts
@@ -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))
+ }
}
diff --git a/tracker/tracker/src/webworker/MessageEncoder.gen.ts b/tracker/tracker/src/webworker/MessageEncoder.gen.ts
index bb0bfc77e..4aa80734c 100644
--- a/tracker/tracker/src/webworker/MessageEncoder.gen.ts
+++ b/tracker/tracker/src/webworker/MessageEncoder.gen.ts
@@ -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