Merge branch 'assist' into dev

This commit is contained in:
ShiKhu 2021-07-13 19:37:07 +03:00
commit ae3981bf74
106 changed files with 4313 additions and 1552 deletions

View file

@ -1,6 +1,6 @@
package intervals
const EVENTS_COMMIT_INTERVAL = 1 * 60 * 1000
const EVENTS_COMMIT_INTERVAL = 30 * 1000
const HEARTBEAT_INTERVAL = 2 * 60 * 1000
const INTEGRATIONS_REQUEST_INTERVAL = 2 * 60 * 1000
const EVENTS_PAGE_EVENT_TIMEOUT = 2 * 60 * 1000

View file

@ -20,7 +20,8 @@ const siteIdRequiredPaths = [
'/rehydrations',
'/sourcemaps',
'/errors',
'/funnels'
'/funnels',
'/assist'
];
const noStoringFetchPathStarts = [

View file

@ -0,0 +1,11 @@
import React from 'react';
import ChatWindow from './ChatWindow';
export default function Assist() {
return (
<div className="absolute">
{/* <ChatWindow /> */}
</div>
)
}

View file

@ -0,0 +1,29 @@
.controls {
height: 38px;
/* margin-top: 5px; */
/* background-color: white; */
/* border-top: solid thin #CCC; */
}
.btnWrapper {
display: flex;
align-items: center;
height: 24px;
font-size: 12px;
color: $gray-medium;
&.disabled {
/* background-color: red; */
& svg {
fill: red;
}
}
}
.endButton {
background-color: $red;
border-radius: 3px;
padding: 2px 8px;
color: white;
font-size: 12px;
}

View file

@ -0,0 +1,54 @@
import React, { useState } from 'react'
import stl from './ChatControls.css'
import cn from 'classnames'
import { Button, Icon } from 'UI'
interface Props {
stream: MediaStream | null,
endCall: () => void
}
function ChatControls({ stream, endCall } : Props) {
const [audioEnabled, setAudioEnabled] = useState(true)
const [videoEnabled, setVideoEnabled] = useState(true)
const toggleAudio = () => {
if (!stream) { return; }
const aEn = !audioEnabled
stream.getAudioTracks().forEach(track => track.enabled = aEn);
setAudioEnabled(aEn);
}
const toggleVideo = () => {
if (!stream) { return; }
const vEn = !videoEnabled;
stream.getVideoTracks().forEach(track => track.enabled = vEn);
setVideoEnabled(vEn)
}
return (
<div className={cn(stl.controls, "flex items-center w-full justify-start bottom-0 px-2")}>
<div className="flex items-center">
<div className={cn(stl.btnWrapper, { [stl.disabled]: !audioEnabled})}>
<Button plain size="small" onClick={toggleAudio} noPadding className="flex items-center">
<Icon name={audioEnabled ? 'mic' : 'mic-mute'} size="16" />
<span className="ml-2 color-gray-medium text-sm">{audioEnabled ? 'Mute' : 'Unmute'}</span>
</Button>
</div>
<div className={cn(stl.btnWrapper, { [stl.disabled]: !videoEnabled})}>
<Button plain size="small" onClick={toggleVideo} noPadding className="flex items-center">
<Icon name={ videoEnabled ? 'camera-video' : 'camera-video-off' } size="16" />
<span className="ml-2 color-gray-medium text-sm">{videoEnabled ? 'Stop Video' : 'Start Video'}</span>
</Button>
</div>
</div>
<div className="ml-auto">
<button className={stl.endButton} onClick={endCall}>
END
</button>
</div>
</div>
)
}
export default ChatControls

View file

@ -0,0 +1 @@
export { default } from './ChatControls'

View file

@ -0,0 +1,42 @@
import React, { useState, FC } from 'react'
import VideoContainer from '../components/VideoContainer'
import { Icon, Popup, Button } from 'UI'
import cn from 'classnames'
import Counter from 'App/components/shared/SessionItem/Counter'
import stl from './chatWindow.css'
import ChatControls from '../ChatControls/ChatControls'
import Draggable from 'react-draggable';
export interface Props {
incomeStream: MediaStream | null,
localStream: MediaStream | null,
userId: String,
endCall: () => void
}
const ChatWindow: FC<Props> = function ChatWindow({ userId, incomeStream, localStream, endCall }) {
const [minimize, setMinimize] = useState(false)
return (
<Draggable handle=".handle" bounds="body">
<div
className={cn(stl.wrapper, "fixed radius bg-white shadow-xl mt-16")}
style={{ width: '280px' }}
>
<div className="handle flex items-center p-2 cursor-move select-none">
<div className={stl.headerTitle}><b>Meeting</b> {userId}</div>
<Counter startTime={new Date().getTime() } className="text-sm ml-auto" />
</div>
<div className={cn(stl.videoWrapper, {'hidden' : minimize}, 'relative')}>
<VideoContainer stream={ incomeStream } />
<div className="absolute bottom-0 right-0 z-50">
<VideoContainer stream={ localStream } muted width={50} />
</div>
</div>
<ChatControls stream={localStream} endCall={endCall} />
</div>
</Draggable>
)
}
export default ChatWindow

View file

@ -0,0 +1,21 @@
.wrapper {
background-color: white;
border: solid thin #000;
border-radius: 3px;
position: fixed;
width: 300px;
}
.headerTitle {
font-size: 12px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.videoWrapper {
height: 180px;
overflow: hidden;
background-color: #000;
}

View file

@ -0,0 +1 @@
export { default } from './ChatWindow'

View file

@ -0,0 +1,45 @@
import React from 'react'
import { Button } from 'UI'
function ScreenSharing() {
const videoRef = React.createRef<HTMLVideoElement>()
function handleSuccess(stream) {
// startButton.disabled = true;
//videoRef.current?.srcObject = stream;
// @ts-ignore
window.stream = stream; // make variable available to browser console
stream.getVideoTracks()[0].addEventListener('ended', () => {
console.log('The user has ended sharing the screen');
});
}
function handleError(error) {
console.log(`getDisplayMedia error: ${error.name}`, error);
}
const startScreenSharing = () => {
// @ts-ignore
navigator.mediaDevices.getDisplayMedia({video: true})
.then(handleSuccess, handleError);
}
const stopScreenSharing = () => {
// @ts-ignore
window.stream.stop()
console.log('Stop screen sharing')
}
return (
<div className="fixed inset-0 z-50 bg-red">
<video ref={ videoRef } id="screen-share" autoPlay loop muted></video>
<div className="absolute left-0 right-0 bottom-0">
<Button onClick={startScreenSharing}>Start</Button>
<Button onClick={stopScreenSharing}>Stop</Button>
</div>
</div>
)
}
export default ScreenSharing

View file

@ -0,0 +1 @@
export { default } from './ScreenSharing'

View file

@ -0,0 +1,8 @@
import { storiesOf } from '@storybook/react';
import ChatWindow from './ChatWindow';
storiesOf('Assist', module)
.add('ChatWindow', () => (
<ChatWindow userId="test@test.com" />
))

View file

@ -0,0 +1,11 @@
.inCall {
& svg {
fill: $red
}
color: $red;
}
.disabled {
opacity: 0.5;
pointer-events: none;
}

View file

@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react'
import { Popup, Icon } from 'UI'
import { connect } from 'react-redux'
import cn from 'classnames'
import { toggleChatWindow } from 'Duck/sessions';
import { connectPlayer } from 'Player/store';
import ChatWindow from '../../ChatWindow';
import { callPeer } from 'Player'
import { CallingState, ConnectionStatus } from 'Player/MessageDistributor/managers/AssistManager';
import { toast } from 'react-toastify';
import stl from './AassistActions.css'
interface Props {
userId: String,
toggleChatWindow: (state) => void,
calling: CallingState,
peerConnectionStatus: ConnectionStatus
}
function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus }: Props) {
const [ incomeStream, setIncomeStream ] = useState<MediaStream | null>(null);
const [ localStream, setLocalStream ] = useState<MediaStream | null>(null);
const [ endCall, setEndCall ] = useState<()=>void>(()=>{});
const [ disconnected, setDisconnected ] = useState(false);
useEffect(() => {
return endCall
}, [])
useEffect(() => {
console.log('peerConnectionStatus', peerConnectionStatus)
if (peerConnectionStatus == 4) {
toast.info(`Live session is closed.`);
setDisconnected(true)
}
}, [peerConnectionStatus])
function onClose(stream) {
stream.getTracks().forEach(t=>t.stop());
}
function onReject() {
toast.info(`Call was rejected.`);
}
function onError() {
toast.error(`Something went wrong!`);
}
function call() {
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(lStream => {
setLocalStream(lStream);
setEndCall(() => callPeer(
lStream,
setIncomeStream,
onClose.bind(null, lStream),
onReject,
onError
));
}).catch(onError);
}
const inCall = calling == 0 || calling == 1
return (
<div className="flex items-center">
<Popup
trigger={
<div
className={
cn(
'cursor-pointer p-2 mr-2 flex items-center',
{[stl.inCall] : inCall },
{[stl.disabled]: disconnected}
)
}
onClick={inCall ? endCall : call}
role="button"
>
<Icon
name="headset"
size="20"
color={ inCall ? "red" : "gray-darkest" }
/>
<span className={cn("ml-2", { 'text-red' : inCall })}>{ inCall ? 'End Meeting' : 'Start Meeting' }</span>
</div>
}
content={ `Call ${userId}` }
size="tiny"
inverted
position="top right"
/>
<div className="fixed ml-3 left-0 top-0" style={{ zIndex: 999 }}>
{ inCall && <ChatWindow endCall={endCall} userId={userId} incomeStream={incomeStream} localStream={localStream} /> }
</div>
</div>
)
}
const con = connect(null, { toggleChatWindow })
export default con(connectPlayer(state => ({
calling: state.calling,
peerConnectionStatus: state.peerConnectionStatus,
}))(AssistActions))

View file

@ -0,0 +1 @@
export { default } from './AssistActions'

View file

@ -0,0 +1,25 @@
import React, { useEffect, useRef } from 'react'
interface Props {
stream: MediaStream | null
muted?: boolean,
width?: number
}
function VideoContainer({ stream, muted = false, width = 280 }: Props) {
const ref = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (ref.current) {
ref.current.srcObject = stream;
}
}, [ ref.current, stream ])
return (
<div>
<video autoPlay ref={ ref } muted={ muted } style={{ width: width }} />
</div>
)
}
export default VideoContainer

View file

@ -0,0 +1 @@
export { default } from './VideoContainer'

View file

@ -0,0 +1 @@
export { default } from './Assist'

View file

