feat(tracker/ui): add websocket support (#1733)

* feat(tracker/ui): add websocket support

* feat(tracker): expose ws tracker method

* fix(ui): add docs, fix types for ws methods

* fix(ui): some style fixes, rename field in mob

* fix(ui): change ws modal

* fix(ui): change ws modal

* fix(ui): rm mock data
This commit is contained in:
Delirium 2024-01-03 15:54:18 +01:00 committed by GitHub
parent 7a5e2be138
commit 9e1add4ad9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 809 additions and 386 deletions

File diff suppressed because it is too large Load diff

View file

@ -1254,6 +1254,30 @@ func DecodeNetworkRequest(reader BytesReader) (Message, error) {
return msg, err
}
func DecodeWSChannel(reader BytesReader) (Message, error) {
var err error = nil
msg := &WSChannel{}
if msg.ChType, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.ChannelName, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Data, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Timestamp, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.Dir, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.MessageType, err = reader.ReadString(); err != nil {
return nil, err
}
return msg, err
}
func DecodeInputChange(reader BytesReader) (Message, error) {
var err error = nil
msg := &InputChange{}
@ -1991,6 +2015,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodePartitionedMessage(reader)
case 83:
return DecodeNetworkRequest(reader)
case 84:
return DecodeWSChannel(reader)
case 112:
return DecodeInputChange(reader)
case 113:

View file

@ -723,6 +723,18 @@ class NetworkRequest(Message):
self.transferred_body_size = transferred_body_size
class WSChannel(Message):
__id__ = 84
def __init__(self, ch_type, channel_name, data, timestamp, dir, message_type):
self.ch_type = ch_type
self.channel_name = channel_name
self.data = data
self.timestamp = timestamp
self.dir = dir
self.message_type = message_type
class InputChange(Message):
__id__ = 112

View file

@ -1069,6 +1069,25 @@ cdef class NetworkRequest(PyMessage):
self.transferred_body_size = transferred_body_size
cdef class WSChannel(PyMessage):
cdef public int __id__
cdef public str ch_type
cdef public str channel_name
cdef public str data
cdef public unsigned long timestamp
cdef public str dir
cdef public str message_type
def __init__(self, str ch_type, str channel_name, str data, unsigned long timestamp, str dir, str message_type):
self.__id__ = 84
self.ch_type = ch_type
self.channel_name = channel_name
self.data = data
self.timestamp = timestamp
self.dir = dir
self.message_type = message_type
cdef class InputChange(PyMessage):
cdef public int __id__
cdef public unsigned long id

View file

@ -660,6 +660,16 @@ class MessageCodec(Codec):
transferred_body_size=self.read_uint(reader)
)
if message_id == 84:
return WSChannel(
ch_type=self.read_string(reader),
channel_name=self.read_string(reader),
data=self.read_string(reader),
timestamp=self.read_uint(reader),
dir=self.read_string(reader),
message_type=self.read_string(reader)
)
if message_id == 112:
return InputChange(
id=self.read_uint(reader),

View file

@ -758,6 +758,16 @@ cdef class MessageCodec:
transferred_body_size=self.read_uint(reader)
)
if message_id == 84:
return WSChannel(
ch_type=self.read_string(reader),
channel_name=self.read_string(reader),
data=self.read_string(reader),
timestamp=self.read_uint(reader),
dir=self.read_string(reader),
message_type=self.read_string(reader)
)
if message_id == 112:
return InputChange(
id=self.read_uint(reader),

View file

@ -9,14 +9,15 @@ function EventsList({ scale }: { scale: number }) {
const { tabStates, eventCount } = store.get();
const events = React.useMemo(() => {
return Object.values(tabStates)[0]?.eventList || [];
return Object.values(tabStates)[0]?.eventList.filter(e => e.time) || [];
}, [eventCount]);
return (
<>
{events.map((e) => (
<div
/*@ts-ignore TODO */
key={e.key}
key={`${e.key}_${e.time}`}
className={stl.event}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/>
@ -35,7 +36,7 @@ function MobileEventsList({ scale }: { scale: number }) {
{events.map((e) => (
<div
/*@ts-ignore TODO */
key={e.key}
key={`${e.key}_${e.time}`}
className={stl.event}
style={{ left: `${getTimelinePosition(e.time, scale)}%` }}
/>

View file

@ -18,6 +18,7 @@ import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import WSModal from './WSModal'
const INDEX_KEY = 'network';
@ -28,6 +29,7 @@ const CSS = 'css';
const IMG = 'img';
const MEDIA = 'media';
const OTHER = 'other';
const WS = 'websocket';
const TYPE_TO_TAB = {
[ResourceType.XHR]: XHR,
@ -36,10 +38,11 @@ const TYPE_TO_TAB = {
[ResourceType.CSS]: CSS,
[ResourceType.IMG]: IMG,
[ResourceType.MEDIA]: MEDIA,
[ResourceType.WS]: WS,
[ResourceType.OTHER]: OTHER,
};
const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER] as const;
const TAP_KEYS = [ALL, XHR, JS, CSS, IMG, MEDIA, OTHER, WS] as const;
const TABS = TAP_KEYS.map((tab) => ({
text: tab === 'xhr' ? 'Fetch/XHR' : tab,
key: tab,
@ -156,6 +159,8 @@ function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panel
resourceList = [],
fetchListNow = [],
resourceListNow = [],
websocketList = [],
websocketListNow = [],
} = tabStates[currentTab];
return (
@ -170,11 +175,19 @@ function NetworkPanelCont({ startedAt, panelHeight }: { startedAt: number; panel
resourceListNow={resourceListNow}
player={player}
startedAt={startedAt}
websocketList={websocketList as WSMessage[]}
websocketListNow={websocketListNow as WSMessage[]}
/>
);
}
function MobileNetworkPanelCont({ startedAt, panelHeight }: { startedAt: number, panelHeight: number }) {
function MobileNetworkPanelCont({
startedAt,
panelHeight,
}: {
startedAt: number;
panelHeight: number;
}) {
const { player, store } = React.useContext(MobilePlayerContext);
const domContentLoadedTime = undefined;
@ -185,6 +198,8 @@ function MobileNetworkPanelCont({ startedAt, panelHeight }: { startedAt: number,
resourceList = [],
fetchListNow = [],
resourceListNow = [],
websocketList = [],
websocketListNow = [],
} = store.get();
return (
@ -200,10 +215,22 @@ function MobileNetworkPanelCont({ startedAt, panelHeight }: { startedAt: number,
resourceListNow={resourceListNow}
player={player}
startedAt={startedAt}
// @ts-ignore
websocketList={websocketList}
// @ts-ignore
websocketListNow={websocketListNow}
/>
);
}
type WSMessage = Timed & {
channelName: string;
data: string;
timestamp: number;
dir: 'up' | 'down';
messageType: string;
}
interface Props {
domContentLoadedTime?: {
time: number;
@ -218,6 +245,8 @@ interface Props {
resourceList: Timed[];
fetchListNow: Timed[];
resourceListNow: Timed[];
websocketList: Array<WSMessage>;
websocketListNow: Array<WSMessage>;
player: WebPlayer | MobilePlayer;
startedAt: number;
isMobile?: boolean;
@ -237,6 +266,7 @@ const NetworkPanelComp = observer(
startedAt,
isMobile,
panelHeight,
websocketList,
}: Props) => {
const { showModal } = useModal();
const [sortBy, setSortBy] = useState('time');
@ -251,6 +281,14 @@ const NetworkPanelComp = observer(
const activeTab = devTools[INDEX_KEY].activeTab;
const activeIndex = devTools[INDEX_KEY].index;
const socketList = useMemo(
() =>
websocketList.filter(
(ws, i, arr) => arr.findIndex((it) => it.channelName === ws.channelName) === i
),
[websocketList]
);
const list = useMemo(
() =>
// TODO: better merge (with body size info) - do it in player
@ -283,8 +321,20 @@ const NetworkPanelComp = observer(
})
)
.concat(fetchList)
.concat(
socketList.map((ws) => ({
...ws,
type: 'websocket',
method: 'ws',
url: ws.channelName,
name: ws.channelName,
status: '101',
duration: 0,
transferredBodySize: 0,
}))
)
.sort((a, b) => a.time - b.time),
[resourceList.length, fetchList.length]
[resourceList.length, fetchList.length, socketList]
);
let filteredList = useMemo(() => {
@ -354,6 +404,18 @@ const NetworkPanelComp = observer(
}, [domContentLoadedTime, loadTime]);
const showDetailsModal = (item: any) => {
if (item.type === 'websocket') {
const socketMsgList = websocketList.filter((ws) => ws.channelName === item.channelName);
console.log(socketMsgList)
return showModal(
<WSModal
socketMsgList={socketMsgList}
/>, {
right: true, width: 700,
}
)
}
setIsDetailsModalActive(true);
showModal(
<FetchDetailsModal

View file

@ -0,0 +1,93 @@
import { durationFromMs } from 'App/date';
import { Timed } from 'Player';
import React from 'react';
import { Icon } from 'UI';
type SocketMsg = Timed & {
channelName: string;
data: string;
timestamp: number;
dir: 'up' | 'down';
messageType: string;
};
interface Props {
socketMsgList: Array<SocketMsg>;
}
function WSModal({ socketMsgList }: Props) {
return (
<div className={'h-screen w-full bg-white shadow'}>
<div className={'grid grid-cols-12 font-semibold border-b px-4 py-2'}>
<div className={'col-span-9 flex items-center gap-2'}>Data</div>
<div className={'col-span-1'}>Length</div>
<div className={'col-span-2 text-right'}>Time</div>
</div>
<div style={{ height: '100%', maxWidth: 700, overflowY: 'auto' }}>
{socketMsgList.map((msg) => (
<Row msg={msg} key={msg.timestamp} />
))}
</div>
</div>
);
}
function MsgDirection({ dir }: { dir: 'up' | 'down' }) {
return (
<Icon
name={dir === 'up' ? 'arrow-up' : 'arrow-down'}
size="12"
color={dir === 'up' ? 'red' : 'main'}
/>
);
}
function Row({ msg }) {
const [isOpen, setIsOpen] = React.useState(false);
return (
<>
<div
className={`border-b grid grid-cols-12 ${
msg.data.length > 100 ? 'hover:bg-active-blue cursor-pointer' : ''
}`}
onClick={() => (msg.data.length > 100 ? setIsOpen(!isOpen) : null)}
style={{ width: 700 }}
>
<div className={'col-span-9 flex items-center gap-2 p-2'}>
<MsgDirection dir={msg.dir} />
<span className={'bg-active-blue px-2 py-1'}>{msg.messageType}</span>
<span
className={'overflow-hidden text-ellipsis whitespace-nowrap'}
style={{ maxHeight: 44 }}
>
{msg.data}
</span>
{msg.data.length > 100 ? (
<div
className={
'rounded-full font-bold text-xl p-2 bg-white w-6 h-6 flex items-center justify-center'
}
>
<span>{isOpen ? '-' : '+'}</span>
</div>
) : null}
</div>
<div className={'col-span-1 p-2'}>{msg.data.length}</div>
<div className={'col-span-2 p-2 text-right'}>{durationFromMs(msg.time, true)}</div>
</div>
{isOpen ? (
<div className={'w-full h-fit bg-active-blue pl-6 pb-4 pr-2'}>
<div
style={{ maxHeight: "100%", overflow: "auto" }}
className={'whitespace-pre-wrap'}
>
{msg.data}
</div>
</div>
) : null}
</>
);
}
export default WSModal;

View file

@ -44,10 +44,10 @@ export function durationFromMsFormatted(ms: number): string {
return durationFormatted(Duration.fromMillis(ms || 0));
}
export function durationFromMs(ms: number): string {
export function durationFromMs(ms: number, isFull?: boolean): string {
const dur = Duration.fromMillis(ms)
return dur.toFormat('hh:mm:ss')
return dur.toFormat(`hh:mm:ss${ isFull ? '.SSS' : '' }`)
}
export const durationFormattedFull = (duration: Duration): string => {

View file

@ -10,7 +10,7 @@ const SIMPLE_LIST_NAMES = [
"frustrations",
"performance"
] as const
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack", "websocket" ] as const
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ] as const

View file

@ -226,6 +226,9 @@ export default class IOSMessageManager implements IMessageManager {
case MType.IosNetworkCall:
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
break;
case MType.WsChannel:
this.lists.lists.websocket.insert(msg)
break;
case MType.IosEvent:
// @ts-ignore
this.lists.lists.event.insert({...msg, source: 'openreplay'});

View file

@ -4,7 +4,7 @@ import type { Timed } from '../common/types';
const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles", "frustrations"] as const
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack", "websocket" ] as const
//const entityNamesSimple = [ "event", "profile" ];
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ] as const

View file

@ -1,12 +1,21 @@
import type { Store } from "Player";
import { getResourceFromNetworkRequest, getResourceFromResourceTiming, Log, ResourceType } from "Player";
import ListWalker from "Player/common/ListWalker";
import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE, InitialLists, State as ListsState } from "Player/web/Lists";
import CanvasManager from "Player/web/managers/CanvasManager";
import { VElement } from "Player/web/managers/DOM/VirtualDOM";
import PagesManager from "Player/web/managers/PagesManager";
import PerformanceTrackManager from "Player/web/managers/PerformanceTrackManager";
import WindowNodeCounter from "Player/web/managers/WindowNodeCounter";
import type { Store } from 'Player';
import {
getResourceFromNetworkRequest,
getResourceFromResourceTiming,
Log,
ResourceType,
} from 'Player';
import ListWalker from 'Player/common/ListWalker';
import Lists, {
INITIAL_STATE as LISTS_INITIAL_STATE,
InitialLists,
State as ListsState,
} from 'Player/web/Lists';
import CanvasManager from 'Player/web/managers/CanvasManager';
import { VElement } from 'Player/web/managers/DOM/VirtualDOM';
import PagesManager from 'Player/web/managers/PagesManager';
import PerformanceTrackManager from 'Player/web/managers/PerformanceTrackManager';
import WindowNodeCounter from 'Player/web/managers/WindowNodeCounter';
import {
CanvasNode,
ConnectionInformation,
@ -15,22 +24,22 @@ import {
ResourceTiming,
SetPageLocation,
SetViewportScroll,
SetViewportSize
} from "Player/web/messages";
import { isDOMType } from "Player/web/messages/filters.gen";
import Screen from "Player/web/Screen/Screen";
SetViewportSize,
} from 'Player/web/messages';
import { isDOMType } from 'Player/web/messages/filters.gen';
import Screen from 'Player/web/Screen/Screen';
// @ts-ignore
import { Decoder } from "syncod";
import { TYPES as EVENT_TYPES } from "Types/session/event";
import type { PerformanceChartPoint } from "./managers/PerformanceTrackManager";
import { Decoder } from 'syncod';
import { TYPES as EVENT_TYPES } from 'Types/session/event';
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
export interface TabState extends ListsState {
performanceAvailability?: PerformanceTrackManager['availability']
performanceChartData: PerformanceChartPoint[],
performanceChartTime: PerformanceChartPoint[]
cssLoading: boolean
location: string
urlsList: SetPageLocation[]
performanceAvailability?: PerformanceTrackManager['availability'];
performanceChartData: PerformanceChartPoint[];
performanceChartTime: PerformanceChartPoint[];
cssLoading: boolean;
location: string;
urlsList: SetPageLocation[];
}
/**
@ -46,10 +55,10 @@ export default class TabSessionManager {
cssLoading: false,
location: '',
urlsList: [],
}
};
public locationManager: ListWalker<SetPageLocation> = new ListWalker();
private locationEventManager: ListWalker<any>/*<LocationEvent>*/ = new ListWalker();
private locationEventManager: ListWalker<any> /*<LocationEvent>*/ = new ListWalker();
private loadedLocationManager: ListWalker<SetPageLocation> = new ListWalker();
private connectionInfoManger: ListWalker<ConnectionInformation> = new ListWalker();
private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager();
@ -61,8 +70,10 @@ export default class TabSessionManager {
public readonly decoder = new Decoder();
private lists: Lists;
private navigationStartOffset = 0
private canvasManagers: { [key: string]: { manager: CanvasManager, start: number, running: boolean } } = {}
private navigationStartOffset = 0;
private canvasManagers: {
[key: string]: { manager: CanvasManager; start: number; running: boolean };
} = {};
private canvasReplayWalker: ListWalker<CanvasNode> = new ListWalker();
constructor(
@ -70,36 +81,37 @@ export default class TabSessionManager {
private readonly state: Store<{ tabStates: { [tabId: string]: TabState } }>,
private readonly screen: Screen,
private readonly id: string,
private readonly setSize: ({ height, width }: { height: number, width: number }) => void,
private readonly setSize: ({ height, width }: { height: number; width: number }) => void,
private readonly sessionStart: number,
initialLists?: Partial<InitialLists>,
initialLists?: Partial<InitialLists>
) {
this.pagesManager = new PagesManager(screen, this.session.isMobile, this.setCSSLoading)
this.lists = new Lists(initialLists)
initialLists?.event?.forEach((e: Record<string, string>) => { // TODO: to one of "Movable" module
this.pagesManager = new PagesManager(screen, this.session.isMobile, this.setCSSLoading);
this.lists = new Lists(initialLists);
initialLists?.event?.forEach((e: Record<string, string>) => {
// TODO: to one of "Movable" module
if (e.type === EVENT_TYPES.LOCATION) {
this.locationEventManager.append(e);
}
})
});
}
public getNode = (id: number) => {
return this.pagesManager.getNode(id)
}
return this.pagesManager.getNode(id);
};
public updateLists(lists: Partial<InitialLists>) {
Object.keys(lists).forEach((key: 'event' | 'stack' | 'exceptions') => {
const currentList = this.lists.lists[key]
lists[key]!.forEach(item => currentList.insert(item))
})
const currentList = this.lists.lists[key];
lists[key]!.forEach((item) => currentList.insert(item));
});
lists?.event?.forEach((e: Record<string, string>) => {
if (e.type === EVENT_TYPES.LOCATION) {
this.locationEventManager.append(e);
}
})
const eventCount = lists?.event?.length || 0
});
const eventCount = lists?.event?.length || 0;
const currentState = this.state.get()
const currentState = this.state.get();
this.state.update({
// @ts-ignore comes from parent state
eventCount: currentState.eventCount + eventCount,
@ -108,9 +120,9 @@ export default class TabSessionManager {
[this.id]: {
...currentState.tabStates[this.id],
...this.lists.getFullListsState(),
}
}
})
},
},
});
}
updateLocalState(state: Partial<TabState>) {
@ -119,22 +131,22 @@ export default class TabSessionManager {
...this.state.get().tabStates,
[this.id]: {
...this.state.get().tabStates[this.id],
...state
}
}
})
...state,
},
},
});
}
private setCSSLoading = (cssLoading: boolean) => {
this.screen.displayFrame(!cssLoading)
this.screen.displayFrame(!cssLoading);
this.updateLocalState({
cssLoading
})
cssLoading,
});
this.state.update({
// @ts-ignore
ready: !this.state.get().messagesLoading && !cssLoading
})
}
// @ts-ignore
ready: !this.state.get().messagesLoading && !cssLoading,
});
};
public resetMessageManagers() {
this.locationEventManager = new ListWalker();
@ -144,12 +156,11 @@ export default class TabSessionManager {
this.scrollManager = new ListWalker();
this.resizeManager = new ListWalker();
this.performanceTrackManager = new PerformanceTrackManager()
this.performanceTrackManager = new PerformanceTrackManager();
this.windowNodeCounter = new WindowNodeCounter();
this.pagesManager = new PagesManager(this.screen, this.session.isMobile, this.setCSSLoading)
this.pagesManager = new PagesManager(this.screen, this.session.isMobile, this.setCSSLoading);
}
distributeMessage(msg: Message): void {
switch (msg.tp) {
case MType.CanvasNode:
@ -166,7 +177,6 @@ export default class TabSessionManager {
);
this.canvasManagers[managerId] = { manager, start: msg.timestamp, running: false };
this.canvasReplayWalker.append(msg);
}
break;
case MType.SetPageLocation:
@ -185,7 +195,7 @@ export default class TabSessionManager {
this.performanceTrackManager.append(msg);
break;
case MType.SetPageVisibility:
this.performanceTrackManager.handleVisibility(msg)
this.performanceTrackManager.handleVisibility(msg);
break;
case MType.ConnectionInformation:
this.connectionInfoManger.append(msg);
@ -199,18 +209,23 @@ export default class TabSessionManager {
this.lists.lists.log.append(
// @ts-ignore : TODO: enums in the message schema
Log(msg)
)
);
break;
case MType.ResourceTimingDeprecated:
case MType.ResourceTiming:
// TODO: merge `resource` and `fetch` lists into one here instead of UI
if (msg.initiator !== ResourceType.FETCH && msg.initiator !== ResourceType.XHR) {
this.lists.lists.resource.insert(getResourceFromResourceTiming(msg as ResourceTiming, this.sessionStart))
this.lists.lists.resource.insert(
getResourceFromResourceTiming(msg as ResourceTiming, this.sessionStart)
);
}
break;
case MType.Fetch:
case MType.NetworkRequest:
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart));
break;
case MType.WsChannel:
this.lists.lists.websocket.insert(msg);
break;
case MType.Redux:
this.lists.lists.redux.append(msg);
@ -222,8 +237,8 @@ export default class TabSessionManager {
this.lists.lists.vuex.append(msg);
break;
case MType.Zustand:
this.lists.lists.zustand.append(msg)
break
this.lists.lists.zustand.append(msg);
break;
case MType.MobX:
this.lists.lists.mobx.append(msg);
break;
@ -254,8 +269,8 @@ export default class TabSessionManager {
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
}
this.performanceTrackManager.addNodeCountPointIfNeed(msg.time)
isDOMType(msg.tp) && this.pagesManager.appendMessage(msg)
this.performanceTrackManager.addNodeCountPointIfNeed(msg.time);
isDOMType(msg.tp) && this.pagesManager.appendMessage(msg);
break;
}
}
@ -274,13 +289,13 @@ export default class TabSessionManager {
stateToUpdate.domContentLoadedTime = {
time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db)
value: llEvent.domContentLoadedTime,
}
};
}
if (llEvent.loadTime != null) {
stateToUpdate.loadTime = {
time: llEvent.loadTime + this.navigationStartOffset,
value: llEvent.loadTime,
}
};
}
if (llEvent.domBuildingTime != null) {
stateToUpdate.domBuildingTime = llEvent.domBuildingTime;
@ -290,7 +305,7 @@ export default class TabSessionManager {
const lastLocationMsg = this.locationManager.moveGetLast(t, index);
if (!!lastLocationMsg) {
// @ts-ignore comes from parent state
this.state.update({ location: lastLocationMsg.url })
this.state.update({ location: lastLocationMsg.url });
}
// ConnectionInformation message is not used at this moment
// const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index);
@ -303,40 +318,42 @@ export default class TabSessionManager {
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
}
Object.assign(stateToUpdate, this.lists.moveGetState(t))
Object.assign(stateToUpdate, this.lists.moveGetState(t));
Object.keys(stateToUpdate).length > 0 && this.updateLocalState(stateToUpdate);
/* Sequence of the managers is important here */
// Preparing the size of "screen"
const lastResize = this.resizeManager.moveGetLast(t, index);
if (!!lastResize) {
this.setSize(lastResize)
this.setSize(lastResize);
}
this.pagesManager.moveReady(t).then(() => {
const lastScroll = this.scrollManager.moveGetLast(t, index);
if (!!lastScroll && this.screen.window) {
this.screen.window.scrollTo(lastScroll.x, lastScroll.y);
}
const canvasMsg = this.canvasReplayWalker.moveGetLast(t)
const canvasMsg = this.canvasReplayWalker.moveGetLast(t);
if (canvasMsg) {
this.canvasManagers[`${canvasMsg.timestamp}_${canvasMsg.nodeId}`].manager.startVideo();
this.canvasManagers[`${canvasMsg.timestamp}_${canvasMsg.nodeId}`].running = true;
}
const runningManagers = Object.keys(this.canvasManagers).filter((key) => this.canvasManagers[key].running);
const runningManagers = Object.keys(this.canvasManagers).filter(
(key) => this.canvasManagers[key].running
);
runningManagers.forEach((key) => {
const manager = this.canvasManagers[key].manager;
manager.move(t);
})
})
});
});
}
public decodeMessage(msg: Message) {
return this.decoder.decode(msg)
return this.decoder.decode(msg);
}
public _sortMessagesHack = (msgs: Message[]) => {
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
const headChildrenIds = msgs.filter((m) => m.parentID === 1).map((m) => m.id);
this.pagesManager.sortPages((m1, m2) => {
if (m1.time === m2.time) {
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
@ -347,7 +364,7 @@ export default class TabSessionManager {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
@ -358,25 +375,25 @@ export default class TabSessionManager {
}
}
return 0;
})
}
});
};
public onFileReadSuccess = () => {
const stateToUpdate : Partial<Record<string,any>> = {
const stateToUpdate: Partial<Record<string, any>> = {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvailability: this.performanceTrackManager.availability,
urlsList: this.locationManager.list,
...this.lists.getFullListsState(),
}
};
this.updateLocalState(stateToUpdate)
}
this.updateLocalState(stateToUpdate);
};
public getListsFullState = () => {
return this.lists.getFullListsState()
}
return this.lists.getFullListsState();
};
clean() {
this.pagesManager.reset()
this.pagesManager.reset();
}
}
}

