diff --git a/backend/pkg/intervals/intervals.go b/backend/pkg/intervals/intervals.go index d7e6bfad5..5cc603ad6 100644 --- a/backend/pkg/intervals/intervals.go +++ b/backend/pkg/intervals/intervals.go @@ -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 diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index bbba3a246..760123199 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -20,7 +20,8 @@ const siteIdRequiredPaths = [ '/rehydrations', '/sourcemaps', '/errors', - '/funnels' + '/funnels', + '/assist' ]; const noStoringFetchPathStarts = [ diff --git a/frontend/app/components/Assist/Assist.tsx b/frontend/app/components/Assist/Assist.tsx new file mode 100644 index 000000000..74f2095f8 --- /dev/null +++ b/frontend/app/components/Assist/Assist.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import ChatWindow from './ChatWindow'; + + +export default function Assist() { + return ( +
+ {/* */} +
+ ) +} diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.css b/frontend/app/components/Assist/ChatControls/ChatControls.css new file mode 100644 index 000000000..7ec77f758 --- /dev/null +++ b/frontend/app/components/Assist/ChatControls/ChatControls.css @@ -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; +} \ No newline at end of file diff --git a/frontend/app/components/Assist/ChatControls/ChatControls.tsx b/frontend/app/components/Assist/ChatControls/ChatControls.tsx new file mode 100644 index 000000000..6ca747455 --- /dev/null +++ b/frontend/app/components/Assist/ChatControls/ChatControls.tsx @@ -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 ( +
+
+
+ +
+ +
+ +
+
+
+ +
+
+ ) +} + +export default ChatControls diff --git a/frontend/app/components/Assist/ChatControls/index.js b/frontend/app/components/Assist/ChatControls/index.js new file mode 100644 index 000000000..0b52c7325 --- /dev/null +++ b/frontend/app/components/Assist/ChatControls/index.js @@ -0,0 +1 @@ +export { default } from './ChatControls' \ No newline at end of file diff --git a/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx new file mode 100644 index 000000000..e1e5ba1a6 --- /dev/null +++ b/frontend/app/components/Assist/ChatWindow/ChatWindow.tsx @@ -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 = function ChatWindow({ userId, incomeStream, localStream, endCall }) { + const [minimize, setMinimize] = useState(false) + + return ( + +
+
+
Meeting {userId}
+ +
+
+ +
+ +
+
+ +
+
+ ) +} + +export default ChatWindow diff --git a/frontend/app/components/Assist/ChatWindow/chatWindow.css b/frontend/app/components/Assist/ChatWindow/chatWindow.css new file mode 100644 index 000000000..0f1f7694b --- /dev/null +++ b/frontend/app/components/Assist/ChatWindow/chatWindow.css @@ -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; +} \ No newline at end of file diff --git a/frontend/app/components/Assist/ChatWindow/index.ts b/frontend/app/components/Assist/ChatWindow/index.ts new file mode 100644 index 000000000..fbbd1c4c5 --- /dev/null +++ b/frontend/app/components/Assist/ChatWindow/index.ts @@ -0,0 +1 @@ +export { default } from './ChatWindow' \ No newline at end of file diff --git a/frontend/app/components/Assist/ScreenSharing/ScreenSharing.tsx b/frontend/app/components/Assist/ScreenSharing/ScreenSharing.tsx new file mode 100644 index 000000000..35d67f91b --- /dev/null +++ b/frontend/app/components/Assist/ScreenSharing/ScreenSharing.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { Button } from 'UI' + +function ScreenSharing() { + const videoRef = React.createRef() + + 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 ( +
+ +
+ + +
+
+ ) +} + +export default ScreenSharing diff --git a/frontend/app/components/Assist/ScreenSharing/index.js b/frontend/app/components/Assist/ScreenSharing/index.js new file mode 100644 index 000000000..f2bc82ab6 --- /dev/null +++ b/frontend/app/components/Assist/ScreenSharing/index.js @@ -0,0 +1 @@ +export { default } from './ScreenSharing' \ No newline at end of file diff --git a/frontend/app/components/Assist/assist.stories.js b/frontend/app/components/Assist/assist.stories.js new file mode 100644 index 000000000..6259d45bd --- /dev/null +++ b/frontend/app/components/Assist/assist.stories.js @@ -0,0 +1,8 @@ +import { storiesOf } from '@storybook/react'; +import ChatWindow from './ChatWindow'; + +storiesOf('Assist', module) + .add('ChatWindow', () => ( + + )) + diff --git a/frontend/app/components/Assist/components/AssistActions/AassistActions.css b/frontend/app/components/Assist/components/AssistActions/AassistActions.css new file mode 100644 index 000000000..85f5867c6 --- /dev/null +++ b/frontend/app/components/Assist/components/AssistActions/AassistActions.css @@ -0,0 +1,11 @@ +.inCall { + & svg { + fill: $red + } + color: $red; +} + +.disabled { + opacity: 0.5; + pointer-events: none; +} \ No newline at end of file diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx new file mode 100644 index 000000000..b3f16664c --- /dev/null +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -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(null); + const [ localStream, setLocalStream ] = useState(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 ( +
+ + + { inCall ? 'End Meeting' : 'Start Meeting' } +
+ } + content={ `Call ${userId}` } + size="tiny" + inverted + position="top right" + /> +
+ { inCall && } +
+ + ) +} + +const con = connect(null, { toggleChatWindow }) + +export default con(connectPlayer(state => ({ + calling: state.calling, + peerConnectionStatus: state.peerConnectionStatus, +}))(AssistActions)) diff --git a/frontend/app/components/Assist/components/AssistActions/index.ts b/frontend/app/components/Assist/components/AssistActions/index.ts new file mode 100644 index 000000000..3e5108198 --- /dev/null +++ b/frontend/app/components/Assist/components/AssistActions/index.ts @@ -0,0 +1 @@ +export { default } from './AssistActions' \ No newline at end of file diff --git a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.css b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx new file mode 100644 index 000000000..b0b600cac --- /dev/null +++ b/frontend/app/components/Assist/components/VideoContainer/VideoContainer.tsx @@ -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(null); + + useEffect(() => { + if (ref.current) { + ref.current.srcObject = stream; + } + }, [ ref.current, stream ]) + + return ( +
+
+ ) +} + +export default VideoContainer diff --git a/frontend/app/components/Assist/components/VideoContainer/index.ts b/frontend/app/components/Assist/components/VideoContainer/index.ts new file mode 100644 index 000000000..546964d7a --- /dev/null +++ b/frontend/app/components/Assist/components/VideoContainer/index.ts @@ -0,0 +1 @@ +export { default } from './VideoContainer' \ No newline at end of file diff --git a/frontend/app/components/Assist/index.ts b/frontend/app/components/Assist/index.ts new file mode 100644 index 000000000..1c61fa2d8 --- /dev/null +++ b/frontend/app/components/Assist/index.ts @@ -0,0 +1 @@ +export { default } from './Assist' \ No newline at end of file diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js index b9b784ab6..3aa234b26 100644 --- a/frontend/app/components/BugFinder/BugFinder.js +++ b/frontend/app/components/BugFinder/BugFinder.js @@ -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" > - - {activeFlow && activeFlow.type === 'flows' ? - - : - - } + + { activeFlow && activeFlow.type === 'flows' && } + { activeTab.type !== 'live' && } + { activeTab.type === 'live' && } +
{ list[ j ] && this.renderFilterItem(type, list[ j ]) }
); @@ -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 :
{ loading && @@ -173,22 +178,26 @@ export default class FilterModal extends React.PureComponent { { searchQuery && {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')} + + )} }
@@ -201,7 +210,7 @@ export default class FilterModal extends React.PureComponent {
{ this.renderList(category.type, category.keys) }
- + )) } diff --git a/frontend/app/components/BugFinder/CustomFilters/filterModal.css b/frontend/app/components/BugFinder/CustomFilters/filterModal.css index 526016764..5b84dd732 100644 --- a/frontend/app/components/BugFinder/CustomFilters/filterModal.css +++ b/frontend/app/components/BugFinder/CustomFilters/filterModal.css @@ -88,4 +88,9 @@ h5.title { & .filterGroup { width: 205px; } +} + +.disabled { + opacity: 0.5; + pointer-events: none; } \ No newline at end of file diff --git a/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx new file mode 100644 index 000000000..b82e80139 --- /dev/null +++ b/frontend/app/components/BugFinder/LiveSessionList/LiveSessionList.tsx @@ -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, + fetchLiveList: () => void, + filters: List +} + +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 ( +
+ + + {list && (userId ? list.filter(i => i.userId === userId) : list).map(session => ( + + ))} + + +
+ ) +} + +export default connect(state => ({ + list: state.getIn(['sessions', 'liveSessions']), + loading: state.getIn([ 'sessions', 'loading' ]), + filters: state.getIn([ 'filters', 'appliedFilter', 'filters' ]), +}), { fetchLiveList })(LiveSessionList) diff --git a/frontend/app/components/BugFinder/LiveSessionList/index.js b/frontend/app/components/BugFinder/LiveSessionList/index.js new file mode 100644 index 000000000..eb38fa3e7 --- /dev/null +++ b/frontend/app/components/BugFinder/LiveSessionList/index.js @@ -0,0 +1 @@ +export { default } from './LiveSessionList' \ No newline at end of file diff --git a/frontend/app/components/BugFinder/SessionList/SessionList.js b/frontend/app/components/BugFinder/SessionList/SessionList.js index d048c75ae..41375a6af 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionList.js +++ b/frontend/app/components/BugFinder/SessionList/SessionList.js @@ -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 diff --git a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js index af5adf937..129862b29 100644 --- a/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js +++ b/frontend/app/components/BugFinder/SessionsMenu/SessionsMenu.js @@ -72,6 +72,16 @@ function SessionsMenu(props) { /> ))} +
+
+ onMenuItemClick({ name: 'Assist', type: 'live' })} + /> +
+
onMenuItemClick({ name: 'Bookmarks', type: 'bookmark' })} /> -
+
+
diff --git a/frontend/app/components/Client/Sites/NewSiteForm.js b/frontend/app/components/Client/Sites/NewSiteForm.js index dfb8087fe..a9306048c 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.js +++ b/frontend/app/components/Client/Sites/NewSiteForm.js @@ -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) }); } } diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index bb8e1a29a..f1d7bb907 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -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 = () => { diff --git a/frontend/app/components/Session/EventsToggleButton/EventsToggleButton.css b/frontend/app/components/Session/EventsToggleButton/EventsToggleButton.css new file mode 100644 index 000000000..19eda2568 --- /dev/null +++ b/frontend/app/components/Session/EventsToggleButton/EventsToggleButton.css @@ -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); +} \ No newline at end of file diff --git a/frontend/app/components/Session/EventsToggleButton/EventsToggleButton.tsx b/frontend/app/components/Session/EventsToggleButton/EventsToggleButton.tsx new file mode 100644 index 000000000..55ebf9523 --- /dev/null +++ b/frontend/app/components/Session/EventsToggleButton/EventsToggleButton.tsx @@ -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 ( + + + + } + content={ showEvents ? 'Hide Events' : 'Show Events' } + size="tiny" + inverted + position="bottom right" + /> + ) +} + +export default connectPlayer(state => ({ + showEvents: !state.showEvents +}), { toggleEvents })(EventsToggleButton) + diff --git a/frontend/app/components/Session/EventsToggleButton/index.js b/frontend/app/components/Session/EventsToggleButton/index.js new file mode 100644 index 000000000..b391f169d --- /dev/null +++ b/frontend/app/components/Session/EventsToggleButton/index.js @@ -0,0 +1 @@ +export { default } from './EventsToggleButton' \ No newline at end of file diff --git a/frontend/app/components/Session/LivePlayer.js b/frontend/app/components/Session/LivePlayer.js new file mode 100644 index 000000000..cf10dfbea --- /dev/null +++ b/frontend/app/components/Session/LivePlayer.js @@ -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 ( + + + { showAssist && } + +
+ +
+
+
+ ); +} + + +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) + diff --git a/frontend/app/components/Session/Session.js b/frontend/app/components/Session/Session.js index 27c87875c..36fa66d7f 100644 --- a/frontend/app/components/Session/Session.js +++ b/frontend/app/components/Session/Session.js @@ -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({ { session.isIOS ? - : + : (session.live ? : ) } diff --git a/frontend/app/components/Session/WebPlayer.js b/frontend/app/components/Session/WebPlayer.js index 9cfa607a0..28183d9b4 100644 --- a/frontend/app/components/Session/WebPlayer.js +++ b/frontend/app/components/Session/WebPlayer.js @@ -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 ( +
+ + { showEvents && !live && !fullscreen && } +
+ ) +} function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt }) { useEffect(() => { @@ -44,10 +59,7 @@ function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscr -
- - { !live && !fullscreen && } -
+
); @@ -61,5 +73,5 @@ export default connect(state => ({ }), { toggleFullscreen, closeBottomBlock, -})(WebPlayer) +})(WebPlayer) diff --git a/frontend/app/components/Session_/Player/Controls/Controls.js b/frontend/app/components/Session_/Player/Controls/Controls.js index 2be999282..1017ee46a 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.js +++ b/frontend/app/components/Session_/Player/Controls/Controls.js @@ -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 ( -
- +
+ { !live && } { !fullscreen &&
{ !live ? @@ -278,10 +276,7 @@ export default class Controls extends React.Component {
:
- + {'Elapsed'}
@@ -308,7 +303,7 @@ export default class Controls extends React.Component { }
- { !live && showDevTools && + { !live && toggleBottomBlock(NETWORK) } @@ -330,7 +325,7 @@ export default class Controls extends React.Component { icon="fetch" /> } - { showGraphql && + { !live && showGraphql && toggleBottomBlock(GRAPHQL) } @@ -340,7 +335,7 @@ export default class Controls extends React.Component { icon="vendors/graphql" /> } - { showStorage && showDevTools && + { !live && showStorage && toggleBottomBlock(STORAGE) } @@ -350,7 +345,7 @@ export default class Controls extends React.Component { icon={ getStorageIconName(storageType) } /> } - { showDevTools && + { toggleBottomBlock(CONSOLE) } @@ -361,7 +356,7 @@ export default class Controls extends React.Component { hasErrors={ logRedCount > 0 } /> } - { showExceptions && showDevTools && + { showExceptions && toggleBottomBlock(EXCEPTIONS) } @@ -372,7 +367,7 @@ export default class Controls extends React.Component { hasErrors={ exceptionsCount > 0 } /> } - { !live && showDevTools && showStack && + { showStack && toggleBottomBlock(STACKEVENTS) } @@ -383,7 +378,7 @@ export default class Controls extends React.Component { hasErrors={ stackRedCount > 0 } /> } - { showProfiler && showDevTools && + { !live && showProfiler && toggleBottomBlock(PROFILER) } @@ -392,15 +387,18 @@ export default class Controls extends React.Component { label="Profiler" icon="code" /> + } + { + !live && + toggleBottomBlock(PERFORMANCE) } + active={ bottomBlock === PERFORMANCE } + label="Performance" + icon="tachometer-slow" + /> } - toggleBottomBlock(PERFORMANCE) } - active={ bottomBlock === PERFORMANCE } - label="Performance" - icon="tachometer-slow" - /> - { showLongtasks && + { !live && showLongtasks && toggleBottomBlock(LONGTASKS) } @@ -421,13 +419,15 @@ export default class Controls extends React.Component { } - + {!live && ( + toggleBottomBlock(INSPECTOR) } + icon={ inspectorMode ? 'close' : 'inspect' } + label="Inspect" + /> + )}
} diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.js b/frontend/app/components/Session_/Player/Controls/Timeline.js index 0b6a76cf1..d3af75d4f 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.js +++ b/frontend/app/components/Session_/Player/Controls/Timeline.js @@ -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 => (
)) } - { showDevTools && logList + { logList .map(l => l.isRed() && (
)) } - { showDevTools && resourceList + { resourceList .filter(r => r.isRed() || r.isYellow()) .map(r => (
)) } - { showDevTools && fetchList + { fetchList .filter(e => e.isRed()) .map(e => (
)) } - { showDevTools && stackList + { stackList .filter(e => e.isRed()) .map(e => (
)); @@ -18,9 +19,10 @@ const ScreenWrapper = withOverlay()(React.memo(() =>
({ //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" // /> } -
- { !inspectorMode && + {!live && !fullscreen && } +
+ { !removeOverlay &&
- -
- - } + { !live && ( + <> + +
- } - /> + + } + /> + + )} { !live && jiraConfig && jiraConfig.token && }
diff --git a/frontend/app/components/shared/LiveTag/LiveTag.css b/frontend/app/components/shared/LiveTag/LiveTag.css new file mode 100644 index 000000000..cecf45bad --- /dev/null +++ b/frontend/app/components/shared/LiveTag/LiveTag.css @@ -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; + } + } +} \ No newline at end of file diff --git a/frontend/app/components/shared/LiveTag/LiveTag.tsx b/frontend/app/components/shared/LiveTag/LiveTag.tsx new file mode 100644 index 000000000..8adfa398d --- /dev/null +++ b/frontend/app/components/shared/LiveTag/LiveTag.tsx @@ -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 ( + + ) +} + +export default LiveTag diff --git a/frontend/app/components/shared/LiveTag/index.js b/frontend/app/components/shared/LiveTag/index.js new file mode 100644 index 000000000..3e91f2d0e --- /dev/null +++ b/frontend/app/components/shared/LiveTag/index.js @@ -0,0 +1 @@ +export { default } from './LiveTag' \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/Counter.tsx b/frontend/app/components/shared/SessionItem/Counter.tsx new file mode 100644 index 000000000..a91f13d09 --- /dev/null +++ b/frontend/app/components/shared/SessionItem/Counter.tsx @@ -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 ( +
+ {startTime && Duration.fromMillis(duration).toFormat('m:ss')} +
+ ) +} + +export default Counter diff --git a/frontend/app/components/shared/SessionItem/SessionItem.js b/frontend/app/components/shared/SessionItem/SessionItem.js index f93aeefcf..36a7f186a 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.js +++ b/frontend/app/components/shared/SessionItem/SessionItem.js @@ -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'}) => (
{label}
@@ -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 {
-
{ formattedDuration }
+
+ { live ? : formattedDuration } +
-
-
{ eventsCount }
-
+ {!live && ( +
+
{ eventsCount }
+
+ )}
-
-
{ errorsCount }
-
+ {!live && ( +
+
{ errorsCount }
+
+ )} + + { live && }
{ @@ -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, diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.js b/frontend/app/player/MessageDistributor/MessageDistributor.ts similarity index 50% rename from frontend/app/player/MessageDistributor/MessageDistributor.js rename to frontend/app/player/MessageDistributor/MessageDistributor.ts index 925e7ccbf..b65e4aa40 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.js +++ b/frontend/app/player/MessageDistributor/MessageDistributor.ts @@ -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 // } -function initLists() { - const lists = {}; +function initLists(): ListsObject { + const lists: Partial = {} ; 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 = new ListWalker(); - #loadedLocationManager: ListWalker = new ListWalker(); - #titleManager: ListWalker = new ListWalker(); - #connectionInfoManger: ListWalker = new ListWalker(); - #performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager(); - #windowNodeCounter: WindowNodeCounter = new WindowNodeCounter(); - #clickManager: ListWalker = new ListWalker(); + private readonly locationEventManager: ListWalker/**/ = new ListWalker(); + private readonly locationManager: ListWalker = new ListWalker(); + private readonly loadedLocationManager: ListWalker = new ListWalker(); + private readonly connectionInfoManger: ListWalker = new ListWalker(); + private readonly performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager(); + private readonly windowNodeCounter: WindowNodeCounter = new WindowNodeCounter(); + private readonly clickManager: ListWalker = new ListWalker(); - #resizeManager: ListWalker = new ListWalker(); - #pagesManager: PagesManager; - #mouseManager: MouseManager; + private readonly resizeManager: ListWalker = new ListWalker([]); + private readonly pagesManager: PagesManager; + private readonly mouseManager: MouseManager; + private readonly assistManager: AssistManager; - #scrollManager: ListWalker = new ListWalker(); + private readonly scrollManager: ListWalker = 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 = []; - 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 = {}; /* == 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(); } } \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/MessageGenerator.js b/frontend/app/player/MessageDistributor/MessageGenerator.js deleted file mode 100644 index 7b1bce463..000000000 --- a/frontend/app/player/MessageDistributor/MessageGenerator.js +++ /dev/null @@ -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]; - } - } -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/MessageReader.ts b/frontend/app/player/MessageDistributor/MessageReader.ts new file mode 100644 index 000000000..dea8759c9 --- /dev/null +++ b/frontend/app/player/MessageDistributor/MessageReader.ts @@ -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; + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/PrimitiveReader.ts b/frontend/app/player/MessageDistributor/PrimitiveReader.ts new file mode 100644 index 000000000..6ee5ade4e --- /dev/null +++ b/frontend/app/player/MessageDistributor/PrimitiveReader.ts @@ -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; + } +} diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts index e7392c252..e48416cf2 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts @@ -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() { diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts index 730abce16..321dc21b0 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts @@ -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; diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.css b/frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.css index 5c6385dd1..6f5da4549 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.css +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/screen.css @@ -9,6 +9,7 @@ .iframe { position: absolute; border: none; + background: whilte; } .overlay { position: absolute; diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.js b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.js deleted file mode 100644 index f80a89db1..000000000 --- a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.js +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts new file mode 100644 index 000000000..50147f90a --- /dev/null +++ b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts @@ -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(); + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/StatedScreen/index.js b/frontend/app/player/MessageDistributor/StatedScreen/index.ts similarity index 100% rename from frontend/app/player/MessageDistributor/StatedScreen/index.js rename to frontend/app/player/MessageDistributor/StatedScreen/index.ts diff --git a/frontend/app/player/MessageDistributor/Timed.js b/frontend/app/player/MessageDistributor/Timed.js deleted file mode 100644 index 9c0c28b52..000000000 --- a/frontend/app/player/MessageDistributor/Timed.js +++ /dev/null @@ -1,5 +0,0 @@ -// @flow -import type { Message } from './messages'; - -export type Timed = { +time: number }; -export type TimedMessage = Timed & Message; diff --git a/frontend/app/player/MessageDistributor/Timed.ts b/frontend/app/player/MessageDistributor/Timed.ts new file mode 100644 index 000000000..e0a1d6a82 --- /dev/null +++ b/frontend/app/player/MessageDistributor/Timed.ts @@ -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; diff --git a/frontend/app/player/MessageDistributor/managers/ActivityManager.js b/frontend/app/player/MessageDistributor/managers/ActivityManager.js deleted file mode 100644 index 9881ecac1..000000000 --- a/frontend/app/player/MessageDistributor/managers/ActivityManager.js +++ /dev/null @@ -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 { - #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 })); - } - - } - - -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/ActivityManager.ts b/frontend/app/player/MessageDistributor/managers/ActivityManager.ts new file mode 100644 index 000000000..ba17d295e --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/ActivityManager.ts @@ -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; + + +export default class ActivityManager extends ListWalker { + 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)); + } + + } + +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/MessageDistributor/managers/AssistManager.ts new file mode 100644 index 000000000..7700c540d --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/AssistManager.ts @@ -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).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; + } +} + + diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.js b/frontend/app/player/MessageDistributor/managers/DOMManager.js deleted file mode 100644 index d2faf4b68..000000000 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.js +++ /dev/null @@ -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 { - #isMobile: boolean; - #screen: StatedScreen; - // #prop compiles to method that costs mor than strict property call. - _nl: Array = []; - _isLink: Array = []; // Optimisations - _bodyId: number = -1; - _postponedBodyMessage: ?CreateElementNode = null; - #nodeScrollManagers: Array> = []; - - #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 || "" }`); - 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 { - 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); - } -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOMManager.ts new file mode 100644 index 000000000..1d723a923 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/DOMManager.ts @@ -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 { + private isMobile: boolean; + private screen: StatedScreen; + private nl: Array = []; + private isLink: Array = []; // Optimisations + private bodyId: number = -1; + private postponedBodyMessage: CreateElementNode | null = null; + private nodeScrollManagers: Array> = []; + + 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 || "" }`); + // @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 { + 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); + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/ListWalker.js b/frontend/app/player/MessageDistributor/managers/ListWalker.ts similarity index 89% rename from frontend/app/player/MessageDistributor/managers/ListWalker.js rename to frontend/app/player/MessageDistributor/managers/ListWalker.ts index 0354a365c..6283ff3ab 100644 --- a/frontend/app/player/MessageDistributor/managers/ListWalker.js +++ b/frontend/app/player/MessageDistributor/managers/ListWalker.ts @@ -1,8 +1,6 @@ -//@flow - import type { Timed } from '../Timed'; -export default class ListWalker { +export default class ListWalker { // Optimisation: #prop compiles to method that costs mor than strict property call. _p = 0; _list: Array; @@ -15,7 +13,7 @@ export default class ListWalker { } 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 { } 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 { Assumed that the current message is already handled so if pointer doesn't cahnge 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 { 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 { // 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(); diff --git a/frontend/app/player/MessageDistributor/managers/MouseManager.js b/frontend/app/player/MessageDistributor/managers/MouseManager.ts similarity index 50% rename from frontend/app/player/MessageDistributor/managers/MouseManager.js rename to frontend/app/player/MessageDistributor/managers/MouseManager.ts index 7352043c1..a86408823 100644 --- a/frontend/app/player/MessageDistributor/managers/MouseManager.js +++ b/frontend/app/player/MessageDistributor/managers/MouseManager.ts @@ -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 { - #screen: StatedScreen; - #hoverElements: Array = []; + private hoverElements: Array = []; - 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(); } } diff --git a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.js b/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.js deleted file mode 100644 index 005b7004a..000000000 --- a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.js +++ /dev/null @@ -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 { - #chart: Array = []; - #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 { - return this.#chart; - } - - get avaliability(): { cpu: boolean, fps: boolean, heap: boolean } { - return { - cpu: this.#cpuAvaliable, - fps: this.#fpsAvaliable, - heap: this.#heapAvaliable, - nodes: true, - } - } - -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts b/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts new file mode 100644 index 000000000..1b11813b2 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts @@ -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 { + private chart: Array = []; + 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 { + 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, + } + } + +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/messages.js b/frontend/app/player/MessageDistributor/messages.js deleted file mode 100644 index 6ff087eaf..000000000 --- a/frontend/app/player/MessageDistributor/messages.js +++ /dev/null @@ -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]; -} diff --git a/frontend/app/player/MessageDistributor/messages.ts b/frontend/app/player/MessageDistributor/messages.ts new file mode 100644 index 000000000..286ec024e --- /dev/null +++ b/frontend/app/player/MessageDistributor/messages.ts @@ -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; + } +} diff --git a/frontend/app/player/MessageDistributor/readPrimitives.js b/frontend/app/player/MessageDistributor/readPrimitives.js deleted file mode 100644 index 3279c08f3..000000000 --- a/frontend/app/player/MessageDistributor/readPrimitives.js +++ /dev/null @@ -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]; -} diff --git a/frontend/app/player/Player.js b/frontend/app/player/Player.ts similarity index 75% rename from frontend/app/player/Player.js rename to frontend/app/player/Player.ts index e6a90fd2c..ffc36c022 100644 --- a/frontend/app/player/Player.js +++ b/frontend/app/player/Player.ts @@ -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() { diff --git a/frontend/app/player/singletone.js b/frontend/app/player/singletone.js index 1e1f34234..63865de91 100644 --- a/frontend/app/player/singletone.js +++ b/frontend/app/player/singletone.js @@ -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 } diff --git a/frontend/app/svg/icons/camera-video-off.svg b/frontend/app/svg/icons/camera-video-off.svg new file mode 100644 index 000000000..17dcd971a --- /dev/null +++ b/frontend/app/svg/icons/camera-video-off.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/camera-video.svg b/frontend/app/svg/icons/camera-video.svg new file mode 100644 index 000000000..568193ff7 --- /dev/null +++ b/frontend/app/svg/icons/camera-video.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/chevron-double-left.svg b/frontend/app/svg/icons/chevron-double-left.svg new file mode 100644 index 000000000..7181fd111 --- /dev/null +++ b/frontend/app/svg/icons/chevron-double-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/chevron-double-right.svg b/frontend/app/svg/icons/chevron-double-right.svg new file mode 100644 index 000000000..73e1b352d --- /dev/null +++ b/frontend/app/svg/icons/chevron-double-right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/controller.svg b/frontend/app/svg/icons/controller.svg new file mode 100644 index 000000000..15e777456 --- /dev/null +++ b/frontend/app/svg/icons/controller.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/headset.svg b/frontend/app/svg/icons/headset.svg new file mode 100644 index 000000000..c0b56fe8b --- /dev/null +++ b/frontend/app/svg/icons/headset.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/mic-mute.svg b/frontend/app/svg/icons/mic-mute.svg new file mode 100644 index 000000000..b88c405d2 --- /dev/null +++ b/frontend/app/svg/icons/mic-mute.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/mic.svg b/frontend/app/svg/icons/mic.svg new file mode 100644 index 000000000..ff4015706 --- /dev/null +++ b/frontend/app/svg/icons/mic.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/person.svg b/frontend/app/svg/icons/person.svg new file mode 100644 index 000000000..98de1b339 --- /dev/null +++ b/frontend/app/svg/icons/person.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/telephone-fill.svg b/frontend/app/svg/icons/telephone-fill.svg new file mode 100644 index 000000000..3eb871ed5 --- /dev/null +++ b/frontend/app/svg/icons/telephone-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/telephone.svg b/frontend/app/svg/icons/telephone.svg new file mode 100644 index 000000000..0ec6550a4 --- /dev/null +++ b/frontend/app/svg/icons/telephone.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/types/session/session.js b/frontend/app/types/session/session.js index 132afcc7d..3926c1901 100644 --- a/frontend/app/types/session/session.js +++ b/frontend/app/types/session/session.js @@ -26,6 +26,8 @@ function hashString(s: string): number { export default Record({ sessionId: '', siteId: '', + projectKey: '', + peerId: '', live: false, startedAt: 0, duration: 0, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dccb5df68..6b75658ca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 8cc292e32..d9816a716 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 53a66aa44..2058dcc16 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,6 +1,7 @@ const colors = require('./app/theme/colors'); module.exports = { + important: true, purge: [], corePlugins: [ 'preflight', diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 3a86bade4..b5d3a9688 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -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": [ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..f85b56483 --- /dev/null +++ b/package-lock.json @@ -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==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..92fa0e3d7 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "react-draggable": "^4.4.3" + } +} diff --git a/tracker/tracker-assist/.gitignore b/tracker/tracker-assist/.gitignore new file mode 100644 index 000000000..6ddaccbb5 --- /dev/null +++ b/tracker/tracker-assist/.gitignore @@ -0,0 +1,6 @@ +node_modules +npm-debug.log +lib +cjs +.cache +*.DS_Store \ No newline at end of file diff --git a/tracker/tracker-assist/.npmignore b/tracker/tracker-assist/.npmignore new file mode 100644 index 000000000..a3e81897a --- /dev/null +++ b/tracker/tracker-assist/.npmignore @@ -0,0 +1,5 @@ +src +tsconfig-cjs.json +tsconfig.json +.prettierrc.json +.cache diff --git a/tracker/tracker-assist/LICENSE b/tracker/tracker-assist/LICENSE new file mode 100644 index 000000000..b57f138e0 --- /dev/null +++ b/tracker/tracker-assist/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2021 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. diff --git a/tracker/tracker-assist/README.md b/tracker/tracker-assist/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/tracker/tracker-assist/package-lock.json b/tracker/tracker-assist/package-lock.json new file mode 100644 index 000000000..4ccf9ba06 --- /dev/null +++ b/tracker/tracker-assist/package-lock.json @@ -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" + } + } + } +} diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json new file mode 100644 index 000000000..581eb7cd4 --- /dev/null +++ b/tracker/tracker-assist/package.json @@ -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 ", + "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" + } +} diff --git a/tracker/tracker-assist/src/CallWindow.ts b/tracker/tracker-assist/src/CallWindow.ts new file mode 100644 index 000000000..30a69f291 --- /dev/null +++ b/tracker/tracker-assist/src/CallWindow.ts @@ -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; + 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); + } + } + +} \ No newline at end of file diff --git a/tracker/tracker-assist/src/Confirm.ts b/tracker/tracker-assist/src/Confirm.ts new file mode 100644 index 000000000..a7da63abd --- /dev/null +++ b/tracker/tracker-assist/src/Confirm.ts @@ -0,0 +1,91 @@ + +const declineIcon = ``; + +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); + } +} diff --git a/tracker/tracker-assist/src/Mouse.ts b/tracker/tracker-assist/src/Mouse.ts new file mode 100644 index 000000000..03558ce1c --- /dev/null +++ b/tracker/tracker-assist/src/Mouse.ts @@ -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); + } + } +} \ No newline at end of file diff --git a/tracker/tracker-assist/src/confirm.ts b/tracker/tracker-assist/src/confirm.ts new file mode 100644 index 000000000..a7da63abd --- /dev/null +++ b/tracker/tracker-assist/src/confirm.ts @@ -0,0 +1,91 @@ + +const declineIcon = ``; + +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); + } +} diff --git a/tracker/tracker-assist/src/index.ts b/tracker/tracker-assist/src/index.ts new file mode 100644 index 000000000..ffa3502bc --- /dev/null +++ b/tracker/tracker-assist/src/index.ts @@ -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 = {}) { + 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): 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); + } + }); + }); + }); + }); + }); + }); + } +} diff --git a/tracker/tracker-assist/tsconfig-cjs.json b/tracker/tracker-assist/tsconfig-cjs.json new file mode 100644 index 000000000..72d985654 --- /dev/null +++ b/tracker/tracker-assist/tsconfig-cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "./cjs" + }, +} \ No newline at end of file diff --git a/tracker/tracker-assist/tsconfig.json b/tracker/tracker-assist/tsconfig.json new file mode 100644 index 000000000..bb8f6a4c4 --- /dev/null +++ b/tracker/tracker-assist/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "noImplicitThis": true, + "strictNullChecks": true, + "alwaysStrict": true, + "target": "es6", + "module": "es6", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "declaration": true, + "outDir": "./lib" + } +} diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 4765ac2c5..3692baa66 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "3.0.5", + "version": "3.1.0", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 44efd9f84..6e862e0bb 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -9,7 +9,7 @@ import { deviceMemory, jsHeapSizeLimit } from '../modules/performance'; import type { Options as ObserverOptions } from './observer'; -import type { Options as WebworkerOptions, MessageData } from '../../webworker/types'; +import type { Options as WebworkerOptions, WorkerMessageData } from '../../messages/webworker'; export type Options = { revID: string; @@ -23,19 +23,22 @@ export type Options = { } & ObserverOptions & WebworkerOptions; type Callback = () => void; +type CommitCallback = (messages: Array) => void; export const DEFAULT_INGEST_POINT = 'https://ingest.openreplay.com'; export default class App { readonly nodes: Nodes; readonly ticker: Ticker; + readonly projectKey: string; private readonly messages: Array = []; private readonly observer: Observer; - private readonly startCallbacks: Array; - private readonly stopCallbacks: Array; + private readonly startCallbacks: Array = []; + private readonly stopCallbacks: Array = []; + private readonly commitCallbacks: Array = []; private readonly options: Options; - private readonly projectKey: string; private readonly revID: string; + private _sessionID: string | null = null; private isActive = false; private version = 'TRACKER_VERSION'; private readonly worker?: Worker; @@ -67,8 +70,6 @@ export default class App { this.observer = new Observer(this, this.options); this.ticker = new Ticker(this); this.ticker.attach(() => this.commit()); - this.startCallbacks = []; - this.stopCallbacks = []; try { this.worker = new Worker( URL.createObjectURL( @@ -94,8 +95,10 @@ export default class App { this.worker.postMessage(null); } } + // TODO: keep better tactics, discard others (look https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) this.attachEventListener(window, 'beforeunload', alertWorker, false); this.attachEventListener(document, 'mouseleave', alertWorker, false, false); + this.attachEventListener(document, 'visibilitychange', alertWorker, false); } catch (e) { /* TODO: send report */} } send(message: Message, urgent = false): void { @@ -111,10 +114,16 @@ export default class App { if (this.worker && this.messages.length) { this.messages.unshift(new Timestamp(timestamp())); this.worker.postMessage(this.messages); + this.commitCallbacks.forEach(cb => cb(this.messages)); this.messages.length = 0; } } + addCommitCallback(cb: CommitCallback): void { + this.commitCallbacks.push(cb) + } + + safe void>(fn: T): T { const app = this; return function (this: any, ...args: any) { @@ -161,9 +170,11 @@ export default class App { return token; } } - // @Depricated; for the old fetch-plugin versions - sessionID(): string | undefined { - return this.getSessionToken(); + getSessionID(): string | undefined { + return this._sessionID || undefined; + } + getHost(): string { + return new URL(this.options.ingestPoint).host; } isServiceURL(url: string): boolean { @@ -189,7 +200,7 @@ export default class App { sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()); const startTimestamp = timestamp(); - const messageData: MessageData = { + const messageData: WorkerMessageData = { ingestPoint: this.options.ingestPoint, pageNo, startTimestamp, @@ -197,10 +208,6 @@ export default class App { connAttemptGap: this.options.connAttemptGap, } this.worker.postMessage(messageData); // brings delay of 10th ms? - this.observer.observe(); - this.startCallbacks.forEach((cb) => cb()); - this.ticker.start(); - window.fetch(this.options.ingestPoint + '/v1/web/start', { method: 'POST', headers: { @@ -227,21 +234,27 @@ export default class App { } }) .then(r => { - const { token, userUUID } = r; + const { token, userUUID, sessionID } = r; if (typeof token !== 'string' || typeof userUUID !== 'string') { throw new Error("Incorrect server responce"); } sessionStorage.setItem(this.options.session_token_key, token); localStorage.setItem(this.options.local_uuid_key, userUUID); + if (typeof sessionID === 'string') { + this._sessionID = sessionID; + } if (!this.worker) { throw new Error("Stranger things: no worker found after start request"); } this.worker.postMessage({ token }); + this.observer.observe(); + this.startCallbacks.forEach((cb) => cb()); + this.ticker.start(); log("OpenReplay tracking started."); if (typeof this.options.onStart === 'function') { - this.options.onStart({ sessionToken: token, userUUID, sessionID: token /* back compat (depricated) */ }); + this.options.onStart({ sessionToken: token, userUUID, sessionID }); } }) .catch(e => { diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index d6c8481df..ff90b51e5 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -64,12 +64,12 @@ function processOptions(obj: any): obj is Options { export default class API { private readonly app: App | null = null; - constructor(options: Options) { + constructor(private readonly options: Options) { if (!IN_BROWSER || !processOptions(options)) { return; } if (!options.__DISABLE_SECURE_MODE && location.protocol !== 'https:') { - console.error("OpenReplay: Your website must be publicly accessible and running on SSL in order for OpenReplay to properly capture and replay the user session.") + console.error("OpenReplay: Your website must be publicly accessible and running on SSL in order for OpenReplay to properly capture and replay the user session. You can disable this check by setting `__DISABLE_SECURE_MODE` option to `true` if you are testing in localhost. Keep in mind, that asset files on a local machine are not available to the outside world. This might affect tracking if you use css files.") return; } const doNotTrack = options.respectDoNotTrack && (navigator.doNotTrack == '1' || window.doNotTrack == '1'); @@ -114,8 +114,8 @@ export default class API { } } - use(fn: (app: App | null) => T): T { - return fn(this.app); + use(fn: (app: App | null, options?: Options) => T): T { + return fn(this.app, this.options); } isActive(): boolean { @@ -152,9 +152,15 @@ export default class API { } return this.app.getSessionToken(); } + getSessionID(): string | null | undefined { + if (this.app === null) { + return null; + } + return this.app.getSessionID(); + } sessionID(): string | null | undefined { - depricationWarn("'sessionID' method", "'getSessionToken' method", "/") - return this.getSessionToken(); + depricationWarn("'sessionID' method", "'getSessionID' method", "/"); + return this.getSessionID(); } setUserID(id: string): void { diff --git a/tracker/tracker/src/main/modules/cssrules.ts b/tracker/tracker/src/main/modules/cssrules.ts index 50c6fde39..366a7d3fe 100644 --- a/tracker/tracker/src/main/modules/cssrules.ts +++ b/tracker/tracker/src/main/modules/cssrules.ts @@ -1,5 +1,6 @@ import App from '../app'; -import { CSSInsertRule, CSSDeleteRule, TechnicalInfo } from '../../messages'; +import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../../messages'; +import { getBaseURI } from '../utils'; export default function(app: App | null) { if (app === null) { @@ -13,7 +14,7 @@ export default function(app: App | null) { const processOperation = app.safe( (stylesheet: CSSStyleSheet, index: number, rule?: string) => { const sendMessage = typeof rule === 'string' - ? (nodeID: number) => app.send(new CSSInsertRule(nodeID, rule, index)) + ? (nodeID: number) => app.send(new CSSInsertRuleURLBased(nodeID, rule, index, getBaseURI())) : (nodeID: number) => app.send(new CSSDeleteRule(nodeID, index)); // TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule) if (stylesheet.ownerNode == null) { diff --git a/tracker/tracker/src/webworker/transformer.js.temp b/tracker/tracker/src/webworker/transformer.js.temp new file mode 100644 index 000000000..cf80d681b --- /dev/null +++ b/tracker/tracker/src/webworker/transformer.js.temp @@ -0,0 +1,21 @@ +import Message from '../messages/message'; + + + + +class MessageTransformer { + private urlRewriter?: URLRewriter + + constructor() { + + } + + transform(m: Message): Message { + if (m instanceof SetNodeAttribute) { + if (m.name == "src" || m.name == "href") { + sendAssetForCache + } + } + } + +} \ No newline at end of file