move redux plugin hashing to worker thread, update redux panel look and style

* feat tracker moving redux stuff to worker thread

* feat ui: sync redux messages to action time

* feat ui: starting new redux ui

* fix backend mob gen

* feat tracker moving redux stuff to worker thread

* feat ui: sync redux messages to action time

* feat ui: starting new redux ui

* fix backend mob gen

* styles, third party etc

* rm dead code

* design fixes

* wrapper around old redux stuff

* prettier

* icon sw

* some changes to default style

* some code style fixes
This commit is contained in:
Delirium 2024-04-09 14:47:31 +02:00 committed by GitHub
parent d58a356f1f
commit 5421aedfe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 3336 additions and 3147 deletions

View file

@ -40,7 +40,7 @@ const (
MsgProfiler = 40 MsgProfiler = 40
MsgOTable = 41 MsgOTable = 41
MsgStateAction = 42 MsgStateAction = 42
MsgRedux = 44 MsgReduxDeprecated = 44
MsgVuex = 45 MsgVuex = 45
MsgMobX = 46 MsgMobX = 46
MsgNgRx = 47 MsgNgRx = 47
@ -87,6 +87,7 @@ const (
MsgTabData = 118 MsgTabData = 118
MsgCanvasNode = 119 MsgCanvasNode = 119
MsgTagTrigger = 120 MsgTagTrigger = 120
MsgRedux = 121
MsgIssueEvent = 125 MsgIssueEvent = 125
MsgSessionEnd = 126 MsgSessionEnd = 126
MsgSessionSearch = 127 MsgSessionSearch = 127
@ -1104,14 +1105,14 @@ func (msg *StateAction) TypeID() int {
return 42 return 42
} }
type Redux struct { type ReduxDeprecated struct {
message message
Action string Action string
State string State string
Duration uint64 Duration uint64
} }
func (msg *Redux) Encode() []byte { func (msg *ReduxDeprecated) Encode() []byte {
buf := make([]byte, 31+len(msg.Action)+len(msg.State)) buf := make([]byte, 31+len(msg.Action)+len(msg.State))
buf[0] = 44 buf[0] = 44
p := 1 p := 1
@ -1121,11 +1122,11 @@ func (msg *Redux) Encode() []byte {
return buf[:p] return buf[:p]
} }
func (msg *Redux) Decode() Message { func (msg *ReduxDeprecated) Decode() Message {
return msg return msg
} }
func (msg *Redux) TypeID() int { func (msg *ReduxDeprecated) TypeID() int {
return 44 return 44
} }
@ -2323,6 +2324,33 @@ func (msg *TagTrigger) TypeID() int {
return 120 return 120
} }
type Redux struct {
message
Action string
State string
Duration uint64
ActionTime uint64
}
func (msg *Redux) Encode() []byte {
buf := make([]byte, 41+len(msg.Action)+len(msg.State))
buf[0] = 121
p := 1
p = WriteString(msg.Action, buf, p)
p = WriteString(msg.State, buf, p)
p = WriteUint(msg.Duration, buf, p)
p = WriteUint(msg.ActionTime, buf, p)
return buf[:p]
}
func (msg *Redux) Decode() Message {
return msg
}
func (msg *Redux) TypeID() int {
return 121
}
type IssueEvent struct { type IssueEvent struct {
message message
MessageID uint64 MessageID uint64

View file

@ -639,9 +639,9 @@ func DecodeStateAction(reader BytesReader) (Message, error) {
return msg, err return msg, err
} }
func DecodeRedux(reader BytesReader) (Message, error) { func DecodeReduxDeprecated(reader BytesReader) (Message, error) {
var err error = nil var err error = nil
msg := &Redux{} msg := &ReduxDeprecated{}
if msg.Action, err = reader.ReadString(); err != nil { if msg.Action, err = reader.ReadString(); err != nil {
return nil, err return nil, err
} }
@ -1410,6 +1410,24 @@ func DecodeTagTrigger(reader BytesReader) (Message, error) {
return msg, err return msg, err
} }
func DecodeRedux(reader BytesReader) (Message, error) {
var err error = nil
msg := &Redux{}
if msg.Action, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.State, err = reader.ReadString(); err != nil {
return nil, err
}
if msg.Duration, err = reader.ReadUint(); err != nil {
return nil, err
}
if msg.ActionTime, err = reader.ReadUint(); err != nil {
return nil, err
}
return msg, err
}
func DecodeIssueEvent(reader BytesReader) (Message, error) { func DecodeIssueEvent(reader BytesReader) (Message, error) {
var err error = nil var err error = nil
msg := &IssueEvent{} msg := &IssueEvent{}
@ -1951,7 +1969,7 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
case 42: case 42:
return DecodeStateAction(reader) return DecodeStateAction(reader)
case 44: case 44:
return DecodeRedux(reader) return DecodeReduxDeprecated(reader)
case 45: case 45:
return DecodeVuex(reader) return DecodeVuex(reader)
case 46: case 46:
@ -2044,6 +2062,8 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
return DecodeCanvasNode(reader) return DecodeCanvasNode(reader)
case 120: case 120:
return DecodeTagTrigger(reader) return DecodeTagTrigger(reader)
case 121:
return DecodeRedux(reader)
case 125: case 125:
return DecodeIssueEvent(reader) return DecodeIssueEvent(reader)
case 126: case 126:

View file

@ -8,3 +8,7 @@ ignore:
- "**/*/build/**" - "**/*/build/**"
- "**/*/.test.*" - "**/*/.test.*"
- "**/*/version.ts" - "**/*/version.ts"
review:
poem: false
review_status: false
collapse_walkthrough: true

View file

@ -370,7 +370,7 @@ class StateAction(Message):
self.type = type self.type = type
class Redux(Message): class ReduxDeprecated(Message):
__id__ = 44 __id__ = 44
def __init__(self, action, state, duration): def __init__(self, action, state, duration):
@ -815,6 +815,16 @@ class TagTrigger(Message):
self.tag_id = tag_id self.tag_id = tag_id
class Redux(Message):
__id__ = 121
def __init__(self, action, state, duration, action_time):
self.action = action
self.state = state
self.duration = duration
self.action_time = action_time
class IssueEvent(Message): class IssueEvent(Message):
__id__ = 125 __id__ = 125

View file

@ -548,7 +548,7 @@ cdef class StateAction(PyMessage):
self.type = type self.type = type
cdef class Redux(PyMessage): cdef class ReduxDeprecated(PyMessage):
cdef public int __id__ cdef public int __id__
cdef public str action cdef public str action
cdef public str state cdef public str state
@ -1203,6 +1203,21 @@ cdef class TagTrigger(PyMessage):
self.tag_id = tag_id self.tag_id = tag_id
cdef class Redux(PyMessage):
cdef public int __id__
cdef public str action
cdef public str state
cdef public unsigned long duration
cdef public unsigned long action_time
def __init__(self, str action, str state, unsigned long duration, unsigned long action_time):
self.__id__ = 121
self.action = action
self.state = state
self.duration = duration
self.action_time = action_time
cdef class IssueEvent(PyMessage): cdef class IssueEvent(PyMessage):
cdef public int __id__ cdef public int __id__
cdef public unsigned long message_id cdef public unsigned long message_id

View file

@ -382,7 +382,7 @@ class MessageCodec(Codec):
) )
if message_id == 44: if message_id == 44:
return Redux( return ReduxDeprecated(
action=self.read_string(reader), action=self.read_string(reader),
state=self.read_string(reader), state=self.read_string(reader),
duration=self.read_uint(reader) duration=self.read_uint(reader)
@ -732,6 +732,14 @@ class MessageCodec(Codec):
tag_id=self.read_int(reader) tag_id=self.read_int(reader)
) )
if message_id == 121:
return Redux(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader),
action_time=self.read_uint(reader)
)
if message_id == 125: if message_id == 125:
return IssueEvent( return IssueEvent(
message_id=self.read_uint(reader), message_id=self.read_uint(reader),

View file

@ -480,7 +480,7 @@ cdef class MessageCodec:
) )
if message_id == 44: if message_id == 44:
return Redux( return ReduxDeprecated(
action=self.read_string(reader), action=self.read_string(reader),
state=self.read_string(reader), state=self.read_string(reader),
duration=self.read_uint(reader) duration=self.read_uint(reader)
@ -830,6 +830,14 @@ cdef class MessageCodec:
tag_id=self.read_int(reader) tag_id=self.read_int(reader)
) )
if message_id == 121:
return Redux(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader),
action_time=self.read_uint(reader)
)
if message_id == 125: if message_id == 125:
return IssueEvent( return IssueEvent(
message_id=self.read_uint(reader), message_id=self.read_uint(reader),

View file

@ -1,221 +0,0 @@
import { useStore } from 'App/mstore';
import SummaryBlock from "Components/Session/Player/ReplayPlayer/SummaryBlock";
import React, { useMemo } from 'react';
import { Icon, Tooltip } from 'UI';
import QueueControls from 'App/components/Session_/QueueControls';
import Bookmark from 'Shared/Bookmark';
import SharePopup from 'Shared/SharePopup/SharePopup';
import Issues from 'App/components/Session_/Issues/Issues';
import NotePopup from 'App/components/Session_/components/NotePopup';
import ItemMenu from 'App/components/Session_/components/HeaderMenu';
import { useModal } from 'App/components/Modal';
import BugReportModal from 'App/components/Session_/BugReport/BugReportModal';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import AutoplayToggle from 'Shared/AutoplayToggle';
import { connect } from 'react-redux';
import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs';
import { IFRAME } from 'App/constants/storageKeys';
import cn from 'classnames';
import { Switch } from 'antd';
const localhostWarn = (project: string): string => project + '_localhost_warn';
const disableDevtools = 'or_devtools_uxt_toggle';
function SubHeader(props: any) {
const localhostWarnKey = localhostWarn(props.siteId);
const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1';
const [showWarningModal, setWarning] = React.useState(defaultLocalhostWarn);
const { player, store } = React.useContext(PlayerContext);
const { width, height, endTime, location: currentLocation = 'loading...' } = store.get();
const hasIframe = localStorage.getItem(IFRAME) === 'true';
const { uxtestingStore } = useStore();
const enabledIntegration = useMemo(() => {
const { integrations } = props;
if (!integrations || !integrations.size) {
return false;
}
return integrations.some((i: Record<string, any>) => i.token);
}, [props.integrations]);
const { showModal, hideModal } = useModal();
const location =
currentLocation && currentLocation.length > 70
? `${currentLocation.slice(0, 25)}...${currentLocation.slice(-40)}`
: currentLocation;
const showReportModal = () => {
const { tabStates, currentTab } = store.get();
const resourceList = tabStates[currentTab]?.resourceList || [];
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
const eventsList = tabStates[currentTab]?.eventList || [];
const graphqlList = tabStates[currentTab]?.graphqlList || [];
const fetchList = tabStates[currentTab]?.fetchList || [];
const mappedResourceList = resourceList
.filter((r) => r.isRed || r.isYellow)
// @ts-ignore
.concat(fetchList.filter((i) => parseInt(i.status) >= 400))
// @ts-ignore
.concat(graphqlList.filter((i) => parseInt(i.status) >= 400));
player.pause();
const xrayProps = {
currentLocation: currentLocation,
resourceList: mappedResourceList,
exceptionsList: exceptionsList,
eventsList: eventsList,
endTime: endTime,
};
showModal(
// @ts-ignore
<BugReportModal width={width} height={height} xrayProps={xrayProps} hideModal={hideModal} />,
{ right: true, width: 620 }
);
};
const showSummary = () => {
player.pause();
showModal(<SummaryBlock sessionId={props.sessionId} />, { right: true, width: 330 })
}
const showWarning =
location && /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(location) && showWarningModal;
const closeWarning = () => {
localStorage.setItem(localhostWarnKey, '1');
setWarning(false);
};
const toggleDevtools = (enabled: boolean): void => {
localStorage.setItem(disableDevtools, enabled ? '0' : '1');
uxtestingStore.setHideDevtools(!enabled);
};
const additionalMenu = [
{
key: 1,
component: <AutoplayToggle />,
},
{
key: 2,
component: <Bookmark noMargin sessionId={props.sessionId} />,
},
{
key: 3,
component: (
<div onClick={showReportModal} className={'flex items-center gap-2 p-3 text-black'}>
<Icon name={'file-pdf'} size={16} />
<div>Create Bug Report</div>
</div>
),
},
{
key: 4,
autoclose: true,
component: (
<SharePopup
entity="sessions"
id={props.sessionId}
showCopyLink={true}
trigger={
<div className={'flex items-center gap-2 p-3 text-black'}>
<Icon name={'share-alt'} size={16} />
<div>Share</div>
</div>
}
/>
),
},
];
if (enabledIntegration) {
additionalMenu.push({
key: 5,
component: <Issues isInline={true} sessionId={props.sessionId} />,
});
}
return (
<>
<div
className="w-full px-4 flex items-center border-b relative"
style={{
background: uxtestingStore.isUxt() ? (props.live ? '#F6FFED' : '#EBF4F5') : undefined,
}}
>
{showWarning ? (
<div
className="px-3 py-1 border border-gray-lighter drop-shadow-md rounded bg-active-blue flex items-center justify-between"
style={{
zIndex: 999,
position: 'absolute',
left: '50%',
bottom: '-24px',
transform: 'translate(-50%, 0)',
fontWeight: 500,
}}
>
Some assets may load incorrectly on localhost.
<a
href="https://docs.openreplay.com/en/troubleshooting/session-recordings/#testing-in-localhost"
target="_blank"
rel="noreferrer"
className="link ml-1"
>
Learn More
</a>
<div className="py-1 ml-3 cursor-pointer" onClick={closeWarning}>
<Icon name="close" size={16} color="black" />
</div>
</div>
) : null}
<SessionTabs />
<div
className={cn(
'ml-auto text-sm flex items-center color-gray-medium gap-2',
hasIframe ? 'opacity-50 pointer-events-none' : ''
)}
style={{ width: 'max-content' }}
>
<SummaryButton onClick={showSummary} />
<NotePopup />
<ItemMenu items={additionalMenu} useSc />
{uxtestingStore.isUxt() ? (
<Switch
checkedChildren={'DevTools'}
unCheckedChildren={'DevTools'}
onChange={toggleDevtools}
defaultChecked={!uxtestingStore.hideDevtools}
/>
) : (
<div>
{/* @ts-ignore */}
<QueueControls />
</div>
)}
</div>
</div>
{location && (
<div className={'w-full bg-white border-b border-gray-lighter'}>
<div className="flex w-fit items-center cursor-pointer color-gray-medium text-sm p-1">
<Icon size="20" name="event/link" className="mr-1" />
<Tooltip title="Open in new tab" delay={0}>
<a href={currentLocation} target="_blank">
{location}
</a>
</Tooltip>
</div>
</div>
)}
</>
);
}
export default connect((state: Record<string, any>) => ({
siteId: state.getIn(['site', 'siteId']),
integrations: state.getIn(['issues', 'list']),
modules: state.getIn(['user', 'account', 'modules']) || [],
}))(observer(SubHeader));

View file

@ -3,7 +3,6 @@ import cn from 'classnames';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Player from './PlayerInst'; import Player from './PlayerInst';
import SubHeader from 'Components/Session_/Subheader'; import SubHeader from 'Components/Session_/Subheader';
import AiSubheader from 'Components/Session/Player/ReplayPlayer/AiSubheader';
import styles from 'Components/Session_/playerBlock.module.css'; import styles from 'Components/Session_/playerBlock.module.css';
interface IProps { interface IProps {

View file

@ -33,11 +33,24 @@ function WebPlayer(props: any) {
const [activeTab, setActiveTab] = useState(''); const [activeTab, setActiveTab] = useState('');
const [noteItem, setNoteItem] = useState<Note | undefined>(undefined); const [noteItem, setNoteItem] = useState<Note | undefined>(undefined);
const [visuallyAdjusted, setAdjusted] = useState(false); const [visuallyAdjusted, setAdjusted] = useState(false);
const [windowActive, setWindowActive] = useState(!document.hidden);
// @ts-ignore // @ts-ignore
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue); const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
const params: { sessionId: string } = useParams(); const params: { sessionId: string } = useParams();
const [fullView, setFullView] = useState(false); const [fullView, setFullView] = useState(false);
React.useEffect(() => {
if (windowActive) {
const handleActivation = () => {
if (!document.hidden) {
setWindowActive(true);
document.removeEventListener('visibilitychange', handleActivation);
}
}
document.addEventListener('visibilitychange', handleActivation);
}
}, [])
useEffect(() => { useEffect(() => {
playerInst = undefined; playerInst = undefined;
if (!session.sessionId || contextValue.player !== undefined) return; if (!session.sessionId || contextValue.player !== undefined) return;
@ -79,9 +92,8 @@ function WebPlayer(props: any) {
React.useEffect(() => { React.useEffect(() => {
if (noteItem !== undefined) { if (noteItem !== undefined) {
contextValue.player.pause(); contextValue.player.pause();
} } else {
if (activeTab === '' && ready && contextValue.player && windowActive) {
if (activeTab === '' && !noteItem !== undefined && messagesProcessed && contextValue.player) {
const jumpToTime = props.query.get('jumpto'); const jumpToTime = props.query.get('jumpto');
const shouldAdjustOffset = visualOffset !== 0 && !visuallyAdjusted; const shouldAdjustOffset = visualOffset !== 0 && !visuallyAdjusted;
@ -96,7 +108,16 @@ function WebPlayer(props: any) {
contextValue.player.play(); contextValue.player.play();
} }
}, [activeTab, noteItem, visualOffset, messagesProcessed]); }
}, [activeTab, noteItem, visualOffset, ready, windowActive]);
useEffect(() => {
if (cssLoading) {
contextValue.player?.pause();
} else if (ready) {
contextValue.player?.play();
}
}, [cssLoading, ready])
React.useEffect(() => { React.useEffect(() => {
if (activeTab === 'Click Map') { if (activeTab === 'Click Map') {

View file

@ -2,12 +2,18 @@ import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import stl from './bottomBlock.module.css'; import stl from './bottomBlock.module.css';
interface Props {
children?: React.ReactNode;
className?: string;
additionalHeight?: number;
}
const BottomBlock = ({ const BottomBlock = ({
children = null, children = null,
className = '', className = '',
additionalHeight = 0, additionalHeight = 0,
...props ...props
}) => ( }: Props) => (
<div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } > <div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } >
{ children } { children }
</div> </div>

View file

@ -2,7 +2,9 @@ import BottomBlock from './BottomBlock';
import Header from './Header'; import Header from './Header';
import Content from './Content'; import Content from './Content';
// @ts-ignore
BottomBlock.Header = Header; BottomBlock.Header = Header;
// @ts-ignore
BottomBlock.Content = Content; BottomBlock.Content = Content;
export default BottomBlock; export default BottomBlock as typeof BottomBlock & { Header: typeof Header, Content: typeof Content }

View file

@ -27,12 +27,29 @@ function DiffRow({ diff, path }: Props) {
const [shortenOldVal, setShortenOldVal] = React.useState(true); const [shortenOldVal, setShortenOldVal] = React.useState(true);
const [shortenNewVal, setShortenNewVal] = React.useState(true); const [shortenNewVal, setShortenNewVal] = React.useState(true);
const oldValue = diff.item // Adjust to handle the difference based on its type
? JSON.stringify(diff.item.lhs, getCircularReplacer(), 1) let oldValue;
: JSON.stringify(diff.lhs, getCircularReplacer(), 1); let newValue;
const newValue = diff.item
? JSON.stringify(diff.item.rhs, getCircularReplacer(), 1) switch (diff.type) {
: JSON.stringify(diff.rhs, getCircularReplacer(), 1); case 'CHANGE':
oldValue = JSON.stringify(diff.oldValue, null, 2);
newValue = JSON.stringify(diff.value, null, 2);
break;
case 'CREATE':
oldValue = 'undefined'; // No oldValue in CREATE type
newValue = JSON.stringify(diff.value, null, 2);
break;
case 'REMOVE':
oldValue = JSON.stringify(diff.oldValue, null, 2);
newValue = 'undefined'; // No newValue in REMOVE type
break;
default:
// Handle unexpected types, though not expected in current microdiff version
console.error('Unexpected diff type:', diff.type);
oldValue = 'error';
newValue = 'error';
}
const length = path.length; const length = path.length;
const diffLengths = [oldValue?.length || 0, newValue?.length || 0]; const diffLengths = [oldValue?.length || 0, newValue?.length || 0];

View file

@ -0,0 +1,137 @@
import { selectStorageListNow } from 'Player';
import { GitCommitVertical } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { PlayerContext } from 'App/components/Session/playerContext';
import { durationFromMs } from 'App/date';
import { Icon, JSONTree } from 'UI';
import JumpButton from "../../shared/DevTools/JumpButton";
import BottomBlock from '../BottomBlock/index';
interface ListItem {
action: { type: string; payload?: any };
actionTime: number;
duration: number;
state: Record<string, any>;
tabId: string;
time: number;
}
function ReduxViewer() {
const { player, store } = React.useContext(PlayerContext);
const { tabStates, currentTab, sessionStart } = store.get();
const state = tabStates[currentTab] || {};
const listNow = selectStorageListNow(state) || [];
const decodeMessage = (msg: any) => {
const decoded = {};
const pureMSG = { ...msg };
const keys = ['state', 'action'];
try {
keys.forEach((key) => {
if (pureMSG[key]) {
// @ts-ignore TODO: types for decoder
decoded[key] = player.decodeMessage(pureMSG[key]);
}
});
} catch (e) {
console.error('Error on state message decoding: ', e, pureMSG);
return null;
}
return { ...pureMSG, ...decoded };
};
const decodedList = React.useMemo(() => {
return listNow.map((msg) => {
return decodeMessage(msg) as ListItem;
});
}, [listNow.length]);
return (
<BottomBlock>
<>
<BottomBlock.Header>
<h3
style={{ width: '25%', marginRight: 20 }}
className="font-semibold color-gray-medium"
>
Redux
</h3>
</BottomBlock.Header>
<BottomBlock.Content className={'overflow-y-auto'}>
{decodedList.map((msg, i) => (
<StateEvent
msg={msg}
key={i}
sessionStart={sessionStart}
prevMsg={decodedList[i - 1]}
onJump={player.jump}
/>
))}
</BottomBlock.Content>
</>
</BottomBlock>
);
}
function StateEvent({
msg,
sessionStart,
prevMsg,
onJump,
}: {
msg: ListItem;
sessionStart: number;
onJump: (time: number) => void;
prevMsg?: ListItem;
}) {
const [isOpen, setOpen] = React.useState(false);
return (
<div
className={
'w-full py-1 px-4 border-b border-gray-lightest flex flex-col hover:bg-active-blue group relative'
}
style={{ fontFamily: 'Menlo, Monaco, Consolas', letterSpacing: '-0.025rem' }}
>
<div
className={'w-full gap-2 flex items-center cursor-pointer h-full'}
onClick={() => setOpen(!isOpen)}
>
<Icon name={isOpen ? 'chevron-up' : 'chevron-down'} />
<GitCommitVertical strokeWidth={1} />
<div className={'font-medium'}>{msg.action.type ?? 'action'}</div>
<div className={'text-gray-medium'}>
@ {durationFromMs(msg.actionTime - sessionStart)} (in{' '}
{durationFromMs(msg.duration)})
</div>
</div>
{isOpen ? (
<div className={'py-4 flex flex-col gap-2'} style={{ paddingLeft: '3.7rem' }}>
{prevMsg ? (
<div className={'flex items-start gap-2'}>
<div className={'text-gray-darkest tracking-tight'}>
prev state
</div>
<JSONTree src={prevMsg.state} collapsed />
</div>
) : null}
<div className={'flex items-start gap-2'}>
<div className={'text-yellow2'}>action</div>
<JSONTree src={msg.action} collapsed />
</div>
<div className={'flex items-start gap-2'}>
<div className={'text-tealx'}>next state</div>
<JSONTree src={msg.state} collapsed />
</div>
</div>
) : null}
<JumpButton onClick={() => onJump(msg.time)} />
</div>
);
}
export default observer(ReduxViewer);

View file

@ -1,367 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { hideHint } from 'Duck/components/player';
import {
connectPlayer,
selectStorageType,
STORAGE_TYPES,
selectStorageListNow,
selectStorageList,
} from 'Player';
import { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date';
import { diff } from 'deep-diff';
import { jump } from 'Player';
import BottomBlock from '../BottomBlock/index';
import DiffRow from './DiffRow';
import stl from './storage.module.css';
import { List, CellMeasurer, CellMeasurerCache, AutoSizer } from 'react-virtualized';
const ROW_HEIGHT = 90;
function getActionsName(type) {
switch (type) {
case STORAGE_TYPES.MOBX:
case STORAGE_TYPES.VUEX:
return 'MUTATIONS';
default:
return 'ACTIONS';
}
}
@connectPlayer((state) => ({
type: selectStorageType(state),
list: selectStorageList(state),
listNow: selectStorageListNow(state),
}))
@connect(
(state) => ({
hintIsHidden: state.getIn(['components', 'player', 'hiddenHints', 'storage']),
}),
{
hideHint,
}
)
export default class Storage extends React.PureComponent {
constructor(props) {
super(props);
this.lastBtnRef = React.createRef();
this._list = React.createRef();
this.cache = new CellMeasurerCache({
fixedWidth: true,
keyMapper: (index) => this.props.listNow[index],
});
this._rowRenderer = this._rowRenderer.bind(this);
}
focusNextButton() {
if (this.lastBtnRef.current) {
this.lastBtnRef.current.focus();
}
}
componentDidMount() {
this.focusNextButton();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.listNow.length !== this.props.listNow.length) {
this.focusNextButton();
/** possible performance gain, but does not work with dynamic list insertion for some reason
* getting NaN offsets, maybe I detect changed rows wrongly
*/
// const newRows = this.props.listNow.filter(evt => prevProps.listNow.indexOf(evt._index) < 0);
// if (newRows.length > 0) {
// const newRowsIndexes = newRows.map(r => this.props.listNow.indexOf(r))
// newRowsIndexes.forEach(ind => this.cache.clear(ind))
// this._list.recomputeRowHeights(newRowsIndexes)
// }
}
}
renderDiff(item, prevItem) {
if (!prevItem) {
// we don't have state before first action
return <div style={{ flex: 3 }} className="p-1" />;
}
const stateDiff = diff(prevItem.state, item.state);
if (!stateDiff) {
return (
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
No diff
</div>
);
}
return (
<div
style={{ flex: 3, maxHeight: ROW_HEIGHT, overflowY: 'scroll' }}
className="flex flex-col p-1 font-mono"
>
{stateDiff.map((d, i) => this.renderDiffs(d, i))}
</div>
);
}
renderDiffs(diff, i) {
const path = this.createPath(diff);
return (
<React.Fragment key={i}>
<DiffRow shades={this.pathShades} path={path} diff={diff} />
</React.Fragment>
);
}
createPath = (diff) => {
let path = [];
if (diff.path) {
path = path.concat(diff.path);
}
if (typeof diff.index !== 'undefined') {
path.push(diff.index);
}
const pathStr = path.length ? path.join('.') : '';
return pathStr;
};
ensureString(actionType) {
if (typeof actionType === 'string') return actionType;
return 'UNKNOWN';
}
goNext = () => {
const { list, listNow } = this.props;
jump(list[listNow.length].time, list[listNow.length]._index);
};
renderTab() {
const { listNow } = this.props;
if (listNow.length === 0) {
return 'Not initialized'; //?
}
return <JSONTree collapsed={2} src={listNow[listNow.length - 1].state} />;
}
renderItem(item, i, prevItem, style) {
const { type } = this.props;
let src;
let name;
switch (type) {
case STORAGE_TYPES.REDUX:
case STORAGE_TYPES.NGRX:
src = item.action;
name = src && src.type;
break;
case STORAGE_TYPES.VUEX:
src = item.mutation;
name = src && src.type;
break;
case STORAGE_TYPES.MOBX:
src = item.payload;
name = `@${item.type} ${src && src.type}`;
break;
case STORAGE_TYPES.ZUSTAND:
src = null;
name = item.mutation.join('');
}
return (
<div
style={{ ...style, height: ROW_HEIGHT }}
className="flex justify-between items-start border-b"
key={`store-${i}`}
>
{src === null ? (
<div className="font-mono" style={{ flex: 2, marginLeft: '26.5%' }}>
{name}
</div>
) : (
<>
{this.renderDiff(item, prevItem, i)}
<div
style={{ flex: 2, maxHeight: ROW_HEIGHT, overflowY: 'scroll', overflowX: 'scroll' }}
className="flex pl-10 pt-2"
>
<JSONTree
name={this.ensureString(name)}
src={src}
collapsed
collapseStringsAfterLength={7}
/>
</div>
</>
)}
<div
style={{ flex: 1 }}
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
>
{typeof item.duration === 'number' && (
<div className="font-size-12 color-gray-medium">{formatMs(item.duration)}</div>
)}
<div className="w-12">
{i + 1 < this.props.listNow.length && (
<button className={stl.button} onClick={() => jump(item.time, item._index)}>
{'JUMP'}
</button>
)}
{i + 1 === this.props.listNow.length && i + 1 < this.props.list.length && (
<button className={stl.button} ref={this.lastBtnRef} onClick={this.goNext}>
{'NEXT'}
</button>
)}
</div>
</div>
</div>
);
}
_rowRenderer({ index, parent, key, style }) {
const { listNow } = this.props;
if (!listNow[index]) return console.warn(index, listNow);
return (
<CellMeasurer cache={this.cache} columnIndex={0} key={key} rowIndex={index} parent={parent}>
{this.renderItem(listNow[index], index, index > 0 ? listNow[index - 1] : undefined, style)}
</CellMeasurer>
);
}
render() {
const { type, list, listNow, hintIsHidden } = this.props;
const showStore = type !== STORAGE_TYPES.MOBX;
return (
<BottomBlock>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
{showStore && (
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
{'STATE'}
</h3>
)}
{type !== STORAGE_TYPES.ZUSTAND ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
</div>
)}
</BottomBlock.Header>
<BottomBlock.Content className="flex">
<NoContent
title="Nothing to display yet"
subtext={
!hintIsHidden ? (
<>
{
'Inspect your application state while youre replaying your users sessions. OpenReplay supports '
}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/redux"
target="_blank"
>
Redux
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/vuex"
target="_blank"
>
VueX
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/pinia"
target="_blank"
>
Pinia
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/zustand"
target="_blank"
>
Zustand
</a>
{', '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/mobx"
target="_blank"
>
MobX
</a>
{' and '}
<a
className="underline color-teal"
href="https://docs.openreplay.com/plugins/ngrx"
target="_blank"
>
NgRx
</a>
.
<br />
<br />
<button className="color-teal" onClick={() => this.props.hideHint('storage')}>
Got It!
</button>
</>
) : null
}
size="small"
show={listNow.length === 0}
>
{showStore && (
<div className="ph-10 scroll-y" style={{ width: '25%' }}>
{listNow.length === 0 ? (
<div className="color-gray-light font-size-16 mt-20 text-center">
{'Empty state.'}
</div>
) : (
this.renderTab()
)}
</div>
)}
<div className="flex" style={{ width: showStore ? '75%' : '100%' }}>
<AutoSizer>
{({ height, width }) => (
<List
ref={(element) => {
this._list = element;
}}
deferredMeasurementCache={this.cache}
overscanRowCount={1}
rowCount={Math.ceil(parseInt(this.props.listNow.length) || 1)}
rowHeight={ROW_HEIGHT}
rowRenderer={this._rowRenderer}
width={width}
height={height}
/>
)}
</AutoSizer>
</div>
</NoContent>
</BottomBlock.Content>
</BottomBlock>
);
}
}

View file

@ -5,8 +5,7 @@ import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { JSONTree, NoContent, Tooltip } from 'UI'; import { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date'; import { formatMs } from 'App/date';
// @ts-ignore import diff from 'microdiff'
import { diff } from 'deep-diff';
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player'; import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
import Autoscroll from '../Autoscroll'; import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock/index'; import BottomBlock from '../BottomBlock/index';
@ -14,6 +13,7 @@ import DiffRow from './DiffRow';
import cn from 'classnames'; import cn from 'classnames';
import stl from './storage.module.css'; import stl from './storage.module.css';
import logger from "App/logger"; import logger from "App/logger";
import ReduxViewer from './ReduxViewer'
function getActionsName(type: string) { function getActionsName(type: string) {
switch (type) { switch (type) {
@ -129,7 +129,7 @@ function Storage(props: Props) {
}; };
const renderDiffs = (diff: Record<string, any>, i: number) => { const renderDiffs = (diff: Record<string, any>, i: number) => {
const path = createPath(diff); const path = diff.path.join('.')
return ( return (
<React.Fragment key={i}> <React.Fragment key={i}>
<DiffRow path={path} diff={diff} /> <DiffRow path={path} diff={diff} />
@ -137,20 +137,6 @@ function Storage(props: Props) {
); );
}; };
const createPath = (diff: Record<string, any>) => {
let path: string[] = [];
if (diff.path) {
path = path.concat(diff.path);
}
if (typeof diff.index !== 'undefined') {
path.push(diff.index);
}
const pathStr = path.length ? path.join('.') : '';
return pathStr;
};
const ensureString = (actionType: string) => { const ensureString = (actionType: string) => {
if (typeof actionType === 'string') return actionType; if (typeof actionType === 'string') return actionType;
return 'UNKNOWN'; return 'UNKNOWN';
@ -239,8 +225,13 @@ function Storage(props: Props) {
const { hintIsHidden } = props; const { hintIsHidden } = props;
if (type === STORAGE_TYPES.REDUX) {
return <ReduxViewer />
}
return ( return (
<BottomBlock> <BottomBlock>
{/*@ts-ignore*/}
<>
<BottomBlock.Header> <BottomBlock.Header>
{list.length > 0 && ( {list.length > 0 && (
<div className="flex w-full"> <div className="flex w-full">
@ -348,6 +339,7 @@ function Storage(props: Props) {
</div> </div>
</NoContent> </NoContent>
</BottomBlock.Content> </BottomBlock.Content>
</>
</BottomBlock> </BottomBlock>
); );
} }

View file

@ -8,10 +8,10 @@ interface Props {
function JumpButton(props: Props) { function JumpButton(props: Props) {
const { tooltip } = props; const { tooltip } = props;
return ( return (
<div className="absolute right-0 top-0 bottom-0 my-auto flex items-center"> <div className="absolute right-2 top-0 bottom-0 my-auto flex items-center">
<Tooltip title={tooltip} disabled={!tooltip}> <Tooltip title={tooltip} disabled={!tooltip}>
<div <div
className="mr-2 border cursor-pointer invisible group-hover:visible rounded-lg bg-active-blue text-xs flex items-center px-2 py-1 color-teal hover:shadow h-6" className="mr-2 border cursor-pointer invisible group-hover:visible rounded bg-white text-xs flex items-center px-2 py-1 color-teal hover:shadow h-6"
onClick={(e: any) => { onClick={(e: any) => {
e.stopPropagation(); e.stopPropagation();
props.onClick(); props.onClick();

View file

@ -1,24 +0,0 @@
import React from 'react';
import JSONTree from 'react-json-view';
function updateObjectLink(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) return [ ...obj ];
return { ...obj }
}
export default ({ src, ...props }) => (
<JSONTree
name={ false }
collapsed={ 1 }
enableClipboard={ false }
iconStyle="circle"
indentWidth={ 1 }
sortKeys
displayDataTypes={ false }
displayObjectSize={ false }
src={ updateObjectLink(src) }
iconStle="triangle"
{ ...props }
/>
);

View file

@ -0,0 +1,62 @@
import React from 'react';
import JsonView from 'react18-json-view';
function updateObjectLink(obj: any): any {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) return [...obj];
return { ...obj };
}
interface Props {
src: any;
className?: string;
dark?: boolean;
theme?:
| 'default'
| 'a11y'
| 'github'
| 'vscode'
| 'atom'
| 'winter-is-coming';
enableClipboard?: boolean;
matchesURL?: boolean;
displaySize?: boolean | number | 'collapsed';
collapseStringsAfterLength?: number;
collapsed?: number | boolean;
editable?: boolean;
onAdd?: (params: {
indexOrName: string | number;
depth: number;
src: any;
parentType: 'object' | 'array';
}) => void;
onDelete?: (params: {
value: any;
indexOrName: string | number;
depth: number;
src: any;
parentType: 'object' | 'array';
}) => void;
onEdit?: (params: {
newValue: any;
oldValue: any;
depth: number;
src: any;
indexOrName: string | number;
parentType: 'object' | 'array';
}) => void;
}
function JSONTree(props: Props) {
return (
<JsonView
src={updateObjectLink(props.src)}
collapsed={1}
displaySize={'collapsed'}
enableClipboard={true}
{...props}
/>
);
}
export default JSONTree;

View file

@ -0,0 +1,148 @@
.json-view {
display: block;
color: #4d4d4d;
text-align: left;
--json-property: #009033;
--json-index: #676dff;
--json-number: #676dff;
--json-string: #b2762e;
--json-boolean: #dc155e;
--json-null: #dc155e;
}
.json-view .json-view--property {
color: var(--json-property);
}
.json-view .json-view--index {
color: var(--json-index);
}
.json-view .json-view--number {
color: var(--json-number);
}
.json-view .json-view--string {
color: var(--json-string);
}
.json-view .json-view--boolean {
color: var(--json-boolean);
}
.json-view .json-view--null {
color: var(--json-null);
}
.json-view .jv-indent {
padding-left: 1em;
}
.json-view .jv-chevron {
display: inline-block;
vertical-align: -20%;
cursor: pointer;
opacity: 0.4;
width: 1em;
height: 1em;
}
:is(.json-view .jv-chevron:hover, .json-view .jv-size:hover + .jv-chevron) {
opacity: 0.8;
}
.json-view .jv-size {
cursor: pointer;
opacity: 0.4;
font-size: 0.875em;
font-style: italic;
margin-left: 0.5em;
vertical-align: -5%;
line-height: 1;
}
.json-view :is(.json-view--copy, .json-view--edit),
.json-view .json-view--link svg {
display: none;
width: 1em;
height: 1em;
margin-left: 0.25em;
cursor: pointer;
}
.json-view .json-view--input {
width: 120px;
margin-left: 0.25em;
border-radius: 4px;
border: 1px solid currentColor;
padding: 0px 4px;
font-size: 87.5%;
line-height: 1.25;
background: transparent;
}
.json-view .json-view--deleting {
outline: 1px solid #da0000;
background-color: #da000011;
text-decoration-line: line-through;
}
:is(.json-view:hover, .json-view--pair:hover) > :is(.json-view--copy, .json-view--edit),
:is(.json-view:hover, .json-view--pair:hover) > .json-view--link svg {
display: inline-block;
}
.json-view .jv-button {
background: transparent;
outline: none;
border: none;
cursor: pointer;
color: inherit;
}
.json-view .cursor-pointer {
cursor: pointer;
}
.json-view svg {
vertical-align: -10%;
}
.jv-size-chevron ~ svg {
vertical-align: -16%;
}
/* Themes */
.json-view_a11y {
color: #545454;
--json-property: #aa5d00;
--json-index: #007299;
--json-number: #007299;
--json-string: #008000;
--json-boolean: #d91e18;
--json-null: #d91e18;
}
.json-view_github {
color: #005cc5;
--json-property: #005cc5;
--json-index: #005cc5;
--json-number: #005cc5;
--json-string: #032f62;
--json-boolean: #005cc5;
--json-null: #005cc5;
}
.json-view_vscode {
color: #005cc5;
--json-property: #0451a5;
--json-index: #0000ff;
--json-number: #0000ff;
--json-string: #a31515;
--json-boolean: #0000ff;
--json-null: #0000ff;
}
.json-view_atom {
color: #383a42;
--json-property: #e45649;
--json-index: #986801;
--json-number: #986801;
--json-string: #50a14f;
--json-boolean: #0184bc;
--json-null: #0184bc;
}
.json-view_winter-is-coming {
color: #0431fa;
--json-property: #3a9685;
--json-index: #ae408b;
--json-number: #ae408b;
--json-string: #8123a9;
--json-boolean: #0184bc;
--json-null: #0184bc;
}

View file

@ -1,14 +1,21 @@
import type { Store, SessionFilesInfo, PlayerMsg } from 'Player'; import type { PlayerMsg, SessionFilesInfo, Store } from 'Player';
import { decryptSessionBytes } from './network/crypto';
import MFileReader from './messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools, requestTarball } from './network/loadFiles';
import logger from 'App/logger';
import unpack from 'Player/common/unpack';
import unpackTar from 'Player/common/tarball'; import unpackTar from 'Player/common/tarball';
import MessageManager from 'Player/web/MessageManager'; import unpack from 'Player/common/unpack';
import IOSMessageManager from 'Player/mobile/IOSMessageManager'; import IOSMessageManager from 'Player/mobile/IOSMessageManager';
import MessageManager from 'Player/web/MessageManager';
import { MType } from 'Player/web/messages'; import { MType } from 'Player/web/messages';
import logger from 'App/logger';
import MFileReader from './messages/MFileReader';
import { decryptSessionBytes } from './network/crypto';
import {
loadFiles,
requestEFSDevtools,
requestEFSDom,
requestTarball,
} from './network/loadFiles';
interface State { interface State {
firstFileLoading: boolean; firstFileLoading: boolean;
domLoading: boolean; domLoading: boolean;
@ -41,7 +48,10 @@ export default class MessageLoader {
shouldDecrypt && this.session.fileKey shouldDecrypt && this.session.fileKey
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey!) ? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey!)
: (b: Uint8Array) => Promise.resolve(b); : (b: Uint8Array) => Promise.resolve(b);
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt); const fileReader = new MFileReader(
new Uint8Array(),
this.session.startedAt
);
let fileNum = 0; let fileNum = 0;
return async (b: Uint8Array) => { return async (b: Uint8Array) => {
try { try {
@ -65,6 +75,14 @@ export default class MessageLoader {
let artificialStartTime = Infinity; let artificialStartTime = Infinity;
let startTimeSet = false; let startTimeSet = false;
msgs.forEach((msg) => { msgs.forEach((msg) => {
if (msg.tp === MType.Redux || msg.tp === MType.ReduxDeprecated) {
if ('actionTime' in msg && msg.actionTime) {
msg.time = msg.actionTime - this.session.startedAt;
} else {
// @ts-ignore
Object.assign(msg, { actionTime: msg.time + this.session.startedAt });
}
}
if ( if (
msg.tp === MType.CreateDocument && msg.tp === MType.CreateDocument &&
msg.time !== undefined && msg.time !== undefined &&
@ -96,8 +114,10 @@ export default class MessageLoader {
const sortedMsgs = msgs.sort((m1, m2) => { const sortedMsgs = msgs.sort((m1, m2) => {
if (m1.time !== m2.time) return m1.time - m2.time; if (m1.time !== m2.time) return m1.time - m2.time;
if (m1.tp === MType.CreateDocument && m2.tp !== MType.CreateDocument) return -1; if (m1.tp === MType.CreateDocument && m2.tp !== MType.CreateDocument)
if (m1.tp !== MType.CreateDocument && m2.tp === MType.CreateDocument) return 1; return -1;
if (m1.tp !== MType.CreateDocument && m2.tp === MType.CreateDocument)
return 1;
const m1IsDOM = DOMMessages.includes(m1.tp); const m1IsDOM = DOMMessages.includes(m1.tp);
const m2IsDOM = DOMMessages.includes(m2.tp); const m2IsDOM = DOMMessages.includes(m2.tp);
@ -189,7 +209,10 @@ export default class MessageLoader {
try { try {
await this.loadEFSMobs(); await this.loadEFSMobs();
} catch (unprocessedLoadError) { } catch (unprocessedLoadError) {
this.messageManager.onFileReadFailed(sessionLoadError, unprocessedLoadError); this.messageManager.onFileReadFailed(
sessionLoadError,
unprocessedLoadError
);
} }
} finally { } finally {
this.store.update({ domLoading: false, devtoolsLoading: false }); this.store.update({ domLoading: false, devtoolsLoading: false });
@ -201,15 +224,21 @@ export default class MessageLoader {
this.session.domURL && this.session.domURL.length > 0 this.session.domURL && this.session.domURL.length > 0
? { ? {
mobUrls: this.session.domURL, mobUrls: this.session.domURL,
parser: () => this.createNewParser(true, this.processMessages, 'dom'), parser: () =>
this.createNewParser(true, this.processMessages, 'dom'),
} }
: { : {
mobUrls: this.session.mobsUrl, mobUrls: this.session.mobsUrl,
parser: () => this.createNewParser(false, this.processMessages, 'dom'), parser: () =>
this.createNewParser(false, this.processMessages, 'dom'),
}; };
const parser = loadMethod.parser(); const parser = loadMethod.parser();
const devtoolsParser = this.createNewParser(true, this.processMessages, 'devtools'); const devtoolsParser = this.createNewParser(
true,
this.processMessages,
'devtools'
);
/** /**
* to speed up time to replay * to speed up time to replay
@ -220,7 +249,10 @@ export default class MessageLoader {
* */ * */
await loadFiles([loadMethod.mobUrls[0]], parser); await loadFiles([loadMethod.mobUrls[0]], parser);
this.messageManager.onFileReadFinally(); this.messageManager.onFileReadFinally();
const restDomFilesPromise = this.loadDomFiles([...loadMethod.mobUrls.slice(1)], parser); const restDomFilesPromise = this.loadDomFiles(
[...loadMethod.mobUrls.slice(1)],
parser
);
const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser); const restDevtoolsFilesPromise = this.loadDevtools(devtoolsParser);
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]); await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
@ -236,8 +268,16 @@ export default class MessageLoader {
efsDomFilePromise, efsDomFilePromise,
efsDevtoolsFilePromise, efsDevtoolsFilePromise,
]); ]);
const domParser = this.createNewParser(false, this.processMessages, 'domEFS'); const domParser = this.createNewParser(
const devtoolsParser = this.createNewParser(false, this.processMessages, 'devtoolsEFS'); false,
this.processMessages,
'domEFS'
);
const devtoolsParser = this.createNewParser(
false,
this.processMessages,
'devtoolsEFS'
);
const parseDomPromise: Promise<any> = const parseDomPromise: Promise<any> =
domData.status === 'fulfilled' domData.status === 'fulfilled'
? domParser(domData.value) ? domParser(domData.value)

View file

@ -236,6 +236,7 @@ export default class TabSessionManager {
case MType.WsChannel: case MType.WsChannel:
this.lists.lists.websocket.insert(msg); this.lists.lists.websocket.insert(msg);
break; break;
case MType.ReduxDeprecated:
case MType.Redux: case MType.Redux:
this.lists.lists.redux.append(msg); this.lists.lists.redux.append(msg);
break; break;

View file

@ -304,7 +304,7 @@ export default class RawMessageReader extends PrimitiveReader {
const state = this.readString(); if (state === null) { return resetPointer() } const state = this.readString(); if (state === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() } const duration = this.readUint(); if (duration === null) { return resetPointer() }
return { return {
tp: MType.Redux, tp: MType.ReduxDeprecated,
action, action,
state, state,
duration, duration,
@ -749,6 +749,20 @@ export default class RawMessageReader extends PrimitiveReader {
}; };
} }
case 121: {
const action = this.readString(); if (action === null) { return resetPointer() }
const state = this.readString(); if (state === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
const actionTime = this.readUint(); if (actionTime === null) { return resetPointer() }
return {
tp: MType.Redux,
action,
state,
duration,
actionTime,
};
}
case 93: { case 93: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() } const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const length = this.readUint(); if (length === null) { return resetPointer() } const length = this.readUint(); if (length === null) { return resetPointer() }

View file

@ -28,7 +28,7 @@ import type {
RawFetch, RawFetch,
RawProfiler, RawProfiler,
RawOTable, RawOTable,
RawRedux, RawReduxDeprecated,
RawVuex, RawVuex,
RawMobX, RawMobX,
RawNgRx, RawNgRx,
@ -64,6 +64,7 @@ import type {
RawTabData, RawTabData,
RawCanvasNode, RawCanvasNode,
RawTagTrigger, RawTagTrigger,
RawRedux,
RawIosEvent, RawIosEvent,
RawIosScreenChanges, RawIosScreenChanges,
RawIosClickEvent, RawIosClickEvent,
@ -127,7 +128,7 @@ export type Profiler = RawProfiler & Timed
export type OTable = RawOTable & Timed export type OTable = RawOTable & Timed
export type Redux = RawRedux & Timed export type ReduxDeprecated = RawReduxDeprecated & Timed
export type Vuex = RawVuex & Timed export type Vuex = RawVuex & Timed
@ -199,6 +200,8 @@ export type CanvasNode = RawCanvasNode & Timed
export type TagTrigger = RawTagTrigger & Timed export type TagTrigger = RawTagTrigger & Timed
export type Redux = RawRedux & Timed
export type IosEvent = RawIosEvent & Timed export type IosEvent = RawIosEvent & Timed
export type IosScreenChanges = RawIosScreenChanges & Timed export type IosScreenChanges = RawIosScreenChanges & Timed

View file

@ -26,7 +26,7 @@ export const enum MType {
Fetch = 39, Fetch = 39,
Profiler = 40, Profiler = 40,
OTable = 41, OTable = 41,
Redux = 44, ReduxDeprecated = 44,
Vuex = 45, Vuex = 45,
MobX = 46, MobX = 46,
NgRx = 47, NgRx = 47,
@ -62,6 +62,7 @@ export const enum MType {
TabData = 118, TabData = 118,
CanvasNode = 119, CanvasNode = 119,
TagTrigger = 120, TagTrigger = 120,
Redux = 121,
IosEvent = 93, IosEvent = 93,
IosScreenChanges = 96, IosScreenChanges = 96,
IosClickEvent = 100, IosClickEvent = 100,
@ -239,8 +240,8 @@ export interface RawOTable {
value: string, value: string,
} }
export interface RawRedux { export interface RawReduxDeprecated {
tp: MType.Redux, tp: MType.ReduxDeprecated,
action: string, action: string,
state: string, state: string,
duration: number, duration: number,
@ -500,6 +501,14 @@ export interface RawTagTrigger {
tagId: number, tagId: number,
} }
export interface RawRedux {
tp: MType.Redux,
action: string,
state: string,
duration: number,
actionTime: number,
}
export interface RawIosEvent { export interface RawIosEvent {
tp: MType.IosEvent, tp: MType.IosEvent,
timestamp: number, timestamp: number,
@ -592,4 +601,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 | RawWsChannel | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawCanvasNode | RawTagTrigger | 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 | RawReduxDeprecated | 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 | RawTagTrigger | RawRedux | RawIosEvent | RawIosScreenChanges | RawIosClickEvent | RawIosInputEvent | RawIosPerformanceEvent | RawIosLog | RawIosInternalError | RawIosNetworkCall | RawIosSwipeEvent | RawIosIssueEvent;

View file

@ -27,7 +27,7 @@ export const TP_MAP = {
39: MType.Fetch, 39: MType.Fetch,
40: MType.Profiler, 40: MType.Profiler,
41: MType.OTable, 41: MType.OTable,
44: MType.Redux, 44: MType.ReduxDeprecated,
45: MType.Vuex, 45: MType.Vuex,
46: MType.MobX, 46: MType.MobX,
47: MType.NgRx, 47: MType.NgRx,
@ -63,6 +63,7 @@ export const TP_MAP = {
118: MType.TabData, 118: MType.TabData,
119: MType.CanvasNode, 119: MType.CanvasNode,
120: MType.TagTrigger, 120: MType.TagTrigger,
121: MType.Redux,
93: MType.IosEvent, 93: MType.IosEvent,
96: MType.IosScreenChanges, 96: MType.IosScreenChanges,
100: MType.IosClickEvent, 100: MType.IosClickEvent,

View file

@ -216,7 +216,7 @@ type TrStateAction = [
type: string, type: string,
] ]
type TrRedux = [ type TrReduxDeprecated = [
type: 44, type: 44,
action: string, action: string,
state: string, state: string,
@ -514,8 +514,16 @@ type TrTagTrigger = [
tagId: number, tagId: number,
] ]
type TrRedux = [
type: 121,
action: string,
state: string,
duration: number,
actionTime: number,
]
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 | TrTagTrigger
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 | TrReduxDeprecated | 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 | TrTagTrigger | TrRedux
export default function translate(tMsg: TrackerMessage): RawMessage | null { export default function translate(tMsg: TrackerMessage): RawMessage | null {
switch(tMsg[0]) { switch(tMsg[0]) {
@ -726,7 +734,7 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
case 44: { case 44: {
return { return {
tp: MType.Redux, tp: MType.ReduxDeprecated,
action: tMsg[1], action: tMsg[1],
state: tMsg[2], state: tMsg[2],
duration: tMsg[3], duration: tMsg[3],
@ -1040,6 +1048,16 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
} }
} }
case 121: {
return {
tp: MType.Redux,
action: tMsg[1],
state: tMsg[2],
duration: tMsg[3],
actionTime: tMsg[4],
}
}
default: default:
return null return null
} }

View file

@ -184,3 +184,155 @@ svg {
overflow-y: hidden; overflow-y: hidden;
padding-right: 15px; padding-right: 15px;
} }
.json-view {
display: block;
color: #4d4d4d;
text-align: left;
--json-property: #009033;
--json-index: #676dff;
--json-number: #676dff;
--json-string: #b2762e;
--json-boolean: #dc155e;
--json-null: #dc155e;
}
.json-view .json-view--property {
color: var(--json-property);
}
.json-view .json-view--index {
color: var(--json-index);
}
.json-view .json-view--number {
color: var(--json-number);
}
.json-view .json-view--string {
color: var(--json-string);
}
.json-view .json-view--boolean {
color: var(--json-boolean);
}
.json-view .json-view--null {
color: var(--json-null);
}
.json-view .jv-indent {
padding-left: 1em;
}
.json-view .jv-chevron {
display: inline-block;
vertical-align: -20%;
cursor: pointer;
opacity: 0.4;
width: 1em;
height: 1em;
}
:is(.json-view .jv-chevron:hover, .json-view .jv-size:hover + .jv-chevron) {
opacity: 0.8;
}
.json-view .jv-size {
cursor: pointer;
opacity: 0.4;
font-size: 0.875em;
font-style: italic;
margin-left: 0.5em;
vertical-align: -5%;
line-height: 1;
}
.json-view :is(.json-view--copy, .json-view--edit),
.json-view .json-view--link svg {
display: none;
width: 1em;
height: 1em;
margin-left: 0.25em;
cursor: pointer;
}
.json-view .json-view--input {
width: 120px;
margin-left: 0.25em;
border-radius: 4px;
border: 1px solid currentColor;
padding: 0px 4px;
font-size: 87.5%;
line-height: 1.25;
background: transparent;
}
.json-view .json-view--deleting {
outline: 1px solid #da0000;
background-color: #da000011;
text-decoration-line: line-through;
}
:is(.json-view:hover, .json-view--pair:hover) > :is(.json-view--copy, .json-view--edit),
:is(.json-view:hover, .json-view--pair:hover) > .json-view--link svg {
display: inline-block;
}
.json-view .jv-button {
background: transparent;
outline: none;
border: none;
cursor: pointer;
color: inherit;
}
.json-view .cursor-pointer {
cursor: pointer;
}
.json-view svg {
vertical-align: -10%;
}
.jv-size-chevron ~ svg {
vertical-align: -16%;
}
/* Themes */
.json-view_a11y {
color: #545454;
--json-property: #aa5d00;
--json-index: #007299;
--json-number: #007299;
--json-string: #008000;
--json-boolean: #d91e18;
--json-null: #d91e18;
}
.json-view .jv-size {
opacity: 0.6!important;
}
.json-view_github {
color: #005cc5;
--json-property: #005cc5;
--json-index: #005cc5;
--json-number: #005cc5;
--json-string: #032f62;
--json-boolean: #005cc5;
--json-null: #005cc5;
}
.json-view_vscode {
color: #005cc5;
--json-property: #0451a5;
--json-index: #0000ff;
--json-number: #0000ff;
--json-string: #a31515;
--json-boolean: #0000ff;
--json-null: #0000ff;
}
.json-view_atom {
color: #383a42;
--json-property: #e45649;
--json-index: #986801;
--json-number: #986801;
--json-string: #50a14f;
--json-boolean: #0184bc;
--json-null: #0184bc;
}
.json-view_winter-is-coming {
color: #0431fa;
--json-property: #3a9685;
--json-index: #ae408b;
--json-number: #ae408b;
--json-string: #8123a9;
--json-boolean: #0184bc;
--json-null: #0184bc;
}

View file

@ -37,7 +37,6 @@
"classnames": "^2.3.1", "classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.5.7", "country-flag-icons": "^1.5.7",
"deep-diff": "^1.0.2",
"fflate": "^0.7.4", "fflate": "^0.7.4",
"fzstd": "^0.1.0", "fzstd": "^0.1.0",
"html-to-image": "^1.9.0", "html-to-image": "^1.9.0",
@ -51,6 +50,7 @@
"jsx-runtime": "^1.2.0", "jsx-runtime": "^1.2.0",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"luxon": "^1.24.1", "luxon": "^1.24.1",
"microdiff": "^1.4.0",
"mobx": "^6.3.8", "mobx": "^6.3.8",
"mobx-react-lite": "^3.1.6", "mobx-react-lite": "^3.1.6",
"moment": "^2.29.4", "moment": "^2.29.4",
@ -67,7 +67,6 @@
"react-draggable": "^4.4.5", "react-draggable": "^4.4.5",
"react-google-recaptcha": "^2.1.0", "react-google-recaptcha": "^2.1.0",
"react-highlight": "^0.14.0", "react-highlight": "^0.14.0",
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0", "react-lazyload": "^3.2.0",
"react-merge-refs": "^2.0.1", "react-merge-refs": "^2.0.1",
"react-redux": "^5.1.2", "react-redux": "^5.1.2",
@ -78,6 +77,7 @@
"react-tippy": "^1.4.0", "react-tippy": "^1.4.0",
"react-toastify": "^9.1.1", "react-toastify": "^9.1.1",
"react-virtualized": "^9.22.3", "react-virtualized": "^9.22.3",
"react18-json-view": "^0.2.8",
"recharts": "^2.8.0", "recharts": "^2.8.0",
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-immutable": "^4.0.0", "redux-immutable": "^4.0.0",

File diff suppressed because it is too large Load diff

View file

@ -224,7 +224,7 @@ message 42, 'StateAction', :replayer => false do
string 'Type' string 'Type'
end end
## 43 ## 43
message 44, 'Redux', :replayer => :devtools do message 44, 'ReduxDeprecated', :replayer => :devtools do
string 'Action' string 'Action'
string 'State' string 'State'
uint 'Duration' uint 'Duration'
@ -522,6 +522,13 @@ message 120, 'TagTrigger', :replayer => :devtools do
int 'TagId' int 'TagId'
end end
message 121, 'Redux', :replayer => :devtools do
string 'Action'
string 'State'
uint 'Duration'
uint 'ActionTime'
end
## Backend-only ## Backend-only
message 125, 'IssueEvent', :replayer => false, :tracker => false do message 125, 'IssueEvent', :replayer => false, :tracker => false do
uint 'MessageID' uint 'MessageID'

View file

@ -3,7 +3,7 @@
Below is the list of dependencies used in OpenReplay software. Licenses may change between versions, so please keep this up to date with every new library you use. Below is the list of dependencies used in OpenReplay software. Licenses may change between versions, so please keep this up to date with every new library you use.
| Library | License | Scope | | Library | License | Scope |
|----------|-------------|-------------| |----------------------------|-------------|-------------|
| btcutil | IST | Go | | btcutil | IST | Go |
| confluent-kafka-go | Apache2 | Go | | confluent-kafka-go | Apache2 | Go |
| compress | Apache2 | Go | | compress | Apache2 | Go |
@ -60,7 +60,7 @@ Below is the list of dependencies used in OpenReplay software. Licenses may chan
| copy-to-clipboard | MIT | JavaScript | | copy-to-clipboard | MIT | JavaScript |
| jsonwebtoken | MIT | JavaScript | | jsonwebtoken | MIT | JavaScript |
| datamaps | MIT | JavaScript | | datamaps | MIT | JavaScript |
| deep-diff | MIT | JavaScript | | microdiff | MIT | JavaScript |
| immutable | MIT | JavaScript | | immutable | MIT | JavaScript |
| jsbi | Apache2 | JavaScript | | jsbi | Apache2 | JavaScript |
| jshint | MIT | JavaScript | | jshint | MIT | JavaScript |

View file

@ -0,0 +1 @@
"use strict";const e={};["DEL","UNDEF","TRUE","FALSE","NUMBER","BIGINT","FUNCTION","STRING","SYMBOL","NULL","OBJECT","ARRAY"].forEach(((t,r)=>e[t]=String.fromCharCode(r+57344)));const t=new class{constructor(e,t=1/0){this._hash=e,this._slen=t,this._refmap=new Map,this._refset=new Set}_ref_str(t){if(t.length<this._slen&&!t.includes(e.DEL))return t;let r=this._refmap.get(t);return void 0===r&&(r=this._hash(t),this._refmap.set(t,r)),r}_encode_prim(t){switch(typeof t){case"undefined":return e.UNDEF;case"boolean":return t?e.TRUE:e.FALSE;case"number":return e.NUMBER+t.toString();case"bigint":return e.BIGINT+t.toString();case"function":return e.FUNCTION;case"string":return e.STRING+this._ref_str(t);case"symbol":return e.SYMBOL+this._ref_str(t.toString().slice(7,-1))}if(null===t)return e.NULL}_encode_obj(t,r=this._refmap.get(t)){return(Array.isArray(t)?e.ARRAY:e.OBJECT)+r}_encode_term(e){return this._encode_prim(e)||this._encode_obj(e)}_encode_deep(t,r){const s=this._encode_prim(t);if(void 0!==s)return s;const n=this._refmap.get(t);switch(typeof n){case"number":return(r-n).toString();case"string":return this._encode_obj(t,n)}this._refmap.set(t,r);const a=this._hash((Array.isArray(t)?t.map((e=>this._encode_deep(e,r+1))):Object.keys(t).sort().map((s=>this._ref_str(s)+e.DEL+this._encode_deep(t[s],r+1)))).join(e.DEL));return this._refmap.set(t,a),this._encode_obj(t,a)}encode(e){return this._encode_deep(e,0)}commit(){const t={};return this._refmap.forEach(((r,s)=>{this._refset.has(r)||(this._refset.add(r),"string"!=typeof s&&(s=(Array.isArray(s)?s.map((e=>this._encode_term(e))):Object.keys(s).map((t=>this._ref_str(t)+e.DEL+this._encode_term(s[t])))).join(e.DEL)),t[r]=s)})),this._refmap.clear(),t}clear(){this._refmap.clear(),this._refset.clear()}}((function(e,t=0){return function(e){let t=new ArrayBuffer(4);return new DataView(t).setUint32(0,e,!1),btoa(String.fromCharCode.apply(null,new Uint8Array(t)))}(function(e,t){var r,s,n,a,o,c,i,h,_,d,f,u,m,p;for(o=(a=(r=e.length)-(s=3&r))-(n=7&a),c=t,_=11601,d=3432906752,f=13715,u=461832192,p=3864292196,h=0;h<o;)i=255&e.charCodeAt(h)|(255&e.charCodeAt(++h))<<8|(255&e.charCodeAt(++h))<<16|(255&e.charCodeAt(++h))<<24,m=255&e.charCodeAt(++h)|(255&e.charCodeAt(++h))<<8|(255&e.charCodeAt(++h))<<16|(255&e.charCodeAt(++h))<<24,++h,c=5*(c=(c^=i=(u*(i=(i=(d*i|0)+_*i)<<15|i>>>17)|0)+f*i)<<13|c>>>19)+p,c=5*(c=(c^=m=(u*(m=(m=(d*m|0)+_*m)<<15|m>>>17)|0)+f*m)<<13|c>>>19)+p;switch(n&&(i=255&e.charCodeAt(h)|(255&e.charCodeAt(++h))<<8|(255&e.charCodeAt(++h))<<16|(255&e.charCodeAt(++h))<<24,++h,c=5*(c=(c^=i=(u*(i=(i=(d*i|0)+_*i)<<15|i>>>17)|0)+f*i)<<13|c>>>19)+p),i=0,s){case 3:i^=(255&e.charCodeAt(h+2))<<16;case 2:i^=(255&e.charCodeAt(h+1))<<8;case 1:c^=i=(u*(i=(i=(d*(i^=255&e.charCodeAt(h))|0)+_*i)<<15|i>>>17)|0)+f*i}return c^=r,c=(2246770688*(c^=c>>>16)|0)+51819*c,c=(3266445312*(c^=c>>>13)|0)+44597*c,(c^=c>>>16)>>>0}(e,t))}),50),r={enabled:!0,throttle:50};let s,n=null;self.onmessage=({data:e})=>{if("action"===e.type)try{const a=t.encode(e.action);let o;r.enabled&&(o=(e=>{if(!n||!s||Date.now()-s>r.throttle){const r=t.encode(e);return s=Date.now(),n=r,r}return n})(e.state));const c=t.commit();postMessage({type:"encoded",action:a,state:o,table:c,timestamp:e.timestamp})}catch{t.clear()}};

Binary file not shown.

View file

@ -16,19 +16,26 @@
"main": "./lib/index.js", "main": "./lib/index.js",
"scripts": { "scripts": {
"lint": "prettier --write 'src/**/*.ts' README.md && tsc --noEmit", "lint": "prettier --write 'src/**/*.ts' README.md && tsc --noEmit",
"build": "npm run build-es && npm run build-cjs", "build": "npm run build-es && npm run build-cjs && bun run rollup && bun run compile",
"compile": "node --experimental-modules --experimental-json-modules script/compile.cjs",
"build-es": "rm -Rf lib && tsc", "build-es": "rm -Rf lib && tsc",
"rollup": "rollup --config rollup.config.js",
"build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs'", "build-cjs": "rm -Rf cjs && tsc --project tsconfig-cjs.json && echo '{ \"type\": \"commonjs\" }' > cjs/package.json && replace-in-files cjs/* --string='@openreplay/tracker' --replacement='@openreplay/tracker/cjs'",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"peerDependencies": { "peerDependencies": {
"@openreplay/tracker": ">=12.0.6", "@openreplay/tracker": ">=13.0.0",
"redux": "^4.0.0" "redux": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@openreplay/tracker": "file:../tracker", "@openreplay/tracker": "file:../tracker",
"prettier": "^1.18.2", "prettier": "^1.18.2",
"replace-in-files-cli": "^1.0.0", "replace-in-files-cli": "^1.0.0",
"typescript": "^4.6.0-dev.20211126" "typescript": "^4.6.0-dev.20211126",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^15.2.3",
"replace-in-files": "^3.0.0",
"rollup": "^4.14.0",
"rollup-plugin-terser": "^7.0.2"
} }
} }

View file

@ -0,0 +1,12 @@
import resolve from '@rollup/plugin-node-resolve'
import { babel } from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
export default {
input: 'lib/worker/worker.js',
output: {
file: 'build/webworker.js',
format: 'cjs',
},
plugins: [resolve(), babel({ babelHelpers: 'bundled' }), terser({ mangle: { reserved: ['$'] } })],
}

View file

@ -0,0 +1,30 @@
const { promises: fs } = require('fs');
const replaceInFiles = require('replace-in-files');
async function main() {
const webworker = await fs.readFile('build/webworker.js', 'utf8');
await replaceInFiles({
files: 'cjs/**/*',
from: 'WEBWORKER_BODY',
to: webworker.replace(/'/g, "\\'").replace(/\n/g, ''),
});
await replaceInFiles({
files: 'lib/**/*',
from: 'WEBWORKER_BODY',
to: webworker.replace(/'/g, "\\'").replace(/\n/g, ''),
});
await fs.writeFile('cjs/package.json', `{ "type": "commonjs" }`);
await replaceInFiles({
files: 'cjs/*',
from: /\.\.\/common/g,
to: './common',
});
await replaceInFiles({
files: 'cjs/**/*',
from: /\.\.\/\.\.\/common/g,
to: '../common',
});
}
main()
.then(() => console.log('compiled'))
.catch(err => console.error(err));

View file

@ -30,6 +30,9 @@ export default function(opts: Partial<Options> = {}) {
if (app === null) { if (app === null) {
return () => next => action => next(action); return () => next => action => next(action);
} }
const worker = new Worker(
URL.createObjectURL(new Blob(['WEBWORKER_BODY'], { type: 'text/javascript' })),
);
const encoder = new Encoder(murmur, 50); const encoder = new Encoder(murmur, 50);
app.attachStopCallback(() => { app.attachStopCallback(() => {
encoder.clear() encoder.clear()
@ -55,24 +58,31 @@ export default function(opts: Partial<Options> = {}) {
const startTime = performance.now(); const startTime = performance.now();
const result = next(action); const result = next(action);
const duration = performance.now() - startTime; const duration = performance.now() - startTime;
try { const actionTs = app?.timestamp() ?? 0
worker.postMessage({
type: 'action',
action: options.actionTransformer(action),
state: options.stateTransformer(getState()),
timestamp: actionTs,
})
worker.onmessage = ({ data }) => {
if (data.type === 'encoded') {
const _action = data.action;
const _currState = data.state;
const _table = data.table;
const _timestamp = data.timestamp;
console.log('encoded', _action, _currState, _table, _timestamp, app?.timestamp())
for (let key in _table) app.send(Messages.OTable(key, _table[key]));
app.send(Messages.Redux(_action, _currState, duration, _timestamp)); // TODO: add timestamp
}
}
worker.onerror = (e) => {
console.error('OR Redux: worker_error', e)
}
const type = options.actionType(action); const type = options.actionType(action);
if (typeof type === 'string' && type) { if (typeof type === 'string' && type) {
app.send(Messages.StateAction(type)); app.send(Messages.StateAction(type));
} }
const _action = encoder.encode(options.actionTransformer(action));
let _currState: string
if (options.stateUpdateBatching.enabled) {
_currState = batchEncoding(getState());
} else {
_currState = encoder.encode(options.stateTransformer(getState()));
}
const _table = encoder.commit();
for (let key in _table) app.send(Messages.OTable(key, _table[key]));
app.send(Messages.Redux(_action, _currState, duration));
} catch {
encoder.clear();
}
return result; return result;
}; };
}; };

View file

@ -0,0 +1,70 @@
import { Encoder, murmur } from '../syncod-v2/index.js';
type FromWorker = {
type: 'encoded';
action: string;
state: string;
table: Record<string, any>;
timestamp: number;
};
type ToWorker = {
type: 'action';
action: Record<string, any>;
state: Record<string, any>;
timestamp: number;
};
declare function postMessage(message: FromWorker): void;
const encoder = new Encoder(murmur, 50);
const options = {
stateUpdateBatching: {
enabled: true,
throttle: 50,
},
};
let lastCommit: number;
let lastState: string | null = null;
const batchEncoding = (state: Record<string, any>) => {
if (
!lastState ||
!lastCommit ||
Date.now() - lastCommit > options.stateUpdateBatching.throttle
) {
const _state = encoder.encode(state);
lastCommit = Date.now();
lastState = _state;
return _state;
} else {
return lastState;
}
};
// @ts-ignore
self.onmessage = ({ data }: ToWorker) => {
switch (data.type) {
case 'action': {
try {
const _action = encoder.encode(data.action);
let _currState: string;
if (options.stateUpdateBatching.enabled) {
_currState = batchEncoding(data.state);
} else {
_currState = encoder.encode(data.state);
}
const _table = encoder.commit();
postMessage({
type: 'encoded',
action: _action,
state: _currState,
table: _table,
timestamp: data.timestamp,
});
} catch {
encoder.clear();
}
}
}
};

View file

@ -33,7 +33,7 @@ export declare const enum Type {
Profiler = 40, Profiler = 40,
OTable = 41, OTable = 41,
StateAction = 42, StateAction = 42,
Redux = 44, ReduxDeprecated = 44,
Vuex = 45, Vuex = 45,
MobX = 46, MobX = 46,
NgRx = 47, NgRx = 47,
@ -74,6 +74,7 @@ export declare const enum Type {
TabData = 118, TabData = 118,
CanvasNode = 119, CanvasNode = 119,
TagTrigger = 120, TagTrigger = 120,
Redux = 121,
} }
@ -288,8 +289,8 @@ export type StateAction = [
/*type:*/ string, /*type:*/ string,
] ]
export type Redux = [ export type ReduxDeprecated = [
/*type:*/ Type.Redux, /*type:*/ Type.ReduxDeprecated,
/*action:*/ string, /*action:*/ string,
/*state:*/ string, /*state:*/ string,
/*duration:*/ number, /*duration:*/ number,
@ -586,6 +587,14 @@ export type TagTrigger = [
/*tagId:*/ number, /*tagId:*/ number,
] ]
export type Redux = [
/*type:*/ Type.Redux,
/*action:*/ string,
/*state:*/ string,
/*duration:*/ number,
/*actionTime:*/ number,
]
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 | TagTrigger
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 | ReduxDeprecated | 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 | TagTrigger | Redux
export default Message export default Message

View file

@ -665,6 +665,8 @@ export default class App {
deviceMemory, deviceMemory,
jsHeapSizeLimit, jsHeapSizeLimit,
timezone: getTimezone(), timezone: getTimezone(),
width: window.innerWidth,
height: window.innerHeight,
}), }),
}) })
const { const {

View file

@ -396,13 +396,13 @@ export function StateAction(
] ]
} }
export function Redux( export function ReduxDeprecated(
action: string, action: string,
state: string, state: string,
duration: number, duration: number,
): Messages.Redux { ): Messages.ReduxDeprecated {
return [ return [
Messages.Type.Redux, Messages.Type.ReduxDeprecated,
action, action,
state, state,
duration, duration,
@ -951,3 +951,18 @@ export function TagTrigger(
] ]
} }
export function Redux(
action: string,
state: string,
duration: number,
actionTime: number,
): Messages.Redux {
return [
Messages.Type.Redux,
action,
state,
duration,
actionTime,
]
}

View file

@ -134,7 +134,7 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.string(msg[1]) return this.string(msg[1])
break break
case Messages.Type.Redux: case Messages.Type.ReduxDeprecated:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3])
break break
@ -298,6 +298,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.int(msg[1]) return this.int(msg[1])
break break
case Messages.Type.Redux:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.uint(msg[4])
break
} }
} }