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

View file

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

View file

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

View file

@ -370,7 +370,7 @@ class StateAction(Message):
self.type = type
class Redux(Message):
class ReduxDeprecated(Message):
__id__ = 44
def __init__(self, action, state, duration):
@ -815,6 +815,16 @@ class TagTrigger(Message):
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):
__id__ = 125

View file

@ -548,7 +548,7 @@ cdef class StateAction(PyMessage):
self.type = type
cdef class Redux(PyMessage):
cdef class ReduxDeprecated(PyMessage):
cdef public int __id__
cdef public str action
cdef public str state
@ -1203,6 +1203,21 @@ cdef class TagTrigger(PyMessage):
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 public int __id__
cdef public unsigned long message_id

View file

@ -382,7 +382,7 @@ class MessageCodec(Codec):
)
if message_id == 44:
return Redux(
return ReduxDeprecated(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader)
@ -732,6 +732,14 @@ class MessageCodec(Codec):
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:
return IssueEvent(
message_id=self.read_uint(reader),

View file

@ -480,7 +480,7 @@ cdef class MessageCodec:
)
if message_id == 44:
return Redux(
return ReduxDeprecated(
action=self.read_string(reader),
state=self.read_string(reader),
duration=self.read_uint(reader)
@ -830,6 +830,14 @@ cdef class MessageCodec:
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:
return IssueEvent(
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 Player from './PlayerInst';
import SubHeader from 'Components/Session_/Subheader';
import AiSubheader from 'Components/Session/Player/ReplayPlayer/AiSubheader';
import styles from 'Components/Session_/playerBlock.module.css';
interface IProps {

View file

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

View file

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

View file

@ -2,7 +2,9 @@ import BottomBlock from './BottomBlock';
import Header from './Header';
import Content from './Content';
// @ts-ignore
BottomBlock.Header = Header;
// @ts-ignore
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 [shortenNewVal, setShortenNewVal] = React.useState(true);
const oldValue = diff.item
? JSON.stringify(diff.item.lhs, getCircularReplacer(), 1)
: JSON.stringify(diff.lhs, getCircularReplacer(), 1);
const newValue = diff.item
? JSON.stringify(diff.item.rhs, getCircularReplacer(), 1)
: JSON.stringify(diff.rhs, getCircularReplacer(), 1);
// Adjust to handle the difference based on its type
let oldValue;
let newValue;
switch (diff.type) {
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 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 { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date';
// @ts-ignore
import { diff } from 'deep-diff';
import diff from 'microdiff'
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock/index';
@ -14,6 +13,7 @@ import DiffRow from './DiffRow';
import cn from 'classnames';
import stl from './storage.module.css';
import logger from "App/logger";
import ReduxViewer from './ReduxViewer'
function getActionsName(type: string) {
switch (type) {
@ -129,7 +129,7 @@ function Storage(props: Props) {
};
const renderDiffs = (diff: Record<string, any>, i: number) => {
const path = createPath(diff);
const path = diff.path.join('.')
return (
<React.Fragment key={i}>
<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) => {
if (typeof actionType === 'string') return actionType;
return 'UNKNOWN';
@ -239,8 +225,13 @@ function Storage(props: Props) {
const { hintIsHidden } = props;
if (type === STORAGE_TYPES.REDUX) {
return <ReduxViewer />
}
return (
<BottomBlock>
{/*@ts-ignore*/}
<>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
@ -348,6 +339,7 @@ function Storage(props: Props) {
</div>
</NoContent>
</BottomBlock.Content>
</>
</BottomBlock>
);
}

View file

@ -8,10 +8,10 @@ interface Props {
function JumpButton(props: Props) {
const { tooltip } = props;
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}>
<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) => {
e.stopPropagation();
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 { 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 type { PlayerMsg, SessionFilesInfo, Store } from 'Player';
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 MessageManager from 'Player/web/MessageManager';
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 {
firstFileLoading: boolean;
domLoading: boolean;
@ -41,7 +48,10 @@ export default class MessageLoader {
shouldDecrypt && this.session.fileKey
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey!)
: (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;
return async (b: Uint8Array) => {
try {
@ -65,6 +75,14 @@ export default class MessageLoader {
let artificialStartTime = Infinity;
let startTimeSet = false;
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 (
msg.tp === MType.CreateDocument &&
msg.time !== undefined &&
@ -96,8 +114,10 @@ export default class MessageLoader {
const sortedMsgs = msgs.sort((m1, m2) => {
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) return 1;
if (m1.tp === MType.CreateDocument && m2.tp !== MType.CreateDocument)
return -1;
if (m1.tp !== MType.CreateDocument && m2.tp === MType.CreateDocument)
return 1;
const m1IsDOM = DOMMessages.includes(m1.tp);
const m2IsDOM = DOMMessages.includes(m2.tp);
@ -189,7 +209,10 @@ export default class MessageLoader {
try {
await this.loadEFSMobs();
} catch (unprocessedLoadError) {
this.messageManager.onFileReadFailed(sessionLoadError, unprocessedLoadError);
this.messageManager.onFileReadFailed(
sessionLoadError,
unprocessedLoadError
);
}
} finally {
this.store.update({ domLoading: false, devtoolsLoading: false });
@ -201,15 +224,21 @@ export default class MessageLoader {
this.session.domURL && this.session.domURL.length > 0
? {
mobUrls: this.session.domURL,
parser: () => this.createNewParser(true, this.processMessages, 'dom'),
parser: () =>
this.createNewParser(true, this.processMessages, 'dom'),
}
: {
mobUrls: this.session.mobsUrl,
parser: () => this.createNewParser(false, this.processMessages, 'dom'),
parser: () =>
this.createNewParser(false, this.processMessages, 'dom'),
};
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
@ -220,7 +249,10 @@ export default class MessageLoader {
* */
await loadFiles([loadMethod.mobUrls[0]], parser);
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);
await Promise.allSettled([restDomFilesPromise, restDevtoolsFilesPromise]);
@ -236,8 +268,16 @@ export default class MessageLoader {
efsDomFilePromise,
efsDevtoolsFilePromise,
]);
const domParser = this.createNewParser(false, this.processMessages, 'domEFS');
const devtoolsParser = this.createNewParser(false, this.processMessages, 'devtoolsEFS');
const domParser = this.createNewParser(
false,
this.processMessages,
'domEFS'
);
const devtoolsParser = this.createNewParser(
false,
this.processMessages,
'devtoolsEFS'
);
const parseDomPromise: Promise<any> =
domData.status === 'fulfilled'
? domParser(domData.value)

View file

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

View file

@ -304,7 +304,7 @@ export default class RawMessageReader extends PrimitiveReader {
const state = this.readString(); if (state === null) { return resetPointer() }
const duration = this.readUint(); if (duration === null) { return resetPointer() }
return {
tp: MType.Redux,
tp: MType.ReduxDeprecated,
action,
state,
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: {
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
const length = this.readUint(); if (length === null) { return resetPointer() }

View file

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

View file

@ -26,7 +26,7 @@ export const enum MType {
Fetch = 39,
Profiler = 40,
OTable = 41,
Redux = 44,
ReduxDeprecated = 44,
Vuex = 45,
MobX = 46,
NgRx = 47,
@ -62,6 +62,7 @@ export const enum MType {
TabData = 118,
CanvasNode = 119,
TagTrigger = 120,
Redux = 121,
IosEvent = 93,
IosScreenChanges = 96,
IosClickEvent = 100,
@ -239,8 +240,8 @@ export interface RawOTable {
value: string,
}
export interface RawRedux {
tp: MType.Redux,
export interface RawReduxDeprecated {
tp: MType.ReduxDeprecated,
action: string,
state: string,
duration: number,
@ -500,6 +501,14 @@ export interface RawTagTrigger {
tagId: number,
}
export interface RawRedux {
tp: MType.Redux,
action: string,
state: string,
duration: number,
actionTime: number,
}
export interface RawIosEvent {
tp: MType.IosEvent,
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,
40: MType.Profiler,
41: MType.OTable,
44: MType.Redux,
44: MType.ReduxDeprecated,
45: MType.Vuex,
46: MType.MobX,
47: MType.NgRx,
@ -63,6 +63,7 @@ export const TP_MAP = {
118: MType.TabData,
119: MType.CanvasNode,
120: MType.TagTrigger,
121: MType.Redux,
93: MType.IosEvent,
96: MType.IosScreenChanges,
100: MType.IosClickEvent,

View file

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

View file

@ -184,3 +184,155 @@ svg {
overflow-y: hidden;
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",
"copy-to-clipboard": "^3.3.1",
"country-flag-icons": "^1.5.7",
"deep-diff": "^1.0.2",
"fflate": "^0.7.4",
"fzstd": "^0.1.0",
"html-to-image": "^1.9.0",
@ -51,6 +50,7 @@
"jsx-runtime": "^1.2.0",
"lucide-react": "^0.363.0",
"luxon": "^1.24.1",
"microdiff": "^1.4.0",
"mobx": "^6.3.8",
"mobx-react-lite": "^3.1.6",
"moment": "^2.29.4",
@ -67,7 +67,6 @@
"react-draggable": "^4.4.5",
"react-google-recaptcha": "^2.1.0",
"react-highlight": "^0.14.0",
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0",
"react-merge-refs": "^2.0.1",
"react-redux": "^5.1.2",
@ -78,6 +77,7 @@
"react-tippy": "^1.4.0",
"react-toastify": "^9.1.1",
"react-virtualized": "^9.22.3",
"react18-json-view": "^0.2.8",
"recharts": "^2.8.0",
"redux": "^4.0.5",
"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'
end
## 43
message 44, 'Redux', :replayer => :devtools do
message 44, 'ReduxDeprecated', :replayer => :devtools do
string 'Action'
string 'State'
uint 'Duration'
@ -522,6 +522,13 @@ message 120, 'TagTrigger', :replayer => :devtools do
int 'TagId'
end
message 121, 'Redux', :replayer => :devtools do
string 'Action'
string 'State'
uint 'Duration'
uint 'ActionTime'
end
## Backend-only
message 125, 'IssueEvent', :replayer => false, :tracker => false do
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.
| Library | License | Scope |
|----------|-------------|-------------|
|----------------------------|-------------|-------------|
| btcutil | IST | Go |
| confluent-kafka-go | 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 |
| jsonwebtoken | MIT | JavaScript |
| datamaps | MIT | JavaScript |
| deep-diff | MIT | JavaScript |
| microdiff | MIT | JavaScript |
| immutable | MIT | JavaScript |
| jsbi | Apache2 | 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",
"scripts": {
"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",
"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'",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@openreplay/tracker": ">=12.0.6",
"@openreplay/tracker": ">=13.0.0",
"redux": "^4.0.0"
},
"devDependencies": {
"@openreplay/tracker": "file:../tracker",
"prettier": "^1.18.2",
"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) {
return () => next => action => next(action);
}
const worker = new Worker(
URL.createObjectURL(new Blob(['WEBWORKER_BODY'], { type: 'text/javascript' })),
);
const encoder = new Encoder(murmur, 50);
app.attachStopCallback(() => {
encoder.clear()
@ -55,24 +58,31 @@ export default function(opts: Partial<Options> = {}) {
const startTime = performance.now();
const result = next(action);
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);
if (typeof type === 'string' && 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;
};
};

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

View file

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

View file

@ -396,13 +396,13 @@ export function StateAction(
]
}
export function Redux(
export function ReduxDeprecated(
action: string,
state: string,
duration: number,
): Messages.Redux {
): Messages.ReduxDeprecated {
return [
Messages.Type.Redux,
Messages.Type.ReduxDeprecated,
action,
state,
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])
break
case Messages.Type.Redux:
case Messages.Type.ReduxDeprecated:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3])
break
@ -298,6 +298,10 @@ export default class MessageEncoder extends PrimitiveEncoder {
return this.int(msg[1])
break
case Messages.Type.Redux:
return this.string(msg[1]) && this.string(msg[2]) && this.uint(msg[3]) && this.uint(msg[4])
break
}
}