@ -25,6 +25,7 @@ import { LAST_7_DAYS } from 'Types/app/period';
import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from '../shared/NoSessionsMessage';
import LiveSessionList from './LiveSessionList'
const AUTOREFRESH_INTERVAL = 10 * 60 * 1000;
@ -134,7 +135,6 @@ export default class BugFinder extends React.PureComponent {
setActiveTab = tab => {
this.props.setActiveTab(tab);
}
render() {
@ -157,12 +157,10 @@ export default class BugFinder extends React.PureComponent {
className="mb-5"
>
<EventFilter />
</div>
{activeFlow && activeFlow.type === 'flows' ?
<FunnelList />
:
<SessionList onMenuItemClick={this.setActiveTab} />
}
</div>
{ activeFlow && activeFlow.type === 'flows' && <FunnelList /> }
{ activeTab.type !== 'live' && <SessionList onMenuItemClick={this.setActiveTab} /> }
{ activeTab.type === 'live' && <LiveSessionList /> }
</div>
</div>
<RehydrateSlidePanel

View file

@ -21,6 +21,7 @@ const customFilterAutoCompleteKeys = ['METADATA', KEYS.CLICK, KEYS.USER_BROWSER,
customFilters: state.getIn([ 'filters', 'customFilters' ]),
variables: state.getIn([ 'customFields', 'list' ]),
sources: state.getIn([ 'customFields', 'sources' ]),
activeTab: state.getIn([ 'sessions', 'activeTab', 'type' ]),
}), {
applyFilter,
setActiveKey,
@ -81,10 +82,11 @@ export default class FilterModal extends React.PureComponent {
};
renderList(type, list) {
const { activeTab } = this.props;
const blocks = [];
for (let j = 0; j < list.length; j++) {
blocks.push(
<div key={`${ j }-block`} className="mr-5" >
<div key={`${ j }-block`} className={cn("mr-5", { [stl.disabled]: activeTab === 'live' && list[j].key !== 'USERID' })} >
{ list[ j ] && this.renderFilterItem(type, list[ j ]) }
</div>
);
@ -136,6 +138,7 @@ export default class FilterModal extends React.PureComponent {
loading = false,
searchedEvents,
searchQuery = '',
activeTab,
} = this.props;
const { query } = this.state;
const reg = getRE(query, 'i');
@ -158,6 +161,8 @@ export default class FilterModal extends React.PureComponent {
const staticFilters = preloadedFilters
.filter(({ value, actualValue }) => !this.props.loading && this.test(actualValue || value))
// console.log('filteredList', filteredList);
return (!displayed ? null :
<div className={ stl.modal }>
{ loading &&
@ -173,22 +178,26 @@ export default class FilterModal extends React.PureComponent {
{ searchQuery &&
<React.Fragment>
{this.renderEventDropdownPart(TYPES.USERID, 'User Id')}
{this.renderEventDropdownPart(TYPES.METADATA, 'Metadata')}
{this.renderEventDropdownPart(TYPES.CONSOLE, 'Errors')}
{this.renderEventDropdownPart(TYPES.CUSTOM, 'Custom Events')}
{this.renderEventDropdownPart(KEYS.USER_COUNTRY, 'Country', _appliedFilterKeys)}
{this.renderEventDropdownPart(KEYS.USER_BROWSER, 'Browser', _appliedFilterKeys)}
{this.renderEventDropdownPart(KEYS.USER_DEVICE, 'Device', _appliedFilterKeys)}
{this.renderEventDropdownPart(TYPES.LOCATION, 'Page')}
{this.renderEventDropdownPart(TYPES.CLICK, 'Click')}
{this.renderEventDropdownPart(TYPES.FETCH, 'Fetch')}
{this.renderEventDropdownPart(TYPES.INPUT, 'Input')}
{this.renderEventDropdownPart(KEYS.USER_OS, 'Operating System', _appliedFilterKeys)}
{this.renderEventDropdownPart(KEYS.REFERRER, 'Referrer', _appliedFilterKeys)}
{this.renderEventDropdownPart(TYPES.GRAPHQL, 'GraphQL')}
{this.renderEventDropdownPart(TYPES.STATEACTION, 'Store Action')}
{this.renderEventDropdownPart(TYPES.REVID, 'Rev ID')}
{activeTab !== 'live' && (
<>
{this.renderEventDropdownPart(TYPES.METADATA, 'Metadata')}
{this.renderEventDropdownPart(TYPES.CONSOLE, 'Errors')}
{this.renderEventDropdownPart(TYPES.CUSTOM, 'Custom Events')}
{this.renderEventDropdownPart(KEYS.USER_COUNTRY, 'Country', _appliedFilterKeys)}
{this.renderEventDropdownPart(KEYS.USER_BROWSER, 'Browser', _appliedFilterKeys)}
{this.renderEventDropdownPart(KEYS.USER_DEVICE, 'Device', _appliedFilterKeys)}
{this.renderEventDropdownPart(TYPES.LOCATION, 'Page')}
{this.renderEventDropdownPart(TYPES.CLICK, 'Click')}
{this.renderEventDropdownPart(TYPES.FETCH, 'Fetch')}
{this.renderEventDropdownPart(TYPES.INPUT, 'Input')}
{this.renderEventDropdownPart(KEYS.USER_OS, 'Operating System', _appliedFilterKeys)}
{this.renderEventDropdownPart(KEYS.REFERRER, 'Referrer', _appliedFilterKeys)}
{this.renderEventDropdownPart(TYPES.GRAPHQL, 'GraphQL')}
{this.renderEventDropdownPart(TYPES.STATEACTION, 'Store Action')}
{this.renderEventDropdownPart(TYPES.REVID, 'Rev ID')}
</>
)}
</React.Fragment>
}
</div>
@ -201,7 +210,7 @@ export default class FilterModal extends React.PureComponent {
<div className={ stl.list }>
{ this.renderList(category.type, category.keys) }
</div>
</div>
</div>
))
}
</div>

View file

@ -88,4 +88,9 @@ h5.title {
& .filterGroup {
width: 205px;
}
}
.disabled {
opacity: 0.5;
pointer-events: none;
}

View file

@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react';
import { fetchLiveList } from 'Duck/sessions';
import { connect } from 'react-redux';
import { NoContent, Loader } from 'UI';
import { List, Map } from 'immutable';
import SessionItem from 'Shared/SessionItem';
interface Props {
loading: Boolean,
list?: List<any>,
fetchLiveList: () => void,
filters: List<any>
}
function LiveSessionList(props: Props) {
const { loading, list, filters } = props;
const [userId, setUserId] = useState(undefined)
useEffect(() => {
props.fetchLiveList();
}, [])
useEffect(() => {
if (filters) {
const userIdFilter = filters.filter(i => i.key === 'USERID').first()
if (userIdFilter)
setUserId(userIdFilter.value[0])
else
setUserId(undefined)
}
}, [filters])
return (
<div>
<NoContent
title={"No live sessions!"}
subtext="Please try changing your search parameters."
icon="exclamation-circle"
show={ !loading && list && list.size === 0}
>
<Loader loading={ loading }>
{list && (userId ? list.filter(i => i.userId === userId) : list).map(session => (
<SessionItem
key={ session.sessionId }
session={ session }
live
// hasUserFilter={hasUserFilter}
/>
))}
</Loader>
</NoContent>
</div>
)
}
export default connect(state => ({
list: state.getIn(['sessions', 'liveSessions']),
loading: state.getIn([ 'sessions', 'loading' ]),
filters: state.getIn([ 'filters', 'appliedFilter', 'filters' ]),
}), { fetchLiveList })(LiveSessionList)

View file

@ -0,0 +1 @@
export { default } from './LiveSessionList'

View file

@ -123,7 +123,7 @@ export default class SessionList extends React.PureComponent {
const { activeTab, allList, total } = this.props;
var filteredList;
if (activeTab.type !== ALL && activeTab.type !== 'bookmark') { // Watchdog sessions
if (activeTab.type !== ALL && activeTab.type !== 'bookmark' && activeTab.type !== 'live') { // Watchdog sessions
filteredList = allList.filter(session => activeTab.fits(session))
} else {
filteredList = allList

View file

@ -72,6 +72,16 @@ function SessionsMenu(props) {
/>
))}
<div className={stl.divider} />
<div className="my-3">
<SideMenuitem
title="Assist"
iconName="person"
active={activeTab.type === 'live'}
onClick={() => onMenuItemClick({ name: 'Assist', type: 'live' })}
/>
</div>
<div className={stl.divider} />
<div className="my-3">
<SideMenuitem
@ -80,7 +90,8 @@ function SessionsMenu(props) {
active={activeTab.type === 'bookmark'}
onClick={() => onMenuItemClick({ name: 'Bookmarks', type: 'bookmark' })}
/>
</div>
</div>
<div className={cn(stl.divider, 'mb-4')} />
<SavedSearchList />
</div>

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { Input, Button, Label } from 'UI';
import { save, edit, update , fetchList } from 'Duck/site';
import { pushNewSite } from 'Duck/user';
import { pushNewSite, setSiteId } from 'Duck/user';
import styles from './siteForm.css';
@connect(state => ({
@ -14,7 +14,8 @@ import styles from './siteForm.css';
edit,
update,
pushNewSite,
fetchList
fetchList,
setSiteId
})
export default class NewSiteForm extends React.PureComponent {
state = {
@ -34,8 +35,12 @@ export default class NewSiteForm extends React.PureComponent {
})
} else {
this.props.save(this.props.site).then(() => {
const { sites } = this.props;
this.props.onClose(null, sites.last())
const { sites } = this.props;
const site = sites.last();
this.props.pushNewSite(site)
this.props.setSiteId(site.id)
this.props.onClose(null, site)
});
}
}

View file

@ -23,11 +23,7 @@ export default class SiteDropdown extends React.PureComponent {
state = { showProductModal: false }
closeModal = (e, newSite) => {
this.setState({ showProductModal: false })
if (newSite) {
this.props.pushNewSite(newSite)
this.props.setSiteId(newSite.id)
}
this.setState({ showProductModal: false })
};
newSite = () => {

View file

@ -0,0 +1,7 @@
.wrapper {
background-color: rgba(255, 255, 255, 1);
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
padding: 5px;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.5);
}

View file

@ -0,0 +1,32 @@
import React from 'react'
import { Icon, Popup } from 'UI'
import { connectPlayer, toggleEvents } from 'Player';
import cn from 'classnames'
import stl from './EventsToggleButton.css'
function EventsToggleButton({ showEvents, toggleEvents }) {
return (
<Popup
trigger={
<button
className={cn("absolute right-0 z-50", stl.wrapper)}
onClick={toggleEvents}
>
<Icon
name={ showEvents ? 'chevron-double-right' : 'chevron-double-left' }
size="12"
/>
</button>
}
content={ showEvents ? 'Hide Events' : 'Show Events' }
size="tiny"
inverted
position="bottom right"
/>
)
}
export default connectPlayer(state => ({
showEvents: !state.showEvents
}), { toggleEvents })(EventsToggleButton)

View file

@ -0,0 +1 @@
export { default } from './EventsToggleButton'

View file

@ -0,0 +1,67 @@
import { useEffect } from 'react';
import { connect } from 'react-redux';
import { Loader } from 'UI';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import {
PlayerProvider,
connectPlayer,
init as initPlayer,
clean as cleanPlayer,
} from 'Player';
import { Controls as PlayerControls } from 'Player';
import Assist from 'Components/Assist'
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import EventsBlock from '../Session_/EventsBlock';
import PlayerBlock from '../Session_/PlayerBlock';
import styles from '../Session_/session.css';
const EventsBlockConnected = connectPlayer(state => ({
currentTimeEventIndex: state.eventListNow.length > 0 ? state.eventListNow.length - 1 : 0,
playing: state.playing,
}))(EventsBlock)
const InitLoader = connectPlayer(state => ({
loading: !state.initialized
}))(Loader);
function WebPlayer ({ showAssist, session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt }) {
useEffect(() => {
initPlayer(session, jwt);
return () => cleanPlayer()
}, [ session.sessionId ]);
// LAYOUT (TODO: local layout state - useContext or something..)
useEffect(() => () => {
toggleFullscreen(false);
closeBottomBlock();
}, [])
return (
<PlayerProvider>
<InitLoader className="flex-1 p-3">
{ showAssist && <Assist session={session} /> }
<PlayerBlockHeader fullscreen={fullscreen}/>
<div className={ styles.session } data-fullscreen={fullscreen}>
<PlayerBlock />
</div>
</InitLoader>
</PlayerProvider>
);
}
export default connect(state => ({
session: state.getIn([ 'sessions', 'current' ]),
showAssist: state.getIn([ 'sessions', 'showChatWindow' ]),
jwt: state.get('jwt'),
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
}), {
toggleFullscreen,
closeBottomBlock,
})(WebPlayer)

View file

@ -6,6 +6,7 @@ import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { Link, NoContent, Loader } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
import LivePlayer from './LivePlayer';
import WebPlayer from './WebPlayer';
import IOSPlayer from './IOSPlayer';
@ -48,7 +49,7 @@ function Session({
<Loader className="flex-1" loading={ loading || sessionId !== session.sessionId }>
{ session.isIOS
? <IOSPlayer session={session} />
: <WebPlayer />
: (session.live ? <LivePlayer /> : <WebPlayer />)
}
</Loader>
</NoContent>

View file

@ -8,13 +8,15 @@ import {
init as initPlayer,
clean as cleanPlayer,
} from 'Player';
import { Controls as PlayerControls } from 'Player';
import { Controls as PlayerControls, toggleEvents } from 'Player';
import cn from 'classnames'
import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
import EventsBlock from '../Session_/EventsBlock';
import PlayerBlock from '../Session_/PlayerBlock';
import styles from '../Session_/session.css';
import EventsToggleButton from './EventsToggleButton';
@ -28,6 +30,19 @@ const InitLoader = connectPlayer(state => ({
loading: !state.initialized
}))(Loader);
const PlayerContentConnected = connectPlayer(state => ({
showEvents: !state.showEvents
}), { toggleEvents })(PlayerContent);
function PlayerContent({ live, fullscreen, showEvents, toggleEvents }) {
return (
<div className={ cn(styles.session, 'relative') } data-fullscreen={fullscreen}>
<PlayerBlock />
{ showEvents && !live && !fullscreen && <EventsBlockConnected player={PlayerControls}/> }
</div>
)
}
function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt }) {
useEffect(() => {
@ -44,10 +59,7 @@ function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscr
<PlayerProvider>
<InitLoader className="flex-1">
<PlayerBlockHeader fullscreen={fullscreen}/>
<div className={ styles.session } data-fullscreen={fullscreen}>
<PlayerBlock />
{ !live && !fullscreen && <EventsBlockConnected player={PlayerControls}/> }
</div>
<PlayerContentConnected fullscreen={fullscreen} live={live} />
</InitLoader>
</PlayerProvider>
);
@ -61,5 +73,5 @@ export default connect(state => ({
}), {
toggleFullscreen,
closeBottomBlock,
})(WebPlayer)
})(WebPlayer)

View file

@ -6,6 +6,7 @@ import {
selectStorageType,
selectStorageListNow,
} from 'Player/store';
import LiveTag from 'Shared/LiveTag';
import { Popup, Icon } from 'UI';
import { toggleInspectorMode } from 'Player';
@ -96,8 +97,7 @@ function getStorageName(type) {
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
}))
@connect((state, props) => ({
showDevTools: state.getIn([ 'user', 'account', 'appearance', 'sessionsDevtools' ]),
@connect((state, props) => ({
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]),
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
@ -118,7 +118,7 @@ export default class Controls extends React.Component {
}
shouldComponentUpdate(nextProps) {
if (nextProps.showDevTools !== this.props.showDevTools ||
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.endTime !== this.props.endTime ||
@ -218,8 +218,7 @@ export default class Controls extends React.Component {
}
render() {
const {
showDevTools,
const {
bottomBlock,
toggleBottomBlock,
live,
@ -247,16 +246,15 @@ export default class Controls extends React.Component {
showLongtasks,
exceptionsCount,
showExceptions,
fullscreen,
skipToIssue,
inspectorMode
fullscreen,
skipToIssue
} = this.props;
// const inspectorMode = bottomBlock === INSPECTOR;
const inspectorMode = bottomBlock === INSPECTOR;
return (
<div className={ styles.controls }>
<Timeline jump={ this.props.jump } />
<div className={ cn(styles.controls, {'px-5 pt-0' : live}) }>
{ !live && <Timeline jump={ this.props.jump } /> }
{ !fullscreen &&
<div className={ styles.buttons } data-is-live={ live }>
{ !live ?
@ -278,10 +276,7 @@ export default class Controls extends React.Component {
</div>
:
<div className={ styles.buttonsLeft }>
<button onClick={ this.goLive } className={ styles.liveTag } data-is-live={ livePlay }>
<Icon name="circle" size="8" marginRight="5" color="white" />
<div>{'Live'}</div>
</button>
<LiveTag isLive={livePlay} />
{'Elapsed'}
<ReduxTime name="time" />
</div>
@ -308,7 +303,7 @@ export default class Controls extends React.Component {
</React.Fragment>
}
<div className={ styles.divider } />
{ !live && showDevTools &&
{ !live &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(NETWORK) }
@ -330,7 +325,7 @@ export default class Controls extends React.Component {
icon="fetch"
/>
}
{ showGraphql &&
{ !live && showGraphql &&
<ControlButton
disabled={disabled}
onClick={ ()=> toggleBottomBlock(GRAPHQL) }
@ -340,7 +335,7 @@ export default class Controls extends React.Component {
icon="vendors/graphql"
/>
}
{ showStorage && showDevTools &&
{ !live && showStorage &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(STORAGE) }
@ -350,7 +345,7 @@ export default class Controls extends React.Component {
icon={ getStorageIconName(storageType) }
/>
}
{ showDevTools &&
{
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(CONSOLE) }
@ -361,7 +356,7 @@ export default class Controls extends React.Component {
hasErrors={ logRedCount > 0 }
/>
}
{ showExceptions && showDevTools &&
{ showExceptions &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(EXCEPTIONS) }
@ -372,7 +367,7 @@ export default class Controls extends React.Component {
hasErrors={ exceptionsCount > 0 }
/>
}
{ !live && showDevTools && showStack &&
{ showStack &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(STACKEVENTS) }
@ -383,7 +378,7 @@ export default class Controls extends React.Component {
hasErrors={ stackRedCount > 0 }
/>
}
{ showProfiler && showDevTools &&
{ !live && showProfiler &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(PROFILER) }
@ -392,15 +387,18 @@ export default class Controls extends React.Component {
label="Profiler"
icon="code"
/>
}
{
!live &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(PERFORMANCE) }
active={ bottomBlock === PERFORMANCE }
label="Performance"
icon="tachometer-slow"
/>
}
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(PERFORMANCE) }
active={ bottomBlock === PERFORMANCE }
label="Performance"
icon="tachometer-slow"
/>
{ showLongtasks &&
{ !live && showLongtasks &&
<ControlButton
disabled={ disabled }
onClick={ () => toggleBottomBlock(LONGTASKS) }
@ -421,13 +419,15 @@ export default class Controls extends React.Component {
</React.Fragment>
}
<ControlButton
// disabled={ disabled && !inspectorMode }
active={ bottomBlock === INSPECTOR }
onClick={ toggleInspectorMode }
icon={ inspectorMode ? 'close' : 'inspect' }
label="Inspect"
/>
{!live && (
<ControlButton
disabled={ disabled && !inspectorMode }
active={ bottomBlock === INSPECTOR }
onClick={ () => toggleBottomBlock(INSPECTOR) }
icon={ inspectorMode ? 'close' : 'inspect' }
label="Inspect"
/>
)}
</div>
</div>
}

View file

@ -64,8 +64,7 @@ const getPointerIcon = (type) => {
fetchList: state.fetchList,
}))
@connect(state => ({
issues: state.getIn([ 'sessions', 'current', 'issues' ]),
showDevTools: state.getIn([ 'user', 'account', 'appearance', 'sessionsDevtools' ]),
issues: state.getIn([ 'sessions', 'current', 'issues' ]),
clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) &&
state.getIn([ 'sessions', 'current', 'clickRageTime' ]),
returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) &&
@ -102,8 +101,7 @@ export default class Timeline extends React.PureComponent {
live,
logList,
exceptionsList,
resourceList,
showDevTools,
resourceList,
clickRageTime,
stackList,
fetchList,
@ -255,7 +253,7 @@ export default class Timeline extends React.PureComponent {
}
/>
*/ }
{ showDevTools && exceptionsList
{ exceptionsList
.map(e => (
<div
key={ e.key }
@ -297,7 +295,7 @@ export default class Timeline extends React.PureComponent {
// />
))
}
{ showDevTools && logList
{ logList
.map(l => l.isRed() && (
<div
key={ l.key }
@ -352,7 +350,7 @@ export default class Timeline extends React.PureComponent {
// />
))
}
{ showDevTools && resourceList
{ resourceList
.filter(r => r.isRed() || r.isYellow())
.map(r => (
<div
@ -402,7 +400,7 @@ export default class Timeline extends React.PureComponent {
// />
))
}
{ showDevTools && fetchList
{ fetchList
.filter(e => e.isRed())
.map(e => (
<div
@ -443,7 +441,7 @@ export default class Timeline extends React.PureComponent {
// />
))
}
{ showDevTools && stackList
{ stackList
.filter(e => e.isRed())
.map(e => (
<div

View file

@ -9,6 +9,7 @@ import { attach as attachPlayer, Controls as PlayerControls, connectPlayer } fro
import Controls from './Controls';
import stl from './player.css';
import AutoplayTimer from '../AutoplayTimer';
import EventsToggleButton from '../../Session/EventsToggleButton';
const ScreenWrapper = withOverlay()(React.memo(() => <div className={ stl.screenWrapper } />));
@ -18,9 +19,10 @@ const ScreenWrapper = withOverlay()(React.memo(() => <div className={ stl.screen
loading: state.messagesLoading,
disconnected: state.disconnected,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode,
inspectorMode: state.inspectorMode,
removeOverlay: !state.messagesLoading && state.inspectorMode || state.live,
completed: state.completed,
autoplay: state.autoplay
autoplay: state.autoplay,
live: state.live
}))
@connect(state => ({
//session: state.getIn([ 'sessions', 'current' ]),
@ -96,7 +98,7 @@ export default class Player extends React.PureComponent {
className,
playing,
disabled,
inspectorMode,
removeOverlay,
bottomBlockIsActive,
loading,
disconnected,
@ -105,6 +107,7 @@ export default class Player extends React.PureComponent {
completed,
autoplay,
nextId,
live,
} = this.props;
return (
@ -124,8 +127,9 @@ export default class Player extends React.PureComponent {
// label="Esc"
// />
}
<div className={ stl.playerView }>
{ !inspectorMode &&
{!live && !fullscreen && <EventsToggleButton /> }
<div className="relative flex-1">
{ !removeOverlay &&
<div
className={ stl.overlay }
onClick={ disabled ? null : this.togglePlay }

View file

@ -14,6 +14,7 @@ import { fetchList as fetchListIntegration } from 'Duck/integrations/actions';
import cls from './playerBlockHeader.css';
import Issues from './Issues/Issues';
import Autoplay from './Autoplay';
import AssistActions from '../Assist/components/AssistActions';
const SESSIONS_ROUTE = sessionsRoute();
@ -89,7 +90,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
live,
disabled,
jiraConfig,
fullscreen
fullscreen,
} = this.props;
return (
@ -112,31 +113,36 @@ export default class PlayerBlockHeader extends React.PureComponent {
<HeaderInfo icon={ osIcon(userOs) } label={ userOs } />
<div className='ml-auto flex items-center'>
<Autoplay />
<div className={ cls.divider } />
<IconButton
className="mr-2"
tooltip="Bookmark"
onClick={ this.toggleFavorite }
loading={ loading }
icon={ favorite ? 'star-solid' : 'star' }
// label={ favorite ? 'Favourited' : 'Favourite' }
plain
/>
<SharePopup
entity="sessions"
id={ sessionId }
trigger={
{ live && <AssistActions isLive userId={userId} /> }
{ !live && (
<>
<Autoplay />
<div className={ cls.divider } />
<IconButton
className="mr-2"
tooltip="Share Session"
disabled={ disabled }
icon={ 'share-alt' }
//label="Share"
tooltip="Bookmark"
onClick={ this.toggleFavorite }
loading={ loading }
icon={ favorite ? 'star-solid' : 'star' }
// label={ favorite ? 'Favourited' : 'Favourite' }
plain
/>
}
/>
<SharePopup
entity="sessions"
id={ sessionId }
trigger={
<IconButton
className="mr-2"
tooltip="Share Session"
disabled={ disabled }
icon={ 'share-alt' }
//label="Share"
plain
/>
}
/>
</>
)}
{ !live && jiraConfig && jiraConfig.token && <Issues sessionId={ sessionId } /> }
</div>
</div>

View file

@ -0,0 +1,33 @@
@keyframes fade {
0% { opacity: 1}
50% { opacity: 0}
100% { opacity: 1}
}
.liveTag {
cursor: pointer;
user-select: none;
height: 26px;
width: 56px;
border-radius: 3px;
background-color: $gray-light;
display: flex;
align-items: center;
justify-content: center;
color: $gray-dark;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 1px;
margin-right: 10px;
& svg {
fill: $gray-dark;
}
&[data-is-live=true] {
background-color: #42AE5E;
color: white;
& svg {
fill: white;
animation: fade 1s infinite;
}
}
}

View file

@ -0,0 +1,19 @@
import React from 'react'
import { Icon } from 'UI'
import stl from './LiveTag.css'
interface Props {
onClick: () => void,
isLive: Boolean
}
function LiveTag({ isLive, onClick }: Props) {
return (
<button onClick={ onClick } className={ stl.liveTag } data-is-live={ isLive }>
<Icon name="circle" size="8" marginRight="5" color="white" />
<div>{'Live'}</div>
</button>
)
}
export default LiveTag

View file

@ -0,0 +1 @@
export { default } from './LiveTag'

View file

@ -0,0 +1,29 @@
import React, { useState, useEffect } from 'react'
import { Duration } from 'luxon';
interface Props {
startTime: any,
className: string
}
function Counter({ startTime, className }: Props) {
let intervalId;
const [duration, setDuration] = useState(new Date().getTime() - startTime)
useEffect(() => {
if (!intervalId) {
intervalId = setInterval(() => {
setDuration(duration + 1000)
}, 1000)
}
return () => clearInterval(intervalId)
}, [duration])
return (
<div className={className}>
{startTime && Duration.fromMillis(duration).toFormat('m:ss')}
</div>
)
}
export default Counter

View file

@ -14,6 +14,9 @@ import { toggleFavorite } from 'Duck/sessions';
import { session as sessionRoute } from 'App/routes';
import { durationFormatted, formatTimeOrDate } from 'App/date';
import stl from './sessionItem.css';
import LiveTag from 'Shared/LiveTag';
import { session } from '../../../routes';
import Counter from './Counter'
const Label = ({ label = '', color = 'color-gray-medium'}) => (
<div className={ cn('font-light text-sm', color)}>{label}</div>
@ -51,7 +54,8 @@ export default class SessionItem extends React.PureComponent {
favorite,
userDeviceType,
userUuid,
userNumericHash,
userNumericHash,
live
},
timezone,
onUserClick,
@ -85,22 +89,30 @@ export default class SessionItem extends React.PureComponent {
</div>
</div>
<div className="flex flex-col items-center px-4" style={{ width: '150px'}}>
<div className="text-xl">{ formattedDuration }</div>
<div className="text-xl">
{ live ? <Counter startTime={startedAt} /> : formattedDuration }
</div>
<Label label="Duration" />
</div>
<div className="flex flex-col items-center px-4">
<div className={ stl.count }>{ eventsCount }</div>
<Label label={ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } />
</div>
{!live && (
<div className="flex flex-col items-center px-4">
<div className={ stl.count }>{ eventsCount }</div>
<Label label={ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' } />
</div>
)}
</div>
<div className="flex items-center">
<div className="flex flex-col items-center px-4">
<div className={ cn(stl.count, { "color-gray-medium": errorsCount === 0 }) } >{ errorsCount }</div>
<Label label="Errors" color={errorsCount > 0 ? '' : 'color-gray-medium'} />
</div>
{!live && (
<div className="flex flex-col items-center px-4">
<div className={ cn(stl.count, { "color-gray-medium": errorsCount === 0 }) } >{ errorsCount }</div>
<Label label="Errors" color={errorsCount > 0 ? '' : 'color-gray-medium'} />
</div>
)}
{ live && <LiveTag isLive={true} /> }
<div className={ cn(stl.iconDetails, 'px-4') }>
<div

View file

@ -12,6 +12,7 @@ const INIT = 'sessions/INIT';
const FETCH_LIST = new RequestTypes('sessions/FETCH_LIST');
const FETCH = new RequestTypes('sessions/FETCH');
const FETCH_FAVORITE_LIST = new RequestTypes('sessions/FETCH_FAVORITE_LIST');
const FETCH_LIVE_LIST = new RequestTypes('sessions/FETCH_LIVE_LIST');
const TOGGLE_FAVORITE = new RequestTypes('sessions/TOGGLE_FAVORITE');
const FETCH_ERROR_STACK = new RequestTypes('sessions/FETCH_ERROR_STACK');
const SORT = 'sessions/SORT';
@ -19,6 +20,7 @@ const REDEFINE_TARGET = 'sessions/REDEFINE_TARGET';
const SET_TIMEZONE = 'sessions/SET_TIMEZONE';
const SET_EVENT_QUERY = 'sessions/SET_EVENT_QUERY';
const SET_AUTOPLAY_VALUES = 'sessions/SET_AUTOPLAY_VALUES';
const TOGGLE_CHAT_WINDOW = 'sessions/TOGGLE_CHAT_WINDOW';
const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
@ -35,7 +37,9 @@ const initialState = Map({
errorStack: List(),
eventsIndex: [],
sourcemapUploaded: true,
filteredEvents: null
filteredEvents: null,
showChatWindow: false,
liveSessions: List()
});
const reducer = (state = initialState, action = {}) => {
@ -49,6 +53,11 @@ const reducer = (state = initialState, action = {}) => {
: state;
case FETCH_ERROR_STACK.SUCCESS:
return state.set('errorStack', List(action.data.trace).map(ErrorStack)).set('sourcemapUploaded', action.data.sourcemapUploaded)
case FETCH_LIVE_LIST.SUCCESS:
// const { sessions, total } = action.data;
const liveList = List(action.data).map(s => new Session({...s, live: true}));
return state
.set('liveSessions', liveList)
case FETCH_LIST.SUCCESS:
const { sessions, total } = action.data;
const list = List(sessions).map(Session);
@ -98,8 +107,7 @@ const reducer = (state = initialState, action = {}) => {
.set('sessionIds', list.map(({ sessionId }) => sessionId ).toJS())
.set('total', total)
.set('keyMap', keyMap)
.set('wdTypeCount', wdTypeCount);
.set('wdTypeCount', wdTypeCount);
case SET_AUTOPLAY_VALUES: {
const sessionIds = state.get('sessionIds')
const currentSessionId = state.get('current').sessionId
@ -194,6 +202,9 @@ const reducer = (state = initialState, action = {}) => {
.set('sessionIds', allList.map(({ sessionId }) => sessionId ).toJS())
case SET_TIMEZONE:
return state.set('timezone', action.timezone)
case TOGGLE_CHAT_WINDOW:
console.log(action)
return state.set('showChatWindow', action.state)
default:
return state;
}
@ -252,6 +263,20 @@ export function fetchFavoriteList() {
};
}
export function fetchLiveList() {
return {
types: FETCH_LIVE_LIST.toArray(),
call: client => client.get('/assist/sessions'),
};
}
export function toggleChatWindow(state) {
return {
type: TOGGLE_CHAT_WINDOW,
state
};
}
export function sort(sortKey, sign = 1, listName = 'list') {
return {
type: SORT,

View file

@ -1,4 +1,3 @@
//@flow
import { Decoder } from "syncod";
import logger from 'App/logger';
@ -24,43 +23,68 @@ import MouseManager from './managers/MouseManager';
import PerformanceTrackManager from './managers/PerformanceTrackManager';
import WindowNodeCounter from './managers/WindowNodeCounter';
import ActivityManager from './managers/ActivityManager';
import AssistManager from './managers/AssistManager';
import MessageGenerator from './MessageGenerator';
import MessageReader from './MessageReader';
import { INITIAL_STATE as PARENT_INITIAL_STATE } from './StatedScreen';
import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './StatedScreen';
import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './managers/AssistManager';
import type { TimedMessage } from './Timed';
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
import type { SkipInterval } from './managers/ActivityManager';
const LIST_NAMES = [ "redux", "mobx", "vuex", "ngrx", "graphql", "exceptions", "profiles", "longtasks" ]
const LIST_NAMES = [ "redux", "mobx", "vuex", "ngrx", "graphql", "exceptions", "profiles", "longtasks" ] as const;
const LISTS_INITIAL_STATE = {};
LIST_NAMES.forEach(name => {
LISTS_INITIAL_STATE[`${name}ListNow`] = [];
LISTS_INITIAL_STATE[`${name}List`] = [];
})
export const INITIAL_STATE = {
...PARENT_INITIAL_STATE,
export interface State extends SuperState, AssistState {
performanceChartData: PerformanceChartPoint[],
skipIntervals: SkipInterval[],
connType?: string,
connBandwidth?: number,
location?: string,
performanceChartTime?: number,
domContentLoadedTime?: any,
domBuildingTime?: any,
loadTime?: any,
}
export const INITIAL_STATE: State = {
...SUPER_INITIAL_STATE,
...LISTS_INITIAL_STATE,
...ASSIST_INITIAL_STATE,
performanceChartData: [],
skipIntervals: [],
};
type ListsObject = {
[key in typeof LIST_NAMES[number]]: ListWalker<any> //
}
function initLists() {
const lists = {};
function initLists(): ListsObject {
const lists: Partial<ListsObject> = {} ;
for (var i = 0; i < LIST_NAMES.length; i++) {
lists[ LIST_NAMES[i] ] = new ListWalker();
}
return lists;
return lists as ListsObject;
}
import type {
Message,
SetLocation,
SetTitle,
SetPageLocation,
ConnectionInformation,
SetViewportSize,
SetViewportScroll,
} from './messages';
interface Timed { //TODO: to common space
time: number;
}
type ReduxDecoded = Timed & {
action: {},
@ -70,71 +94,71 @@ type ReduxDecoded = Timed & {
export default class MessageDistributor extends StatedScreen {
// TODO: consistent with the other data-lists
#locationEventManager: ListWalker<> = new ListWalker();
#locationManager: ListWalker<SetLocation> = new ListWalker();
#loadedLocationManager: ListWalker<SetLocation> = new ListWalker();
#titleManager: ListWalker<SetTitle> = new ListWalker();
#connectionInfoManger: ListWalker<ConnectionInformation> = new ListWalker();
#performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager();
#windowNodeCounter: WindowNodeCounter = new WindowNodeCounter();
#clickManager: ListWalker = new ListWalker();
private readonly locationEventManager: ListWalker<any>/*<LocationEvent>*/ = new ListWalker();
private readonly locationManager: ListWalker<SetPageLocation & Timed> = new ListWalker();
private readonly loadedLocationManager: ListWalker<SetPageLocation & Timed> = new ListWalker();
private readonly connectionInfoManger: ListWalker<ConnectionInformation & Timed> = new ListWalker();
private readonly performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager();
private readonly windowNodeCounter: WindowNodeCounter = new WindowNodeCounter();
private readonly clickManager: ListWalker<Timed> = new ListWalker();
#resizeManager: ListWalker<SetViewportSize> = new ListWalker();
#pagesManager: PagesManager;
#mouseManager: MouseManager;
private readonly resizeManager: ListWalker<SetViewportSize & Timed> = new ListWalker([]);
private readonly pagesManager: PagesManager;
private readonly mouseManager: MouseManager;
private readonly assistManager: AssistManager;
#scrollManager: ListWalker<SetViewportScroll> = new ListWalker();
private readonly scrollManager: ListWalker<SetViewportScroll & Timed> = new ListWalker();
#decoder = new Decoder();
#lists = initLists();
private readonly decoder = new Decoder();
private readonly lists = initLists();
#activirtManager: ActivityManager;
private activirtManager: ActivityManager | null = null;
#sessionStart: number;
#navigationStartOffset: number = 0;
#lastMessageTime: number = 0;
private readonly sessionStart: number;
private navigationStartOffset: number = 0;
private lastMessageTime: number = 0;
constructor(sess: any /*Session*/, jwt: string) {
constructor(private readonly session: any /*Session*/, jwt: string) {
super();
this.#pagesManager = new PagesManager(this, sess.isMobile)
this.#mouseManager = new MouseManager(this);
this.pagesManager = new PagesManager(this, this.session.isMobile)
this.mouseManager = new MouseManager(this);
this.assistManager = new AssistManager(session, this);
this.#activirtManager = new ActivityManager(sess.duration.milliseconds);
this.sessionStart = this.session.startedAt;
this.#sessionStart = sess.startedAt;
/* == REFACTOR_ME == */
const eventList = sess.events.toJSON();
initListsDepr({
event: eventList,
stack: sess.stackEvents.toJSON(),
resource: sess.resources.toJSON(),
});
eventList.forEach(e => {
if (e.type === EVENT_TYPES.LOCATION) { //TODO type system
this.#locationEventManager.add(e);
}
if (e.type === EVENT_TYPES.CLICK) {
this.#clickManager.add(e);
}
});
sess.errors.forEach(e => {
this.#lists.exceptions.add(e);
});
/* === */
if (sess.live) {
// const sockUrl = `wss://live.openreplay.com/1/${ sess.siteId }/${ sess.sessionId }/${ jwt }`;
// this.#subscribeOnMessages(sockUrl);
if (this.session.live) {
// const sockUrl = `wss://live.openreplay.com/1/${ this.session.siteId }/${ this.session.sessionId }/${ jwt }`;
// this.subscribeOnMessages(sockUrl);
initListsDepr({})
this.assistManager.connect();
} else {
this._loadMessages(sess.mobsUrl);
this.activirtManager = new ActivityManager(this.session.duration.milliseconds);
/* == REFACTOR_ME == */
const eventList = this.session.events.toJSON();
initListsDepr({
event: eventList,
stack: this.session.stackEvents.toJSON(),
resource: this.session.resources.toJSON(),
});
eventList.forEach(e => {
if (e.type === EVENT_TYPES.LOCATION) { //TODO type system
this.locationEventManager.add(e);
}
if (e.type === EVENT_TYPES.CLICK) {
this.clickManager.add(e);
}
});
this.session.errors.forEach(e => {
this.lists.exceptions.add(e);
});
/* === */
this.loadMessages();
}
}
// #subscribeOnMessages(sockUrl) {
// subscribeOnMessages(sockUrl) {
// this.setMessagesLoading(true);
// const socket = new WebSocket(sockUrl);
// socket.binaryType = 'arraybuffer';
@ -147,36 +171,35 @@ export default class MessageDistributor extends StatedScreen {
// const msgs = [];
// messageGenerator // parseBuffer(msgs, data);
// // TODO: count indexes. Now will not work due to wrong indexes
// //msgs.forEach(this.#distributeMessage);
// //msgs.forEach(this.distributeMessage);
// this.setMessagesLoading(false);
// this.setDisconnected(false);
// }
// this._socket = socket;
// }
_loadMessages(fileUrl): void {
private loadMessages(): void {
const fileUrl: string = this.session.mobsUrl;
this.setMessagesLoading(true);
window.fetch(fileUrl)
.then(r => r.arrayBuffer())
.then(b => {
const mGen = new MessageGenerator(new Uint8Array(b), this.#sessionStart);
let mCount = 0;
const msgs = [];
const r = new MessageReader(new Uint8Array(b), this.sessionStart);
const msgs: Array<Message> = [];
while (mGen.hasNext()) {
mCount++;
const next = mGen.next();
while (r.hasNext()) {
const next = r.next();
if (next != null) {
this.#lastMessageTime = next[0].time;
this.#distributeMessage(next[0], next[1]);
this.lastMessageTime = next[0].time;
this.distributeMessage(next[0], next[1]);
msgs.push(next[0]);
}
}
// Hack for upet (TODO: fix ordering in one mutation (removes first))
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation (removes first))
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
//const createNodeTypes = ["create_text_node", "create_element_node"];
this.#pagesManager.sort((m1, m2) =>{
this.pagesManager.sort((m1, m2) =>{
if (m1.time === m2.time) {
if (m1.tp === "remove_node" && m2.tp !== "remove_node") {
if (headChildrenIds.includes(m1.id)) {
@ -198,22 +221,22 @@ export default class MessageDistributor extends StatedScreen {
}
return 0;
})
//
logger.info("Messages count: ", mCount, msgs);
logger.info("Messages count: ", msgs.length, msgs);
const stateToUpdate = {
performanceChartData: this.#performanceTrackManager.chartData,
performanceAvaliability: this.#performanceTrackManager.avaliability,
const stateToUpdate: {[key:string]: any} = {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
};
this.#activirtManager.end();
stateToUpdate.skipIntervals = this.#activirtManager.list;
this.activirtManager?.end();
stateToUpdate.skipIntervals = this.activirtManager?.list || [];
LIST_NAMES.forEach(key => {
stateToUpdate[ `${ key }List` ] = this.#lists[ key ].list;
stateToUpdate[ `${ key }List` ] = this.lists[ key ].list;
});
update(stateToUpdate);
this.#windowNodeCounter.reset();
this.windowNodeCounter.reset();
this.setMessagesLoading(false);
})
@ -224,25 +247,25 @@ export default class MessageDistributor extends StatedScreen {
});
}
move(t: number, index: ?number):void {
const stateToUpdate = {};
move(t: number, index?: number):void {
const stateToUpdate: Partial<State> = {};
/* == REFACTOR_ME == */
const lastLoadedLocationMsg = this.#loadedLocationManager.moveToLast(t, index);
const lastLoadedLocationMsg = this.loadedLocationManager.moveToLast(t, index);
if (!!lastLoadedLocationMsg) {
setListsStartTime(lastLoadedLocationMsg.time)
this.#navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.#sessionStart;
this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart;
}
const llEvent = this.#locationEventManager.moveToLast(t, index);
const llEvent = this.locationEventManager.moveToLast(t, index);
if (!!llEvent) {
if (llEvent.domContentLoadedTime != null) {
stateToUpdate.domContentLoadedTime = {
time: llEvent.domContentLoadedTime + this.#navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & setLocation: add navigationStart to db)
time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db)
value: llEvent.domContentLoadedTime,
}
}
if (llEvent.loadTime != null) {
stateToUpdate.loadTime = {
time: llEvent.loadTime + this.#navigationStartOffset,
time: llEvent.loadTime + this.navigationStartOffset,
value: llEvent.loadTime,
}
}
@ -251,28 +274,24 @@ export default class MessageDistributor extends StatedScreen {
}
}
/* === */
const lastLocationMsg = this.#locationManager.moveToLast(t, index);
const lastLocationMsg = this.locationManager.moveToLast(t, index);
if (!!lastLocationMsg) {
stateToUpdate.location = lastLocationMsg.url;
}
const lastTitleMsg = this.#titleManager.moveToLast(t, index);
if (!!lastTitleMsg) {
stateToUpdate.title = lastTitleMsg.title;
}
const lastConnectionInfoMsg = this.#connectionInfoManger.moveToLast(t, index);
const lastConnectionInfoMsg = this.connectionInfoManger.moveToLast(t, index);
if (!!lastConnectionInfoMsg) {
stateToUpdate.connType = lastConnectionInfoMsg.type;
stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink;
}
const lastPerformanceTrackMessage = this.#performanceTrackManager.moveToLast(t, index);
const lastPerformanceTrackMessage = this.performanceTrackManager.moveToLast(t, index);
if (!!lastPerformanceTrackMessage) {
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
}
LIST_NAMES.forEach(key => {
const lastMsg = this.#lists[ key ].moveToLast(t, key === 'exceptions' ? null : index);
const lastMsg = this.lists[ key ].moveToLast(t, key === 'exceptions' ? undefined : index);
if (lastMsg != null) {
stateToUpdate[`${key}ListNow`] = this.#lists[ key ].listNow;
stateToUpdate[`${key}ListNow`] = this.lists[ key ].listNow;
}
});
@ -280,19 +299,21 @@ export default class MessageDistributor extends StatedScreen {
/* Sequence of the managers is important here */
// Preparing the size of "screen"
const lastResize = this.#resizeManager.moveToLast(t, index);
const lastResize = this.resizeManager.moveToLast(t, index);
if (!!lastResize) {
this.setSize(lastResize)
}
this.#pagesManager.moveReady(t).then(() => {
this.pagesManager.moveReady(t).then(() => {
const lastScroll = this.#scrollManager.moveToLast(t, index);
const lastScroll = this.scrollManager.moveToLast(t, index);
// @ts-ignore ??can't see double inheritance
if (!!lastScroll && this.window) {
// @ts-ignore
this.window.scrollTo(lastScroll.x, lastScroll.y);
}
// Moving mouse and setting :hover classes on ready view
this.#mouseManager.move(t);
const lastClick = this.#clickManager.moveToLast(t);
this.mouseManager.move(t);
const lastClick = this.clickManager.moveToLast(t);
// if (!!lastClick) {
// this.cursor.click();
// }
@ -305,7 +326,7 @@ export default class MessageDistributor extends StatedScreen {
const decoded = {};
try {
keys.forEach(key => {
decoded[ key ] = this.#decoder.decode(msg[ key ]);
decoded[ key ] = this.decoder.decode(msg[ key ]);
});
} catch (e) {
logger.error("Error on message decoding: ", e, msg);
@ -315,7 +336,7 @@ export default class MessageDistributor extends StatedScreen {
}
/* Binded */
#distributeMessage = (msg: Message, index: number): void => {
distributeMessage = (msg: TimedMessage, index: number): void => {
if ([
"mouse_move",
"set_input_value",
@ -323,24 +344,13 @@ export default class MessageDistributor extends StatedScreen {
"set_viewport_size",
"set_viewport_scroll",
].includes(msg.tp)) {
this.#activirtManager.updateAcctivity(msg.time);
this.activirtManager?.updateAcctivity(msg.time);
}
//const index = #i + index; //?
//const index = i + index; //?
let decoded;
const time = msg.time;
switch (msg.tp) {
/* Lists: */
case "resource_timing":
logger.log(msg)
listAppend("resource", Resource({
time,
duration: msg.duration,
ttfb: msg.ttfb,
url: msg.url,
initiator: msg.initiator,
index,
}));
break;
case "console_log":
if (msg.level === 'debug') break;
listAppend("log", Log({
@ -359,60 +369,57 @@ export default class MessageDistributor extends StatedScreen {
status: msg.status,
duration: msg.duration,
type: TYPES.FETCH,
time: msg.timestamp - this.#sessionStart, //~
time: msg.timestamp - this.sessionStart, //~
index,
}));
break;
/* */
case "set_page_location":
this.#locationManager.add(msg);
this.locationManager.add(msg);
if (msg.navigationStart > 0) {
this.#loadedLocationManager.add(msg);
this.loadedLocationManager.add(msg);
}
break;
case "set_title":
this.#titleManager.add(msg);
break;
case "set_viewport_size":
this.#resizeManager.add(msg);
this.resizeManager.add(msg);
break;
case "mouse_move":
this.#mouseManager.add(msg);
this.mouseManager.add(msg);
break;
case "set_viewport_scroll":
this.#scrollManager.add(msg);
this.scrollManager.add(msg);
break;
case "performance_track":
this.#performanceTrackManager.add(msg);
this.performanceTrackManager.add(msg);
break;
case "set_page_visibility":
this.#performanceTrackManager.handleVisibility(msg)
this.performanceTrackManager.handleVisibility(msg)
break;
case "connection_information":
this.#connectionInfoManger.add(msg);
this.connectionInfoManger.add(msg);
break;
case "o_table":
this.#decoder.set(msg.key, msg.value);
this.decoder.set(msg.key, msg.value);
break;
case "redux":
decoded = this._decodeMessage(msg, ["state", "action"]);
logger.log(decoded)
if (decoded != null) {
this.#lists.redux.add(decoded);
this.lists.redux.add(decoded);
}
break;
case "ng_rx":
decoded = this._decodeMessage(msg, ["state", "action"]);
logger.log(decoded)
if (decoded != null) {
this.#lists.ngrx.add(decoded);
this.lists.ngrx.add(decoded);
}
break;
case "vuex":
decoded = this._decodeMessage(msg, ["state", "mutation"]);
logger.log(decoded)
if (decoded != null) {
this.#lists.vuex.add(decoded);
this.lists.vuex.add(decoded);
}
break;
case "mob_x":
@ -420,59 +427,62 @@ export default class MessageDistributor extends StatedScreen {
logger.log(decoded)
if (decoded != null) {
this.#lists.mobx.add(decoded);
this.lists.mobx.add(decoded);
}
break;
case "graph_ql":
// @ts-ignore some hack? TODO: remove
msg.duration = 0;
this.#lists.graphql.add(msg);
this.lists.graphql.add(msg);
break;
case "profiler":
this.#lists.profiles.add(msg);
this.lists.profiles.add(msg);
break;
case "long_task":
this.#lists.longtasks.add({
this.lists.longtasks.add({
...msg,
time: msg.timestamp - this.#sessionStart,
time: msg.timestamp - this.sessionStart,
});
break;
default:
switch (msg.tp){
case "create_document":
this.#windowNodeCounter.reset();
this.#performanceTrackManager.setCurrentNodesCount(this.#windowNodeCounter.count);
this.windowNodeCounter.reset();
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
case "create_text_node":
case "create_element_node":
this.#windowNodeCounter.addNode(msg.id, msg.parentID);
this.#performanceTrackManager.setCurrentNodesCount(this.#windowNodeCounter.count);
this.windowNodeCounter.addNode(msg.id, msg.parentID);
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
case "move_node":
this.#windowNodeCounter.moveNode(msg.id, msg.parentID);
this.#performanceTrackManager.setCurrentNodesCount(this.#windowNodeCounter.count);
this.windowNodeCounter.moveNode(msg.id, msg.parentID);
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
case "remove_node":
this.#windowNodeCounter.removeNode(msg.id);
this.#performanceTrackManager.setCurrentNodesCount(this.#windowNodeCounter.count);
this.windowNodeCounter.removeNode(msg.id);
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
break;
}
this.#pagesManager.add(msg);
this.pagesManager.add(msg);
break;
}
}
getLastMessageTime():number {
return this.#lastMessageTime;
return this.lastMessageTime;
}
getFirstMessageTime():number {
return 0; //this.#pagesManager.minTime;
return 0; //this.pagesManager.minTime;
}
// TODO: clean managers?
clean() {
// @ts-ignore
super.clean();
if (this._socket) this._socket.close();
//if (this._socket) this._socket.close();
update(INITIAL_STATE);
this.assistManager.clear();
}
}

View file

@ -1,82 +0,0 @@
import type { TimedMessage } from './Timed';
import logger from 'App/logger';
import readMessage from './messages';
function needSkipMessage(data: Uint8Array, p: number, pLast: number): boolean {
for (let i = 7; i >= 0; i--) {
if (data[ p + i ] !== data[ pLast + i ]) {
return data[ p + i ] - data[ pLast + i ] < 0
}
}
return true
}
export default class MessageGenerator {
#data: Uint8Array;
#p: number = 0;
#pLastMessageID: number = 0;
#startTime: number;
#currentTime: ?number;
#error: boolean = false;
constructor(data: Uint8Array, startTime: number) {
this.#startTime = startTime;
this.#data = data;
}
_needSkipMessage():boolean {
if (this.#p === 0) return false;
for (let i = 7; i >= 0; i--) {
if (this.#data[ this.#p + i ] !== this.#data[ this.#pLastMessageID + i ]) {
return this.#data[ this.#p + i ] - this.#data[ this.#pLastMessageID + i ] < 0;
}
}
return true;
}
_readMessage(): ?Message {
this.#p += 8;
try {
let msg
[ msg, this.#p ] = readMessage(this.#data, this.#p);
return msg;
} catch (e) {
this.#error = true;
logger.error("Read message error:", e);
return null;
}
}
hasNext():boolean {
return !this.#error && this.#data.length > this.#p;
}
next(): ?[ TimedMessage, number] {
if (!this.hasNext()) {
return null;
}
while (this._needSkipMessage()) {
this._readMessage();
}
this.#pLastMessageID = this.#p;
const msg = this._readMessage();
if (!msg) {
return null;
}
if (msg.tp === "timestamp") {
// if (this.#startTime == null) {
// this.#startTime = msg.timestamp
// }
this.#currentTime = msg.timestamp - this.#startTime;
} else {
msg.time = this.#currentTime;
msg._index = this.#pLastMessageID;
return [msg, this.#pLastMessageID];
}
}
}

View file

@ -0,0 +1,80 @@
import type { TimedMessage, Indexed } from './Timed';
import logger from 'App/logger';
import readMessage, { Message } from './messages';
import PrimitiveReader from './PrimitiveReader';
// function needSkipMessage(data: Uint8Array, p: number, pLast: number): boolean {
// for (let i = 7; i >= 0; i--) {
// if (data[ p + i ] !== data[ pLast + i ]) {
// return data[ p + i ] - data[ pLast + i ] < 0
// }
// }
// return true
// }
export default class MessageReader extends PrimitiveReader {
private pLastMessageID: number = 0;
private currentTime: number = 0;
public error: boolean = false;
constructor(data: Uint8Array, private readonly startTime: number) {
super(data);
}
private needSkipMessage(): boolean {
if (this.p === 0) return false;
for (let i = 7; i >= 0; i--) {
if (this.buf[ this.p + i ] !== this.buf[ this.pLastMessageID + i ]) {
return this.buf[ this.p + i ] - this.buf[ this.pLastMessageID + i ] < 0;
}
}
return true;
}
private readMessage(): Message | null {
this.skip(8);
try {
let msg
msg = readMessage(this);
return msg;
} catch (e) {
this.error = true;
logger.error("Read message error:", e);
return null;
}
}
hasNext():boolean {
return !this.error && this.buf.length > this.p;
}
next(): [ TimedMessage, number] | null {
if (!this.hasNext()) {
return null;
}
while (this.needSkipMessage()) {
this.readMessage();
}
this.pLastMessageID = this.p;
const msg = this.readMessage();
if (!msg) {
return null;
}
if (msg.tp === "timestamp") {
// if (this.startTime == null) {
// this.startTime = msg.timestamp
// }
this.currentTime = msg.timestamp - this.startTime;
} else {
const tMsg = Object.assign(msg, {
time: this.currentTime,
_index: this.pLastMessageID,
})
return [tMsg, this.pLastMessageID];
}
return null;
}
}

View file

@ -0,0 +1,36 @@
export default class PrimitiveReader {
protected p = 0
constructor(protected readonly buf: Uint8Array) {}
readUint() {
var r = 0, s = 1, b;
do {
b = this.buf[this.p++];
r += (b & 0x7F) * s;
s *= 128;
} while (b >= 0x80)
return r;
}
readInt() {
let u = this.readUint();
if (u % 2) {
u = (u + 1) / -2;
} else {
u = u / 2;
}
return u;
}
readString() {
var l = this.readUint();
return new TextDecoder().decode(this.buf.subarray(this.p, this.p+=l));
}
readBoolean() {
return !!this.buf[this.p++];
}
skip(n: number) {
this.p += n;
}
}

View file

@ -1,23 +1,23 @@
import Marker from './Marker';
import Cursor from './Cursor';
import Inspector from './Inspector';
import styles from './screen.css';
import { getState } from '../../../store';
import type { Point } from './types';
export const INITIAL_STATE: {
export interface State {
width: number;
height: number;
} = {
}
export const INITIAL_STATE: State = {
width: 0,
height: 0,
}
export default class BaseScreen {
export default abstract class BaseScreen {
public readonly overlay: HTMLDivElement;
private readonly iframe: HTMLIFrameElement;
public readonly overlay: HTMLDivElement;
private readonly _screen: HTMLDivElement;
protected parentElement: HTMLElement | null = null;
constructor() {
@ -68,8 +68,18 @@ export default class BaseScreen {
return this.iframe.contentDocument;
}
_getInternalCoordinates({ x, y }: Point): Point {
const { x: overlayX, y: overlayY, width } = this.overlay.getBoundingClientRect();
private boundingRect: DOMRect | null = null;
private getBoundingClientRect(): DOMRect {
//if (this.boundingRect === null) {
return this.boundingRect = this.overlay.getBoundingClientRect(); // expensive operation?
//}
//return this.boundingRect;
}
getInternalCoordinates({ x, y }: Point): Point {
const { x: overlayX, y: overlayY, width } = this.getBoundingClientRect();
//console.log("x y ", x,y,'ovx y', overlayX, overlayY, width)
const screenWidth = this.overlay.offsetWidth;
const scale = screenWidth / width;
@ -98,11 +108,11 @@ export default class BaseScreen {
}
getElementFromPoint(point: Point): Element | null {
return this.getElementFromInternalPoint(this._getInternalCoordinates(point));
return this.getElementFromInternalPoint(this.getInternalCoordinates(point));
}
getElementsFromPoint(point: Point): Element[] {
return this.getElementsFromInternalPoint(this._getInternalCoordinates(point));
return this.getElementsFromInternalPoint(this.getInternalCoordinates(point));
}
display(flag: boolean = true) {
@ -130,8 +140,13 @@ export default class BaseScreen {
this._screen.style.height = height + 'px';
this.iframe.style.width = width + 'px';
this.iframe.style.height = height + 'px';
this.boundingRect = this.overlay.getBoundingClientRect();
}
scale = () => { // TODO: solve classes inheritance issues in typescript
this._scale();
}
scale = () => this._scale()
clean() {

View file

@ -5,7 +5,7 @@ import styles from './screen.css';
import { getState } from '../../../store';
import BaseScreen from './BaseScreen';
export { INITIAL_STATE } from './BaseScreen';
export { INITIAL_STATE, State } from './BaseScreen';
export default class Screen extends BaseScreen {
private cursor: Cursor;

View file

@ -9,6 +9,7 @@
.iframe {
position: absolute;
border: none;
background: whilte;
}
.overlay {
position: absolute;

View file

@ -1,39 +0,0 @@
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE } from './Screen';
import { update, getState } from '../../store';
export const INITIAL_STATE = {
...SUPER_INITIAL_STATE,
messagesLoading: false,
cssLoading: false,
disconnected: false,
userPageLoading: false,
}
export default class StatedScreen extends Screen {
setMessagesLoading(messagesLoading) {
this.display(!messagesLoading);
update({ messagesLoading });
}
setCSSLoading(cssLoading) {
this.displayFrame(!cssLoading);
update({ cssLoading });
}
setDisconnected(disconnected) {
if (!getState().live) return; //?
this.display(!disconnected);
update({ disconnected });
}
setUserPageLoading(userPageLoading) {
this.display(!userPageLoading);
update({ userPageLoading });
}
setSize({ height, width }) {
update({ width, height });
this.scale();
}
}

View file

@ -0,0 +1,54 @@
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen';
import { update, getState } from '../../store';
export interface State extends SuperState {
messagesLoading: boolean,
cssLoading: boolean,
disconnected: boolean,
userPageLoading: boolean,
}
export const INITIAL_STATE: State = {
...SUPER_INITIAL_STATE,
messagesLoading: false,
cssLoading: false,
disconnected: false,
userPageLoading: false,
}
export default class StatedScreen extends Screen {
constructor() { super(); }
setMessagesLoading(messagesLoading: boolean) {
// @ts-ignore
this.display(!messagesLoading);
update({ messagesLoading });
}
setCSSLoading(cssLoading: boolean) {
// @ts-ignore
this.displayFrame(!cssLoading);
update({ cssLoading });
}
setDisconnected(disconnected: boolean) {
if (!getState().live) return; //?
// @ts-ignore
this.display(!disconnected);
update({ disconnected });
}
setUserPageLoading(userPageLoading: boolean) {
// @ts-ignore
this.display(!userPageLoading);
update({ userPageLoading });
}
setSize({ height, width }: { height: number, width: number }) {
update({ width, height });
// @ts-ignore
this.scale();
}
}

View file

@ -1,5 +0,0 @@
// @flow
import type { Message } from './messages';
export type Timed = { +time: number };
export type TimedMessage = Timed & Message;

View file

@ -0,0 +1,5 @@
import type { Message } from './messages';
export interface Timed { readonly time: number };
export interface Indexed { readonly _index: number }; // TODO: remove dash (evwrywhere)
export type TimedMessage = Timed & Message;

View file

@ -1,43 +0,0 @@
import ListWalker from './ListWalker';
class SkipInterval {
constructor({ start = 0, end = 0 }) {
this.start = start;
this.end = end;
}
get time(): number {
return this.start;
}
contains(ts) {
return ts > this.start && ts < this.end;
}
}
export default class ActivityManager extends ListWalker<SkipInterval> {
#endTime: number = 0;
#minInterval: number = 0;
#lastActivity: number = 0;
constructor(duration: number) {
super();
this.#endTime = duration;
this.#minInterval = duration * 0.1;
}
updateAcctivity(time: number) {
if (time - this.#lastActivity >= this.#minInterval) {
this.add(new SkipInterval({ start: this.#lastActivity, end: time }));
}
this.#lastActivity = time;
}
end() {
if (this.#endTime - this.#lastActivity >= this.#minInterval) {
this.add(new SkipInterval({ start: this.#lastActivity, end: this.#endTime }));
}
}
}

View file

@ -0,0 +1,42 @@
import ListWalker from './ListWalker';
class SkipIntervalCls {
constructor(private readonly start = 0, private readonly end = 0) {}
get time(): number {
return this.start;
}
contains(ts) {
return ts > this.start && ts < this.end;
}
}
export type SkipInterval = InstanceType<typeof SkipIntervalCls>;
export default class ActivityManager extends ListWalker<SkipInterval> {
private endTime: number = 0;
private minInterval: number = 0;
private lastActivity: number = 0;
constructor(duration: number) {
super();
this.endTime = duration;
this.minInterval = duration * 0.1;
}
updateAcctivity(time: number) {
if (time - this.lastActivity >= this.minInterval) {
this.add(new SkipIntervalCls(this.lastActivity, time));
}
this.lastActivity = time;
}
end() {
if (this.endTime - this.lastActivity >= this.minInterval) {
this.add(new SkipIntervalCls(this.lastActivity, this.endTime));
}
}
}

View file

@ -0,0 +1,339 @@
import type Peer from 'peerjs';
import type { DataConnection, MediaConnection } from 'peerjs';
import type MessageDistributor from '../MessageDistributor';
import type { TimedMessage } from '../Timed';
import type { Message } from '../messages'
import { ID_TP_MAP } from '../messages';
import store from 'App/store';
import { update, getState } from '../../store';
export enum CallingState {
Requesting,
True,
False,
};
export enum ConnectionStatus {
Connecting,
Connected,
Inactive,
Disconnected,
Error,
};
export interface State {
calling: CallingState,
peerConnectionStatus: ConnectionStatus,
}
export const INITIAL_STATE: State = {
calling: CallingState.False,
peerConnectionStatus: ConnectionStatus.Connecting,
}
const MAX_RECONNECTION_COUNT = 4;
function resolveURL(baseURL: string, relURL: string): string {
if (relURL.startsWith('#') || relURL === "") {
return relURL;
}
return new URL(relURL, baseURL).toString();
}
var match = /bar/.exec("foobar");
const re1 = /url\(("[^"]*"|'[^']*'|[^)]*)\)/g
const re2 = /@import "(.*?)"/g
function cssUrlsIndex(css: string): Array<[number, number]> {
const idxs: Array<[number, number]> = [];
const i1 = css.matchAll(re1);
// @ts-ignore
for (let m of i1) {
// @ts-ignore
const s: number = m.index + m[0].indexOf(m[1]);
const e: number = s + m[1].length;
idxs.push([s, e]);
}
const i2 = css.matchAll(re2);
// @ts-ignore
for (let m of i2) {
// @ts-ignore
const s = m.index + m[0].indexOf(m[1]);
const e = s + m[1].length;
idxs.push([s, e])
}
return idxs;
}
function unquote(str: string): [string, string] {
str = str.trim();
if (str.length <= 2) {
return [str, ""]
}
if (str[0] == '"' && str[str.length-1] == '"') {
return [ str.substring(1, str.length-1), "\""];
}
if (str[0] == '\'' && str[str.length-1] == '\'') {
return [ str.substring(1, str.length-1), "'" ];
}
return [str, ""]
}
function rewriteCSSLinks(css: string, rewriter: (rawurl: string) => string): string {
for (let idx of cssUrlsIndex(css)) {
const f = idx[0]
const t = idx[1]
const [ rawurl, q ] = unquote(css.substring(f, t));
css = css.substring(0,f) + q + rewriter(rawurl) + q + css.substring(t);
}
return css
}
function resolveCSS(baseURL: string, css: string): string {
return rewriteCSSLinks(css, rawurl => resolveURL(baseURL, rawurl));
}
export default class AssistManager {
constructor(private session, private md: MessageDistributor) {}
private get peerID(): string {
return `${this.session.projectKey}-${this.session.sessionId}`
}
private peer: Peer | null = null;
connectionAttempts: number = 0;
connect() {
if (this.peer != null) {
console.error("AssistManager: trying to connect more than once");
return;
}
this.md.setMessagesLoading(true);
import('peerjs').then(({ default: Peer }) => {
// @ts-ignore
const peer = new Peer({
// @ts-ignore
host: new URL(window.ENV.API_EDP).host,
path: '/assist',
port: location.protocol === 'https:' ? 443 : 80,
});
this.peer = peer;
this.peer.on('error', e => {
if (e.type === 'peer-unavailable') {
if (this.peer && this.connectionAttempts++ < MAX_RECONNECTION_COUNT) {
update({ peerConnectionStatus: ConnectionStatus.Connecting })
this.connectToPeer();
} else {
update({ peerConnectionStatus: ConnectionStatus.Disconnected })
}
} else {
console.error(`PeerJS error (on peer). Type ${e.type}`, e);
update({ peerConnectionStatus: ConnectionStatus.Error })
}
})
peer.on("open", me => {
console.log("peer opened", me);
this.connectToPeer();
});
});
}
private connectToPeer() {
if (!this.peer) { return; }
const id = this.peerID;
console.log("trying to connect to", id)
const conn = this.peer.connect(id, { serialization: 'json', reliable: true});
conn.on('open', () => {
update({ peerConnectionStatus: ConnectionStatus.Inactive });
console.log("peer connected")
let i = 0;
let firstMessage = true;
conn.on('data', (data) => {
if (typeof data === 'string') { return this.handleCommand(data); }
if (!Array.isArray(data)) { return; }
if (firstMessage) {
firstMessage = false;
this.md.setMessagesLoading(false);
update({ peerConnectionStatus: ConnectionStatus.Connected })
}
let time = 0;
let ts0 = 0;
(data as Array<Message & { _id: number}>).forEach(msg => {
// TODO: more appropriate way to do it.
if (msg._id === 60) {
// @ts-ignore
if (msg.name === 'src' || msg.name === 'href') {
// @ts-ignore
msg.value = resolveURL(msg.baseURL, msg.value);
// @ts-ignore
} else if (msg.name === 'style') {
// @ts-ignore
msg.value = resolveCSS(msg.baseURL, msg.value);
}
msg._id = 12;
} else if (msg._id === 61) { // "SetCSSDataURLBased"
// @ts-ignore
msg.data = resolveCSS(msg.baseURL, msg.data);
msg._id = 15;
} else if (msg._id === 67) { // "insert_rule"
// @ts-ignore
msg.rule = resolveCSS(msg.baseURL, msg.rule);
msg._id = 37;
}
msg.tp = ID_TP_MAP[msg._id]; // _id goes from tracker
if (msg.tp === "timestamp") {
ts0 = ts0 || msg.timestamp
time = msg.timestamp - ts0;
return;
}
const tMsg: TimedMessage = Object.assign(msg, {
time,
_index: i,
});
this.md.distributeMessage(tMsg, i++);
});
});
});
const intervalID = setInterval(() => {
if (!conn.open) {
this.md.setMessagesLoading(true);
this.assistentCallEnd();
update({ peerConnectionStatus: ConnectionStatus.Disconnected })
clearInterval(intervalID);
}
}, 5000);
conn.on('close', () => this.onDataClose());// Doesn't work ?
}
private onDataClose() {
this.md.setMessagesLoading(true);
this.assistentCallEnd();
console.log('closed peer conn. Reconnecting...')
update({ peerConnectionStatus: ConnectionStatus.Connecting })
setTimeout(() => this.connectToPeer(), 300); // reconnect
}
private get dataConnection(): DataConnection | undefined {
return this.peer?.connections[this.peerID]?.find(c => c.type === 'data' && c.open);
}
private get callConnection(): MediaConnection | undefined {
return this.peer?.connections[this.peerID]?.find(c => c.type === 'media' && c.open);
}
private onCallEnd: null | (()=>void) = null;
private assistentCallEnd = () => {
console.log('assistentCallEnd')
const conn = this.callConnection?.close();
const dataConn = this.dataConnection;
if (dataConn) {
console.log("call_end send")
dataConn.send("call_end");
}
this.onCallEnd?.();
}
private onTrackerCallEnd = () => {
const conn = this.callConnection;
if (conn && conn.open) {
conn.close();
}
this.onCallEnd?.();
}
private handleCommand(command: string) {
switch (command) {
case "call_end":
this.onTrackerCallEnd();
return;
case "call_error":
this.onTrackerCallEnd();
update({ peerConnectionStatus: ConnectionStatus.Error });
return;
}
}
//private blocked: boolean = false;
private onMouseMove = (e: MouseEvent ): void => {
//if (this.blocked) { return; }
//this.blocked = true;
//setTimeout(() => this.blocked = false, 200);
const conn = this.dataConnection;
if (!conn || !conn.open) { return; }
// @ts-ignore ???
const data = this.md.getInternalCoordinates(e);
conn.send({ x: Math.round(data.x), y: Math.round(data.y) });
}
call(localStream: MediaStream, onStream: (s: MediaStream)=>void, onClose: () => void, onReject: () => void, onError?: ()=> void): null | Function {
if (!this.peer || getState().calling !== CallingState.False) { return null; }
update({ calling: CallingState.Requesting });
console.log('calling...')
const call = this.peer.call(this.peerID, localStream);
call.on('stream', stream => {
update({ calling: CallingState.True });
onStream(stream);
this.dataConnection?.send({
name: store.getState().getIn([ 'user', 'account', 'name']),
});
// @ts-ignore ??
this.md.overlay.addEventListener("mousemove", this.onMouseMove)
});
this.onCallEnd = () => {
if (getState().calling === CallingState.Requesting) {
onReject();
}
onClose();
// @ts-ignore ??
this.md.overlay.removeEventListener("mousemove", this.onMouseMove);
update({ calling: CallingState.False });
this.onCallEnd = null;
}
//call.on("close", this.onCallEnd);
call.on("error", (e) => {
console.error("PeerJS error (on call):", e)
this.onCallEnd?.();
onError?.();
});
const intervalID = setInterval(() => {
if (!call.open && getState().calling !== CallingState.Requesting) {
this.onCallEnd?.();
clearInterval(intervalID);
}
}, 5000);
window.addEventListener("beforeunload", this.assistentCallEnd)
return this.assistentCallEnd;
}
clear() {
console.log('clearing', this.peerID)
this.assistentCallEnd();
console.log("destroying peer...")
this.peer?.destroy();
this.peer = null;
}
}

View file

@ -1,283 +0,0 @@
//@flow
import type StatedScreen from '../StatedScreen';
import type { Message, SetNodeScroll, CreateElementNode } from '../messages';
import type { TimedMessage } from '../Timed';
import logger from 'App/logger';
import StylesManager from './StylesManager';
import ListWalker from './ListWalker';
const IGNORED_ATTRS = [ "autocomplete", "name" ];
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
export default class DOMManager extends ListWalker<TimedMessage> {
#isMobile: boolean;
#screen: StatedScreen;
// #prop compiles to method that costs mor than strict property call.
_nl: Array<Element> = [];
_isLink: Array<boolean> = []; // Optimisations
_bodyId: number = -1;
_postponedBodyMessage: ?CreateElementNode = null;
#nodeScrollManagers: Array<ListWalker<SetNodeScroll>> = [];
#stylesManager: StylesManager;
#startTime: number;
constructor(screen: StatedScreen, isMobile: boolean, startTime: number) {
super();
this.#startTime = startTime;
this.#isMobile = isMobile;
this.#screen = screen;
this.#stylesManager = new StylesManager(screen);
}
get time(): number {
return this.#startTime;
}
add(m: TimedMessage): void {
switch (m.tp) {
case "set_node_scroll":
if (!this.#nodeScrollManagers[ m.id ]) {
this.#nodeScrollManagers[ m.id ] = new ListWalker();
}
this.#nodeScrollManagers[ m.id ].add(m);
return;
//case "css_insert_rule": // || //set_css_data ???
//case "css_delete_rule":
// (m.tp === "set_node_attribute" && this._isLink[ m.id ] && m.key === "href")) {
// this.#stylesManager.add(m);
// return;
default:
if (m.tp === "create_element_node") {
switch(m.tag) {
case "LINK":
this._isLink[ m.id ] = true;
break;
case "BODY":
this._bodyId = m.id; // Can be several body nodes at one document session?
break;
}
} else if (m.tp === "set_node_attribute" &&
(IGNORED_ATTRS.includes(m.key) || !ATTR_NAME_REGEXP.test(m.key))) {
logger.log("Ignorring message: ", m)
return; // Ignoring...
}
super.add(m);
}
}
_removeBodyScroll(id: number): void {
if (this.#isMobile && this._bodyId === id) {
this._nl[ id ].style.overflow = "hidden";
}
}
// May be make it as a message on message add?
_removeAutocomplete({ id, tag }: { id: number, tag: string }): boolean {
const node = this._nl[ id ];
if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) {
node.setAttribute("autocomplete", "off");
return true;
}
if (tag === "INPUT") {
node.setAttribute("autocomplete", "new-password");
return true;
}
return false;
}
// type = NodeMessage ?
_insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void {
if (!this._nl[ id ]) {
logger.error("Insert error. Node not found", id);
return;
}
if (!this._nl[ parentID ]) {
logger.error("Insert error. Parent node not found", parentID);
return;
}
// WHAT if text info contains some rules and the ordering is just wrong???
if ((this._nl[ parentID ] instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker
this._nl[ parentID ].sheet &&
this._nl[ parentID ].sheet.cssRules &&
this._nl[ parentID ].sheet.cssRules.length > 0) {
logger.log("Trying to insert child to style tag with virtual rules: ", this._nl[ parentID ], this._nl[ id ]);
return;
}
const childNodes = this._nl[ parentID ].childNodes;
if (!childNodes) {
logger.error("Node has no childNodes", this._nl[ parentID ]);
return;
}
this._nl[ parentID ]
.insertBefore(this._nl[ id ], childNodes[ index ]);
}
#applyMessage: (Message => void) = msg => {
let node;
switch (msg.tp) {
case "create_document":
this.#screen.document.open();
this.#screen.document.write(`${ msg.doctype || "<!DOCTYPE html>" }<html></html>`);
this.#screen.document.close();
const fRoot = this.#screen.document.documentElement;
fRoot.innerText = '';
//this._nl[ 0 ] = fRoot; // vs
this._nl = [ fRoot ];
// the last load event I can control
//if (this.document.fonts) {
// this.document.fonts.onloadingerror = () => this.marker.redraw();
// this.document.fonts.onloadingdone = () => this.marker.redraw();
//}
//this.#screen.setDisconnected(false);
this.#stylesManager.reset();
break;
case "create_text_node":
this._nl[ msg.id ] = document.createTextNode('');
this._insertNode(msg);
break;
case "create_element_node":
if (msg.svg) {
this._nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag);
} else {
this._nl[ msg.id ] = document.createElement(msg.tag);
}
if (this._bodyId === msg.id) {
this._postponedBodyMessage = msg;
} else {
this._insertNode(msg);
}
this._removeBodyScroll(msg.id);
this._removeAutocomplete(msg);
break;
case "move_node":
this._insertNode(msg);
break;
case "remove_node":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
if (!this._nl[ msg.id ].parentElement) { logger.error("Parent node not found", msg); break; }
this._nl[ msg.id ].parentElement.removeChild(this._nl[ msg.id ]);
break;
case "set_node_attribute":
let { id, name, value } = msg;
node = this._nl[ id ];
if (!node) { logger.error("Node not found", msg); break; }
if (this._isLink[ id ] && name === "href") {
if (value.startsWith(window.ENV.ASSETS_HOST)) { // Hack for queries in rewrited urls
value = value.replace("?", "%3F");
}
this.#stylesManager.setStyleHandlers(node, value);
}
try {
node.setAttribute(name, value);
} catch(e) {
logger.error(e, msg);
}
this._removeBodyScroll(msg.id);
break;
case "remove_node_attribute":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
try {
this._nl[ msg.id ].removeAttribute(msg.name);
} catch(e) {
logger.error(e, msg);
}
break;
case "set_input_value":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value;
this._nl[ msg.id ].value = val;
break;
case "set_input_checked":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
this._nl[ msg.id ].checked = msg.checked;
break;
case "set_node_data":
case "set_css_data":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
this._nl[ msg.id ].data = msg.data;
break;
case "css_insert_rule":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
if (!(this._nl[ msg.id ] instanceof HTMLStyleElement) // link or null
|| this._nl[ msg.id ].sheet == null) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg);
// prev version fallback (TODO: delete on 30.10.20)
let styleSheet = this.#screen.document.styleSheets[ msg.id ];
if (!styleSheet) {
styleSheet = this.#screen.document.styleSheets[0];
}
if (!styleSheet) {
logger.log("Old-fasion insert rule: No stylesheet found;", msg);
break;
}
try {
styleSheet.insertRule(msg.rule, msg.index);
} catch(e) {
logger.log("Old-fasion insert rule:", e, msg);
styleSheet.insertRule(msg.rule);
}
//
break;
}
try {
this._nl[ msg.id ].sheet.insertRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
this._nl[ msg.id ].sheet.insertRule(msg.rule)
}
break;
case "css_delete_rule":
if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; }
if (!this._nl[ msg.id ] instanceof HTMLStyleElement) { // link or null
logger.warn("Non-style node in CSS rules message", msg);
break;
}
try {
this._nl[ msg.id ].sheet.deleteRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
}
break;
//not sure what to do with this one
//case "disconnected":
//setTimeout(() => {
// if last one
//if (this.msgs[ this.msgs.length - 1 ] === msg) {
// this.setDisconnected(true);
// }
//}, 10000);
//break;
}
}
moveReady(t: number): Promise<void> {
this.moveApply(t, this.#applyMessage); // This function autoresets pointer if necessary (better name?)
this.#nodeScrollManagers.forEach(manager => {
const msg = manager.moveToLast(t); // TODO: reset (?)
if (!!msg && !!this._nl[msg.id]) {
this._nl[msg.id].scrollLeft = msg.x;
this._nl[msg.id].scrollTop = msg.y;
}
});
/* Mount body as late as possible */
if (this._postponedBodyMessage != null) {
this._insertNode(this._postponedBodyMessage)
this._postponedBodyMessage = null;
}
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?
return this.#stylesManager.moveReady(t);
}
}

View file

@ -0,0 +1,277 @@
import type StatedScreen from '../StatedScreen';
import type { Message, SetNodeScroll, CreateElementNode } from '../messages';
import type { TimedMessage } from '../Timed';
import logger from 'App/logger';
import StylesManager from './StylesManager';
import ListWalker from './ListWalker';
import type { Timed }from '../Timed';
const IGNORED_ATTRS = [ "autocomplete", "name" ];
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~
export default class DOMManager extends ListWalker<TimedMessage> {
private isMobile: boolean;
private screen: StatedScreen;
private nl: Array<Node> = [];
private isLink: Array<boolean> = []; // Optimisations
private bodyId: number = -1;
private postponedBodyMessage: CreateElementNode | null = null;
private nodeScrollManagers: Array<ListWalker<Timed & SetNodeScroll>> = [];
private stylesManager: StylesManager;
private startTime: number;
constructor(screen: StatedScreen, isMobile: boolean, startTime: number) {
super();
this.startTime = startTime;
this.isMobile = isMobile;
this.screen = screen;
this.stylesManager = new StylesManager(screen);
}
get time(): number {
return this.startTime;
}
add(m: TimedMessage): void {
switch (m.tp) {
case "set_node_scroll":
if (!this.nodeScrollManagers[ m.id ]) {
this.nodeScrollManagers[ m.id ] = new ListWalker();
}
this.nodeScrollManagers[ m.id ].add(m);
return;
//case "css_insert_rule": // || //set_css_data ???
//case "css_delete_rule":
// (m.tp === "set_node_attribute" && this.isLink[ m.id ] && m.key === "href")) {
// this.stylesManager.add(m);
// return;
default:
if (m.tp === "create_element_node") {
switch(m.tag) {
case "LINK":
this.isLink[ m.id ] = true;
break;
case "BODY":
this.bodyId = m.id; // Can be several body nodes at one document session?
break;
}
} else if (m.tp === "set_node_attribute" &&
(IGNORED_ATTRS.includes(m.name) || !ATTR_NAME_REGEXP.test(m.name))) {
logger.log("Ignorring message: ", m)
return; // Ignoring...
}
super.add(m);
}
}
private removeBodyScroll(id: number): void {
if (this.isMobile && this.bodyId === id) {
(this.nl[ id ] as HTMLBodyElement).style.overflow = "hidden";
}
}
// May be make it as a message on message add?
private removeAutocomplete({ id, tag }: CreateElementNode): boolean {
const node = this.nl[ id ] as HTMLElement;
if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) {
node.setAttribute("autocomplete", "off");
return true;
}
if (tag === "INPUT") {
node.setAttribute("autocomplete", "new-password");
return true;
}
return false;
}
// type = NodeMessage ?
private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void {
if (!this.nl[ id ]) {
logger.error("Insert error. Node not found", id);
return;
}
if (!this.nl[ parentID ]) {
logger.error("Insert error. Parent node not found", parentID);
return;
}
// WHAT if text info contains some rules and the ordering is just wrong???
const el = this.nl[ parentID ]
if ((el instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker
el.sheet &&
el.sheet.cssRules &&
el.sheet.cssRules.length > 0) {
logger.log("Trying to insert child to style tag with virtual rules: ", this.nl[ parentID ], this.nl[ id ]);
return;
}
const childNodes = this.nl[ parentID ].childNodes;
if (!childNodes) {
logger.error("Node has no childNodes", this.nl[ parentID ]);
return;
}
this.nl[ parentID ]
.insertBefore(this.nl[ id ], childNodes[ index ]);
}
private applyMessage = (msg: Message): void => {
let node;
switch (msg.tp) {
case "create_document":
// @ts-ignore ??
this.screen.document.open();
// @ts-ignore ??
this.screen.document.write(`${ msg.doctype || "<!DOCTYPE html>" }<html></html>`);
// @ts-ignore ??
this.screen.document.close();
// @ts-ignore ??
const fRoot = this.screen.document.documentElement;
fRoot.innerText = '';
this.nl = [ fRoot ];
// the last load event I can control
//if (this.document.fonts) {
// this.document.fonts.onloadingerror = () => this.marker.redraw();
// this.document.fonts.onloadingdone = () => this.marker.redraw();
//}
//this.screen.setDisconnected(false);
this.stylesManager.reset();
break;
case "create_text_node":
this.nl[ msg.id ] = document.createTextNode('');
this.insertNode(msg);
break;
case "create_element_node":
if (msg.svg) {
this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag);
} else {
this.nl[ msg.id ] = document.createElement(msg.tag);
}
if (this.bodyId === msg.id) {
this.postponedBodyMessage = msg;
} else {
this.insertNode(msg);
}
this.removeBodyScroll(msg.id);
this.removeAutocomplete(msg);
break;
case "move_node":
this.insertNode(msg);
break;
case "remove_node":
node = this.nl[ msg.id ]
if (!node) { logger.error("Node not found", msg); break; }
if (!node.parentElement) { logger.error("Parent node not found", msg); break; }
node.parentElement.removeChild(node);
break;
case "set_node_attribute":
let { id, name, value } = msg;
node = this.nl[ id ];
if (!node) { logger.error("Node not found", msg); break; }
if (this.isLink[ id ] && name === "href") {
// @ts-ignore TODO: global ENV type
if (value.startsWith(window.ENV.ASSETS_HOST)) { // Hack for queries in rewrited urls
value = value.replace("?", "%3F");
}
this.stylesManager.setStyleHandlers(node, value);
}
try {
node.setAttribute(name, value);
} catch(e) {
logger.error(e, msg);
}
this.removeBodyScroll(msg.id);
break;
case "remove_node_attribute":
if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; }
try {
(this.nl[ msg.id ] as HTMLElement).removeAttribute(msg.name);
} catch(e) {
logger.error(e, msg);
}
break;
case "set_input_value":
if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; }
const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value;
(this.nl[ msg.id ] as HTMLInputElement).value = val;
break;
case "set_input_checked":
node = this.nl[ msg.id ];
if (!node) { logger.error("Node not found", msg); break; }
(node as HTMLInputElement).checked = msg.checked;
break;
case "set_node_data":
case "set_css_data":
if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; }
// @ts-ignore
this.nl[ msg.id ].data = msg.data;
break;
case "css_insert_rule":
node = this.nl[ msg.id ];
if (!node) { logger.error("Node not found", msg); break; }
if (!(node instanceof HTMLStyleElement) // link or null
|| node.sheet == null) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg);
break;
}
try {
node.sheet.insertRule(msg.rule, msg.index)
} catch (e) {
logger.warn(e, msg)
node.sheet.insertRule(msg.rule)
}
break;
case "css_delete_rule":
node = this.nl[ msg.id ];
if (!node) { logger.error("Node not found", msg); break; }
if (!(node instanceof HTMLStyleElement) // link or null
|| node.sheet == null) {
logger.warn("Non-style node in CSS rules message (or sheet is null)", msg);
break;
}
try {
node.sheet.deleteRule(msg.index)
} catch (e) {
logger.warn(e, msg)
}
break;
//not sure what to do with this one
//case "disconnected":
//setTimeout(() => {
// if last one
//if (this.msgs[ this.msgs.length - 1 ] === msg) {
// this.setDisconnected(true);
// }
//}, 10000);
//break;
}
}
moveReady(t: number): Promise<void> {
this.moveApply(t, this.applyMessage); // This function autoresets pointer if necessary (better name?)
this.nodeScrollManagers.forEach(manager => {
const msg = manager.moveToLast(t); // TODO: reset (?)
if (!!msg && !!this.nl[msg.id]) {
const node = this.nl[msg.id] as HTMLElement;
node.scrollLeft = msg.x;
node.scrollTop = msg.y;
}
});
/* Mount body as late as possible */
if (this.postponedBodyMessage != null) {
this.insertNode(this.postponedBodyMessage)
this.postponedBodyMessage = null;
}
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?
return this.stylesManager.moveReady(t);
}
}

View file

@ -1,8 +1,6 @@
//@flow
import type { Timed } from '../Timed';
export default class ListWalker<T: Timed> {
export default class ListWalker<T extends Timed> {
// Optimisation: #prop compiles to method that costs mor than strict property call.
_p = 0;
_list: Array<T>;
@ -15,7 +13,7 @@ export default class ListWalker<T: Timed> {
}
append(m: T): void {
if (this.length > 0 && m.time < this.last.time) {
if (this.length > 0 && this.last && m.time < this.last.time) {
console.error("Trying to append message with the less time then the list tail: ", m);
}
this._list.push(m);
@ -26,6 +24,7 @@ export default class ListWalker<T: Timed> {
}
sort(comparator): void {
// @ts-ignore
this._list.sort((m1,m2) => comparator(m1,m2) || (m1._index - m2._index) ); // indexes for sort stability (TODO: fix types???)
}
@ -100,10 +99,10 @@ export default class ListWalker<T: Timed> {
Assumed that the current message is already handled so
if pointer doesn't cahnge <undefined> is returned.
*/
moveToLast(t: number, index: ?number): ?T {
let key = "time"; //TODO
moveToLast(t: number, index?: number): T | null {
let key: string = "time"; //TODO
let val = t;
if (index != null) {
if (index) {
key = "_index";
val = index;
}
@ -117,7 +116,7 @@ export default class ListWalker<T: Timed> {
this._p--;
changed = true;
}
return changed ? this._list[ this._p - 1 ] : undefined;
return changed ? this._list[ this._p - 1 ] : null;
}
// moveToLastByIndex(i: number): ?T {
@ -133,7 +132,7 @@ export default class ListWalker<T: Timed> {
// return changed ? this._list[ this._p - 1 ] : undefined;
// }
moveApply(t: number, fn: T => void): void {
moveApply(t: number, fn: (T) => void): void {
// Applying only in increment order for now
if (t < this.timeNow) {
this.reset();

View file

@ -1,4 +1,3 @@
//@flow
import type StatedScreen from '../StatedScreen';
import type { MouseMove } from '../messages';
import type { Timed } from '../Timed';
@ -10,32 +9,30 @@ type MouseMoveTimed = MouseMove & Timed;
const HOVER_CLASS = "-openreplay-hover";
export default class MouseManager extends ListWalker<MouseMoveTimed> {
#screen: StatedScreen;
#hoverElements: Array<Element> = [];
private hoverElements: Array<Element> = [];
constructor(screen: StatedScreen): void {
super();
this.#screen = screen;
}
constructor(private screen: StatedScreen) {super();}
_updateHover(): void {
const curHoverElements = this.#screen.getCursorTargets();
const diffAdd = curHoverElements.filter(elem => !this.#hoverElements.includes(elem));
const diffRemove = this.#hoverElements.filter(elem => !curHoverElements.includes(elem));
this.#hoverElements = curHoverElements;
private updateHover(): void {
// @ts-ignore TODO
const curHoverElements = this.screen.getCursorTargets();
const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem));
const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem));
this.hoverElements = curHoverElements;
diffAdd.forEach(elem => elem.classList.add(HOVER_CLASS));
diffRemove.forEach(elem => elem.classList.remove(HOVER_CLASS));
}
reset(): void {
this.#hoverElements = [];
this.hoverElements = [];
}
move(t: number) {
const lastMouseMove = this.moveToLast(t);
if (!!lastMouseMove){
this.#screen.cursor.move(lastMouseMove);
this._updateHover();
// @ts-ignore TODO
this.screen.cursor.move(lastMouseMove);
this.updateHover();
}
}

View file

@ -1,104 +0,0 @@
// @flow
import type { PerformanceTrack, SetPageVisibility } from '../messages';
import type { Timed } from '../Timed';
import ListWalker from './ListWalker';
type TimedPerformanceTrack = Timed & PerformanceTrack;
type TimedSetPageVisibility = Timed & SetPageVisibility;
type PerformanceChartPoint = {
time: number,
usedHeap: number,
totalHeap: number,
fps: ?number,
cpu: ?number,
nodesCount: number,
}
export default class PerformanceTrackManager extends ListWalker<TimedPerformanceTrack> {
#chart: Array<PerformanceChartPoint> = [];
#isHidden: boolean = false;
#timeCorrection: number = 0;
#heapAvaliable: boolean = false;
#fpsAvaliable: boolean = false;
#cpuAvaliable: boolean = false;
#prevTime: ?number = null;
#prevNodesCount: number = 0;
add(msg: TimedPerformanceTrack):void {
let fps = null;
let cpu = null;
if (!this.#isHidden && this.#prevTime != null) {
let timePassed = msg.time - this.#prevTime + this.#timeCorrection;
if (timePassed > 0 && msg.frames >= 0) {
if (msg.frames > 0) { this.#fpsAvaliable = true; }
fps = msg.frames*1e3/timePassed; // Multiply by 1e3 as time in ms;
fps = Math.min(fps,60); // What if 120? TODO: alert if more than 60
if (this.#chart.length === 1) {
this.#chart[0].fps = fps;
}
}
if (timePassed > 0 && msg.ticks >= 0) {
this.#cpuAvaliable = true;
let tickRate = msg.ticks * 30 / timePassed;
if (tickRate > 1) {
tickRate = 1;
}
cpu = Math.round(100 - tickRate*100);
if (this.#chart.length === 1) {
this.#chart[0].cpu = cpu;
}
}
}
this.#prevTime = msg.time;
this.#timeCorrection = 0
this.#heapAvaliable = this.#heapAvaliable || msg.usedJSHeapSize > 0;
this.#chart.push({
usedHeap: msg.usedJSHeapSize,
totalHeap: msg.totalJSHeapSize,
fps,
cpu,
time: msg.time,
nodesCount: this.#prevNodesCount,
});
super.add(msg);
}
setCurrentNodesCount(count: number) {
this.#prevNodesCount = count;
if (this.#chart.length > 0) {
this.#chart[ this.#chart.length - 1 ].nodesCount = count;
}
}
handleVisibility(msg: TimedSetPageVisibility):void {
if (!this.#isHidden && msg.hidden && this.#prevTime != null) {
this.#timeCorrection = msg.time - this.#prevTime;
}
if (this.#isHidden && !msg.hidden) {
this.#prevTime = msg.time;
}
this.#isHidden = msg.hidden;
}
get chartData(): Array<PerformanceChartPoint> {
return this.#chart;
}
get avaliability(): { cpu: boolean, fps: boolean, heap: boolean } {
return {
cpu: this.#cpuAvaliable,
fps: this.#fpsAvaliable,
heap: this.#heapAvaliable,
nodes: true,
}
}
}

View file

@ -0,0 +1,102 @@
import type { PerformanceTrack, SetPageVisibility } from '../messages';
import type { Timed } from '../Timed';
import ListWalker from './ListWalker';
type TimedPerformanceTrack = Timed & PerformanceTrack;
type TimedSetPageVisibility = Timed & SetPageVisibility;
export type PerformanceChartPoint = {
time: number,
usedHeap: number,
totalHeap: number,
fps?: number,
cpu?: number,
nodesCount: number,
}
export default class PerformanceTrackManager extends ListWalker<TimedPerformanceTrack> {
private chart: Array<PerformanceChartPoint> = [];
private isHidden: boolean = false;
private timeCorrection: number = 0;
private heapAvaliable: boolean = false;
private fpsAvaliable: boolean = false;
private cpuAvaliable: boolean = false;
private prevTime: number | null = null;
private prevNodesCount: number = 0;
add(msg: TimedPerformanceTrack):void {
let fps: number | undefined;
let cpu: number | undefined;
if (!this.isHidden && this.prevTime != null) {
let timePassed = msg.time - this.prevTime + this.timeCorrection;
if (timePassed > 0 && msg.frames >= 0) {
if (msg.frames > 0) { this.fpsAvaliable = true; }
fps = msg.frames*1e3/timePassed; // Multiply by 1e3 as time in ms;
fps = Math.min(fps,60); // What if 120? TODO: alert if more than 60
if (this.chart.length === 1) {
this.chart[0].fps = fps;
}
}
if (timePassed > 0 && msg.ticks >= 0) {
this.cpuAvaliable = true;
let tickRate = msg.ticks * 30 / timePassed;
if (tickRate > 1) {
tickRate = 1;
}
cpu = Math.round(100 - tickRate*100);
if (this.chart.length === 1) {
this.chart[0].cpu = cpu;
}
}
}
this.prevTime = msg.time;
this.timeCorrection = 0
this.heapAvaliable = this.heapAvaliable || msg.usedJSHeapSize > 0;
this.chart.push({
usedHeap: msg.usedJSHeapSize,
totalHeap: msg.totalJSHeapSize,
fps,
cpu,
time: msg.time,
nodesCount: this.prevNodesCount,
});
super.add(msg);
}
setCurrentNodesCount(count: number) {
this.prevNodesCount = count;
if (this.chart.length > 0) {
this.chart[ this.chart.length - 1 ].nodesCount = count;
}
}
handleVisibility(msg: TimedSetPageVisibility):void {
if (!this.isHidden && msg.hidden && this.prevTime != null) {
this.timeCorrection = msg.time - this.prevTime;
}
if (this.isHidden && !msg.hidden) {
this.prevTime = msg.time;
}
this.isHidden = msg.hidden;
}
get chartData(): Array<PerformanceChartPoint> {
return this.chart;
}
get avaliability(): { cpu: boolean, fps: boolean, heap: boolean, nodes: boolean } {
return {
cpu: this.cpuAvaliable,
fps: this.fpsAvaliable,
heap: this.heapAvaliable,
nodes: true,
}
}
}

View file

@ -1,596 +0,0 @@
// Auto-generated, do not edit
import { readUint, readInt, readString, readBoolean } from './readPrimitives'
export type Timestamp = {
tp: "timestamp",
timestamp: number,
}
export type SessionDisconnect = {
tp: "session_disconnect",
timestamp: number,
}
export type SetPageLocation = {
tp: "set_page_location",
url: string,
referrer: string,
navigationStart: number,
}
export type SetViewportSize = {
tp: "set_viewport_size",
width: number,
height: number,
}
export type SetViewportScroll = {
tp: "set_viewport_scroll",
x: number,
y: number,
}
export type CreateDocument = {
tp: "create_document",
}
export type CreateElementNode = {
tp: "create_element_node",
id: number,
parentID: number,
index: number,
tag: string,
svg: boolean,
}
export type CreateTextNode = {
tp: "create_text_node",
id: number,
parentID: number,
index: number,
}
export type MoveNode = {
tp: "move_node",
id: number,
parentID: number,
index: number,
}
export type RemoveNode = {
tp: "remove_node",
id: number,
}
export type SetNodeAttribute = {
tp: "set_node_attribute",
id: number,
name: string,
value: string,
}
export type RemoveNodeAttribute = {
tp: "remove_node_attribute",
id: number,
name: string,
}
export type SetNodeData = {
tp: "set_node_data",
id: number,
data: string,
}
export type SetCssData = {
tp: "set_css_data",
id: number,
data: string,
}
export type SetNodeScroll = {
tp: "set_node_scroll",
id: number,
x: number,
y: number,
}
export type SetInputValue = {
tp: "set_input_value",
id: number,
value: string,
mask: number,
}
export type SetInputChecked = {
tp: "set_input_checked",
id: number,
checked: boolean,
}
export type MouseMove = {
tp: "mouse_move",
x: number,
y: number,
}
export type ConsoleLog = {
tp: "console_log",
level: string,
value: string,
}
export type PerformanceTrack = {
tp: "performance_track",
frames: number,
ticks: number,
totalJSHeapSize: number,
usedJSHeapSize: number,
}
export type ConnectionInformation = {
tp: "connection_information",
downlink: number,
type: string,
}
export type SetPageVisibility = {
tp: "set_page_visibility",
hidden: boolean,
}
export type CssInsertRule = {
tp: "css_insert_rule",
id: number,
rule: string,
index: number,
}
export type CssDeleteRule = {
tp: "css_delete_rule",
id: number,
index: number,
}
export type Fetch = {
tp: "fetch",
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
}
export type Profiler = {
tp: "profiler",
name: string,
duration: number,
args: string,
result: string,
}
export type OTable = {
tp: "o_table",
key: string,
value: string,
}
export type Redux = {
tp: "redux",
action: string,
state: string,
duration: number,
}
export type Vuex = {
tp: "vuex",
mutation: string,
state: string,
}
export type MobX = {
tp: "mob_x",
type: string,
payload: string,
}
export type NgRx = {
tp: "ng_rx",
action: string,
state: string,
duration: number,
}
export type GraphQl = {
tp: "graph_ql",
operationKind: string,
operationName: string,
variables: string,
response: string,
}
export type LongTask = {
tp: "long_task",
timestamp: number,
duration: number,
context: number,
containerType: number,
containerSrc: string,
containerId: string,
containerName: string,
}
export type TechnicalInfo = {
tp: "technical_info",
type: string,
value: string,
}
export type IosSessionStart = {
tp: "ios_session_start",
timestamp: number,
projectID: number,
trackerVersion: string,
revID: string,
userUUID: string,
userOS: string,
userOSVersion: string,
userDevice: string,
userDeviceType: string,
userCountry: string,
}
export type IosCustomEvent = {
tp: "ios_custom_event",
timestamp: number,
length: number,
name: string,
payload: string,
}
export type IosClickEvent = {
tp: "ios_click_event",
timestamp: number,
length: number,
label: string,
x: number,
y: number,
}
export type IosPerformanceEvent = {
tp: "ios_performance_event",
timestamp: number,
length: number,
name: string,
value: number,
}
export type IosLog = {
tp: "ios_log",
timestamp: number,
length: number,
severity: string,
content: string,
}
export type IosNetworkCall = {
tp: "ios_network_call",
timestamp: number,
length: number,
duration: number,
headers: string,
body: string,
url: string,
success: boolean,
method: string,
status: number,
}
export type Message = Timestamp | SessionDisconnect | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | PerformanceTrack | ConnectionInformation | SetPageVisibility | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | LongTask | TechnicalInfo | IosSessionStart | IosCustomEvent | IosClickEvent | IosPerformanceEvent | IosLog | IosNetworkCall;
export default function (buf: Uint8Array, p: number): [Message, number] {
const msg = {};
let r;
switch (buf[p++]) {
case 0:
(msg:Timestamp).tp = "timestamp";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
break;
case 2:
(msg:SessionDisconnect).tp = "session_disconnect";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
break;
case 4:
(msg:SetPageLocation).tp = "set_page_location";
r = readString(buf, p); msg.url = r[0]; p = r[1];
r = readString(buf, p); msg.referrer = r[0]; p = r[1];
r = readUint(buf, p); msg.navigationStart = r[0]; p = r[1];
break;
case 5:
(msg:SetViewportSize).tp = "set_viewport_size";
r = readUint(buf, p); msg.width = r[0]; p = r[1];
r = readUint(buf, p); msg.height = r[0]; p = r[1];
break;
case 6:
(msg:SetViewportScroll).tp = "set_viewport_scroll";
r = readInt(buf, p); msg.x = r[0]; p = r[1];
r = readInt(buf, p); msg.y = r[0]; p = r[1];
break;
case 7:
(msg:CreateDocument).tp = "create_document";
break;
case 8:
(msg:CreateElementNode).tp = "create_element_node";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readUint(buf, p); msg.parentID = r[0]; p = r[1];
r = readUint(buf, p); msg.index = r[0]; p = r[1];
r = readString(buf, p); msg.tag = r[0]; p = r[1];
r = readBoolean(buf, p); msg.svg = r[0]; p = r[1];
break;
case 9:
(msg:CreateTextNode).tp = "create_text_node";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readUint(buf, p); msg.parentID = r[0]; p = r[1];
r = readUint(buf, p); msg.index = r[0]; p = r[1];
break;
case 10:
(msg:MoveNode).tp = "move_node";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readUint(buf, p); msg.parentID = r[0]; p = r[1];
r = readUint(buf, p); msg.index = r[0]; p = r[1];
break;
case 11:
(msg:RemoveNode).tp = "remove_node";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
break;
case 12:
(msg:SetNodeAttribute).tp = "set_node_attribute";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readString(buf, p); msg.name = r[0]; p = r[1];
r = readString(buf, p); msg.value = r[0]; p = r[1];
break;
case 13:
(msg:RemoveNodeAttribute).tp = "remove_node_attribute";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readString(buf, p); msg.name = r[0]; p = r[1];
break;
case 14:
(msg:SetNodeData).tp = "set_node_data";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readString(buf, p); msg.data = r[0]; p = r[1];
break;
case 15:
(msg:SetCssData).tp = "set_css_data";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readString(buf, p); msg.data = r[0]; p = r[1];
break;
case 16:
(msg:SetNodeScroll).tp = "set_node_scroll";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readInt(buf, p); msg.x = r[0]; p = r[1];
r = readInt(buf, p); msg.y = r[0]; p = r[1];
break;
case 18:
(msg:SetInputValue).tp = "set_input_value";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readString(buf, p); msg.value = r[0]; p = r[1];
r = readInt(buf, p); msg.mask = r[0]; p = r[1];
break;
case 19:
(msg:SetInputChecked).tp = "set_input_checked";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readBoolean(buf, p); msg.checked = r[0]; p = r[1];
break;
case 20:
(msg:MouseMove).tp = "mouse_move";
r = readUint(buf, p); msg.x = r[0]; p = r[1];
r = readUint(buf, p); msg.y = r[0]; p = r[1];
break;
case 22:
(msg:ConsoleLog).tp = "console_log";
r = readString(buf, p); msg.level = r[0]; p = r[1];
r = readString(buf, p); msg.value = r[0]; p = r[1];
break;
case 49:
(msg:PerformanceTrack).tp = "performance_track";
r = readInt(buf, p); msg.frames = r[0]; p = r[1];
r = readInt(buf, p); msg.ticks = r[0]; p = r[1];
r = readUint(buf, p); msg.totalJSHeapSize = r[0]; p = r[1];
r = readUint(buf, p); msg.usedJSHeapSize = r[0]; p = r[1];
break;
case 54:
(msg:ConnectionInformation).tp = "connection_information";
r = readUint(buf, p); msg.downlink = r[0]; p = r[1];
r = readString(buf, p); msg.type = r[0]; p = r[1];
break;
case 55:
(msg:SetPageVisibility).tp = "set_page_visibility";
r = readBoolean(buf, p); msg.hidden = r[0]; p = r[1];
break;
case 37:
(msg:CssInsertRule).tp = "css_insert_rule";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readString(buf, p); msg.rule = r[0]; p = r[1];
r = readUint(buf, p); msg.index = r[0]; p = r[1];
break;
case 38:
(msg:CssDeleteRule).tp = "css_delete_rule";
r = readUint(buf, p); msg.id = r[0]; p = r[1];
r = readUint(buf, p); msg.index = r[0]; p = r[1];
break;
case 39:
(msg:Fetch).tp = "fetch";
r = readString(buf, p); msg.method = r[0]; p = r[1];
r = readString(buf, p); msg.url = r[0]; p = r[1];
r = readString(buf, p); msg.request = r[0]; p = r[1];
r = readString(buf, p); msg.response = r[0]; p = r[1];
r = readUint(buf, p); msg.status = r[0]; p = r[1];
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.duration = r[0]; p = r[1];
break;
case 40:
(msg:Profiler).tp = "profiler";
r = readString(buf, p); msg.name = r[0]; p = r[1];
r = readUint(buf, p); msg.duration = r[0]; p = r[1];
r = readString(buf, p); msg.args = r[0]; p = r[1];
r = readString(buf, p); msg.result = r[0]; p = r[1];
break;
case 41:
(msg:OTable).tp = "o_table";
r = readString(buf, p); msg.key = r[0]; p = r[1];
r = readString(buf, p); msg.value = r[0]; p = r[1];
break;
case 44:
(msg:Redux).tp = "redux";
r = readString(buf, p); msg.action = r[0]; p = r[1];
r = readString(buf, p); msg.state = r[0]; p = r[1];
r = readUint(buf, p); msg.duration = r[0]; p = r[1];
break;
case 45:
(msg:Vuex).tp = "vuex";
r = readString(buf, p); msg.mutation = r[0]; p = r[1];
r = readString(buf, p); msg.state = r[0]; p = r[1];
break;
case 46:
(msg:MobX).tp = "mob_x";
r = readString(buf, p); msg.type = r[0]; p = r[1];
r = readString(buf, p); msg.payload = r[0]; p = r[1];
break;
case 47:
(msg:NgRx).tp = "ng_rx";
r = readString(buf, p); msg.action = r[0]; p = r[1];
r = readString(buf, p); msg.state = r[0]; p = r[1];
r = readUint(buf, p); msg.duration = r[0]; p = r[1];
break;
case 48:
(msg:GraphQl).tp = "graph_ql";
r = readString(buf, p); msg.operationKind = r[0]; p = r[1];
r = readString(buf, p); msg.operationName = r[0]; p = r[1];
r = readString(buf, p); msg.variables = r[0]; p = r[1];
r = readString(buf, p); msg.response = r[0]; p = r[1];
break;
case 59:
(msg:LongTask).tp = "long_task";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.duration = r[0]; p = r[1];
r = readUint(buf, p); msg.context = r[0]; p = r[1];
r = readUint(buf, p); msg.containerType = r[0]; p = r[1];
r = readString(buf, p); msg.containerSrc = r[0]; p = r[1];
r = readString(buf, p); msg.containerId = r[0]; p = r[1];
r = readString(buf, p); msg.containerName = r[0]; p = r[1];
break;
case 63:
(msg:TechnicalInfo).tp = "technical_info";
r = readString(buf, p); msg.type = r[0]; p = r[1];
r = readString(buf, p); msg.value = r[0]; p = r[1];
break;
case 90:
(msg:IosSessionStart).tp = "ios_session_start";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.projectID = r[0]; p = r[1];
r = readString(buf, p); msg.trackerVersion = r[0]; p = r[1];
r = readString(buf, p); msg.revID = r[0]; p = r[1];
r = readString(buf, p); msg.userUUID = r[0]; p = r[1];
r = readString(buf, p); msg.userOS = r[0]; p = r[1];
r = readString(buf, p); msg.userOSVersion = r[0]; p = r[1];
r = readString(buf, p); msg.userDevice = r[0]; p = r[1];
r = readString(buf, p); msg.userDeviceType = r[0]; p = r[1];
r = readString(buf, p); msg.userCountry = r[0]; p = r[1];
break;
case 93:
(msg:IosCustomEvent).tp = "ios_custom_event";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.length = r[0]; p = r[1];
r = readString(buf, p); msg.name = r[0]; p = r[1];
r = readString(buf, p); msg.payload = r[0]; p = r[1];
break;
case 100:
(msg:IosClickEvent).tp = "ios_click_event";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.length = r[0]; p = r[1];
r = readString(buf, p); msg.label = r[0]; p = r[1];
r = readUint(buf, p); msg.x = r[0]; p = r[1];
r = readUint(buf, p); msg.y = r[0]; p = r[1];
break;
case 102:
(msg:IosPerformanceEvent).tp = "ios_performance_event";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.length = r[0]; p = r[1];
r = readString(buf, p); msg.name = r[0]; p = r[1];
r = readUint(buf, p); msg.value = r[0]; p = r[1];
break;
case 103:
(msg:IosLog).tp = "ios_log";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.length = r[0]; p = r[1];
r = readString(buf, p); msg.severity = r[0]; p = r[1];
r = readString(buf, p); msg.content = r[0]; p = r[1];
break;
case 105:
(msg:IosNetworkCall).tp = "ios_network_call";
r = readUint(buf, p); msg.timestamp = r[0]; p = r[1];
r = readUint(buf, p); msg.length = r[0]; p = r[1];
r = readUint(buf, p); msg.duration = r[0]; p = r[1];
r = readString(buf, p); msg.headers = r[0]; p = r[1];
r = readString(buf, p); msg.body = r[0]; p = r[1];
r = readString(buf, p); msg.url = r[0]; p = r[1];
r = readBoolean(buf, p); msg.success = r[0]; p = r[1];
r = readString(buf, p); msg.method = r[0]; p = r[1];
r = readUint(buf, p); msg.status = r[0]; p = r[1];
break;
default:
let len;
[ _, p ] = readUint(buf, p);
[ len, p ] = readUint(buf, p);
return [null, p + len] // skip
//throw `Unknown type (${buf[p-1]})`;
}
return [msg, p];
}

View file

@ -0,0 +1,543 @@
// Auto-generated, do not edit
import PrimitiveReader from './PrimitiveReader';
export const ID_TP_MAP = {
0: "timestamp",
4: "set_page_location",
5: "set_viewport_size",
6: "set_viewport_scroll",
7: "create_document",
8: "create_element_node",
9: "create_text_node",
10: "move_node",
11: "remove_node",
12: "set_node_attribute",
13: "remove_node_attribute",
14: "set_node_data",
15: "set_css_data",
16: "set_node_scroll",
18: "set_input_value",
19: "set_input_checked",
20: "mouse_move",
22: "console_log",
37: "css_insert_rule",
38: "css_delete_rule",
39: "fetch_depricated",
40: "profiler",
41: "o_table",
44: "redux",
45: "vuex",
46: "mob_x",
47: "ng_rx",
48: "graph_ql",
49: "performance_track",
54: "connection_information",
55: "set_page_visibility",
59: "long_task",
68: "fetch",
} as const;
export interface Timestamp {
tp: "timestamp",
timestamp: number,
}
export interface SetPageLocation {
tp: "set_page_location",
url: string,
referrer: string,
navigationStart: number,
}
export interface SetViewportSize {
tp: "set_viewport_size",
width: number,
height: number,
}
export interface SetViewportScroll {
tp: "set_viewport_scroll",
x: number,
y: number,
}
export interface CreateDocument {
tp: "create_document",
}
export interface CreateElementNode {
tp: "create_element_node",
id: number,
parentID: number,
index: number,
tag: string,
svg: boolean,
}
export interface CreateTextNode {
tp: "create_text_node",
id: number,
parentID: number,
index: number,
}
export interface MoveNode {
tp: "move_node",
id: number,
parentID: number,
index: number,
}
export interface RemoveNode {
tp: "remove_node",
id: number,
}
export interface SetNodeAttribute {
tp: "set_node_attribute",
id: number,
name: string,
value: string,
}
export interface RemoveNodeAttribute {
tp: "remove_node_attribute",
id: number,
name: string,
}
export interface SetNodeData {
tp: "set_node_data",
id: number,
data: string,
}
export interface SetCssData {
tp: "set_css_data",
id: number,
data: string,
}
export interface SetNodeScroll {
tp: "set_node_scroll",
id: number,
x: number,
y: number,
}
export interface SetInputValue {
tp: "set_input_value",
id: number,
value: string,
mask: number,
}
export interface SetInputChecked {
tp: "set_input_checked",
id: number,
checked: boolean,
}
export interface MouseMove {
tp: "mouse_move",
x: number,
y: number,
}
export interface ConsoleLog {
tp: "console_log",
level: string,
value: string,
}
export interface CssInsertRule {
tp: "css_insert_rule",
id: number,
rule: string,
index: number,
}
export interface CssDeleteRule {
tp: "css_delete_rule",
id: number,
index: number,
}
export interface FetchDepricated {
tp: "fetch_depricated",
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
}
export interface Profiler {
tp: "profiler",
name: string,
duration: number,
args: string,
result: string,
}
export interface OTable {
tp: "o_table",
key: string,
value: string,
}
export interface Redux {
tp: "redux",
action: string,
state: string,
duration: number,
}
export interface Vuex {
tp: "vuex",
mutation: string,
state: string,
}
export interface MobX {
tp: "mob_x",
type: string,
payload: string,
}
export interface NgRx {
tp: "ng_rx",
action: string,
state: string,
duration: number,
}
export interface GraphQl {
tp: "graph_ql",
operationKind: string,
operationName: string,
variables: string,
response: string,
}
export interface PerformanceTrack {
tp: "performance_track",
frames: number,
ticks: number,
totalJSHeapSize: number,
usedJSHeapSize: number,
}
export interface ConnectionInformation {
tp: "connection_information",
downlink: number,
type: string,
}
export interface SetPageVisibility {
tp: "set_page_visibility",
hidden: boolean,
}
export interface LongTask {
tp: "long_task",
timestamp: number,
duration: number,
context: number,
containerType: number,
containerSrc: string,
containerId: string,
containerName: string,
}
export interface Fetch {
tp: "fetch",
method: string,
url: string,
request: string,
response: string,
status: number,
timestamp: number,
duration: number,
headers: string,
}
export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | FetchDepricated | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask | Fetch;
export default function (r: PrimitiveReader): Message | null {
switch (r.readUint()) {
case 0:
return {
tp: ID_TP_MAP[0],
timestamp: r.readUint(),
};
case 4:
return {
tp: ID_TP_MAP[4],
url: r.readString(),
referrer: r.readString(),
navigationStart: r.readUint(),
};
case 5:
return {
tp: ID_TP_MAP[5],
width: r.readUint(),
height: r.readUint(),
};
case 6:
return {
tp: ID_TP_MAP[6],
x: r.readInt(),
y: r.readInt(),
};
case 7:
return {
tp: ID_TP_MAP[7],
};
case 8:
return {
tp: ID_TP_MAP[8],
id: r.readUint(),
parentID: r.readUint(),
index: r.readUint(),
tag: r.readString(),
svg: r.readBoolean(),
};
case 9:
return {
tp: ID_TP_MAP[9],
id: r.readUint(),
parentID: r.readUint(),
index: r.readUint(),
};
case 10:
return {
tp: ID_TP_MAP[10],
id: r.readUint(),
parentID: r.readUint(),
index: r.readUint(),
};
case 11:
return {
tp: ID_TP_MAP[11],
id: r.readUint(),
};
case 12:
return {
tp: ID_TP_MAP[12],
id: r.readUint(),
name: r.readString(),
value: r.readString(),
};
case 13:
return {
tp: ID_TP_MAP[13],
id: r.readUint(),
name: r.readString(),
};
case 14:
return {
tp: ID_TP_MAP[14],
id: r.readUint(),
data: r.readString(),
};
case 15:
return {
tp: ID_TP_MAP[15],
id: r.readUint(),
data: r.readString(),
};
case 16:
return {
tp: ID_TP_MAP[16],
id: r.readUint(),
x: r.readInt(),
y: r.readInt(),
};
case 18:
return {
tp: ID_TP_MAP[18],
id: r.readUint(),
value: r.readString(),
mask: r.readInt(),
};
case 19:
return {
tp: ID_TP_MAP[19],
id: r.readUint(),
checked: r.readBoolean(),
};
case 20:
return {
tp: ID_TP_MAP[20],
x: r.readUint(),
y: r.readUint(),
};
case 22:
return {
tp: ID_TP_MAP[22],
level: r.readString(),
value: r.readString(),
};
case 37:
return {
tp: ID_TP_MAP[37],
id: r.readUint(),
rule: r.readString(),
index: r.readUint(),
};
case 38:
return {
tp: ID_TP_MAP[38],
id: r.readUint(),
index: r.readUint(),
};
case 39:
return {
tp: ID_TP_MAP[39],
method: r.readString(),
url: r.readString(),
request: r.readString(),
response: r.readString(),
status: r.readUint(),
timestamp: r.readUint(),
duration: r.readUint(),
};
case 40:
return {
tp: ID_TP_MAP[40],
name: r.readString(),
duration: r.readUint(),
args: r.readString(),
result: r.readString(),
};
case 41:
return {
tp: ID_TP_MAP[41],
key: r.readString(),
value: r.readString(),
};
case 44:
return {
tp: ID_TP_MAP[44],
action: r.readString(),
state: r.readString(),
duration: r.readUint(),
};
case 45:
return {
tp: ID_TP_MAP[45],
mutation: r.readString(),
state: r.readString(),
};
case 46:
return {
tp: ID_TP_MAP[46],
type: r.readString(),
payload: r.readString(),
};
case 47:
return {
tp: ID_TP_MAP[47],
action: r.readString(),
state: r.readString(),
duration: r.readUint(),
};
case 48:
return {
tp: ID_TP_MAP[48],
operationKind: r.readString(),
operationName: r.readString(),
variables: r.readString(),
response: r.readString(),
};
case 49:
return {
tp: ID_TP_MAP[49],
frames: r.readInt(),
ticks: r.readInt(),
totalJSHeapSize: r.readUint(),
usedJSHeapSize: r.readUint(),
};
case 54:
return {
tp: ID_TP_MAP[54],
downlink: r.readUint(),
type: r.readString(),
};
case 55:
return {
tp: ID_TP_MAP[55],
hidden: r.readBoolean(),
};
case 59:
return {
tp: ID_TP_MAP[59],
timestamp: r.readUint(),
duration: r.readUint(),
context: r.readUint(),
containerType: r.readUint(),
containerSrc: r.readString(),
containerId: r.readString(),
containerName: r.readString(),
};
case 68:
return {
tp: ID_TP_MAP[68],
method: r.readString(),
url: r.readString(),
request: r.readString(),
response: r.readString(),
status: r.readUint(),
timestamp: r.readUint(),
duration: r.readUint(),
headers: r.readString(),
};
default:
r.readUint(); // IOS skip timestamp
r.skip(r.readUint());
return null;
}
}

View file

@ -1,31 +0,0 @@
export function readUint(buf, p) {
var r = 0, s = 1, b;
do {
b = buf[p++];
r += (b & 0x7F) * s;
s *= 128;
} while (b >= 0x80)
return [r, p];
}
export function readInt(buf, p) {
var r = readUint(buf, p);
if (r[0] % 2) {
r[0] = (r[0] + 1) / -2;
} else {
r[0] = r[0] / 2;
}
return r;
}
export function readString(buf, p) {
var r = readUint(buf, p);
var f = r[1];
r[1] += r[0];
r[0] = new TextDecoder().decode(buf.subarray(f, r[1]));
return r;
}
export function readBoolean(buf, p) {
return [!!buf[p], p+1];
}

View file

@ -1,35 +1,41 @@
import { goTo as listsGoTo } from './lists';
import { update, getState } from './store';
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE } from './MessageDistributor';
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './MessageDistributor';
const fps = 60;
const performance = window.performance || { now: Date.now.bind(Date) };
const requestAnimationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
// @ts-ignore
window.mozRequestAnimationFrame ||
// @ts-ignore
window.oRequestAnimationFrame ||
// @ts-ignore
window.msRequestAnimationFrame ||
(callback => window.setTimeout(() => { callback(performance.now()); }, 1000 / fps));
const cancelAnimationFrame =
window.cancelAnimationFrame ||
// @ts-ignore
window.mozCancelAnimationFrame ||
window.clearTimeout;
const HIGHEST_SPEED = 3;
const HIGHEST_SPEED = 16;
const SPEED_STORAGE_KEY = "__$player-speed$__";
const SKIP_STORAGE_KEY = "__$player-skip$__";
const SKIP_TO_ISSUE_STORAGE_KEY = "__$player-skip-to-issue$__";
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__";
const storedSpeed = +localStorage.getItem(SPEED_STORAGE_KEY);
const initialSpeed = [1,2,3].includes(storedSpeed) ? storedSpeed : 1;
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__";
const storedSpeed: number = parseInt(localStorage.getItem(SPEED_STORAGE_KEY) || "") ;
const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1;
const initialSkip = !!localStorage.getItem(SKIP_STORAGE_KEY);
const initialSkipToIssue = !!localStorage.getItem(SKIP_TO_ISSUE_STORAGE_KEY);
const initialAutoplay = !!localStorage.getItem(AUTOPLAY_STORAGE_KEY);
const initialShowEvents = !!localStorage.getItem(SHOW_EVENTS_STORAGE_KEY);
export const INITIAL_STATE = {
export const INITIAL_STATE: SuperState = {
...SUPER_INITIAL_STATE,
time: 0,
playing: false,
@ -38,28 +44,30 @@ export const INITIAL_STATE = {
inspectorMode: false,
live: false,
livePlay: false,
}
} as const;
export const INITIAL_NON_RESETABLE_STATE = {
skip: initialSkip,
skipToIssue: initialSkipToIssue,
autoplay: initialAutoplay,
speed: initialSpeed,
showEvents: initialShowEvents
}
export default class Player extends MessageDistributor {
_animationFrameRequestId = null;
private _animationFrameRequestId: number = 0;
_setTime(time, index) {
private _setTime(time: number, index?: number) {
update({
time,
completed: false,
});
this.move(time, index);
super.move(time, index);
listsGoTo(time, index);
}
_startAnimation() {
private _startAnimation() {
let prevTime = getState().time;
let animationPrevTime = performance.now();
@ -86,10 +94,10 @@ export default class Player extends MessageDistributor {
const skipInterval = skip && skipIntervals.find(si => si.contains(time)); // TODO: good skip by messages
if (skipInterval) time = skipInterval.end;
const fmt = this.getFirstMessageTime();
const fmt = super.getFirstMessageTime();
if (time < fmt) time = fmt; // ?
const lmt = this.getLastMessageTime();
const lmt = super.getLastMessageTime();
if (livePlay && time < lmt) time = lmt;
if (endTime < lmt) {
update({
@ -144,6 +152,9 @@ export default class Player extends MessageDistributor {
}
jump(time = getState().time, index) {
const { live } = getState();
if (live) return;
if (getState().playing) {
cancelAnimationFrame(this._animationFrameRequestId);
// this._animationFrameRequestId = requestAnimationFrame(() => {
@ -161,7 +172,7 @@ export default class Player extends MessageDistributor {
toggleSkip() {
const skip = !getState().skip;
localStorage.setItem(SKIP_STORAGE_KEY, skip);
localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`);
update({ skip });
}
@ -174,43 +185,49 @@ export default class Player extends MessageDistributor {
if (flag) {
this.pause();
update({ inspectorMode: true });
return this.enableInspector(clickCallback);
return super.enableInspector(clickCallback);
} else {
this.disableInspector();
super.disableInspector();
update({ inspectorMode: false });
}
}
toggleSkipToIssue() {
const skipToIssue = !getState().skipToIssue;
localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, skipToIssue);
localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`);
update({ skipToIssue });
}
toggleAutoplay() {
const autoplay = !getState().autoplay;
localStorage.setItem(AUTOPLAY_STORAGE_KEY, autoplay);
localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`);
update({ autoplay });
}
toggleEvents() {
const showEvents = !getState().showEvents;
localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`);
update({ showEvents });
}
_updateSpeed(speed) {
localStorage.setItem(SPEED_STORAGE_KEY, speed);
_updateSpeed(speed: number) {
localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`);
update({ speed });
}
toggleSpeed() {
const { speed } = getState();
this._updateSpeed(speed < HIGHEST_SPEED ? speed + 1 : 1);
this._updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1);
}
speedUp() {
const { speed } = getState();
this._updateSpeed(Math.min(HIGHEST_SPEED, speed + 1));
this._updateSpeed(Math.min(HIGHEST_SPEED, speed * 2));
}
speedDown() {
const { speed } = getState();
this._updateSpeed(Math.max(1, speed - 1));
this._updateSpeed(Math.max(1, speed/2));
}
clean() {

View file

@ -61,12 +61,14 @@ export const toggleSkip = initCheck((...args) => instance.toggleSkip(...args));
export const toggleSkipToIssue = initCheck((...args) => instance.toggleSkipToIssue(...args));
export const toggleAutoplay = initCheck((...args) => instance.toggleAutoplay(...args));
export const toggleSpeed = initCheck((...args) => instance.toggleSpeed(...args));
export const toggleEvents = initCheck((...args) => instance.toggleEvents(...args));
export const speedUp = initCheck((...args) => instance.speedUp(...args));
export const speedDown = initCheck((...args) => instance.speedDown(...args));
export const attach = initCheck((...args) => instance.attach(...args));
export const markElement = initCheck((...args) => instance.marker && instance.marker.mark(...args));
export const scale = initCheck(() => instance.scale());
export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args));
export const callPeer = initCheck((...args) => instance.assistManager.call(...args))
export const Controls = {
jump,
@ -75,7 +77,9 @@ export const Controls = {
toggleSkip,
toggleSkipToIssue,
toggleAutoplay,
toggleEvents,
toggleSpeed,
speedUp,
speedDown,
callPeer
}

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-camera-video-off" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10.961 12.365a1.99 1.99 0 0 0 .522-1.103l3.11 1.382A1 1 0 0 0 16 11.731V4.269a1 1 0 0 0-1.406-.913l-3.111 1.382A2 2 0 0 0 9.5 3H4.272l.714 1H9.5a1 1 0 0 1 1 1v6a1 1 0 0 1-.144.518l.605.847zM1.428 4.18A.999.999 0 0 0 1 5v6a1 1 0 0 0 1 1h5.014l.714 1H2a2 2 0 0 1-2-2V5c0-.675.334-1.272.847-1.634l.58.814zM15 11.73l-3.5-1.555v-4.35L15 4.269v7.462zm-4.407 3.56-10-14 .814-.58 10 14-.814.58z"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-camera-video" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M0 5a2 2 0 0 1 2-2h7.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 4.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 13H2a2 2 0 0 1-2-2V5zm11.5 5.175 3.5 1.556V4.269l-3.5 1.556v4.35zM2 4a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H2z"/>
</svg>

After

Width:  |  Height:  |  Size: 393 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-double-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8.354 1.646a.5.5 0 0 1 0 .708L2.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
<path fill-rule="evenodd" d="M12.354 1.646a.5.5 0 0 1 0 .708L6.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-double-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L9.293 8 3.646 2.354a.5.5 0 0 1 0-.708z"/>
<path fill-rule="evenodd" d="M7.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L13.293 8 7.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-controller" viewBox="0 0 16 16">
<path d="M11.5 6.027a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-1.5 1.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm2.5-.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-1.5 1.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm-6.5-3h1v1h1v1h-1v1h-1v-1h-1v-1h1v-1z"/>
<path d="M3.051 3.26a.5.5 0 0 1 .354-.613l1.932-.518a.5.5 0 0 1 .62.39c.655-.079 1.35-.117 2.043-.117.72 0 1.443.041 2.12.126a.5.5 0 0 1 .622-.399l1.932.518a.5.5 0 0 1 .306.729c.14.09.266.19.373.297.408.408.78 1.05 1.095 1.772.32.733.599 1.591.805 2.466.206.875.34 1.78.364 2.606.024.816-.059 1.602-.328 2.21a1.42 1.42 0 0 1-1.445.83c-.636-.067-1.115-.394-1.513-.773-.245-.232-.496-.526-.739-.808-.126-.148-.25-.292-.368-.423-.728-.804-1.597-1.527-3.224-1.527-1.627 0-2.496.723-3.224 1.527-.119.131-.242.275-.368.423-.243.282-.494.575-.739.808-.398.38-.877.706-1.513.773a1.42 1.42 0 0 1-1.445-.83c-.27-.608-.352-1.395-.329-2.21.024-.826.16-1.73.365-2.606.206-.875.486-1.733.805-2.466.315-.722.687-1.364 1.094-1.772a2.34 2.34 0 0 1 .433-.335.504.504 0 0 1-.028-.079zm2.036.412c-.877.185-1.469.443-1.733.708-.276.276-.587.783-.885 1.465a13.748 13.748 0 0 0-.748 2.295 12.351 12.351 0 0 0-.339 2.406c-.022.755.062 1.368.243 1.776a.42.42 0 0 0 .426.24c.327-.034.61-.199.929-.502.212-.202.4-.423.615-.674.133-.156.276-.323.44-.504C4.861 9.969 5.978 9.027 8 9.027s3.139.942 3.965 1.855c.164.181.307.348.44.504.214.251.403.472.615.674.318.303.601.468.929.503a.42.42 0 0 0 .426-.241c.18-.408.265-1.02.243-1.776a12.354 12.354 0 0 0-.339-2.406 13.753 13.753 0 0 0-.748-2.295c-.298-.682-.61-1.19-.885-1.465-.264-.265-.856-.523-1.733-.708-.85-.179-1.877-.27-2.913-.27-1.036 0-2.063.091-2.913.27z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-headset" viewBox="0 0 16 16">
<path d="M8 1a5 5 0 0 0-5 5v1h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a6 6 0 1 1 12 0v6a2.5 2.5 0 0 1-2.5 2.5H9.366a1 1 0 0 1-.866.5h-1a1 1 0 1 1 0-2h1a1 1 0 0 1 .866.5H11.5A1.5 1.5 0 0 0 13 12h-1a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h1V6a5 5 0 0 0-5-5z"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-mic-mute" viewBox="0 0 16 16">
<path d="M13 8c0 .564-.094 1.107-.266 1.613l-.814-.814A4.02 4.02 0 0 0 12 8V7a.5.5 0 0 1 1 0v1zm-5 4c.818 0 1.578-.245 2.212-.667l.718.719a4.973 4.973 0 0 1-2.43.923V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 1 0v1a4 4 0 0 0 4 4zm3-9v4.879l-1-1V3a2 2 0 0 0-3.997-.118l-.845-.845A3.001 3.001 0 0 1 11 3z"/>
<path d="m9.486 10.607-.748-.748A2 2 0 0 1 6 8v-.878l-1-1V8a3 3 0 0 0 4.486 2.607zm-7.84-9.253 12 12 .708-.708-12-12-.708.708z"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-mic" viewBox="0 0 16 16">
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 363 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-person" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-telephone-fill" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.885.511a1.745 1.745 0 0 1 2.61.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"/>
</svg>

After

Width:  |  Height:  |  Size: 502 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-telephone" viewBox="0 0 16 16">
<path d="M3.654 1.328a.678.678 0 0 0-1.015-.063L1.605 2.3c-.483.484-.661 1.169-.45 1.77a17.568 17.568 0 0 0 4.168 6.608 17.569 17.569 0 0 0 6.608 4.168c.601.211 1.286.033 1.77-.45l1.034-1.034a.678.678 0 0 0-.063-1.015l-2.307-1.794a.678.678 0 0 0-.58-.122l-2.19.547a1.745 1.745 0 0 1-1.657-.459L5.482 8.062a1.745 1.745 0 0 1-.46-1.657l.548-2.19a.678.678 0 0 0-.122-.58L3.654 1.328zM1.884.511a1.745 1.745 0 0 1 2.612.163L6.29 2.98c.329.423.445.974.315 1.494l-.547 2.19a.678.678 0 0 0 .178.643l2.457 2.457a.678.678 0 0 0 .644.178l2.189-.547a1.745 1.745 0 0 1 1.494.315l2.306 1.794c.829.645.905 1.87.163 2.611l-1.034 1.034c-.74.74-1.846 1.065-2.877.702a18.634 18.634 0 0 1-7.01-4.42 18.634 18.634 0 0 1-4.42-7.009c-.362-1.03-.037-2.137.703-2.877L1.885.511z"/>
</svg>

After

Width:  |  Height:  |  Size: 849 B

View file

@ -26,6 +26,8 @@ function hashString(s: string): number {
export default Record({
sessionId: '',
siteId: '',
projectKey: '',
peerId: '',
live: false,
startedAt: 0,
duration: 0,

View file

@ -16084,6 +16084,34 @@
"sha.js": "^2.4.8"
}
},
"peerjs": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.3.2.tgz",
"integrity": "sha512-+PHfmsC7QGUU8Ye3OLi6tKQZGPCNy7QatUVNw4JtE8alkguF3+DdO5W0bzepqP2OtE9FqH/ltXt37qyvHw2CqA==",
"requires": {
"@types/node": "^10.14.33",
"eventemitter3": "^3.1.2",
"peerjs-js-binarypack": "1.0.1",
"webrtc-adapter": "^7.7.1"
},
"dependencies": {
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
}
}
},
"peerjs-js-binarypack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz",
"integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg=="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@ -21690,6 +21718,14 @@
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==",
"dev": true
},
"rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"requires": {
"sdp": "^2.6.0"
}
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
@ -21871,6 +21907,11 @@
"ajv-keywords": "^3.5.2"
}
},
"sdp": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@ -25871,6 +25912,15 @@
}
}
},
"webrtc-adapter": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
"integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
"requires": {
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.12.0"
}
},
"websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",

View file

@ -29,6 +29,7 @@
"mobx-react-lite": "^3.1.6",
"moment": "^2.27.0",
"moment-range": "^4.0.2",
"peerjs": "^1.3.2",
"rc-time-picker": "^3.7.3",
"react": "^16.13.1",
"react-circular-progressbar": "^2.0.3",

View file

@ -1,6 +1,7 @@
const colors = require('./app/theme/colors');
module.exports = {
important: true,
purge: [],
corePlugins: [
'preflight',

View file

@ -1,10 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"module": "es2020",
"moduleResolution": "node", //?
//"allowJs": true,
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
//"sourceMap": false,
"lib": [ "es2020", "dom" ],
"jsx": "react",
@ -14,7 +15,16 @@
"baseUrl": ".",
"paths": { // TODO: one-source truth
"App": ["./app"],
"UI": ["./app/components/ui"]
"App/*": ["./app/*"],
"Types": ["./app/types" ],
"Types/*": ["./app/types/*"], // Sublime hack
"UI": ["./app/components/ui"],
"Duck": ["./app/duck"],
"Duck/*": ["./app/duck/*"],
"Shared": ["./app/components/shared"],
"Shared/*": ["./app/components/shared/*"],
"Player": ["./app/player"],
"Player/*": ["./app/player/*"],
}
},
"exclude": [

114
package-lock.json generated Normal file
View file

@ -0,0 +1,114 @@
{
"name": "openreplay",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"react-draggable": "^4.4.3"
}
},
"node_modules/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"node_modules/react-draggable": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz",
"integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==",
"dependencies": {
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
},
"dependencies": {
"classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"react-draggable": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz",
"integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==",
"requires": {
"classnames": "^2.2.5",
"prop-types": "^15.6.0"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"dependencies": {
"react-draggable": "^4.4.3"
}
}

6
tracker/tracker-assist/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules
npm-debug.log
lib
cjs
.cache
*.DS_Store

View file

@ -0,0 +1,5 @@
src
tsconfig-cjs.json
tsconfig.json
.prettierrc.json
.cache

View file

@ -0,0 +1,19 @@
Copyright (c) 2021 OpenReplay.com <support@openreplay.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

861
tracker/tracker-assist/package-lock.json generated Normal file
View file

@ -0,0 +1,861 @@
{
"name": "@openreplay/tracker-assist",
"version": "3.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
"integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.14.5"
}
},
"@babel/helper-validator-identifier": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz",
"integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==",
"dev": true
},
"@babel/highlight": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
"integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.5",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
}
},
"@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true
},
"@nodelib/fs.walk": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.7.tgz",
"integrity": "sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==",
"dev": true,
"requires": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
}
},
"@openreplay/tracker": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@openreplay/tracker/-/tracker-3.0.5.tgz",
"integrity": "sha512-hIY7DnQmm7bCe6v+e257WD7OdNuBOWUZ15Q3yUEdyxu7xDNG7brbak9pS97qCt3VY9xGK0RvW/j3ANlRPk8aVg==",
"dev": true,
"requires": {
"error-stack-parser": "^2.0.6"
}
},
"@types/minimist": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz",
"integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==",
"dev": true
},
"@types/node": {
"version": "10.17.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
"integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
"arrify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
"dev": true
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true
},
"camelcase-keys": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
"integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
"dev": true,
"requires": {
"camelcase": "^5.3.1",
"map-obj": "^4.0.0",
"quick-lru": "^4.0.1"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
}
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"dev": true
},
"decamelize-keys": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz",
"integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=",
"dev": true,
"requires": {
"decamelize": "^1.1.0",
"map-obj": "^1.0.0"
},
"dependencies": {
"map-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
"dev": true
}
}
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"requires": {
"path-type": "^4.0.0"
}
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"requires": {
"is-arrayish": "^0.2.1"
}
},
"error-stack-parser": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
"integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
"dev": true,
"requires": {
"stackframe": "^1.1.1"
}
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true
},
"eventemitter3": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
},
"fast-glob": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz",
"integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.0",
"merge2": "^1.3.0",
"micromatch": "^4.0.2",
"picomatch": "^2.2.1"
}
},
"fastq": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz",
"integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"globby": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
"integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==",
"dev": true,
"requires": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.1.1",
"ignore": "^5.1.4",
"merge2": "^1.3.0",
"slash": "^3.0.0"
}
},
"hard-rejection": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz",
"integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==",
"dev": true
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true
},
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
},
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
"indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
"is-core-module": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
"integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
"dev": true
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true
},
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"map-obj": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz",
"integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==",
"dev": true
},
"meow": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/meow/-/meow-7.1.1.tgz",
"integrity": "sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA==",
"dev": true,
"requires": {
"@types/minimist": "^1.2.0",
"camelcase-keys": "^6.2.2",
"decamelize-keys": "^1.1.0",
"hard-rejection": "^2.1.0",
"minimist-options": "4.1.0",
"normalize-package-data": "^2.5.0",
"read-pkg-up": "^7.0.1",
"redent": "^3.0.0",
"trim-newlines": "^3.0.0",
"type-fest": "^0.13.1",
"yargs-parser": "^18.1.3"
}
},
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true
},
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
}
},
"min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true
},
"minimist-options": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz",
"integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==",
"dev": true,
"requires": {
"arrify": "^1.0.1",
"is-plain-obj": "^1.1.0",
"kind-of": "^6.0.3"
},
"dependencies": {
"arrify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
"integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
"dev": true
}
}
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
"dev": true,
"requires": {
"hosted-git-info": "^2.1.4",
"resolve": "^1.10.0",
"semver": "2 || 3 || 4 || 5",
"validate-npm-package-license": "^3.0.1"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"npm-dragndrop": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/npm-dragndrop/-/npm-dragndrop-1.2.0.tgz",
"integrity": "sha1-bgUkAP7Yay8eP0csU4EPkjcRu7U="
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"peerjs": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/peerjs/-/peerjs-1.3.2.tgz",
"integrity": "sha512-+PHfmsC7QGUU8Ye3OLi6tKQZGPCNy7QatUVNw4JtE8alkguF3+DdO5W0bzepqP2OtE9FqH/ltXt37qyvHw2CqA==",
"requires": {
"@types/node": "^10.14.33",
"eventemitter3": "^3.1.2",
"peerjs-js-binarypack": "1.0.1",
"webrtc-adapter": "^7.7.1"
}
},
"peerjs-js-binarypack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/peerjs-js-binarypack/-/peerjs-js-binarypack-1.0.1.tgz",
"integrity": "sha512-N6aeia3NhdpV7kiGxJV5xQiZZCVEEVjRz2T2C6UZQiBkHWHzUv/oWA4myQLcwBwO8LUoR1KWW5oStvwVesmfCg=="
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true
},
"prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"quick-lru": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
"integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
"dev": true
},
"read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
"integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
"dev": true,
"requires": {
"@types/normalize-package-data": "^2.4.0",
"normalize-package-data": "^2.5.0",
"parse-json": "^5.0.0",
"type-fest": "^0.6.0"
},
"dependencies": {
"type-fest": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
"integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
"dev": true
}
}
},
"read-pkg-up": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
"integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
"dev": true,
"requires": {
"find-up": "^4.1.0",
"read-pkg": "^5.2.0",
"type-fest": "^0.8.1"
},
"dependencies": {
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
}
}
},
"redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"requires": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
}
},
"replace-in-files-cli": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/replace-in-files-cli/-/replace-in-files-cli-1.0.0.tgz",
"integrity": "sha512-/HMPLZeCA24CBUQ59ymHji6LyMKM+gEgDZlYsiPvXW6+3PdfOw6SsMCVd9KC2B+KlAEe/8vkJA6gfnexVdF15A==",
"dev": true,
"requires": {
"arrify": "^2.0.1",
"escape-string-regexp": "^4.0.0",
"globby": "^11.0.1",
"meow": "^7.1.1",
"normalize-path": "^3.0.0",
"write-file-atomic": "^3.0.0"
}
},
"resolve": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
}
},
"reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
"rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"requires": {
"sdp": "^2.6.0"
}
},
"run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"requires": {
"queue-microtask": "^1.2.2"
}
},
"sdp": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
"integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
"dev": true,
"requires": {
"spdx-expression-parse": "^3.0.0",
"spdx-license-ids": "^3.0.0"
}
},
"spdx-exceptions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
"integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
"dev": true
},
"spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
"spdx-license-ids": "^3.0.0"
}
},
"spdx-license-ids": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.9.tgz",
"integrity": "sha512-Ki212dKK4ogX+xDo4CtOZBVIwhsKBEfsEEcwmJfLQzirgc2jIWdzg40Unxz/HzEUqM1WFzVlQSMF9kZZ2HboLQ==",
"dev": true
},
"stackframe": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz",
"integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==",
"dev": true
},
"strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"requires": {
"min-indent": "^1.0.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
},
"trim-newlines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
"integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==",
"dev": true
},
"type-fest": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
"dev": true
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
},
"typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"dev": true
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
"dev": true,
"requires": {
"spdx-correct": "^3.0.0",
"spdx-expression-parse": "^3.0.0"
}
},
"webrtc-adapter": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
"integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
"requires": {
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.12.0"
}
},
"write-file-atomic": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
"integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
"dev": true,
"requires": {
"imurmurhash": "^0.1.4",
"is-typedarray": "^1.0.0",
"signal-exit": "^3.0.2",
"typedarray-to-buffer": "^3.1.5"
}
},
"yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
}

View file

@ -0,0 +1,35 @@
{
"name": "@openreplay/tracker-assist",
"description": "Tracker plugin for screen assistance through the WebRTC",
"version": "3.0.0",
"keywords": [
"WebRTC",
"assistance",
"logging",
"replay"
],
"author": "Aleksandr K <alex@openreplay.com>",
"license": "MIT",
"type": "module",
"main": "./lib/index.js",
"scripts": {
"lint": "prettier --write 'src/**/*.ts' README.md && tsc --noEmit",
"build": "npm run build-es && npm run build-cjs",
"build-es": "rm -Rf lib && tsc",
"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' && replace-in-files cjs/* --string='/lib/' --replacement='/'",
"prepublishOnly": "npm run build"
},
"dependencies": {
"npm-dragndrop": "^1.2.0",
"peerjs": "^1.3.2"
},
"peerDependencies": {
"@openreplay/tracker": "^3.1.0"
},
"devDependencies": {
"@openreplay/tracker": "^3.0.5",
"prettier": "^1.18.2",
"replace-in-files-cli": "^1.0.0",
"typescript": "^3.6.4"
}
}

View file

@ -0,0 +1,153 @@
export default class CallWindow {
private iframe: HTMLIFrameElement;
private vRemote: HTMLVideoElement | null = null;
private vLocal: HTMLVideoElement | null = null;
private audioBtn: HTMLAnchorElement | null = null;
private videoBtn: HTMLAnchorElement | null = null;
private tsInterval: ReturnType<typeof setInterval>;
constructor(endCall: () => void) {
const iframe = this.iframe = document.createElement('iframe');
Object.assign(iframe.style, {
position: "absolute",
zIndex: 2147483647 - 1,
//borderRadius: ".25em .25em .4em .4em",
//border: "4px rgba(0, 0, 0, .7)",
border: "none",
bottom: "10px",
right: "10px",
});
//iframe.src = "//static.openreplay.com/tracker-assist/index.html";
iframe.onload = () => {
const doc = iframe.contentDocument;
if (!doc) {
console.error("OpenReplay: CallWindow iframe document is not reachable.")
return;
}
fetch("https://static.openreplay.com/tracker-assist/index.html")
//fetch("file:///Users/shikhu/work/asayer-tester/dist/assist/index.html")
.then(r => r.text())
.then((text) => {
iframe.onload = () => {
iframe.style.height = doc.body.scrollHeight + 'px';
iframe.style.width = doc.body.scrollWidth + 'px';
}
text = text.replace(/href="css/g, "href=\"https://static.openreplay.com/tracker-assist/css")
doc.open();
doc.write(text);
doc.close();
this.vLocal = doc.getElementById("video-local") as HTMLVideoElement;
this.vRemote = doc.getElementById("video-remote") as HTMLVideoElement;
this._trySetStreams();
this.vLocal.parentElement && this.vLocal.parentElement.classList.add("d-none");
this.audioBtn = doc.getElementById("audio-btn") as HTMLAnchorElement;
this.audioBtn.onclick = () => this.toggleAudio();
this.videoBtn = doc.getElementById("video-btn") as HTMLAnchorElement;
this.videoBtn.onclick = () => this.toggleVideo();
const endCallBtn = doc.getElementById("end-call-btn") as HTMLAnchorElement;
endCallBtn.onclick = endCall;
const tsText = doc.getElementById("time-stamp");
const startTs = Date.now();
if (tsText) {
this.tsInterval = setInterval(() => {
const ellapsed = Date.now() - startTs;
const secsFull = ~~(ellapsed / 1000);
const mins = ~~(secsFull / 60);
const secs = secsFull - mins * 60
tsText.innerText = `${mins}:${secs < 10 ? 0 : ''}${secs}`;
}, 500);
}
// TODO: better D'n'D
doc.body.setAttribute("draggable", "true");
doc.body.ondragstart = (e) => {
if (!e.dataTransfer || !e.target) { return; }
e.dataTransfer.setDragImage(doc.body, e.clientX, e.clientY);
};
doc.body.ondragend = e => {
Object.assign(iframe.style, {
left: `${e.clientX}px`,
top: `${e.clientY}px`,
bottom: 'auto',
right: 'auto',
})
}
});
}
document.body.appendChild(iframe);
}
private localStream: MediaStream | null = null;
private remoteStream: MediaStream | null = null;
private _trySetStreams() {
if (this.vRemote && this.remoteStream) {
this.vRemote.srcObject = this.remoteStream;
}
if (this.vLocal && this.localStream) {
this.vLocal.srcObject = this.localStream;
}
}
setRemoteStream(rStream: MediaStream) {
this.remoteStream = rStream;
this._trySetStreams();
}
setLocalStream(lStream: MediaStream) {
this.localStream = lStream;
lStream.getVideoTracks().forEach(track => {
track.enabled = false;
});
this._trySetStreams();
}
toggleAudio() {
let enabled = true;
this.localStream?.getAudioTracks().forEach(track => {
enabled = enabled && !track.enabled;
track.enabled = enabled;
});
const cList = this.audioBtn?.classList;
if (!this.audioBtn) { return; }
if (enabled) {
this.audioBtn.classList.remove("muted");
this.audioBtn.childNodes[1].textContent = "Mute";
} else {
this.audioBtn.classList.add("muted");
this.audioBtn.childNodes[1].textContent = "Unmute";
}
}
toggleVideo() {
let enabled = true;
this.localStream?.getVideoTracks().forEach(track => {
enabled = enabled && !track.enabled;
track.enabled = enabled;
});
if (!this.videoBtn || !this.vLocal || !this.vLocal.parentElement) { return; }
if (enabled) {
this.vLocal.parentElement.classList.remove("d-none");
this.videoBtn.classList.remove("off");
this.videoBtn.childNodes[1].textContent = "Stop Video";
} else {
this.vLocal.parentElement.classList.add("d-none");
this.videoBtn.classList.add("off");
this.videoBtn.childNodes[1].textContent = "Start Video";
}
}
remove() {
clearInterval(this.tsInterval);
if (this.iframe.parentElement) {
document.body.removeChild(this.iframe);
}
}
}

View file

@ -0,0 +1,91 @@
const declineIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 128 128" ><g id="Circle_Grid" data-name="Circle Grid"><circle cx="64" cy="64" fill="#ef5261" r="64"/></g><g id="icon"><path d="m57.831 70.1c8.79 8.79 17.405 12.356 20.508 9.253l4.261-4.26a7.516 7.516 0 0 1 10.629 0l9.566 9.566a7.516 7.516 0 0 1 0 10.629l-7.453 7.453c-7.042 7.042-27.87-2.358-47.832-22.319-9.976-9.981-16.519-19.382-20.748-28.222s-5.086-16.091-1.567-19.61l7.453-7.453a7.516 7.516 0 0 1 10.629 0l9.566 9.563a7.516 7.516 0 0 1 0 10.629l-4.264 4.271c-3.103 3.1.462 11.714 9.252 20.5z" fill="#eeefee"/></g></svg>`;
export default class Confirm {
private wrapper: HTMLDivElement;
constructor(text: string, styles?: Object) {
const wrapper = document.createElement('div');
const popup = document.createElement('div');
const p = document.createElement('p');
p.innerText = text;
const buttons = document.createElement('div');
const answerBtn = document.createElement('button');
answerBtn.innerHTML = declineIcon.replace('fill="#ef5261"', 'fill="green"');
const declineBtn = document.createElement('button');
declineBtn.innerHTML = declineIcon;
buttons.appendChild(answerBtn);
buttons.appendChild(declineBtn);
popup.appendChild(p);
popup.appendChild(buttons);
const btnStyles = {
borderRadius: "50%",
width: "20px",
height: "20px",
background: "transparent",
padding: 0,
margin: 0,
border: 0,
cursor: "pointer",
}
Object.assign(answerBtn.style, btnStyles);
Object.assign(declineBtn.style, btnStyles);
Object.assign(buttons.style, {
display: "flex",
alignItems: "center",
justifyContent: "space-evenly",
});
Object.assign(popup.style, {
position: "relative",
pointerEvents: "auto",
margin: "4em auto",
width: "90%",
maxWidth: "400px",
padding: "25px 30px",
background: "black",
opacity: ".75",
color: "white",
textAlign: "center",
borderRadius: ".25em .25em .4em .4em",
boxShadow: "0 0 20px rgb(0 0 0 / 20%)",
}, styles);
Object.assign(wrapper.style, {
position: "fixed",
left: 0,
top: 0,
height: "100%",
width: "100%",
pointerEvents: "none",
zIndex: 2147483647 - 1,
})
wrapper.appendChild(popup);
this.wrapper = wrapper;
answerBtn.onclick = () => {
this.remove();
this.callback(true);
}
declineBtn.onclick = () => {
this.remove();
this.callback(false);
}
}
mount() {
document.body.appendChild(this.wrapper);
}
private callback: (result: boolean) => void = ()=>{};
onAnswer(callback: (result: boolean) => void) {
this.callback = callback;
}
remove() {
if (!this.wrapper.parentElement) { return; }
document.body.removeChild(this.wrapper);
}
}

View file

@ -0,0 +1,32 @@
export default class Mouse {
private mouse: HTMLDivElement
constructor() {
this.mouse = document.createElement('div');
Object.assign(this.mouse.style, {
width: "20px",
height: "20px",
opacity: ".4",
borderRadius: "50%",
position: "absolute",
zIndex: "999998",
background: "radial-gradient(red, transparent)",
});
document.body.appendChild(this.mouse);
}
move({x, y}: {x?: number, y?: number}) {
Object.assign(this.mouse.style, {
left: `${x || 0}px`,
top: `${y || 0}px`
})
}
remove() {
if (this.mouse.parentElement) {
document.body.removeChild(this.mouse);
}
}
}

View file

@ -0,0 +1,91 @@
const declineIcon = `<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 128 128" ><g id="Circle_Grid" data-name="Circle Grid"><circle cx="64" cy="64" fill="#ef5261" r="64"/></g><g id="icon"><path d="m57.831 70.1c8.79 8.79 17.405 12.356 20.508 9.253l4.261-4.26a7.516 7.516 0 0 1 10.629 0l9.566 9.566a7.516 7.516 0 0 1 0 10.629l-7.453 7.453c-7.042 7.042-27.87-2.358-47.832-22.319-9.976-9.981-16.519-19.382-20.748-28.222s-5.086-16.091-1.567-19.61l7.453-7.453a7.516 7.516 0 0 1 10.629 0l9.566 9.563a7.516 7.516 0 0 1 0 10.629l-4.264 4.271c-3.103 3.1.462 11.714 9.252 20.5z" fill="#eeefee"/></g></svg>`;
export default class Confirm {
private wrapper: HTMLDivElement;
constructor(text: string, styles?: Object) {
const wrapper = document.createElement('div');
const popup = document.createElement('div');
const p = document.createElement('p');
p.innerText = text;
const buttons = document.createElement('div');
const answerBtn = document.createElement('button');
answerBtn.innerHTML = declineIcon.replace('fill="#ef5261"', 'fill="green"');
const declineBtn = document.createElement('button');
declineBtn.innerHTML = declineIcon;
buttons.appendChild(answerBtn);
buttons.appendChild(declineBtn);
popup.appendChild(p);
popup.appendChild(buttons);
const btnStyles = {
borderRadius: "50%",
width: "20px",
height: "20px",
background: "transparent",
padding: 0,
margin: 0,
border: 0,
cursor: "pointer",
}
Object.assign(answerBtn.style, btnStyles);
Object.assign(declineBtn.style, btnStyles);
Object.assign(buttons.style, {
display: "flex",
alignItems: "center",
justifyContent: "space-evenly",
});
Object.assign(popup.style, {
position: "relative",
pointerEvents: "auto",
margin: "4em auto",
width: "90%",
maxWidth: "400px",
padding: "25px 30px",
background: "black",
opacity: ".75",
color: "white",
textAlign: "center",
borderRadius: ".25em .25em .4em .4em",
boxShadow: "0 0 20px rgb(0 0 0 / 20%)",
}, styles);
Object.assign(wrapper.style, {
position: "fixed",
left: 0,
top: 0,
height: "100%",
width: "100%",
pointerEvents: "none",
zIndex: 2147483647 - 1,
})
wrapper.appendChild(popup);
this.wrapper = wrapper;
answerBtn.onclick = () => {
this.remove();
this.callback(true);
}
declineBtn.onclick = () => {
this.remove();
this.callback(false);
}
}
mount() {
document.body.appendChild(this.wrapper);
}
private callback: (result: boolean) => void = ()=>{};
onAnswer(callback: (result: boolean) => void) {
this.callback = callback;
}
remove() {
if (!this.wrapper.parentElement) { return; }
document.body.removeChild(this.wrapper);
}
}

View file

@ -0,0 +1,156 @@
import Peer, { MediaConnection } from 'peerjs';
import type { DataConnection } from 'peerjs';
import { App, Messages } from '@openreplay/tracker';
import type Message from '@openreplay/tracker';
import Mouse from './Mouse';
import CallWindow from './CallWindow';
import Confirm from './Confirm';
export interface Options {
confirmText: string,
confirmStyle: Object, // Styles object
}
export default function(opts: Partial<Options> = {}) {
const options: Options = Object.assign(
{
confirmText: "You have a call. Do you want to answer?",
confirmStyle: {},
},
opts,
);
return function(app: App | null, appOptions: { __DISABLE_SECURE_MODE?: boolean } = {}) {
// @ts-ignore
if (app === null || !navigator?.mediaDevices?.getUserMedia) { // 93.04% browsers
return;
}
let callingPeerDataConn
app.attachStartCallback(function() {
// new CallWindow(()=>{console.log('endcall')});
// @ts-ignore
const peerID = `${app.projectKey}-${app.getSessionID()}`
const peer = new Peer(peerID, {
// @ts-ignore
host: app.getHost(),
path: '/assist',
port: location.protocol === 'http:' && appOptions.__DISABLE_SECURE_MODE ? 80 : 443,
});
console.log(peerID)
peer.on('connection', function(conn) {
console.log('connection')
conn.on('open', function() {
console.log('connection open')
// TODO: onClose
const buffer: Message[][] = [];
let buffering = false;
function sendNext() {
setTimeout(() => {
if (buffer.length) {
conn.send(buffer.shift());
sendNext();
} else {
buffering = false;
}
}, 50);
}
app.stop();
//@ts-ignore (should update tracker dependency)
app.addCommitCallback((messages: Array<Message>): void => {
let i = 0;
while (i < messages.length) {
buffer.push(messages.slice(i, i+=1000));
}
if (!buffering) {
buffering = true;
sendNext();
}
});
app.start();
});
});
let calling = false;
peer.on('call', function(call) {
const dataConn: DataConnection | undefined = peer
.connections[call.peer].find(c => c.type === 'data');
if (calling || !dataConn) {
call.close();
dataConn?.send("call_error");
return;
}
calling = true;
window.addEventListener("beforeunload", () => {
dataConn.open && dataConn.send("call_end");
});
dataConn.on('data', (data) => { // if call closed be a caller before confirm
if (data === "call_end") {
calling = false;
confirm.remove();
}
});
const confirm = new Confirm(options.confirmText, options.confirmStyle);
confirm.mount();
confirm.onAnswer(conf => {
if (!conf || !dataConn.open) {
call.close();
dataConn.open && dataConn.send("call_end");
calling = false;
return;
}
const mouse = new Mouse();
let callUI;
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(oStream => {
const onClose = () => {
console.log("close call...")
if (call.open) { call.close(); }
mouse.remove();
callUI?.remove();
oStream.getTracks().forEach(t => t.stop());
calling = false;
if (dataConn.open) {
dataConn.send("call_end");
}
}
dataConn.on("close", onClose);
call.answer(oStream);
call.on('close', onClose); // Works from time to time (peerjs bug)
const intervalID = setInterval(() => {
if (!call.open) {
onClose();
clearInterval(intervalID);
}
}, 5000);
call.on('error', onClose); // notify about error?
callUI = new CallWindow(onClose);
callUI.setLocalStream(oStream);
call.on('stream', function(iStream) {
callUI.setRemoteStream(iStream);
dataConn.on('data', (data: any) => {
if (data === "call_end") {
onClose();
return;
}
if (call.open && data && typeof data.x === 'number' && typeof data.y === 'number') {
mouse.move(data);
}
});
});
});
});
});
});
}
}

View file

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./cjs"
},
}

Some files were not shown because too many files have changed in this diff Show more