View file

@ -651,6 +651,24 @@ export default class RawMessageReader extends PrimitiveReader {
};
}
case 84: {
const chType = this.readString(); if (chType === null) { return resetPointer() }
const channelName = this.readString(); if (channelName === null) { return resetPointer() }
const data = this.readString(); if (data === null) { return resetPointer() }
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const dir = this.readString(); if (dir === null) { return resetPointer() }
const messageType = this.readString(); if (messageType === null) { return resetPointer() }
return {
tp: MType.WsChannel,
chType,
channelName,
data,
timestamp,
dir,
messageType,
};
}
case 113: {
const selectionStart = this.readUint(); if (selectionStart === null) { return resetPointer() }
const selectionEnd = this.readUint(); if (selectionEnd === null) { return resetPointer() }

View file

@ -56,6 +56,7 @@ import type {
RawAdoptedSsRemoveOwner,
RawZustand,
RawNetworkRequest,
RawWsChannel,
RawSelectionChange,
RawMouseThrashing,
RawResourceTiming,
@ -181,6 +182,8 @@ export type Zustand = RawZustand & Timed
export type NetworkRequest = RawNetworkRequest & Timed
export type WsChannel = RawWsChannel & Timed
export type SelectionChange = RawSelectionChange & Timed
export type MouseThrashing = RawMouseThrashing & Timed

View file

@ -54,6 +54,7 @@ export const enum MType {
AdoptedSsRemoveOwner = 77,
Zustand = 79,
NetworkRequest = 83,
WsChannel = 84,
SelectionChange = 113,
MouseThrashing = 114,
ResourceTiming = 116,
@ -441,6 +442,16 @@ export interface RawNetworkRequest {
transferredBodySize: number,
}
export interface RawWsChannel {
tp: MType.WsChannel,
chType: string,
channelName: string,
data: string,
timestamp: number,
dir: string,
messageType: string,
}
export interface RawSelectionChange {
tp: MType.SelectionChange,
selectionStart: number,
@ -575,4 +586,4 @@ export interface RawIosIssueEvent {
}
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequestDeprecated | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawNetworkRequest | RawWsChannel | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;

View file

@ -55,6 +55,7 @@ export const TP_MAP = {
77: MType.AdoptedSsRemoveOwner,
79: MType.Zustand,
83: MType.NetworkRequest,
84: MType.WsChannel,
113: MType.SelectionChange,
114: MType.MouseThrashing,
116: MType.ResourceTiming,

View file

@ -442,6 +442,16 @@ type TrNetworkRequest = [
transferredBodySize: number,
]
type TrWSChannel = [
type: 84,
chType: string,
channelName: string,
data: string,
timestamp: number,
dir: string,
messageType: string,
]
type TrInputChange = [
type: 112,
id: number,
@ -500,7 +510,7 @@ type TrCanvasNode = [
]
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequestDeprecated | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrNetworkRequest | TrWSChannel | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData | TrCanvasNode
export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) {
@ -952,6 +962,18 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
}
}
case 84: {
return {
tp: MType.WsChannel,
chType: tMsg[1],
channelName: tMsg[2],
data: tMsg[3],
timestamp: tMsg[4],
dir: tMsg[5],
messageType: tMsg[6],
}
}
case 113: {
return {
tp: MType.SelectionChange,

View file

@ -9,6 +9,7 @@ export const enum ResourceType {
CSS = 'css',
IMG = 'img',
MEDIA = 'media',
WS = 'websocket',
OTHER = 'other',
}

View file

@ -458,6 +458,15 @@ message 83, 'NetworkRequest', :replayer => :devtools do
uint 'TransferredBodySize'
end
message 84, 'WSChannel', :replayer => :devtools do
string 'ChType'
string 'ChannelName'
string 'Data'
uint 'Timestamp'
string 'Dir'
string 'MessageType'
end
# 90-111 reserved iOS
message 112, 'InputChange', :replayer => false do

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "11.0.2",
"version": "11.0.0-beta.7",
"keywords": [
"logging",
"replay"

View file

@ -64,6 +64,7 @@ export declare const enum Type {
BatchMetadata = 81,
PartitionedMessage = 82,
NetworkRequest = 83,
WSChannel = 84,
InputChange = 112,
SelectionChange = 113,
MouseThrashing = 114,
@ -512,6 +513,16 @@ export type NetworkRequest = [
/*transferredBodySize:*/ number,
]
export type WSChannel = [
/*type:*/ Type.WSChannel,
/*chType:*/ string,
/*channelName:*/ string,
/*data:*/ string,
/*timestamp:*/ number,
/*dir:*/ string,
/*messageType:*/ string,
]
export type InputChange = [
/*type:*/ Type.InputChange,
/*id:*/ number,
@ -570,5 +581,5 @@ export type CanvasNode = [
]
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequestDeprecated | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | NetworkRequest | WSChannel | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData | CanvasNode
export default Message

View file

@ -1,5 +1,13 @@
import type Message from './messages.gen.js'
import { Timestamp, Metadata, UserID, Type as MType, TabChange, TabData } from './messages.gen.js'
import {
Timestamp,
Metadata,
UserID,
Type as MType,
TabChange,
TabData,
WSChannel,
} from './messages.gen.js'
import {
now,
adjustTimeOrigin,
@ -812,6 +820,26 @@ export default class App {
return this.session.getTabId()
}
/**
* Creates a named hook that expects event name, data string and msg direction (up/down),
* it will skip any message bigger than 5 mb or event name bigger than 255 symbols
* @returns {(msgType: string, data: string, dir: 'up' | 'down') => void}
* */
trackWs(channelName: string): (msgType: string, data: string, dir: 'up' | 'down') => void {
const channel = channelName
return (msgType: string, data: string, dir: 'up' | 'down' = 'down') => {
if (
typeof msgType !== 'string' ||
typeof data !== 'string' ||
data.length > 5 * 1024 * 1024 ||
msgType.length > 255
) {
return
}
this.send(WSChannel('websocket', channel, data, this.timestamp(), dir, msgType))
}
}
stop(stopWorker = true): void {
if (this.activityState !== ActivityState.NotActive) {
try {

View file

@ -817,6 +817,25 @@ export function NetworkRequest(
]
}
export function WSChannel(
chType: string,
channelName: string,
data: string,
timestamp: number,
dir: string,
messageType: string,
): Messages.WSChannel {
return [
Messages.Type.WSChannel,
chType,
channelName,
data,
timestamp,
dir,
messageType,
]
}
export function InputChange(
id: number,
value: string,

View file

@ -234,6 +234,20 @@ export default class API {
return this.app.active()
}
/**
* Creates a named hook that expects event name, data string and msg direction (up/down),
* it will skip any message bigger than 5 mb or event name bigger than 255 symbols
* msg direction is "down" (incoming) by default
*
* @returns {(msgType: string, data: string, dir: 'up' | 'down') => void}
* */
trackWs(channelName: string) {
if (this.app === null) {
return
}
return this.app.trackWs(channelName)
}
start(startOpts?: Partial<StartOptions>): Promise<StartPromiseReturn> {
if (!IN_BROWSER) {
console.error(

View file

@ -258,6 +258,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.string(msg[4]) && this.string(msg[5]) && this.uint(msg[6]) && this.uint(msg[7]) && this.uint(msg[8]) && this.uint(msg[9])
break
case Messages.Type.WSChannel:
return this.string(msg[1]) && this.string(msg[2]) && this.string(msg[3]) && this.uint(msg[4]) && this.string(msg[5]) && this.string(msg[6])
break
case Messages.Type.InputChange:
return this.uint(msg[1]) && this.string(msg[2]) && this.boolean(msg[3]) && this.string(msg[4]) && this.int(msg[5]) && this.int(msg[6])
break