@@ -172,4 +191,22 @@ function TabChange({ from, to, activeUrl, onClick }) {
);
};
+function Incident({ label, onClick }) {
+ 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 33cdaa123..546692b5c 100644
--- a/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
+++ b/frontend/app/components/Session_/EventsBlock/EventsBlock.tsx
@@ -35,7 +35,7 @@ function EventsBlock(props: IProps) {
useStore();
const session = sessionStore.current;
const { notesWithEvents } = session;
- const { uxtVideo } = session;
+ const { uxtVideo, incidents } = session;
const { filteredEvents } = sessionStore;
const query = sessionStore.eventsQuery;
const { eventsIndex } = sessionStore;
@@ -49,8 +49,6 @@ function EventsBlock(props: IProps) {
const { store, player } = React.useContext(PlayerContext);
const [currentTimeEventIndex, setCurrentTimeEventIndex] = React.useState(0);
- console.log('FILTER', uiPlayerStore.showOnlySearchEvents)
-
const {
time,
endTime,
@@ -88,7 +86,7 @@ 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;
@@ -200,6 +198,7 @@ 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;
@@ -218,6 +217,7 @@ function EventsBlock(props: IProps) {
showSelection={!playing}
isNote={isNote}
isTabChange={isTabChange}
+ isIncident={isIncident}
isPrev={isPrev}
filterOutNote={filterOutNote}
setActiveTab={setActiveTab}
diff --git a/frontend/app/components/Session_/Player/Controls/EventsList.tsx b/frontend/app/components/Session_/Player/Controls/EventsList.tsx
index c469c71a9..732613f48 100644
--- a/frontend/app/components/Session_/Player/Controls/EventsList.tsx
+++ b/frontend/app/components/Session_/Player/Controls/EventsList.tsx
@@ -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() {
))}
+ {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..6983948ac 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.INCIDENTS]:
,
} as Record
;
function SessionTags() {
diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts
index 466a7219d..42b4161ce 100644
--- a/frontend/app/mstore/sessionStore.ts
+++ b/frontend/app/mstore/sessionStore.ts
@@ -19,19 +19,6 @@ import { searchStore, searchStoreLive } from './index';
import { checkEventWithFilters } from '@/components/Session_/Player/Controls/checkEventWithFilters';
const range = getDateRangeFromValue(LAST_7_DAYS);
-const mockIncidents = [
- {
- label: 'Inciden 1',
- startTime: 1746629916704,
- endTime: 1746629916704,
- },
- {
- label: 'Incident 2',
- startTime: 1746629916704,
- endTime: 1746629916704,
- },
-]
-
const defaultDateFilters = {
url: '',
rangeValue: LAST_7_DAYS,
@@ -357,9 +344,8 @@ export default class SessionStore {
events: evData.events.map((e) => ({
...e,
isHighlighted: checkEventWithFilters(e, searchStore.instance.filters)
- })).concat(mockIncidents)
+ })),
});
- console.log('!!!!', eventsData)
} catch (e) {
console.error('Failed to fetch events', e);
}
@@ -373,6 +359,7 @@ export default class SessionStore {
stackEvents = [],
userEvents = [],
userTesting = [],
+ incidents = [],
} = eventsData;
const filterEvents = filter.events as Record[];
@@ -413,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 381473c8f..2168038e7 100644
--- a/frontend/app/player/web/Lists.ts
+++ b/frontend/app/player/web/Lists.ts
@@ -59,6 +59,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 270029843..c95301414 100644
--- a/frontend/app/player/web/messages/RawMessageReader.gen.ts
+++ b/frontend/app/player/web/messages/RawMessageReader.gen.ts
@@ -735,8 +735,8 @@ export default class RawMessageReader extends PrimitiveReader {
case 85: {
const label = this.readString(); if (label === null) { return resetPointer() }
- const startTime = this.readString(); if (startTime === null) { return resetPointer() }
- const endTime = this.readString(); if (endTime === 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,
diff --git a/frontend/app/player/web/messages/raw.gen.ts b/frontend/app/player/web/messages/raw.gen.ts
index 56dd6faf6..ef04d7f6d 100644
--- a/frontend/app/player/web/messages/raw.gen.ts
+++ b/frontend/app/player/web/messages/raw.gen.ts
@@ -502,8 +502,8 @@ export interface RawWsChannel {
export interface RawIncident {
tp: MType.Incident,
label: string,
- startTime: string,
- endTime: string,
+ startTime: number,
+ endTime: number,
}
export interface RawSelectionChange {
diff --git a/frontend/app/player/web/messages/tracker.gen.ts b/frontend/app/player/web/messages/tracker.gen.ts
index 957efbccb..16b036a4e 100644
--- a/frontend/app/player/web/messages/tracker.gen.ts
+++ b/frontend/app/player/web/messages/tracker.gen.ts
@@ -492,8 +492,8 @@ type TrWSChannel = [
type TrIncident = [
type: 85,
label: string,
- startTime: string,
- endTime: string,
+ startTime: number,
+ endTime: number,
]
type TrInputChange = [
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 7811d882e..b3435de85 100644
--- a/frontend/app/types/session/event.ts
+++ b/frontend/app/types/session/event.ts
@@ -295,8 +295,7 @@ export class Incident extends Event {
Object.assign(this, {
...evt,
label: evt.label || 'User signaled an incident',
- startTime: evt.startTime,
- endTime: evt.startTime || evt.endTime,
+ type: 'INCIDENT',
});
}
}
@@ -312,7 +311,6 @@ export type InjectedEvent =
| Incident;
export default function (event: EventData) {
- console.log('DEETECT EVENT', event);
if ('allow_typing' in event) {
return new UxtEvent(event);
}
diff --git a/frontend/app/types/session/issue.ts b/frontend/app/types/session/issue.ts
index 6aaf1245b..b8c3e364d 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',
+ INCIDENTS: 'incidents',
} as const;
type TypeKeys = keyof typeof types;
@@ -75,6 +75,13 @@ export const issues_types = [
name: 'Mouse Thrashing',
icon: 'cursor-trash',
},
+ {
+ type: types.INCIDENTS,
+ visible: true,
+ order: 7,
+ name: 'Incidents',
+ icon: 'funnel/message-circle-warning',
+ }
// { '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 55170ced8..20f8793d1 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';
@@ -281,6 +281,8 @@ export default class Session {
frustrations: Array;
+ incidents: Array
+
timezone?: ISession['timezone'];
platform: ISession['platform'];
@@ -443,6 +445,7 @@ export default class Session {
userEvents: any[] = [],
stackEvents: any[] = [],
userTestingEvents: any[] = [],
+ incidents: any[] = [],
) {
const exceptions =
(errors as IError[])?.map((e) => new SessionError(e)) || [];
@@ -503,6 +506,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),
@@ -521,6 +529,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 ab2c67353..d84230ed1 100644
--- a/mobs/messages.rb
+++ b/mobs/messages.rb
@@ -522,8 +522,8 @@ end
message 85, 'Incident', :replayer => :devtools do
string 'Label'
- string 'StartTime'
- string 'EndTime'
+ int 'StartTime'
+ int 'EndTime'
end
# 90-111 reserved iOS
diff --git a/tracker/tracker/src/common/messages.gen.ts b/tracker/tracker/src/common/messages.gen.ts
index 278d770f7..1b81ccb44 100644
--- a/tracker/tracker/src/common/messages.gen.ts
+++ b/tracker/tracker/src/common/messages.gen.ts
@@ -574,8 +574,8 @@ export type WSChannel = [
export type Incident = [
/*type:*/ Type.Incident,
/*label:*/ string,
- /*startTime:*/ string,
- /*endTime:*/ string,
+ /*startTime:*/ number,
+ /*endTime:*/ number,
]
export type InputChange = [
diff --git a/tracker/tracker/src/main/app/messages.gen.ts b/tracker/tracker/src/main/app/messages.gen.ts
index 4abdef128..f659563a1 100644
--- a/tracker/tracker/src/main/app/messages.gen.ts
+++ b/tracker/tracker/src/main/app/messages.gen.ts
@@ -907,8 +907,8 @@ export function WSChannel(
export function Incident(
label: string,
- startTime: string,
- endTime: string,
+ startTime: number,
+ endTime: number,
): Messages.Incident {
return [
Messages.Type.Incident,
diff --git a/tracker/tracker/src/webworker/MessageEncoder.gen.ts b/tracker/tracker/src/webworker/MessageEncoder.gen.ts
index adb0a651e..b16f825da 100644
--- a/tracker/tracker/src/webworker/MessageEncoder.gen.ts
+++ b/tracker/tracker/src/webworker/MessageEncoder.gen.ts
@@ -283,7 +283,7 @@ export default class MessageEncoder extends PrimitiveEncoder {
break
case Messages.Type.Incident:
- return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3])
+ return this.string(msg[1]) && this.int(msg[2]) && this.int(msg[3])
break
case Messages.Type.InputChange: