feat(tracker/ui): support for multi tab sessions (#1236)
* feat(tracker): add support for multi tab sessions
* feat(backend): added support of multitabs
* fix(backend): added support of deprecated batch meta message to pre-decoder
* fix(backend): fixed nil meta issue for TabData messages in sink
* feat(player): add tabmanager
* feat(player): basic tabchange event support
* feat(player): pick tabstate for console panel and timeline
* fix(player): only display tabs that are created
* feat(player): connect performance, xray and events to tab state
* feat(player): merge all tabs data for overview
* feat(backend/tracker): extract tabdata into separate message from batchmeta
* fix(tracker): fix new session check
* fix(backend): remove batchmetadeprecated
* fix(backend): fix switch case
* fix(player): fix for tab message size
* feat(tracker): check for active tabs with broadcast channel
* feat(tracker): prevent multiple messages
* fix(tracker): ignore beacons from same tab, only ask if token isnt present yet, add small delay before start to wait for answer
* feat(player): support new msg struct in assist player
* fix(player): fix some livepl components for multi tab states
* feat(tracker): add option to disable multitab
* feat(tracker): add multitab to assist plugin
* feat(player): back compat for tab id
* fix(ui): fix missing list in controls
* fix(ui): optional list update
* feat(ui): fix visuals for multitab; use window focus event for tabs
* fix(tracker): fix for dying tests (added tabid to writer, refactored other tests)
* feat(ui): update LivePlayerSubHeader.tsx to support tabs
* feat(backend): added tabs support to devtools mob files
* feat(ui): connect state to current tab properly
* feat(backend): added multitab support to assits
* feat(backend): removed data check in agent message
* feat(backend): debug on
* fix(backend): fixed typo in message broadcast
* feat(backend): fixed issue in connect method
* fix(assist): fixed typo
* feat(assist): added more debug logs
* feat(assist): removed one log
* feat(assist): more logs
* feat(assist): use query.peerId
* feat(assist): more logs
* feat(assist): fixed session update
* fix(assist): fixed getSessions
* fix(assist): fixed request_control broadcast
* fix(assist): fixed typo
* fix(assist): added missed line
* fix(assist): fix typo
* feat(tracker): multitab support for assist sessions
* fix(tracker): fix dead tests (tabid prop)
* fix(tracker): fix yaml
* fix(tracker): timers issue
* fix(ui): fix ui E2E tests with magic?
* feat(assist): multitabs support for ee version
* fix(assist): added missed method import
* fix(tracker): fix fix events in assist
* feat(assist): added back compatibility for sessions without tabId
* fix(assist): apply message's top layer structure before broadcast call
* fix(assist): added random tabID for prev version
* fix(assist): added random tabID for prev version (ee)
* feat(assist): added debug logs
* fix(assist): fix typo in sessions_agents_count method
* fix(assist): fixed more typos in copy-pastes
* fix(tracker): fix restart timings
* feat(backend): added tabIDs for some events
* feat(ui): add tab change event to the user steps bar
* Revert "feat(backend): added tabIDs for some events"
This reverts commit 1467ad7f9f.
* feat(ui): revert timeline and xray to grab events from all tabs
* fix(ui): fix typo
---------
Co-authored-by: Alexander Zavorotynskiy <zavorotynskiy@pm.me>
This commit is contained in:
parent
fe0840ee84
commit
2ed4bba33e
94 changed files with 2749 additions and 1540 deletions
65
.github/workflows/tracker-tests.yaml
vendored
Normal file
65
.github/workflows/tracker-tests.yaml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Checking unit tests for tracker and assist
|
||||
name: Tracker tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- tracker/**
|
||||
pull_request:
|
||||
branches: [ "dev", "main" ]
|
||||
paths:
|
||||
- frontend/**
|
||||
- tracker/**
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: macos-latest
|
||||
name: Build and test Tracker
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [ 16.x ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Cache tracker modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tracker/tracker/node_modules
|
||||
key: ${{ runner.OS }}-test_tracker_build-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
test_tracker_build{{ runner.OS }}-build-
|
||||
test_tracker_build{{ runner.OS }}-
|
||||
- name: Cache tracker-assist modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: tracker/tracker-assist/node_modules
|
||||
key: ${{ runner.OS }}-test_tracker_build-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
test_tracker_build{{ runner.OS }}-build-
|
||||
test_tracker_build{{ runner.OS }}-
|
||||
- name: Setup Testing packages
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
npm i -g yarn
|
||||
yarn
|
||||
- name: Setup Testing packages
|
||||
run: |
|
||||
cd tracker/tracker-assist
|
||||
yarn
|
||||
- name: Jest tests
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
yarn test:ci
|
||||
- name: Jest tests
|
||||
run: |
|
||||
cd tracker/tracker-assist
|
||||
yarn test:ci
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: tracker
|
||||
name: tracker
|
||||
10
.github/workflows/ui-tests.js.yml
vendored
10
.github/workflows/ui-tests.js.yml
vendored
|
|
@ -47,16 +47,6 @@ jobs:
|
|||
cd tracker/tracker
|
||||
npm i -g yarn
|
||||
yarn
|
||||
- name: Jest tests
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
yarn test
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: tracker
|
||||
name: tracker
|
||||
- name: Build tracker inst
|
||||
run: |
|
||||
cd tracker/tracker
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const _io = require('socket.io');
|
||||
const express = require('express');
|
||||
const {
|
||||
extractRoomId,
|
||||
extractPeerId,
|
||||
extractProjectKeyFromRequest,
|
||||
extractSessionIdFromRequest,
|
||||
|
|
@ -24,7 +25,7 @@ const {
|
|||
const wsRouter = express.Router();
|
||||
|
||||
let io;
|
||||
const debug = process.env.debug === "1";
|
||||
const debug = true;//process.env.debug === "1";
|
||||
|
||||
const createSocketIOServer = function (server, prefix) {
|
||||
io = _io(server, {
|
||||
|
|
@ -47,25 +48,30 @@ const respond = function (res, data) {
|
|||
const socketsList = async function (req, res) {
|
||||
debug && console.log("[WS]looking for all available sessions");
|
||||
let filters = await extractPayloadFromRequest(req);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessionsPerProject = {};
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(roomId);
|
||||
if (projectKey !== undefined) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
liveSessionsPerProject[projectKey] = liveSessionsPerProject[projectKey] || new Set();
|
||||
if (withFilters) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo
|
||||
&& isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessionsPerProject[projectKey].add(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessionsPerProject[projectKey].add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
let liveSessions = {};
|
||||
liveSessionsPerProject.forEach((sessions, projectId) => {
|
||||
liveSessions[projectId] = Array.from(sessions);
|
||||
});
|
||||
respond(res, liveSessions);
|
||||
}
|
||||
|
||||
|
|
@ -74,35 +80,36 @@ const socketsListByProject = async function (req, res) {
|
|||
let _projectKey = extractProjectKeyFromRequest(req);
|
||||
let _sessionId = extractSessionIdFromRequest(req);
|
||||
let filters = await extractPayloadFromRequest(req);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessions = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(roomId);
|
||||
if (projectKey === _projectKey && (_sessionId === undefined || _sessionId === sessionId)) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
if (withFilters) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo
|
||||
&& isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[_projectKey] = liveSessions[_projectKey] || [];
|
||||
respond(res, _sessionId === undefined ? sortPaginate(liveSessions[_projectKey], filters)
|
||||
: liveSessions[_projectKey].length > 0 ? liveSessions[_projectKey][0]
|
||||
let sessions = Array.from(liveSessions);
|
||||
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters)
|
||||
: sessions.length > 0 ? sessions[0]
|
||||
: null);
|
||||
}
|
||||
|
||||
const socketsLive = async function (req, res) {
|
||||
debug && console.log("[WS]looking for all available LIVE sessions");
|
||||
let filters = await extractPayloadFromRequest(req);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessionsPerProject = {};
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(peerId);
|
||||
|
|
@ -110,18 +117,22 @@ const socketsLive = async function (req, res) {
|
|||
let connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
liveSessionsPerProject[projectKey] = liveSessionsPerProject[projectKey] || new Set();
|
||||
if (withFilters) {
|
||||
if (item.handshake.query.sessionInfo && isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
liveSessionsPerProject[projectKey].add(item.handshake.query.sessionInfo);
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
liveSessionsPerProject[projectKey].add(item.handshake.query.sessionInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let liveSessions = {};
|
||||
liveSessionsPerProject.forEach((sessions, projectId) => {
|
||||
liveSessions[projectId] = Array.from(sessions);
|
||||
});
|
||||
respond(res, sortPaginate(liveSessions, filters));
|
||||
}
|
||||
|
||||
|
|
@ -130,30 +141,36 @@ const socketsLiveByProject = async function (req, res) {
|
|||
let _projectKey = extractProjectKeyFromRequest(req);
|
||||
let _sessionId = extractSessionIdFromRequest(req);
|
||||
let filters = await extractPayloadFromRequest(req);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessions = new Set();
|
||||
const sessIDs = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(roomId);
|
||||
if (projectKey === _projectKey && (_sessionId === undefined || _sessionId === sessionId)) {
|
||||
let connected_sockets = await io.in(peerId).fetchSockets();
|
||||
let connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
if (item.handshake.query.sessionInfo && isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (withFilters) {
|
||||
if (item.handshake.query.sessionInfo &&
|
||||
isValidSession(item.handshake.query.sessionInfo, filters.filter) &&
|
||||
!sessIDs.has(item.handshake.query.sessionInfo.sessionID)
|
||||
) {
|
||||
liveSessions.add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (!sessIDs.has(item.handshake.query.sessionInfo.sessionID)) {
|
||||
liveSessions.add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[_projectKey] = liveSessions[_projectKey] || [];
|
||||
respond(res, _sessionId === undefined ? sortPaginate(liveSessions[_projectKey], filters)
|
||||
: liveSessions[_projectKey].length > 0 ? liveSessions[_projectKey][0]
|
||||
: null);
|
||||
}
|
||||
let sessions = Array.from(liveSessions);
|
||||
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters) : sessions.length > 0 ? sessions[0] : null);
|
||||
}
|
||||
|
||||
const autocomplete = async function (req, res) {
|
||||
|
|
@ -178,10 +195,10 @@ const autocomplete = async function (req, res) {
|
|||
respond(res, uniqueAutocomplete(results));
|
||||
}
|
||||
|
||||
const findSessionSocketId = async (io, peerId) => {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
const findSessionSocketId = async (io, roomId, tabId) => {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.tabId === tabId) {
|
||||
return item.id;
|
||||
}
|
||||
}
|
||||
|
|
@ -191,8 +208,8 @@ const findSessionSocketId = async (io, peerId) => {
|
|||
async function sessions_agents_count(io, socket) {
|
||||
let c_sessions = 0, c_agents = 0;
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.get(socket.peerId)) {
|
||||
const connected_sockets = await io.in(socket.peerId).fetchSockets();
|
||||
if (rooms.get(socket.roomId)) {
|
||||
const connected_sockets = await io.in(socket.roomId).fetchSockets();
|
||||
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
|
|
@ -211,8 +228,8 @@ async function sessions_agents_count(io, socket) {
|
|||
async function get_all_agents_ids(io, socket) {
|
||||
let agents = [];
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.get(socket.peerId)) {
|
||||
const connected_sockets = await io.in(socket.peerId).fetchSockets();
|
||||
if (rooms.get(socket.roomId)) {
|
||||
const connected_sockets = await io.in(socket.roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.agent) {
|
||||
agents.push(item.id);
|
||||
|
|
@ -245,56 +262,74 @@ module.exports = {
|
|||
socket.on(EVENTS_DEFINITION.listen.ERROR, err => errorHandler(EVENTS_DEFINITION.listen.ERROR, err));
|
||||
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
|
||||
socket._connectedAt = new Date();
|
||||
|
||||
let {projectKey: connProjectKey, sessionId: connSessionId, tabId:connTabId} = extractPeerId(socket.handshake.query.peerId);
|
||||
socket.peerId = socket.handshake.query.peerId;
|
||||
socket.roomId = extractRoomId(socket.peerId);
|
||||
connTabId = connTabId ?? (Math.random() + 1).toString(36).substring(2);
|
||||
socket.tabId = connTabId;
|
||||
socket.identity = socket.handshake.query.identity;
|
||||
debug && console.log(`connProjectKey:${connProjectKey}, connSessionId:${connSessionId}, connTabId:${connTabId}, roomId:${socket.roomId}`);
|
||||
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
if (c_sessions > 0) {
|
||||
const rooms = await getAvailableRooms(io);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(roomId);
|
||||
if (projectKey === connProjectKey) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.tabId === connTabId) {
|
||||
debug && console.log(`session already connected, refusing new connexion`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.SESSION_ALREADY_CONNECTED);
|
||||
return socket.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extractSessionInfo(socket);
|
||||
if (c_agents > 0) {
|
||||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
debug && console.log(`notifying new agent about no SESSIONS with peerId:${socket.peerId}`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
await socket.join(socket.peerId);
|
||||
await socket.join(socket.roomId);
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.get(socket.peerId)) {
|
||||
debug && console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${rooms.get(socket.peerId).size}`);
|
||||
if (rooms.get(socket.roomId)) {
|
||||
debug && console.log(`${socket.id} joined room:${socket.roomId}, as:${socket.identity}, members:${rooms.get(socket.roomId).size}`);
|
||||
}
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
if (socket.handshake.query.agentInfo !== undefined) {
|
||||
socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo);
|
||||
}
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
}
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.roomId}`);
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
}
|
||||
debug && console.log("checking for number of connected agents and sessions");
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (c_sessions === -1 && c_agents === -1) {
|
||||
debug && console.log(`room not found: ${socket.peerId}`);
|
||||
debug && console.log(`room not found: ${socket.roomId}`);
|
||||
}
|
||||
if (c_sessions === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
debug && console.log(`notifying everyone in ${socket.roomId} about no SESSIONS`);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
if (c_agents === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
debug && console.log(`notifying everyone in ${socket.roomId} about no AGENTS`);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -304,8 +339,25 @@ module.exports = {
|
|||
debug && console.log('Ignoring update event.');
|
||||
return
|
||||
}
|
||||
socket.handshake.query.sessionInfo = {...socket.handshake.query.sessionInfo, ...args[0]};
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
// Back compatibility (add top layer with meta information)
|
||||
if (args[0].meta === undefined) {
|
||||
args[0] = {meta: {tabId: socket.tabId}, data: args[0]};
|
||||
}
|
||||
Object.assign(socket.handshake.query.sessionInfo, args[0].data, {tabId: args[0].meta.tabId});
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
// Update sessionInfo for all sessions (TODO: rewrite this)
|
||||
const rooms = await getAvailableRooms(io);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(roomId);
|
||||
if (projectKey === connProjectKey) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo) {
|
||||
Object.assign(item.handshake.query.sessionInfo, args[0].data, {tabId: args[0].meta.tabId});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(EVENTS_DEFINITION.listen.CONNECT_ERROR, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_ERROR, err));
|
||||
|
|
@ -316,14 +368,19 @@ module.exports = {
|
|||
debug && console.log(`received event:${eventName}, should be handled by another listener, stopping onAny.`);
|
||||
return
|
||||
}
|
||||
// Back compatibility (add top layer with meta information)
|
||||
if (args[0].meta === undefined) {
|
||||
args[0] = {meta: {tabId: socket.tabId}, data: args[0]};
|
||||
}
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}`);
|
||||
socket.to(socket.peerId).emit(eventName, args[0]);
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.roomId}`);
|
||||
// TODO: emit message to all agents in the room (except tabs)
|
||||
socket.to(socket.roomId).emit(eventName, args[0]);
|
||||
} else {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}`);
|
||||
let socketId = await findSessionSocketId(io, socket.peerId);
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.roomId}`);
|
||||
let socketId = await findSessionSocketId(io, socket.roomId, args[0].meta.tabId);
|
||||
if (socketId === null) {
|
||||
debug && console.log(`session not found for:${socket.peerId}`);
|
||||
debug && console.log(`session not found for:${socket.roomId}`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
} else {
|
||||
debug && console.log("message sent");
|
||||
|
|
@ -342,7 +399,7 @@ module.exports = {
|
|||
const arr = Array.from(rooms);
|
||||
const filtered = arr.filter(room => !room[1].has(room[0]));
|
||||
for (let i of filtered) {
|
||||
let {projectKey, sessionId} = extractPeerId(i[0]);
|
||||
let {projectKey, sessionId, tabId} = extractPeerId(i[0]);
|
||||
if (projectKey !== null && sessionId !== null) {
|
||||
count++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,22 @@
|
|||
let PROJECT_KEY_LENGTH = parseInt(process.env.PROJECT_KEY_LENGTH) || 20;
|
||||
let debug = process.env.debug === "1" || false;
|
||||
const extractRoomId = (peerId) => {
|
||||
let {projectKey, sessionId, tabId} = extractPeerId(peerId);
|
||||
if (projectKey && sessionId) {
|
||||
return `${projectKey}-${sessionId}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const extractTabId = (peerId) => {
|
||||
let {projectKey, sessionId, tabId} = extractPeerId(peerId);
|
||||
if (tabId) {
|
||||
return tabId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const extractPeerId = (peerId) => {
|
||||
let splited = peerId.split("-");
|
||||
if (splited.length !== 2) {
|
||||
if (splited.length < 2 || splited.length > 3) {
|
||||
debug && console.error(`cannot split peerId: ${peerId}`);
|
||||
return {};
|
||||
}
|
||||
|
|
@ -10,7 +24,10 @@ const extractPeerId = (peerId) => {
|
|||
debug && console.error(`wrong project key length for peerId: ${peerId}`);
|
||||
return {};
|
||||
}
|
||||
return {projectKey: splited[0], sessionId: splited[1]};
|
||||
if (splited.length === 2) {
|
||||
return {projectKey: splited[0], sessionId: splited[1], tabId: null};
|
||||
}
|
||||
return {projectKey: splited[0], sessionId: splited[1], tabId: splited[2]};
|
||||
};
|
||||
const request_logger = (identity) => {
|
||||
return (req, res, next) => {
|
||||
|
|
@ -185,7 +202,7 @@ const sortPaginate = function (list, filters) {
|
|||
list.sort((a, b) => {
|
||||
const tA = getValue(a, "timestamp");
|
||||
const tB = getValue(b, "timestamp");
|
||||
return tA < tB ? 1 : tA > tB ? -1 : 0;
|
||||
return tA < tB ? 1 : tA > tB ? -1 : 0; // b - a
|
||||
});
|
||||
if (filters.sort.order) {
|
||||
list.reverse();
|
||||
|
|
@ -246,6 +263,8 @@ const getCompressionConfig = function () {
|
|||
}
|
||||
module.exports = {
|
||||
transformFilters,
|
||||
extractRoomId,
|
||||
extractTabId,
|
||||
extractPeerId,
|
||||
request_logger,
|
||||
getValidAttributes,
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ func main() {
|
|||
}
|
||||
|
||||
// Add message to dev buffer
|
||||
if !messages.IsDOMType(msg.TypeID()) || msg.TypeID() == messages.MsgTimestamp {
|
||||
if !messages.IsDOMType(msg.TypeID()) || msg.TypeID() == messages.MsgTimestamp || msg.TypeID() == messages.MsgTabData {
|
||||
// Write message index
|
||||
n, err = devBuffer.Write(messageIndex)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@ func IsIOSType(id int) bool {
|
|||
}
|
||||
|
||||
func IsDOMType(id int) bool {
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
return 0 == id || 4 == id || 5 == id || 6 == id || 7 == id || 8 == id || 9 == id || 10 == id || 11 == id || 12 == id || 13 == id || 14 == id || 15 == id || 16 == id || 18 == id || 19 == id || 20 == id || 37 == id || 38 == id || 49 == id || 50 == id || 51 == id || 54 == id || 55 == id || 57 == id || 58 == id || 59 == id || 60 == id || 61 == id || 67 == id || 69 == id || 70 == id || 71 == id || 72 == id || 73 == id || 74 == id || 75 == id || 76 == id || 77 == id || 113 == id || 114 == id || 117 == id || 118 == id || 90 == id || 93 == id || 96 == id || 100 == id || 102 == id || 103 == id || 105 == id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ const (
|
|||
MsgMouseThrashing = 114
|
||||
MsgUnbindNodes = 115
|
||||
MsgResourceTiming = 116
|
||||
MsgTabChange = 117
|
||||
MsgTabData = 118
|
||||
MsgIssueEvent = 125
|
||||
MsgSessionEnd = 126
|
||||
MsgSessionSearch = 127
|
||||
|
|
@ -2163,6 +2165,48 @@ func (msg *ResourceTiming) TypeID() int {
|
|||
return 116
|
||||
}
|
||||
|
||||
type TabChange struct {
|
||||
message
|
||||
TabId string
|
||||
}
|
||||
|
||||
func (msg *TabChange) Encode() []byte {
|
||||
buf := make([]byte, 11+len(msg.TabId))
|
||||
buf[0] = 117
|
||||
p := 1
|
||||
p = WriteString(msg.TabId, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *TabChange) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *TabChange) TypeID() int {
|
||||
return 117
|
||||
}
|
||||
|
||||
type TabData struct {
|
||||
message
|
||||
TabId string
|
||||
}
|
||||
|
||||
func (msg *TabData) Encode() []byte {
|
||||
buf := make([]byte, 11+len(msg.TabId))
|
||||
buf[0] = 118
|
||||
p := 1
|
||||
p = WriteString(msg.TabId, buf, p)
|
||||
return buf[:p]
|
||||
}
|
||||
|
||||
func (msg *TabData) Decode() Message {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *TabData) TypeID() int {
|
||||
return 118
|
||||
}
|
||||
|
||||
type IssueEvent struct {
|
||||
message
|
||||
MessageID uint64
|
||||
|
|
|
|||
|
|
@ -1314,6 +1314,24 @@ func DecodeResourceTiming(reader BytesReader) (Message, error) {
|
|||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeTabChange(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &TabChange{}
|
||||
if msg.TabId, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeTabData(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &TabData{}
|
||||
if msg.TabId, err = reader.ReadString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func DecodeIssueEvent(reader BytesReader) (Message, error) {
|
||||
var err error = nil
|
||||
msg := &IssueEvent{}
|
||||
|
|
@ -1927,6 +1945,10 @@ func ReadMessage(t uint64, reader BytesReader) (Message, error) {
|
|||
return DecodeUnbindNodes(reader)
|
||||
case 116:
|
||||
return DecodeResourceTiming(reader)
|
||||
case 117:
|
||||
return DecodeTabChange(reader)
|
||||
case 118:
|
||||
return DecodeTabData(reader)
|
||||
case 125:
|
||||
return DecodeIssueEvent(reader)
|
||||
case 126:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const pubClient = createClient({url: REDIS_URL});
|
|||
const subClient = pubClient.duplicate();
|
||||
console.log(`Using Redis: ${REDIS_URL}`);
|
||||
let io;
|
||||
const debug = process.env.debug === "1";
|
||||
const debug = true;// = process.env.debug === "1";
|
||||
|
||||
const createSocketIOServer = function (server, prefix) {
|
||||
if (process.env.uws !== "true") {
|
||||
|
|
@ -58,6 +58,7 @@ const createSocketIOServer = function (server, prefix) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe we should use a Set instead of an array
|
||||
const uniqueSessions = function (data) {
|
||||
let resArr = [];
|
||||
let resArrIDS = [];
|
||||
|
|
@ -85,25 +86,30 @@ const respond = function (res, data) {
|
|||
const socketsList = async function (req, res) {
|
||||
debug && console.log("[WS]looking for all available sessions");
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessionsPerProject = {};
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
if (projectKey !== undefined) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
liveSessionsPerProject[projectKey] = liveSessionsPerProject[projectKey] || new Set();
|
||||
if (withFilters) {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo
|
||||
&& isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessionsPerProject[projectKey].add(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessionsPerProject[projectKey].add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
let liveSessions = {};
|
||||
liveSessionsPerProject.forEach((sessions, projectId) => {
|
||||
liveSessions[projectId] = Array.from(sessions);
|
||||
});
|
||||
respond(res, liveSessions);
|
||||
}
|
||||
|
||||
|
|
@ -112,35 +118,37 @@ const socketsListByProject = async function (req, res) {
|
|||
let _projectKey = extractProjectKeyFromRequest(req);
|
||||
let _sessionId = extractSessionIdFromRequest(req);
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessions = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
if (projectKey === _projectKey && (_sessionId === undefined || _sessionId === sessionId)) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
if (withFilters) {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo
|
||||
&& isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[_projectKey] = liveSessions[_projectKey] || [];
|
||||
respond(res, _sessionId === undefined ? sortPaginate(liveSessions[_projectKey], filters)
|
||||
: liveSessions[_projectKey].length > 0 ? liveSessions[_projectKey][0]
|
||||
let sessions = Array.from(liveSessions);
|
||||
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters)
|
||||
: sessions.length > 0 ? sessions[0]
|
||||
: null);
|
||||
}
|
||||
|
||||
const socketsLive = async function (req, res) {
|
||||
debug && console.log("[WS]looking for all available LIVE sessions");
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessionsPerProject = {};
|
||||
const sessIDs = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(peerId);
|
||||
|
|
@ -148,19 +156,31 @@ const socketsLive = async function (req, res) {
|
|||
let connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
if (item.handshake.query.sessionInfo && isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
liveSessionsPerProject[projectKey] = liveSessionsPerProject[projectKey] || new Set();
|
||||
if (withFilters) {
|
||||
if (item.handshake.query.sessionInfo &&
|
||||
isValidSession(item.handshake.query.sessionInfo, filters.filter) &&
|
||||
!sessIDs.has(item.handshake.query.sessionInfo.sessionID)
|
||||
) {
|
||||
liveSessionsPerProject[projectKey].add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (!sessIDs.has(item.handshake.query.sessionInfo.sessionID)) {
|
||||
liveSessionsPerProject[projectKey].add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[projectKey] = uniqueSessions(liveSessions[projectKey]);
|
||||
}
|
||||
// Should be already unique
|
||||
// liveSessionsPerProject[projectKey] = uniqueSessions(liveSessionsPerProject[projectKey]);
|
||||
}
|
||||
}
|
||||
let liveSessions = {};
|
||||
liveSessionsPerProject.forEach((sessions, projectId) => {
|
||||
liveSessions[projectId] = Array.from(sessions);
|
||||
});
|
||||
respond(res, sortPaginate(liveSessions, filters));
|
||||
}
|
||||
|
||||
|
|
@ -169,7 +189,9 @@ const socketsLiveByProject = async function (req, res) {
|
|||
let _projectKey = extractProjectKeyFromRequest(req);
|
||||
let _sessionId = extractSessionIdFromRequest(req);
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessions = new Set();
|
||||
const sessIDs = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
|
|
@ -177,23 +199,28 @@ const socketsLiveByProject = async function (req, res) {
|
|||
let connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
if (item.handshake.query.sessionInfo && isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (withFilters) {
|
||||
if (item.handshake.query.sessionInfo &&
|
||||
isValidSession(item.handshake.query.sessionInfo, filters.filter) &&
|
||||
!sessIDs.has(item.handshake.query.sessionInfo.sessionID)
|
||||
) {
|
||||
liveSessions.add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (!sessIDs.has(item.handshake.query.sessionInfo.sessionID)) {
|
||||
liveSessions.add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[projectKey] = uniqueSessions(liveSessions[projectKey] || []);
|
||||
}
|
||||
// Should be unique already because of using sessIDs set
|
||||
// liveSessions[projectKey] = uniqueSessions(liveSessions[projectKey] || []);
|
||||
}
|
||||
}
|
||||
liveSessions[_projectKey] = liveSessions[_projectKey] || [];
|
||||
respond(res, _sessionId === undefined ? sortPaginate(liveSessions[_projectKey], filters)
|
||||
: liveSessions[_projectKey].length > 0 ? liveSessions[_projectKey][0]
|
||||
: null);
|
||||
let sessions = Array.from(liveSessions);
|
||||
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters) : sessions.length > 0 ? sessions[0] : null);
|
||||
}
|
||||
|
||||
const autocomplete = async function (req, res) {
|
||||
|
|
@ -218,10 +245,10 @@ const autocomplete = async function (req, res) {
|
|||
respond(res, uniqueAutocomplete(results));
|
||||
}
|
||||
|
||||
const findSessionSocketId = async (io, peerId) => {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
const findSessionSocketId = async (io, roomId, tabId) => {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.tabId === tabId) {
|
||||
return item.id;
|
||||
}
|
||||
}
|
||||
|
|
@ -231,8 +258,8 @@ const findSessionSocketId = async (io, peerId) => {
|
|||
async function sessions_agents_count(io, socket) {
|
||||
let c_sessions = 0, c_agents = 0;
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.has(socket.peerId)) {
|
||||
const connected_sockets = await io.in(socket.peerId).fetchSockets();
|
||||
if (rooms.has(socket.roomId)) {
|
||||
const connected_sockets = await io.in(socket.roomId).fetchSockets();
|
||||
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
|
|
@ -251,8 +278,8 @@ async function sessions_agents_count(io, socket) {
|
|||
async function get_all_agents_ids(io, socket) {
|
||||
let agents = [];
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.has(socket.peerId)) {
|
||||
const connected_sockets = await io.in(socket.peerId).fetchSockets();
|
||||
if (rooms.has(socket.roomId)) {
|
||||
const connected_sockets = await io.in(socket.roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.agent) {
|
||||
agents.push(item.id);
|
||||
|
|
@ -285,57 +312,76 @@ module.exports = {
|
|||
socket.on(EVENTS_DEFINITION.listen.ERROR, err => errorHandler(EVENTS_DEFINITION.listen.ERROR, err));
|
||||
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
|
||||
socket._connectedAt = new Date();
|
||||
|
||||
let {projectKey: connProjectKey, sessionId: connSessionId, tabId:connTabId} = extractPeerId(socket.handshake.query.peerId);
|
||||
socket.peerId = socket.handshake.query.peerId;
|
||||
socket.roomId = extractRoomId(socket.peerId);
|
||||
// Set default tabId for back compatibility
|
||||
connTabId = connTabId ?? (Math.random() + 1).toString(36).substring(2);
|
||||
socket.tabId = connTabId;
|
||||
socket.identity = socket.handshake.query.identity;
|
||||
debug && console.log(`connProjectKey:${connProjectKey}, connSessionId:${connSessionId}, connTabId:${connTabId}, roomId:${socket.roomId}`);
|
||||
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
if (c_sessions > 0) {
|
||||
const rooms = await getAvailableRooms(io);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(roomId);
|
||||
if (projectKey === connProjectKey) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.tabId === connTabId) {
|
||||
debug && console.log(`session already connected, refusing new connexion`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.SESSION_ALREADY_CONNECTED);
|
||||
return socket.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extractSessionInfo(socket);
|
||||
if (c_agents > 0) {
|
||||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
debug && console.log(`notifying new agent about no SESSIONS with peerId:${socket.peerId}`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
await socket.join(socket.peerId);
|
||||
await socket.join(socket.roomId);
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.has(socket.peerId)) {
|
||||
let connectedSockets = await io.in(socket.peerId).fetchSockets();
|
||||
debug && console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${connectedSockets.length}`);
|
||||
if (rooms.has(socket.roomId)) {
|
||||
let connectedSockets = await io.in(socket.roomId).fetchSockets();
|
||||
debug && console.log(`${socket.id} joined room:${socket.roomId}, as:${socket.identity}, members:${connectedSockets.length}`);
|
||||
}
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
if (socket.handshake.query.agentInfo !== undefined) {
|
||||
socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo);
|
||||
}
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
}
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.roomId}`);
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
}
|
||||
debug && console.log("checking for number of connected agents and sessions");
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (c_sessions === -1 && c_agents === -1) {
|
||||
debug && console.log(`room not found: ${socket.peerId}`);
|
||||
debug && console.log(`room not found: ${socket.roomId}`);
|
||||
}
|
||||
if (c_sessions === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
debug && console.log(`notifying everyone in ${socket.roomId} about no SESSIONS`);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
if (c_agents === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
debug && console.log(`notifying everyone in ${socket.roomId} about no AGENTS`);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -345,8 +391,25 @@ module.exports = {
|
|||
debug && console.log('Ignoring update event.');
|
||||
return
|
||||
}
|
||||
socket.handshake.query.sessionInfo = {...socket.handshake.query.sessionInfo, ...args[0]};
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
// Back compatibility (add top layer with meta information)
|
||||
if (args[0].meta === undefined) {
|
||||
args[0] = {meta: {tabId: socket.tabId}, data: args[0]};
|
||||
}
|
||||
Object.assign(socket.handshake.query.sessionInfo, args[0].data, {tabId: args[0].meta.tabId});
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
// Update sessionInfo for all sessions (TODO: rewrite this)
|
||||
const rooms = await getAvailableRooms(io);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(roomId);
|
||||
if (projectKey === connProjectKey) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo) {
|
||||
Object.assign(item.handshake.query.sessionInfo, args[0].data, {tabId: args[0].meta.tabId});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(EVENTS_DEFINITION.listen.CONNECT_ERROR, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_ERROR, err));
|
||||
|
|
@ -357,14 +420,19 @@ module.exports = {
|
|||
debug && console.log(`received event:${eventName}, should be handled by another listener, stopping onAny.`);
|
||||
return
|
||||
}
|
||||
// Back compatibility (add top layer with meta information)
|
||||
if (args[0].meta === undefined) {
|
||||
args[0] = {meta: {tabId: socket.tabId}, data: args[0]};
|
||||
}
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}`);
|
||||
socket.to(socket.peerId).emit(eventName, args[0]);
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.roomId}`);
|
||||
// TODO: send to all agents in the room
|
||||
socket.to(socket.roomId).emit(eventName, args[0]);
|
||||
} else {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}`);
|
||||
let socketId = await findSessionSocketId(io, socket.peerId);
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.roomId}`);
|
||||
let socketId = await findSessionSocketId(io, socket.roomId, args[0].meta.tabId);
|
||||
if (socketId === null) {
|
||||
debug && console.log(`session not found for:${socket.peerId}`);
|
||||
debug && console.log(`session not found for:${socket.roomId}`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
} else {
|
||||
debug && console.log("message sent");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const _io = require('socket.io');
|
||||
const express = require('express');
|
||||
const {
|
||||
extractRoomId,
|
||||
extractPeerId,
|
||||
hasFilters,
|
||||
isValidSession,
|
||||
|
|
@ -26,7 +27,7 @@ const {
|
|||
const wsRouter = express.Router();
|
||||
|
||||
let io;
|
||||
const debug = process.env.debug === "1";
|
||||
const debug = true;//process.env.debug === "1";
|
||||
|
||||
const createSocketIOServer = function (server, prefix) {
|
||||
if (process.env.uws !== "true") {
|
||||
|
|
@ -67,25 +68,30 @@ const respond = function (res, data) {
|
|||
const socketsList = async function (req, res) {
|
||||
debug && console.log("[WS]looking for all available sessions");
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessionsPerProject = {};
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
if (projectKey !== undefined) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
liveSessionsPerProject[projectKey] = liveSessionsPerProject[projectKey] || new Set();
|
||||
if (withFilters) {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo
|
||||
&& isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessionsPerProject[projectKey].add(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessionsPerProject[projectKey].add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
let liveSessions = {};
|
||||
liveSessionsPerProject.forEach((sessions, projectId) => {
|
||||
liveSessions[projectId] = Array.from(sessions);
|
||||
});
|
||||
respond(res, liveSessions);
|
||||
}
|
||||
|
||||
|
|
@ -94,35 +100,36 @@ const socketsListByProject = async function (req, res) {
|
|||
let _projectKey = extractProjectKeyFromRequest(req);
|
||||
let _sessionId = extractSessionIdFromRequest(req);
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessions = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
if (projectKey === _projectKey && (_sessionId === undefined || _sessionId === sessionId)) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
if (withFilters) {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo
|
||||
&& isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(sessionId);
|
||||
liveSessions.add(sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[_projectKey] = liveSessions[_projectKey] || [];
|
||||
respond(res, _sessionId === undefined ? sortPaginate(liveSessions[_projectKey], filters)
|
||||
: liveSessions[_projectKey].length > 0 ? liveSessions[_projectKey][0]
|
||||
let sessions = Array.from(liveSessions);
|
||||
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters)
|
||||
: sessions.length > 0 ? sessions[0]
|
||||
: null);
|
||||
}
|
||||
|
||||
const socketsLive = async function (req, res) {
|
||||
debug && console.log("[WS]looking for all available LIVE sessions");
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessionsPerProject = {};
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(peerId);
|
||||
|
|
@ -130,18 +137,22 @@ const socketsLive = async function (req, res) {
|
|||
let connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
liveSessionsPerProject[projectKey] = liveSessionsPerProject[projectKey] || new Set();
|
||||
if (hasFilters(filters)) {
|
||||
if (item.handshake.query.sessionInfo && isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
liveSessionsPerProject[projectKey].add(item.handshake.query.sessionInfo);
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
liveSessionsPerProject[projectKey].add(item.handshake.query.sessionInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let liveSessions = {};
|
||||
liveSessionsPerProject.forEach((sessions, projectId) => {
|
||||
liveSessions[projectId] = Array.from(sessions);
|
||||
});
|
||||
respond(res, sortPaginate(liveSessions, filters));
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +161,9 @@ const socketsLiveByProject = async function (req, res) {
|
|||
let _projectKey = extractProjectKeyFromRequest(req);
|
||||
let _sessionId = extractSessionIdFromRequest(req);
|
||||
let filters = await extractPayloadFromRequest(req, res);
|
||||
let liveSessions = {};
|
||||
let withFilters = hasFilters(filters);
|
||||
let liveSessions = new Set();
|
||||
const sessIDs = new Set();
|
||||
let rooms = await getAvailableRooms(io);
|
||||
for (let peerId of rooms.keys()) {
|
||||
let {projectKey, sessionId} = extractPeerId(peerId);
|
||||
|
|
@ -158,22 +171,26 @@ const socketsLiveByProject = async function (req, res) {
|
|||
let connected_sockets = await io.in(peerId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
liveSessions[projectKey] = liveSessions[projectKey] || [];
|
||||
if (hasFilters(filters)) {
|
||||
if (item.handshake.query.sessionInfo && isValidSession(item.handshake.query.sessionInfo, filters.filter)) {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (withFilters) {
|
||||
if (item.handshake.query.sessionInfo &&
|
||||
isValidSession(item.handshake.query.sessionInfo, filters.filter) &&
|
||||
!sessIDs.has(item.handshake.query.sessionInfo.sessionID)
|
||||
) {
|
||||
liveSessions.add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
} else {
|
||||
liveSessions[projectKey].push(item.handshake.query.sessionInfo);
|
||||
if (!sessIDs.has(item.handshake.query.sessionInfo.sessionID)) {
|
||||
liveSessions.add(item.handshake.query.sessionInfo);
|
||||
sessIDs.add(item.handshake.query.sessionInfo.sessionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
liveSessions[_projectKey] = liveSessions[_projectKey] || [];
|
||||
respond(res, _sessionId === undefined ? sortPaginate(liveSessions[_projectKey], filters)
|
||||
: liveSessions[_projectKey].length > 0 ? liveSessions[_projectKey][0]
|
||||
: null);
|
||||
}
|
||||
let sessions = Array.from(liveSessions);
|
||||
respond(res, _sessionId === undefined ? sortPaginate(sessions, filters) : sessions.length > 0 ? sessions[0] : null);
|
||||
}
|
||||
|
||||
const autocomplete = async function (req, res) {
|
||||
|
|
@ -198,10 +215,10 @@ const autocomplete = async function (req, res) {
|
|||
respond(res, uniqueAutocomplete(results));
|
||||
}
|
||||
|
||||
const findSessionSocketId = async (io, peerId) => {
|
||||
const connected_sockets = await io.in(peerId).fetchSockets();
|
||||
const findSessionSocketId = async (io, roomId, tabId) => {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.tabId === tabId) {
|
||||
return item.id;
|
||||
}
|
||||
}
|
||||
|
|
@ -211,8 +228,8 @@ const findSessionSocketId = async (io, peerId) => {
|
|||
async function sessions_agents_count(io, socket) {
|
||||
let c_sessions = 0, c_agents = 0;
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.get(socket.peerId)) {
|
||||
const connected_sockets = await io.in(socket.peerId).fetchSockets();
|
||||
if (rooms.get(socket.roomId)) {
|
||||
const connected_sockets = await io.in(socket.roomId).fetchSockets();
|
||||
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session) {
|
||||
|
|
@ -231,8 +248,8 @@ async function sessions_agents_count(io, socket) {
|
|||
async function get_all_agents_ids(io, socket) {
|
||||
let agents = [];
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.get(socket.peerId)) {
|
||||
const connected_sockets = await io.in(socket.peerId).fetchSockets();
|
||||
if (rooms.get(socket.roomId)) {
|
||||
const connected_sockets = await io.in(socket.roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.agent) {
|
||||
agents.push(item.id);
|
||||
|
|
@ -265,56 +282,75 @@ module.exports = {
|
|||
socket.on(EVENTS_DEFINITION.listen.ERROR, err => errorHandler(EVENTS_DEFINITION.listen.ERROR, err));
|
||||
debug && console.log(`WS started:${socket.id}, Query:${JSON.stringify(socket.handshake.query)}`);
|
||||
socket._connectedAt = new Date();
|
||||
|
||||
let {projectKey: connProjectKey, sessionId: connSessionId, tabId:connTabId} = extractPeerId(socket.handshake.query.peerId);
|
||||
socket.peerId = socket.handshake.query.peerId;
|
||||
socket.roomId = extractRoomId(socket.peerId);
|
||||
// Set default tabId for back compatibility
|
||||
connTabId = connTabId ?? (Math.random() + 1).toString(36).substring(2);
|
||||
socket.tabId = connTabId;
|
||||
socket.identity = socket.handshake.query.identity;
|
||||
debug && console.log(`connProjectKey:${connProjectKey}, connSessionId:${connSessionId}, connTabId:${connTabId}, roomId:${socket.roomId}`);
|
||||
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
if (c_sessions > 0) {
|
||||
const rooms = await getAvailableRooms(io);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(roomId);
|
||||
if (projectKey === connProjectKey) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.tabId === connTabId) {
|
||||
debug && console.log(`session already connected, refusing new connexion`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.SESSION_ALREADY_CONNECTED);
|
||||
return socket.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
extractSessionInfo(socket);
|
||||
if (c_agents > 0) {
|
||||
debug && console.log(`notifying new session about agent-existence`);
|
||||
let agents_ids = await get_all_agents_ids(io, socket);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.AGENTS_CONNECTED, agents_ids);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.SESSION_RECONNECTED, socket.id);
|
||||
}
|
||||
|
||||
} else if (c_sessions <= 0) {
|
||||
debug && console.log(`notifying new agent about no SESSIONS with peerId:${socket.peerId}`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
await socket.join(socket.peerId);
|
||||
await socket.join(socket.roomId);
|
||||
const rooms = await getAvailableRooms(io);
|
||||
if (rooms.get(socket.peerId)) {
|
||||
debug && console.log(`${socket.id} joined room:${socket.peerId}, as:${socket.identity}, members:${rooms.get(socket.peerId).size}`);
|
||||
if (rooms.get(socket.roomId)) {
|
||||
debug && console.log(`${socket.id} joined room:${socket.roomId}, as:${socket.identity}, members:${rooms.get(socket.roomId).size}`);
|
||||
}
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
if (socket.handshake.query.agentInfo !== undefined) {
|
||||
socket.handshake.query.agentInfo = JSON.parse(socket.handshake.query.agentInfo);
|
||||
}
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NEW_AGENT, socket.id, socket.handshake.query.agentInfo);
|
||||
}
|
||||
|
||||
socket.on('disconnect', async () => {
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.peerId}`);
|
||||
debug && console.log(`${socket.id} disconnected from ${socket.roomId}`);
|
||||
if (socket.identity === IDENTITIES.agent) {
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.AGENT_DISCONNECT, socket.id);
|
||||
}
|
||||
debug && console.log("checking for number of connected agents and sessions");
|
||||
let {c_sessions, c_agents} = await sessions_agents_count(io, socket);
|
||||
if (c_sessions === -1 && c_agents === -1) {
|
||||
debug && console.log(`room not found: ${socket.peerId}`);
|
||||
debug && console.log(`room not found: ${socket.roomId}`);
|
||||
}
|
||||
if (c_sessions === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no SESSIONS`);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
debug && console.log(`notifying everyone in ${socket.roomId} about no SESSIONS`);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
}
|
||||
if (c_agents === 0) {
|
||||
debug && console.log(`notifying everyone in ${socket.peerId} about no AGENTS`);
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.NO_AGENTS);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -324,8 +360,25 @@ module.exports = {
|
|||
debug && console.log('Ignoring update event.');
|
||||
return
|
||||
}
|
||||
socket.handshake.query.sessionInfo = {...socket.handshake.query.sessionInfo, ...args[0]};
|
||||
socket.to(socket.peerId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
// Back compatibility (add top layer with meta information)
|
||||
if (args[0].meta === undefined) {
|
||||
args[0] = {meta: {tabId: socket.tabId}, data: args[0]};
|
||||
}
|
||||
Object.assign(socket.handshake.query.sessionInfo, args[0].data, {tabId: args[0].meta.tabId});
|
||||
socket.to(socket.roomId).emit(EVENTS_DEFINITION.emit.UPDATE_EVENT, args[0]);
|
||||
// Update sessionInfo for all sessions (TODO: rewrite this)
|
||||
const rooms = await getAvailableRooms(io);
|
||||
for (let roomId of rooms.keys()) {
|
||||
let {projectKey} = extractPeerId(roomId);
|
||||
if (projectKey === connProjectKey) {
|
||||
const connected_sockets = await io.in(roomId).fetchSockets();
|
||||
for (let item of connected_sockets) {
|
||||
if (item.handshake.query.identity === IDENTITIES.session && item.handshake.query.sessionInfo) {
|
||||
Object.assign(item.handshake.query.sessionInfo, args[0].data, {tabId: args[0].meta.tabId});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(EVENTS_DEFINITION.listen.CONNECT_ERROR, err => errorHandler(EVENTS_DEFINITION.listen.CONNECT_ERROR, err));
|
||||
|
|
@ -336,14 +389,18 @@ module.exports = {
|
|||
debug && console.log(`received event:${eventName}, should be handled by another listener, stopping onAny.`);
|
||||
return
|
||||
}
|
||||
// Back compatibility (add top layer with meta information)
|
||||
if (args[0].meta === undefined) {
|
||||
args[0] = {meta: {tabId: socket.tabId}, data: args[0]};
|
||||
}
|
||||
if (socket.identity === IDENTITIES.session) {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to room:${socket.peerId}`);
|
||||
socket.to(socket.peerId).emit(eventName, args[0]);
|
||||
socket.to(socket.roomId).emit(eventName, args[0]);
|
||||
} else {
|
||||
debug && console.log(`received event:${eventName}, from:${socket.identity}, sending message to session of room:${socket.peerId}`);
|
||||
let socketId = await findSessionSocketId(io, socket.peerId);
|
||||
let socketId = await findSessionSocketId(io, socket.roomId, args[0].meta.tabId);
|
||||
if (socketId === null) {
|
||||
debug && console.log(`session not found for:${socket.peerId}`);
|
||||
debug && console.log(`session not found for:${socket.roomId}`);
|
||||
io.to(socket.id).emit(EVENTS_DEFINITION.emit.NO_SESSIONS);
|
||||
} else {
|
||||
debug && console.log("message sent");
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class CreateDocument(Message):
|
|||
__id__ = 7
|
||||
|
||||
def __init__(self, ):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class CreateElementNode(Message):
|
||||
|
|
@ -759,6 +759,20 @@ class ResourceTiming(Message):
|
|||
self.cached = cached
|
||||
|
||||
|
||||
class TabChange(Message):
|
||||
__id__ = 117
|
||||
|
||||
def __init__(self, tab_id):
|
||||
self.tab_id = tab_id
|
||||
|
||||
|
||||
class TabData(Message):
|
||||
__id__ = 118
|
||||
|
||||
def __init__(self, tab_id):
|
||||
self.tab_id = tab_id
|
||||
|
||||
|
||||
class IssueEvent(Message):
|
||||
__id__ = 125
|
||||
|
||||
|
|
|
|||
|
|
@ -689,6 +689,16 @@ class MessageCodec(Codec):
|
|||
cached=self.read_boolean(reader)
|
||||
)
|
||||
|
||||
if message_id == 117:
|
||||
return TabChange(
|
||||
tab_id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 118:
|
||||
return TabData(
|
||||
tab_id=self.read_string(reader)
|
||||
)
|
||||
|
||||
if message_id == 125:
|
||||
return IssueEvent(
|
||||
message_id=self.read_uint(reader),
|
||||
|
|
|
|||
|
|
@ -24,9 +24,12 @@ function Controls(props: any) {
|
|||
const { jumpToLive } = player;
|
||||
const {
|
||||
livePlay,
|
||||
logMarkedCountNow: logRedCount,
|
||||
exceptionsList,
|
||||
currentTab,
|
||||
tabStates
|
||||
} = store.get();
|
||||
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
const logRedCount = tabStates[currentTab]?.logMarkedCountNow || 0;
|
||||
const showExceptions = exceptionsList.length > 0;
|
||||
const {
|
||||
bottomBlock,
|
||||
|
|
|
|||
|
|
@ -1,39 +1,43 @@
|
|||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import Tab from 'Components/Session/Player/SharedComponents/Tab';
|
||||
|
||||
function SubHeader() {
|
||||
const { store } = React.useContext(PlayerContext)
|
||||
const {
|
||||
location: currentLocation,
|
||||
} = store.get()
|
||||
const [isCopied, setCopied] = React.useState(false);
|
||||
const { store } = React.useContext(PlayerContext);
|
||||
const { tabStates, currentTab, tabs } = store.get();
|
||||
|
||||
const currentLocation = tabStates[currentTab]?.location || '';
|
||||
const location =
|
||||
currentLocation !== undefined ? currentLocation.length > 60
|
||||
? `${currentLocation.slice(0, 60)}...`
|
||||
: currentLocation : undefined;
|
||||
currentLocation !== undefined
|
||||
? currentLocation.length > 70
|
||||
? `${currentLocation.slice(0, 70)}...`
|
||||
: currentLocation
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 py-2 flex items-center border-b min-h-3">
|
||||
<>
|
||||
<div className="w-full px-4 pt-2 flex items-center border-b min-h-3">
|
||||
{tabs.map((tab, i) => (
|
||||
<React.Fragment key={tab}>
|
||||
<Tab i={i} tab={tab} currentTab={tabs.length === 1 ? tab : currentTab} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{location && (
|
||||
<div
|
||||
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-gray-light-shade rounded-md"
|
||||
onClick={() => {
|
||||
copy(currentLocation || '');
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 5000);
|
||||
}}
|
||||
>
|
||||
<div className={'w-full bg-white border-b border-gray-light'}>
|
||||
<div className="flex w-fit items-center cursor-pointer color-gray-medium text-sm p-1">
|
||||
<Icon size="20" name="event/link" className="mr-1" />
|
||||
<Tooltip title={isCopied ? 'URL Copied to clipboard' : 'Click to copy'}>
|
||||
<Tooltip title="Open in new tab" delay={0}>
|
||||
<a href={location} target="_blank">
|
||||
{location}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,13 +25,16 @@ function Overlay({
|
|||
|
||||
const {
|
||||
messagesLoading,
|
||||
cssLoading,
|
||||
peerConnectionStatus,
|
||||
livePlay,
|
||||
calling,
|
||||
remoteControl,
|
||||
recordingState,
|
||||
tabStates,
|
||||
currentTab
|
||||
} = store.get()
|
||||
|
||||
const cssLoading = tabStates[currentTab]?.cssLoading || false
|
||||
const loading = messagesLoading || cssLoading
|
||||
const liveStatusText = getStatusText(peerConnectionStatus)
|
||||
const connectionStatus = peerConnectionStatus
|
||||
|
|
|
|||
|
|
@ -1,174 +0,0 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import cn from 'classnames';
|
||||
import { Icon, TextEllipsis } from 'UI';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { prorata } from 'App/utils';
|
||||
import withOverlay from 'Components/hocs/withOverlay';
|
||||
import LoadInfo from './LoadInfo';
|
||||
import cls from './event.module.css';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
|
||||
@withOverlay()
|
||||
export default class Event extends React.PureComponent {
|
||||
state = {
|
||||
menuOpen: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.wrapper.addEventListener('contextmenu', this.onContextMenu);
|
||||
}
|
||||
|
||||
onContextMenu = (e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ menuOpen: true });
|
||||
}
|
||||
onMouseLeave = () => this.setState({ menuOpen: false })
|
||||
|
||||
copyHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
//const ctrlOrCommandPressed = e.ctrlKey || e.metaKey;
|
||||
//if (ctrlOrCommandPressed && e.keyCode === 67) {
|
||||
const { event } = this.props;
|
||||
copy(event.getIn([ 'target', 'path' ]) || event.url || '');
|
||||
this.setState({ menuOpen: false });
|
||||
}
|
||||
|
||||
toggleInfo = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.toggleInfo();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
renderBody = () => {
|
||||
const { event } = this.props;
|
||||
let title = event.type;
|
||||
let body;
|
||||
switch (event.type) {
|
||||
case TYPES.LOCATION:
|
||||
title = 'Visited';
|
||||
body = event.url;
|
||||
break;
|
||||
case TYPES.CLICK:
|
||||
title = 'Clicked';
|
||||
body = event.label;
|
||||
break;
|
||||
case TYPES.INPUT:
|
||||
title = 'Input';
|
||||
body = event.value;
|
||||
break;
|
||||
case TYPES.CLICKRAGE:
|
||||
title = `${ event.count } Clicks`;
|
||||
body = event.label;
|
||||
break;
|
||||
case TYPES.IOS_VIEW:
|
||||
title = 'View';
|
||||
body = event.name;
|
||||
break;
|
||||
}
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
const isClickrage = event.type === TYPES.CLICKRAGE;
|
||||
|
||||
return (
|
||||
<div className={ cn(cls.main, 'flex flex-col w-full') } >
|
||||
<div className="flex items-center w-full">
|
||||
{ event.type && <Icon name={`event/${event.type.toLowerCase()}`} size="16" color={isClickrage? 'red' : 'gray-dark' } /> }
|
||||
<div className="ml-3 w-full">
|
||||
<div className="flex w-full items-first justify-between">
|
||||
<div className="flex items-center w-full" style={{ minWidth: '0'}}>
|
||||
<span className={cls.title}>{ title }</span>
|
||||
{/* { body && !isLocation && <div className={ cls.data }>{ body }</div> } */}
|
||||
{ body && !isLocation &&
|
||||
<TextEllipsis maxWidth="60%" className="w-full ml-2 text-sm color-gray-medium" text={body} />
|
||||
}
|
||||
</div>
|
||||
{ isLocation && event.speedIndex != null &&
|
||||
<div className="color-gray-medium flex font-medium items-center leading-none justify-end">
|
||||
<div className="font-size-10 pr-2">{"Speed Index"}</div>
|
||||
<div>{ numberWithCommas(event.speedIndex || 0) }</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{ event.target && event.target.label &&
|
||||
<div className={ cls.badge } >{ event.target.label }</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{ isLocation &&
|
||||
<div className="mt-1">
|
||||
<span className="text-sm font-normal color-gray-medium">{ body }</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
event,
|
||||
selected,
|
||||
isCurrent,
|
||||
onClick,
|
||||
showSelection,
|
||||
onCheckboxClick,
|
||||
showLoadInfo,
|
||||
toggleLoadInfo,
|
||||
isRed,
|
||||
extended,
|
||||
highlight = false,
|
||||
presentInSearch = false,
|
||||
isLastInGroup,
|
||||
whiteBg,
|
||||
} = this.props;
|
||||
const { menuOpen } = this.state;
|
||||
return (
|
||||
<div
|
||||
ref={ ref => { this.wrapper = ref } }
|
||||
onMouseLeave={ this.onMouseLeave }
|
||||
data-openreplay-label="Event"
|
||||
data-type={event.type}
|
||||
className={ cn(cls.event, {
|
||||
[ cls.menuClosed ]: !menuOpen,
|
||||
[ cls.highlighted ]: showSelection ? selected : isCurrent,
|
||||
[ cls.selected ]: selected,
|
||||
[ cls.showSelection ]: showSelection,
|
||||
[ cls.red ]: isRed,
|
||||
[ cls.clickType ]: event.type === TYPES.CLICK,
|
||||
[ cls.inputType ]: event.type === TYPES.INPUT,
|
||||
[ cls.clickrageType ]: event.type === TYPES.CLICKRAGE,
|
||||
[ cls.highlight ] : presentInSearch,
|
||||
[ cls.lastInGroup ]: whiteBg,
|
||||
}) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ menuOpen &&
|
||||
<button onClick={ this.copyHandler } className={ cls.contextMenu }>
|
||||
{ event.target ? 'Copy CSS' : 'Copy URL' }
|
||||
</button>
|
||||
}
|
||||
<div className={ cls.topBlock }>
|
||||
<div className={ cls.firstLine }>
|
||||
{ this.renderBody() }
|
||||
</div>
|
||||
{/* { event.type === TYPES.LOCATION &&
|
||||
<div className="text-sm font-normal color-gray-medium">{event.url}</div>
|
||||
} */}
|
||||
</div>
|
||||
{ event.type === TYPES.LOCATION && (event.fcpTime || event.visuallyComplete || event.timeToInteractive) &&
|
||||
<LoadInfo
|
||||
showInfo={ showLoadInfo }
|
||||
onClick={ toggleLoadInfo }
|
||||
event={ event }
|
||||
prorata={ prorata({
|
||||
parts: 100,
|
||||
elements: { a: event.fcpTime, b: event.visuallyComplete, c: event.timeToInteractive },
|
||||
startDivisorFn: elements => elements / 1.2,
|
||||
// eslint-disable-next-line no-mixed-operators
|
||||
divisorFn: (elements, parts) => elements / (2 * parts + 1),
|
||||
}) }
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux'
|
||||
import { TextEllipsis } from 'UI';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import Event from './Event'
|
||||
import stl from './eventGroupWrapper.module.css';
|
||||
import NoteEvent from './NoteEvent';
|
||||
import { setEditNoteTooltip } from 'Duck/sessions';;
|
||||
|
||||
// TODO: incapsulate toggler in LocationEvent
|
||||
@withToggle('showLoadInfo', 'toggleLoadInfo')
|
||||
@connect(
|
||||
(state) => ({
|
||||
members: state.getIn(['members', 'list']),
|
||||
currentUserId: state.getIn(['user', 'account', 'id']),
|
||||
}),
|
||||
{ setEditNoteTooltip }
|
||||
)
|
||||
class EventGroupWrapper extends React.Component {
|
||||
toggleLoadInfo = (e) => {
|
||||
e.stopPropagation();
|
||||
this.props.toggleLoadInfo();
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.showLoadInfo !== this.props.showLoadInfo ||
|
||||
prevProps.query !== this.props.query ||
|
||||
prevProps.event.timestamp !== this.props.event.timestamp ||
|
||||
prevProps.isNote !== this.props.isNote
|
||||
) {
|
||||
this.props.mesureHeight();
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.toggleLoadInfo(this.props.isFirst);
|
||||
this.props.mesureHeight();
|
||||
}
|
||||
|
||||
onEventClick = (e) => this.props.onEventClick(e, this.props.event);
|
||||
|
||||
onCheckboxClick = (e) => this.props.onCheckboxClick(e, this.props.event);
|
||||
|
||||
render() {
|
||||
const {
|
||||
event,
|
||||
isLastEvent,
|
||||
isLastInGroup,
|
||||
isSelected,
|
||||
isCurrent,
|
||||
isEditing,
|
||||
showSelection,
|
||||
showLoadInfo,
|
||||
isFirst,
|
||||
presentInSearch,
|
||||
isNote,
|
||||
filterOutNote,
|
||||
} = this.props;
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
|
||||
const whiteBg =
|
||||
(isLastInGroup && event.type !== TYPES.LOCATION) ||
|
||||
(!isLastEvent && event.type !== TYPES.LOCATION);
|
||||
const safeRef = String(event.referrer || '');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
stl.container,
|
||||
'!py-1',
|
||||
{
|
||||
[stl.last]: isLastInGroup,
|
||||
[stl.first]: event.type === TYPES.LOCATION,
|
||||
[stl.dashAfter]: isLastInGroup && !isLastEvent,
|
||||
},
|
||||
isLastInGroup && '!pb-2',
|
||||
event.type === TYPES.LOCATION && '!pt-2 !pb-2'
|
||||
)}
|
||||
>
|
||||
{isFirst && isLocation && event.referrer && (
|
||||
<div className={stl.referrer}>
|
||||
<TextEllipsis>
|
||||
Referrer: <span className={stl.url}>{safeRef}</span>
|
||||
</TextEllipsis>
|
||||
</div>
|
||||
)}
|
||||
{isNote ? (
|
||||
<NoteEvent
|
||||
note={event}
|
||||
filterOutNote={filterOutNote}
|
||||
onEdit={this.props.setEditNoteTooltip}
|
||||
noEdit={this.props.currentUserId !== event.userId}
|
||||
/>
|
||||
) : isLocation ? (
|
||||
<Event
|
||||
extended={isFirst}
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
selected={isSelected}
|
||||
showLoadInfo={showLoadInfo}
|
||||
toggleLoadInfo={this.toggleLoadInfo}
|
||||
isCurrent={isCurrent}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
) : (
|
||||
<Event
|
||||
key={event.key}
|
||||
event={event}
|
||||
onClick={this.onEventClick}
|
||||
onCheckboxClick={this.onCheckboxClick}
|
||||
selected={isSelected}
|
||||
isCurrent={isCurrent}
|
||||
showSelection={showSelection}
|
||||
overlayed={isEditing}
|
||||
presentInSearch={presentInSearch}
|
||||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={whiteBg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EventGroupWrapper
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import { List, AutoSizer, CellMeasurer } from "react-virtualized";
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import { setEventFilter, filterOutNote } from 'Duck/sessions';
|
||||
import EventGroupWrapper from './EventGroupWrapper';
|
||||
import styles from './eventsBlock.module.css';
|
||||
import EventSearch from './EventSearch/EventSearch';
|
||||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { RootStore } from 'App/duck'
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
|
||||
import { InjectedEvent } from 'Types/session/event'
|
||||
import Session from 'Types/session'
|
||||
|
||||
interface IProps {
|
||||
setEventFilter: (filter: { query: string }) => void
|
||||
filteredEvents: InjectedEvent[]
|
||||
setActiveTab: (tab?: string) => void
|
||||
query: string
|
||||
events: Session['events']
|
||||
notesWithEvents: Session['notesWithEvents']
|
||||
filterOutNote: (id: string) => void
|
||||
eventsIndex: number[]
|
||||
}
|
||||
|
||||
function EventsBlock(props: IProps) {
|
||||
const [mouseOver, setMouseOver] = React.useState(true)
|
||||
const scroller = React.useRef<List>(null)
|
||||
const cache = useCellMeasurerCache( {
|
||||
fixedWidth: true,
|
||||
defaultHeight: 300
|
||||
});
|
||||
|
||||
const { store, player } = React.useContext(PlayerContext)
|
||||
|
||||
const { eventListNow, playing } = store.get()
|
||||
|
||||
const {
|
||||
filteredEvents,
|
||||
eventsIndex,
|
||||
filterOutNote,
|
||||
query,
|
||||
setActiveTab,
|
||||
events,
|
||||
notesWithEvents,
|
||||
} = props
|
||||
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0
|
||||
const usedEvents = filteredEvents || notesWithEvents
|
||||
|
||||
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.setEventFilter({ query: value })
|
||||
|
||||
setTimeout(() => {
|
||||
if (!scroller.current) return;
|
||||
|
||||
scroller.current.scrollToRow(0);
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
props.setEventFilter({ query: '' })
|
||||
if (scroller.current) {
|
||||
scroller.current.forceUpdateGrid();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!scroller.current) return;
|
||||
|
||||
scroller.current.scrollToRow(0);
|
||||
}, 100)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearSearch()
|
||||
}
|
||||
}, [])
|
||||
React.useEffect(() => {
|
||||
if (scroller.current) {
|
||||
scroller.current.forceUpdateGrid();
|
||||
if (!mouseOver) {
|
||||
scroller.current.scrollToRow(currentTimeEventIndex);
|
||||
}
|
||||
}
|
||||
}, [currentTimeEventIndex])
|
||||
|
||||
const onEventClick = (_: React.MouseEvent, event: { time: number }) => player.jump(event.time)
|
||||
const onMouseOver = () => setMouseOver(true)
|
||||
const onMouseLeave = () => setMouseOver(false)
|
||||
|
||||
const renderGroup = ({ index, key, style, parent }: { index: number; key: string; style: React.CSSProperties; parent: any }) => {
|
||||
const isLastEvent = index === usedEvents.length - 1;
|
||||
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
|
||||
const event = usedEvents[index];
|
||||
const isNote = 'noteId' in event
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
|
||||
const heightBug = index === 0 && event?.type === TYPES.LOCATION && 'referrer' in event ? { top: 2 } : {}
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={cache}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
{({measure, registerChild}) => (
|
||||
<div style={{ ...style, ...heightBug }} ref={registerChild}>
|
||||
<EventGroupWrapper
|
||||
query={query}
|
||||
presentInSearch={eventsIndex.includes(index)}
|
||||
isFirst={index==0}
|
||||
mesureHeight={measure}
|
||||
onEventClick={ onEventClick }
|
||||
event={ event }
|
||||
isLastEvent={ isLastEvent }
|
||||
isLastInGroup={ isLastInGroup }
|
||||
isCurrent={ isCurrent }
|
||||
showSelection={ !playing }
|
||||
isNote={isNote}
|
||||
filterOutNote={filterOutNote}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
|
||||
const isEmptySearch = query && (usedEvents.length === 0 || !usedEvents)
|
||||
return (
|
||||
<>
|
||||
<div className={ cn(styles.header, 'p-4') }>
|
||||
<div className={ cn(styles.hAndProgress, 'mt-3') }>
|
||||
<EventSearch
|
||||
onChange={write}
|
||||
setActiveTab={setActiveTab}
|
||||
value={query}
|
||||
header={
|
||||
<div className="text-xl">User Events <span className="color-gray-medium">{ events.length }</span></div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={ cn("flex-1 px-4 pb-4", styles.eventsList) }
|
||||
id="eventList"
|
||||
data-openreplay-masked
|
||||
onMouseOver={ onMouseOver }
|
||||
onMouseLeave={ onMouseLeave }
|
||||
>
|
||||
{isEmptySearch && (
|
||||
<div className='flex items-center'>
|
||||
<Icon name="binoculars" size={18} />
|
||||
<span className='ml-2'>No Matching Results</span>
|
||||
</div>
|
||||
)}
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<List
|
||||
ref={scroller}
|
||||
className={ styles.eventsList }
|
||||
height={height + 10}
|
||||
width={248}
|
||||
overscanRowCount={6}
|
||||
itemSize={230}
|
||||
rowCount={usedEvents.length}
|
||||
deferredMeasurementCache={cache}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={renderGroup}
|
||||
scrollToAlignment="start"
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state: RootStore) => ({
|
||||
session: state.getIn([ 'sessions', 'current' ]),
|
||||
notesWithEvents: state.getIn([ 'sessions', 'current' ]).notesWithEvents,
|
||||
events: state.getIn([ 'sessions', 'current' ]).events,
|
||||
filteredEvents: state.getIn([ 'sessions', 'filteredEvents' ]),
|
||||
query: state.getIn(['sessions', 'eventsQuery']),
|
||||
eventsIndex: state.getIn([ 'sessions', 'eventsIndex' ]),
|
||||
}), {
|
||||
setEventFilter,
|
||||
filterOutNote
|
||||
})(observer(EventsBlock))
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
import styles from './loadInfo.module.css';
|
||||
import { numberWithCommas } from 'App/utils'
|
||||
|
||||
const LoadInfo = ({ showInfo = false, onClick, event: { fcpTime, visuallyComplete, timeToInteractive }, prorata: { a, b, c } }) => (
|
||||
<div>
|
||||
<div className={ styles.bar } onClick={ onClick }>
|
||||
{ typeof fcpTime === 'number' && <div style={ { width: `${ a }%` } } /> }
|
||||
{ typeof visuallyComplete === 'number' && <div style={ { width: `${ b }%` } } /> }
|
||||
{ typeof timeToInteractive === 'number' && <div style={ { width: `${ c }%` } } /> }
|
||||
</div>
|
||||
<div className={ styles.bottomBlock } data-hidden={ !showInfo }>
|
||||
{ typeof fcpTime === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Time to Render' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(fcpTime || 0) }ms` }</div>
|
||||
</div>
|
||||
}
|
||||
{ typeof visuallyComplete === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Visually Complete' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(visuallyComplete || 0) }ms` }</div>
|
||||
</div>
|
||||
}
|
||||
{ typeof timeToInteractive === 'number' &&
|
||||
<div className={ styles.wrapper }>
|
||||
<div className={ styles.lines } />
|
||||
<div className={ styles.label } >{ 'Time To Interactive' }</div>
|
||||
<div className={ styles.value }>{ `${ numberWithCommas(timeToInteractive || 0) }ms` }</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
LoadInfo.displayName = 'LoadInfo';
|
||||
|
||||
export default LoadInfo;
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { tagProps, Note } from 'App/services/NotesService';
|
||||
import { formatTimeOrDate } from 'App/date';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { ItemMenu } from 'UI';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { toast } from 'react-toastify';
|
||||
import { session } from 'App/routes';
|
||||
import { confirm } from 'UI';
|
||||
import { TeamBadge } from 'Shared/SessionListContainer/components/Notes';
|
||||
|
||||
interface Props {
|
||||
note: Note;
|
||||
noEdit: boolean;
|
||||
filterOutNote: (id: number) => void;
|
||||
onEdit: (noteTooltipObj: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
function NoteEvent(props: Props) {
|
||||
const { settingsStore, notesStore } = useStore();
|
||||
const { timezone } = settingsStore.sessionSettings;
|
||||
|
||||
const onEdit = () => {
|
||||
props.onEdit({
|
||||
isVisible: true,
|
||||
isEdit: true,
|
||||
time: props.note.timestamp,
|
||||
note: {
|
||||
timestamp: props.note.timestamp,
|
||||
tag: props.note.tag,
|
||||
isPublic: props.note.isPublic,
|
||||
message: props.note.message,
|
||||
sessionId: props.note.sessionId,
|
||||
noteId: props.note.noteId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCopy = () => {
|
||||
copy(
|
||||
`${window.location.origin}/${window.location.pathname.split('/')[1]}${session(
|
||||
props.note.sessionId
|
||||
)}${props.note.timestamp > 0 ? `?jumpto=${props.note.timestamp}¬e=${props.note.noteId}` : `?note=${props.note.noteId}`}`
|
||||
);
|
||||
toast.success('Note URL copied to clipboard');
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to delete this note?`,
|
||||
})
|
||||
) {
|
||||
notesStore.deleteNote(props.note.noteId).then((r) => {
|
||||
props.filterOutNote(props.note.noteId);
|
||||
toast.success('Note deleted');
|
||||
});
|
||||
}
|
||||
};
|
||||
const menuItems = [
|
||||
{ icon: 'pencil', text: 'Edit', onClick: onEdit, disabled: props.noEdit },
|
||||
{ icon: 'link-45deg', text: 'Copy URL', onClick: onCopy },
|
||||
{ icon: 'trash', text: 'Delete', onClick: onDelete },
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="flex items-start flex-col p-2 border rounded"
|
||||
style={{ background: '#FFFEF5' }}
|
||||
>
|
||||
<div className="flex items-center w-full relative">
|
||||
<div className="p-3 bg-gray-light rounded-full">
|
||||
<Icon name="quotes" color="main" />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<div
|
||||
className="text-base"
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{props.note.userName}
|
||||
</div>
|
||||
<div className="text-disabled-text text-sm">
|
||||
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="cursor-pointer absolute" style={{ right: -5 }}>
|
||||
<ItemMenu bold items={menuItems} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-base capitalize-first my-3 overflow-y-scroll overflow-x-hidden"
|
||||
style={{ maxHeight: 200, maxWidth: 220 }}
|
||||
>
|
||||
{props.note.message}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap w-full">
|
||||
{props.note.tag ? (
|
||||
<div
|
||||
key={props.note.tag}
|
||||
style={{
|
||||
// @ts-ignore
|
||||
background: tagProps[props.note.tag],
|
||||
userSelect: 'none',
|
||||
padding: '1px 6px',
|
||||
}}
|
||||
className="rounded-full text-white text-xs select-none w-fit"
|
||||
>
|
||||
{props.note.tag}
|
||||
</div>
|
||||
) : null}
|
||||
{!props.note.isPublic ? null : <TeamBadge />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(NoteEvent);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './EventsBlock';
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
interface Props {
|
||||
i: number;
|
||||
tab: string;
|
||||
currentTab: string;
|
||||
changeTab?: (tab: string) => void;
|
||||
}
|
||||
|
||||
function Tab({ i, tab, currentTab, changeTab }: Props) {
|
||||
return (
|
||||
<div
|
||||
key={tab}
|
||||
style={{ marginBottom: '-2px' }}
|
||||
onClick={() => changeTab?.(tab)}
|
||||
className={cn(
|
||||
'self-end py-1 px-4 cursor-pointer',
|
||||
currentTab === tab
|
||||
? 'border-gray-light border-t border-l border-r !border-b-white bg-white rounded-tl rounded-tr font-semibold'
|
||||
: 'cursor-pointer border-gray-light !border-b !border-t-0 !border-l-0 !border-r-0'
|
||||
)}
|
||||
>
|
||||
Tab {i + 1}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab;
|
||||
|
|
@ -113,7 +113,7 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps,
|
|||
// REQUIRED FOR FUTURE USAGE AND AS AN EXAMPLE OF THE FUNCTIONALITY
|
||||
|
||||
function buildPng() {
|
||||
html2canvas(reportRef.current, {
|
||||
html2canvas(reportRef.current!, {
|
||||
scale: 2,
|
||||
ignoreElements: (e) => e.id.includes('pdf-ignore'),
|
||||
}).then((canvas) => {
|
||||
|
|
@ -147,11 +147,11 @@ function BugReportModal({ hideModal, session, width, height, account, xrayProps,
|
|||
}
|
||||
function buildText() {
|
||||
doc
|
||||
.html(reportRef.current, {
|
||||
.html(reportRef.current!, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 210,
|
||||
windowWidth: reportRef.current.getBoundingClientRect().width,
|
||||
windowWidth: reportRef.current!.getBoundingClientRect().width,
|
||||
autoPaging: 'text',
|
||||
html2canvas: {
|
||||
ignoreElements: (e) => e.id.includes('pdf-ignore') || e instanceof SVGElement,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { TextEllipsis } from 'UI';
|
||||
import { TextEllipsis, Icon } from 'UI';
|
||||
import withToggle from 'HOCs/withToggle';
|
||||
import { TYPES } from 'Types/session/event';
|
||||
import Event from './Event';
|
||||
|
|
@ -57,6 +57,7 @@ class EventGroupWrapper extends React.Component {
|
|||
isFirst,
|
||||
presentInSearch,
|
||||
isNote,
|
||||
isTabChange,
|
||||
filterOutNote
|
||||
} = this.props;
|
||||
const isLocation = event.type === TYPES.LOCATION;
|
||||
|
|
@ -107,7 +108,7 @@ class EventGroupWrapper extends React.Component {
|
|||
isLastInGroup={isLastInGroup}
|
||||
whiteBg={true}
|
||||
/>
|
||||
) : (
|
||||
) : isTabChange ? (<TabChange from={event.fromTab} to={event.toTab} />) : (
|
||||
<Event
|
||||
key={event.key}
|
||||
event={event}
|
||||
|
|
@ -123,10 +124,24 @@ class EventGroupWrapper extends React.Component {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{isLastInGroup && <div className='border-t border-color-gray-light-shade' />}
|
||||
{(isLastInGroup && !isTabChange) && <div className='border-t border-color-gray-light-shade' />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TabChange({ from, to }) { return (
|
||||
<div className={'text-center p-2 bg-gray-lightest w-full my-2 flex items-center gap-2 justify-center'}>
|
||||
<span>Tab change:</span>
|
||||
<span className={'font-semibold'}>
|
||||
{from}
|
||||
</span>
|
||||
<Icon name={"arrow-right-short"} size={18} color={"gray-dark"}/>
|
||||
<span className={'font-semibold'}>
|
||||
{to}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventGroupWrapper;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { RootStore } from 'App/duck';
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache';
|
||||
import { InjectedEvent } from 'Types/session/event';
|
||||
import Session from 'Types/session';
|
||||
import Session, { mergeEventLists } from 'Types/session';
|
||||
|
||||
interface IProps {
|
||||
setEventFilter: (filter: { query: string }) => void;
|
||||
|
|
@ -36,7 +36,7 @@ function EventsBlock(props: IProps) {
|
|||
|
||||
const { store, player } = React.useContext(PlayerContext);
|
||||
|
||||
const { eventListNow, playing } = store.get();
|
||||
const { playing, tabStates, tabChangeEvents } = store.get();
|
||||
|
||||
const {
|
||||
filteredEvents,
|
||||
|
|
@ -44,12 +44,19 @@ function EventsBlock(props: IProps) {
|
|||
filterOutNote,
|
||||
query,
|
||||
setActiveTab,
|
||||
events,
|
||||
notesWithEvents,
|
||||
notesWithEvents = [],
|
||||
} = props;
|
||||
|
||||
const filteredLength = filteredEvents?.length || 0;
|
||||
const notesWithEvtsLength = notesWithEvents?.length || 0;
|
||||
const eventListNow = Object.values(tabStates).reduce((acc: any[], tab) => {
|
||||
return acc.concat(tab.eventListNow)
|
||||
}, [])
|
||||
|
||||
const currentTimeEventIndex = eventListNow.length > 0 ? eventListNow.length - 1 : 0;
|
||||
const usedEvents = filteredEvents || notesWithEvents;
|
||||
const usedEvents = React.useMemo(() => {
|
||||
return mergeEventLists(filteredEvents || notesWithEvents, tabChangeEvents);
|
||||
}, [filteredLength, notesWithEvtsLength])
|
||||
|
||||
const write = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.setEventFilter({ query: value });
|
||||
|
|
@ -110,6 +117,7 @@ function EventsBlock(props: IProps) {
|
|||
const isLastInGroup = isLastEvent || usedEvents[index + 1]?.type === TYPES.LOCATION;
|
||||
const event = usedEvents[index];
|
||||
const isNote = 'noteId' in event;
|
||||
const isTabChange = event.type === 'TABCHANGE';
|
||||
const isCurrent = index === currentTimeEventIndex;
|
||||
|
||||
const heightBug =
|
||||
|
|
@ -130,6 +138,7 @@ function EventsBlock(props: IProps) {
|
|||
isCurrent={isCurrent}
|
||||
showSelection={!playing}
|
||||
isNote={isNote}
|
||||
isTabChange={isTabChange}
|
||||
filterOutNote={filterOutNote}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ interface IProps {
|
|||
|
||||
function Exceptions({ errorStack, sourcemapUploaded, loading }: IProps) {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { logListNow: logs, exceptionsList: exceptions } = store.get();
|
||||
const { tabStates, currentTab } = store.get();
|
||||
const { logListNow: logs = [], exceptionsList: exceptions = [] } = tabStates[currentTab]
|
||||
const [filter, setFilter] = React.useState('');
|
||||
const [currentError, setCurrentErrorVal] = React.useState(null);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ function renderDefaultStatus() {
|
|||
|
||||
function GraphQL() {
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { graphqlList: list, graphqlListNow: listNow, time, livePlay } = store.get();
|
||||
const { time, livePlay, tabStates, currentTab } = store.get();
|
||||
const { graphqlList: list = [], graphqlListNow: listNow = [] } = tabStates[currentTab]
|
||||
|
||||
const defaultState = {
|
||||
filter: '',
|
||||
|
|
|
|||
|
|
@ -25,15 +25,19 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
|
||||
const {
|
||||
endTime,
|
||||
performanceChartData,
|
||||
stackList: stackEventList,
|
||||
eventList: eventsList,
|
||||
frustrationsList,
|
||||
exceptionsList,
|
||||
resourceList: resourceListUnmap,
|
||||
fetchList,
|
||||
graphqlList,
|
||||
currentTab,
|
||||
tabStates,
|
||||
} = store.get();
|
||||
const states = Object.values(tabStates)
|
||||
|
||||
const stackEventList = tabStates[currentTab]?.stackList || []
|
||||
const eventsList = tabStates[currentTab]?.eventList || []
|
||||
const frustrationsList = tabStates[currentTab]?.frustrationsList || []
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || []
|
||||
const resourceListUnmap = tabStates[currentTab]?.resourceList || []
|
||||
const fetchList = tabStates[currentTab]?.fetchList || []
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || []
|
||||
const performanceChartData = tabStates[currentTab]?.performanceChartData || []
|
||||
|
||||
const fetchPresented = fetchList.length > 0;
|
||||
|
||||
|
|
@ -50,7 +54,7 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
PERFORMANCE: performanceChartData,
|
||||
FRUSTRATIONS: frustrationsList,
|
||||
};
|
||||
}, [dataLoaded]);
|
||||
}, [dataLoaded, currentTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoaded) {
|
||||
|
|
@ -67,7 +71,7 @@ function OverviewPanel({ issuesList }: { issuesList: Record<string, any>[] }) {
|
|||
) {
|
||||
setDataLoaded(true);
|
||||
}
|
||||
}, [resourceList, issuesList, exceptionsList, eventsList, stackEventList, performanceChartData]);
|
||||
}, [resourceList, issuesList, exceptionsList, eventsList, stackEventList, performanceChartData, currentTab]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import stl from './performance.module.css';
|
|||
|
||||
import BottomBlock from '../BottomBlock';
|
||||
import InfoLine from '../BottomBlock/InfoLine';
|
||||
import { toJS } from "mobx";
|
||||
|
||||
const CPU_VISUAL_OFFSET = 10;
|
||||
|
||||
|
|
@ -183,17 +184,22 @@ function Performance({
|
|||
const [_data, setData] = React.useState<any[]>([])
|
||||
|
||||
const {
|
||||
performanceChartTime,
|
||||
performanceChartData,
|
||||
connType,
|
||||
connBandwidth,
|
||||
performanceAvailability: availability,
|
||||
tabStates,
|
||||
currentTab,
|
||||
} = store.get();
|
||||
|
||||
React.useState(() => {
|
||||
const {
|
||||
performanceChartTime = [],
|
||||
performanceChartData = [],
|
||||
performanceAvailability: availability = {}
|
||||
} = tabStates[currentTab];
|
||||
|
||||
React.useEffect(() => {
|
||||
setTicks(generateTicks(performanceChartData));
|
||||
setData(addFpsMetadata(performanceChartData));
|
||||
})
|
||||
}, [currentTab])
|
||||
|
||||
|
||||
const onDotClick = ({ index: pointer }: { index: number }) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectStorageType, STORAGE_TYPES } from 'Player';
|
||||
import { selectStorageType, STORAGE_TYPES, StorageType } from 'Player';
|
||||
import { PlayButton, PlayingState, FullScreenButton } from 'App/player-ui'
|
||||
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
|
|
@ -67,17 +67,21 @@ function Controls(props: any) {
|
|||
completed,
|
||||
skip,
|
||||
speed,
|
||||
cssLoading,
|
||||
messagesLoading,
|
||||
inspectorMode,
|
||||
markedTargets,
|
||||
exceptionsList,
|
||||
profilesList,
|
||||
graphqlList,
|
||||
logMarkedCountNow: logRedCount,
|
||||
resourceMarkedCountNow: resourceRedCount,
|
||||
stackMarkedCountNow: stackRedCount,
|
||||
currentTab,
|
||||
tabStates
|
||||
} = store.get();
|
||||
|
||||
const cssLoading = tabStates[currentTab]?.cssLoading ?? false;
|
||||
const profilesList = tabStates[currentTab]?.profilesList || [];
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||
const logRedCount = tabStates[currentTab]?.logMarkedCountNow || 0;
|
||||
const resourceRedCount = tabStates[currentTab]?.resourceMarkedCountNow || 0;
|
||||
const stackRedCount = tabStates[currentTab]?.stackMarkedCountNow || 0;
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
|
||||
const {
|
||||
bottomBlock,
|
||||
toggleBottomBlock,
|
||||
|
|
@ -86,10 +90,10 @@ function Controls(props: any) {
|
|||
skipInterval,
|
||||
disabledRedux,
|
||||
showStorageRedux,
|
||||
session
|
||||
session,
|
||||
} = props;
|
||||
|
||||
const storageType = selectStorageType(store.get());
|
||||
const storageType = store.get().tabStates[currentTab] ? selectStorageType(store.get().tabStates[currentTab]) : StorageType.NONE
|
||||
const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets;
|
||||
const profilesCount = profilesList.length;
|
||||
const graphqlCount = graphqlList.length;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { observer } from 'mobx-react-lite';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import Issue from "Types/session/issue";
|
||||
import { toJS } from 'mobx'
|
||||
|
||||
function getTimelinePosition(value: number, scale: number) {
|
||||
const pos = value * scale;
|
||||
|
|
@ -35,20 +36,21 @@ function Timeline(props: IProps) {
|
|||
playing,
|
||||
time,
|
||||
skipIntervals,
|
||||
eventList: events,
|
||||
skip,
|
||||
skipToIssue,
|
||||
ready,
|
||||
endTime,
|
||||
devtoolsLoading,
|
||||
domLoading,
|
||||
tabStates,
|
||||
currentTab,
|
||||
} = store.get()
|
||||
const { issues } = props;
|
||||
const notes = notesStore.sessionNotes
|
||||
|
||||
const progressRef = useRef<HTMLDivElement>(null)
|
||||
const timelineRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const events = tabStates[currentTab]?.eventList || [];
|
||||
|
||||
const scale = 100 / endTime;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 65px;
|
||||
height: 55px;
|
||||
padding-left: 10px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,14 @@ function Overlay({
|
|||
const {
|
||||
playing,
|
||||
messagesLoading,
|
||||
cssLoading,
|
||||
completed,
|
||||
autoplay,
|
||||
inspectorMode,
|
||||
markedTargets,
|
||||
activeTargetIndex,
|
||||
tabStates,
|
||||
} = store.get()
|
||||
const cssLoading = Object.values(tabStates).some(({ cssLoading }) => cssLoading)
|
||||
const loading = messagesLoading || cssLoading
|
||||
|
||||
const showAutoplayTimer = completed && autoplay && nextId
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { PlayerContext } from 'App/components/Session/playerContext';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { JSONTree, NoContent, Tooltip } from 'UI';
|
||||
import { formatMs } from 'App/date';
|
||||
// @ts-ignore
|
||||
import { diff } from 'deep-diff';
|
||||
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
|
||||
import Autoscroll from '../Autoscroll';
|
||||
|
|
@ -40,12 +41,28 @@ interface Props {
|
|||
function Storage(props: Props) {
|
||||
const lastBtnRef = React.useRef<HTMLButtonElement>();
|
||||
const [showDiffs, setShowDiffs] = React.useState(false);
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const state = store.get();
|
||||
const [stateObject, setState] = React.useState({});
|
||||
|
||||
const listNow = selectStorageListNow(state);
|
||||
const list = selectStorageList(state);
|
||||
const type = selectStorageType(state);
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const { tabStates, currentTab } = store.get()
|
||||
const state = tabStates[currentTab] || {}
|
||||
|
||||
const listNow = selectStorageListNow(state) || [];
|
||||
const list = selectStorageList(state) || [];
|
||||
const type = selectStorageType(state) || STORAGE_TYPES.NONE
|
||||
|
||||
React.useEffect(() => {
|
||||
let currentState;
|
||||
if (listNow.length === 0) {
|
||||
currentState = decodeMessage(list[0])
|
||||
} else {
|
||||
currentState = decodeMessage(listNow[listNow.length - 1])
|
||||
}
|
||||
const stateObj = currentState?.state || currentState?.payload?.state || {}
|
||||
const newState = Object.assign(stateObject, stateObj);
|
||||
setState(newState);
|
||||
|
||||
}, [listNow.length]);
|
||||
|
||||
const decodeMessage = (msg: any) => {
|
||||
const decoded = {};
|
||||
|
|
@ -84,7 +101,11 @@ function Storage(props: Props) {
|
|||
focusNextButton();
|
||||
}, [listNow]);
|
||||
|
||||
const renderDiff = (item: Record<string, any>, prevItem: Record<string, any>) => {
|
||||
const renderDiff = (item: Record<string, any>, prevItem?: Record<string, any>) => {
|
||||
if (!showDiffs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prevItem) {
|
||||
// we don't have state before first action
|
||||
return <div style={{ flex: 3 }} className="p-1" />;
|
||||
|
|
@ -166,7 +187,7 @@ function Storage(props: Props) {
|
|||
name = itemD.mutation.join('');
|
||||
}
|
||||
|
||||
if (src !== null && !showDiffs) {
|
||||
if (src !== null && !showDiffs && itemD.state) {
|
||||
setShowDiffs(true);
|
||||
}
|
||||
|
||||
|
|
@ -182,7 +203,7 @@ function Storage(props: Props) {
|
|||
) : (
|
||||
<>
|
||||
{renderDiff(itemD, prevItemD)}
|
||||
<div style={{ flex: 2 }} className="flex pl-10 pt-2">
|
||||
<div style={{ flex: 2 }} className={cn("flex pt-2", showDiffs && 'pl-10')}>
|
||||
<JSONTree
|
||||
name={ensureString(name)}
|
||||
src={src}
|
||||
|
|
@ -218,17 +239,14 @@ function Storage(props: Props) {
|
|||
|
||||
const { hintIsHidden } = props;
|
||||
|
||||
const showStore = type !== STORAGE_TYPES.MOBX;
|
||||
return (
|
||||
<BottomBlock>
|
||||
<BottomBlock.Header>
|
||||
{list.length > 0 && (
|
||||
<div className="flex w-full">
|
||||
{showStore && (
|
||||
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
|
||||
{'STATE'}
|
||||
</h3>
|
||||
)}
|
||||
{showDiffs ? (
|
||||
<h3 style={{ width: '39%' }} className="font-semibold">
|
||||
DIFFS
|
||||
|
|
@ -311,22 +329,17 @@ function Storage(props: Props) {
|
|||
size="small"
|
||||
show={list.length === 0}
|
||||
>
|
||||
{showStore && (
|
||||
<div className="ph-10 scroll-y" style={{ width: '25%' }}>
|
||||
{list.length === 0 ? (
|
||||
<div className="color-gray-light font-size-16 mt-20 text-center">
|
||||
{'Empty state.'}
|
||||
</div>
|
||||
) : (
|
||||
<JSONTree collapsed={2} src={
|
||||
listNow.length === 0
|
||||
? decodeMessage(list[0]).state
|
||||
: decodeMessage(listNow[listNow.length - 1]).state}
|
||||
<JSONTree collapsed={2} src={stateObject}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex" style={{ width: showStore ? '75%' : '100%' }}>
|
||||
<div className="flex" style={{ width: '75%' }}>
|
||||
<Autoscroll className="ph-10">
|
||||
{decodedList.map((item: Record<string, any>, i: number) =>
|
||||
renderItem(item, i, i > 0 ? decodedList[i - 1] : undefined)
|
||||
|
|
|
|||
|
|
@ -11,26 +11,24 @@ import BugReportModal from './BugReport/BugReportModal';
|
|||
import { PlayerContext } from 'App/components/Session/playerContext';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import AutoplayToggle from 'Shared/AutoplayToggle';
|
||||
import { connect } from 'react-redux'
|
||||
import { connect } from 'react-redux';
|
||||
import Tab from 'Components/Session/Player/SharedComponents/Tab';
|
||||
|
||||
const localhostWarn = (project) => project + '_localhost_warn'
|
||||
const localhostWarn = (project) => project + '_localhost_warn';
|
||||
|
||||
function SubHeader(props) {
|
||||
const localhostWarnKey = localhostWarn(props.siteId)
|
||||
const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1'
|
||||
const localhostWarnKey = localhostWarn(props.siteId);
|
||||
const defaultLocalhostWarn = localStorage.getItem(localhostWarnKey) !== '1';
|
||||
const [showWarningModal, setWarning] = React.useState(defaultLocalhostWarn);
|
||||
const { player, store } = React.useContext(PlayerContext);
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
location: currentLocation,
|
||||
fetchList,
|
||||
graphqlList,
|
||||
resourceList,
|
||||
exceptionsList,
|
||||
eventList: eventsList,
|
||||
endTime,
|
||||
} = store.get();
|
||||
const { width, height, endTime, tabStates, currentTab, tabs } = store.get();
|
||||
|
||||
const currentLocation = tabStates[currentTab]?.location || '';
|
||||
const resourceList = tabStates[currentTab]?.resourceList || [];
|
||||
const exceptionsList = tabStates[currentTab]?.exceptionsList || [];
|
||||
const eventsList = tabStates[currentTab]?.eventList || [];
|
||||
const graphqlList = tabStates[currentTab]?.graphqlList || [];
|
||||
const fetchList = tabStates[currentTab]?.fetchList || [];
|
||||
|
||||
const enabledIntegration = useMemo(() => {
|
||||
const { integrations } = props;
|
||||
|
|
@ -39,7 +37,7 @@ function SubHeader(props) {
|
|||
}
|
||||
|
||||
return integrations.some((i) => i.token);
|
||||
})
|
||||
});
|
||||
|
||||
const mappedResourceList = resourceList
|
||||
.filter((r) => r.isRed || r.isYellow)
|
||||
|
|
@ -71,11 +69,12 @@ function SubHeader(props) {
|
|||
const showWarning =
|
||||
location && /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(location) && showWarningModal;
|
||||
const closeWarning = () => {
|
||||
localStorage.setItem(localhostWarnKey, '1')
|
||||
setWarning(false)
|
||||
}
|
||||
localStorage.setItem(localhostWarnKey, '1');
|
||||
setWarning(false);
|
||||
};
|
||||
return (
|
||||
<div className="w-full px-4 py-2 flex items-center border-b relative">
|
||||
<>
|
||||
<div className="w-full px-4 flex items-center border-b relative">
|
||||
{showWarning ? (
|
||||
<div
|
||||
className="px-3 py-1 border border-gray-light drop-shadow-md rounded bg-active-blue flex items-center justify-between"
|
||||
|
|
@ -102,18 +101,16 @@ function SubHeader(props) {
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{location && (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center cursor-pointer color-gray-medium text-sm p-1 hover:bg-active-blue hover:!underline rounded-md"
|
||||
>
|
||||
<Icon size="20" name="event/link" className="mr-1" />
|
||||
<Tooltip title="Open in new tab" delay={0}>
|
||||
<a href={currentLocation} target='_blank'>{location}</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{tabs.map((tab, i) => (
|
||||
<React.Fragment key={tab}>
|
||||
<Tab
|
||||
i={i}
|
||||
tab={tab}
|
||||
currentTab={tabs.length === 1 ? tab : currentTab}
|
||||
changeTab={(changeTo) => player.changeTab(changeTo)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div
|
||||
className="ml-auto text-sm flex items-center color-gray-medium gap-2"
|
||||
style={{ width: 'max-content' }}
|
||||
|
|
@ -153,10 +150,23 @@ function SubHeader(props) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{location && (
|
||||
<div className={'w-full bg-white border-b border-gray-light'}>
|
||||
<div className="flex w-fit items-center cursor-pointer color-gray-medium text-sm p-1">
|
||||
<Icon size="20" name="event/link" className="mr-1" />
|
||||
<Tooltip title="Open in new tab" delay={0}>
|
||||
<a href={currentLocation} target="_blank">
|
||||
{location}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect((state) => ({
|
||||
siteId: state.getIn(['site', 'siteId']),
|
||||
integrations: state.getIn([ 'issues', 'list' ])
|
||||
integrations: state.getIn(['issues', 'list']),
|
||||
}))(observer(SubHeader));
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useModal } from 'App/components/Modal';
|
|||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter'
|
||||
import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache'
|
||||
import { toJS } from 'mobx'
|
||||
|
||||
const ALL = 'ALL';
|
||||
const INFO = 'INFO';
|
||||
|
|
@ -74,7 +75,9 @@ function ConsolePanel({ isLive }: { isLive: boolean }) {
|
|||
const { player, store } = React.useContext(PlayerContext)
|
||||
const jump = (t: number) => player.jump(t)
|
||||
|
||||
const { logList, exceptionsList, logListNow, exceptionsListNow } = store.get()
|
||||
const { currentTab, tabStates } = store.get()
|
||||
const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = tabStates[currentTab]
|
||||
|
||||
const list = isLive ?
|
||||
useMemo(() => logListNow.concat(exceptionsListNow).sort((a, b) => a.time - b.time),
|
||||
[logListNow.length, exceptionsListNow.length]
|
||||
|
|
|
|||
|
|
@ -151,11 +151,16 @@ function NetworkPanel({ startedAt }: { startedAt: number }) {
|
|||
domContentLoadedTime,
|
||||
loadTime,
|
||||
domBuildingTime,
|
||||
fetchList,
|
||||
resourceList,
|
||||
fetchListNow,
|
||||
resourceListNow,
|
||||
tabStates,
|
||||
currentTab
|
||||
} = store.get()
|
||||
const {
|
||||
fetchList = [],
|
||||
resourceList = [],
|
||||
fetchListNow = [],
|
||||
resourceListNow = []
|
||||
} = tabStates[currentTab]
|
||||
|
||||
const { showModal } = useModal();
|
||||
const [sortBy, setSortBy] = useState('time');
|
||||
const [sortAscending, setSortAscending] = useState(true);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ const renderName = (p: any) => <TextEllipsis text={p.name} />;
|
|||
|
||||
function ProfilerPanel() {
|
||||
const { store } = React.useContext(PlayerContext)
|
||||
|
||||
const profiles = store.get().profilesList as any[] // TODO lest internal types
|
||||
const { tabStates, currentTab } = store.get()
|
||||
const profiles = tabStates[currentTab].profilesList || [] as any[] // TODO lest internal types
|
||||
|
||||
const { showModal } = useModal();
|
||||
const [ filter, onFilterChange ] = useInputState()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ const TABS = TAB_KEYS.map((tab) => ({ text: tab, key: tab }))
|
|||
function StackEventPanel() {
|
||||
const { player, store } = React.useContext(PlayerContext)
|
||||
const jump = (t: number) => player.jump(t)
|
||||
const { stackList: list, stackListNow: listNow } = store.get()
|
||||
const { currentTab, tabStates } = store.get()
|
||||
|
||||
const {
|
||||
stackList: list = [],
|
||||
stackListNow: listNow = [],
|
||||
} = tabStates[currentTab]
|
||||
|
||||
const {
|
||||
sessionStore: { devTools },
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default class ListWalker<T extends Timed> {
|
|||
if (this.list.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.list[ this.list.length - 1 ];
|
||||
return this.list.slice(-1)[0];
|
||||
}
|
||||
|
||||
get current(): T | null {
|
||||
|
|
@ -108,7 +108,7 @@ export default class ListWalker<T extends Timed> {
|
|||
/**
|
||||
* @returns last message with the time <= t.
|
||||
* Assumed that the current message is already handled so
|
||||
* if pointer doesn't cahnge <null> is returned.
|
||||
* if pointer doesn't change <null> is returned.
|
||||
*/
|
||||
moveGetLast(t: number, index?: number): T | null {
|
||||
let key: string = "time"; //TODO
|
||||
|
|
@ -130,6 +130,30 @@ export default class ListWalker<T extends Timed> {
|
|||
return changed ? this.list[ this.p - 1 ] : null;
|
||||
}
|
||||
|
||||
moveGetLastDebug(t: number, index?: number): T | null {
|
||||
let key: string = "time"; //TODO
|
||||
let val = t;
|
||||
if (index) {
|
||||
key = "_index";
|
||||
val = index;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
while (this.p < this.length && this.list[this.p][key] <= val) {
|
||||
this.moveNext()
|
||||
changed = true;
|
||||
}
|
||||
while (this.p > 0 && this.list[ this.p - 1 ][key] > val) {
|
||||
this.movePrev()
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// console.log(this.list[this.p - 1])
|
||||
return changed ? this.list[ this.p - 1 ] : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Moves over the messages starting from the current+1 to the last one with the time <= t
|
||||
* applying callback on each of them
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default class MessageLoader {
|
|||
private store: Store<State>,
|
||||
private messageManager: MessageManager,
|
||||
private isClickmap: boolean,
|
||||
private uiErrorHandler?: { error: (msg: string) => void }
|
||||
) {}
|
||||
|
||||
createNewParser(shouldDecrypt = true, file?: string, toggleStatus?: (isLoading: boolean) => void) {
|
||||
|
|
@ -57,6 +58,9 @@ export default class MessageLoader {
|
|||
this.messageManager._sortMessagesHack(sorted)
|
||||
toggleStatus?.(false);
|
||||
this.messageManager.setMessagesLoading(false)
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.uiErrorHandler?.error('Error parsing file: ' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,71 +1,59 @@
|
|||
// @ts-ignore
|
||||
import { Decoder } from "syncod";
|
||||
import { Decoder } from 'syncod';
|
||||
import logger from 'App/logger';
|
||||
|
||||
import { TYPES as EVENT_TYPES } from 'Types/session/event';
|
||||
import { Log } from 'Player';
|
||||
import {
|
||||
ResourceType,
|
||||
getResourceFromResourceTiming,
|
||||
getResourceFromNetworkRequest
|
||||
} from 'Player'
|
||||
|
||||
import type { Store } from 'Player';
|
||||
import type { Store, ILog } from 'Player';
|
||||
import ListWalker from '../common/ListWalker';
|
||||
|
||||
import PagesManager from './managers/PagesManager';
|
||||
import MouseMoveManager from './managers/MouseMoveManager';
|
||||
|
||||
import PerformanceTrackManager from './managers/PerformanceTrackManager';
|
||||
import WindowNodeCounter from './managers/WindowNodeCounter';
|
||||
import ActivityManager from './managers/ActivityManager';
|
||||
|
||||
import { MouseThrashing, MType } from "./messages";
|
||||
import { isDOMType } from './messages/filters.gen';
|
||||
import type {
|
||||
Message,
|
||||
SetPageLocation,
|
||||
ConnectionInformation,
|
||||
SetViewportSize,
|
||||
SetViewportScroll,
|
||||
MouseClick,
|
||||
} from './messages';
|
||||
|
||||
import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE, State as ListsState } from './Lists';
|
||||
import { MouseThrashing, MType } from './messages';
|
||||
import type { Message, MouseClick } from './messages';
|
||||
|
||||
import Screen, {
|
||||
INITIAL_STATE as SCREEN_INITIAL_STATE,
|
||||
State as ScreenState,
|
||||
} from './Screen/Screen';
|
||||
|
||||
import type { InitialLists } from './Lists'
|
||||
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
|
||||
import type { InitialLists } from './Lists';
|
||||
import type { SkipInterval } from './managers/ActivityManager';
|
||||
import TabSessionManager, { TabState } from 'Player/web/TabManager';
|
||||
import ActiveTabManager from 'Player/web/managers/ActiveTabManager';
|
||||
|
||||
export interface State extends ScreenState, ListsState {
|
||||
performanceChartData: PerformanceChartPoint[],
|
||||
skipIntervals: SkipInterval[],
|
||||
connType?: string,
|
||||
connBandwidth?: number,
|
||||
location?: string,
|
||||
performanceChartTime?: number,
|
||||
performanceAvailability?: PerformanceTrackManager['availability']
|
||||
|
||||
domContentLoadedTime?: { time: number, value: number },
|
||||
domBuildingTime?: number,
|
||||
loadTime?: { time: number, value: number },
|
||||
error: boolean,
|
||||
messagesLoading: boolean,
|
||||
cssLoading: boolean,
|
||||
|
||||
ready: boolean,
|
||||
lastMessageTime: number,
|
||||
firstVisualEvent: number,
|
||||
messagesProcessed: boolean,
|
||||
interface RawList {
|
||||
event: Record<string, any>[] & { tabId: string | null };
|
||||
frustrations: Record<string, any>[] & { tabId: string | null };
|
||||
stack: Record<string, any>[] & { tabId: string | null };
|
||||
exceptions: ILog[];
|
||||
}
|
||||
|
||||
export interface State extends ScreenState {
|
||||
skipIntervals: SkipInterval[];
|
||||
connType?: string;
|
||||
connBandwidth?: number;
|
||||
location?: string;
|
||||
tabStates: {
|
||||
[tabId: string]: TabState;
|
||||
};
|
||||
|
||||
const visualChanges = [
|
||||
domContentLoadedTime?: { time: number; value: number };
|
||||
domBuildingTime?: number;
|
||||
loadTime?: { time: number; value: number };
|
||||
error: boolean;
|
||||
messagesLoading: boolean;
|
||||
|
||||
ready: boolean;
|
||||
lastMessageTime: number;
|
||||
firstVisualEvent: number;
|
||||
messagesProcessed: boolean;
|
||||
currentTab: string;
|
||||
tabs: string[];
|
||||
tabChangeEvents: { tabId: string; timestamp: number; tabName: string }[];
|
||||
}
|
||||
|
||||
export const visualChanges = [
|
||||
MType.MouseMove,
|
||||
MType.MouseClick,
|
||||
MType.CreateElementNode,
|
||||
|
|
@ -73,258 +61,208 @@ const visualChanges = [
|
|||
MType.SetInputChecked,
|
||||
MType.SetViewportSize,
|
||||
MType.SetViewportScroll,
|
||||
]
|
||||
];
|
||||
|
||||
export default class MessageManager {
|
||||
static INITIAL_STATE: State = {
|
||||
...SCREEN_INITIAL_STATE,
|
||||
...LISTS_INITIAL_STATE,
|
||||
performanceChartData: [],
|
||||
tabStates: {},
|
||||
skipIntervals: [],
|
||||
error: false,
|
||||
cssLoading: false,
|
||||
ready: false,
|
||||
lastMessageTime: 0,
|
||||
firstVisualEvent: 0,
|
||||
messagesProcessed: false,
|
||||
messagesLoading: false,
|
||||
}
|
||||
currentTab: '',
|
||||
tabs: [],
|
||||
tabChangeEvents: [],
|
||||
};
|
||||
|
||||
private locationEventManager: ListWalker<any>/*<LocationEvent>*/ = new ListWalker();
|
||||
private locationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
private loadedLocationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
private connectionInfoManger: ListWalker<ConnectionInformation> = new ListWalker();
|
||||
private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager();
|
||||
private windowNodeCounter: WindowNodeCounter = new WindowNodeCounter();
|
||||
private clickManager: ListWalker<MouseClick> = new ListWalker();
|
||||
private mouseThrashingManager: ListWalker<MouseThrashing> = new ListWalker();
|
||||
|
||||
private resizeManager: ListWalker<SetViewportSize> = new ListWalker([]);
|
||||
private pagesManager: PagesManager;
|
||||
private activityManager: ActivityManager | null = null;
|
||||
private mouseMoveManager: MouseMoveManager;
|
||||
|
||||
private scrollManager: ListWalker<SetViewportScroll> = new ListWalker();
|
||||
private activeTabManager = new ActiveTabManager();
|
||||
|
||||
public readonly decoder = new Decoder();
|
||||
private lists: Lists;
|
||||
|
||||
private activityManager: ActivityManager | null = null;
|
||||
|
||||
private readonly sessionStart: number;
|
||||
private navigationStartOffset: number = 0;
|
||||
private lastMessageTime: number = 0;
|
||||
private firstVisualEventSet = false;
|
||||
public readonly tabs: Record<string, TabSessionManager> = {};
|
||||
private tabChangeEvents: Record<string, number>[] = [];
|
||||
private activeTab = '';
|
||||
|
||||
constructor(
|
||||
private readonly session: any /*Session*/,
|
||||
private readonly state: Store<State>,
|
||||
private readonly session: Record<string, any>,
|
||||
private readonly state: Store<State & { time: number }>,
|
||||
private readonly screen: Screen,
|
||||
initialLists?: Partial<InitialLists>,
|
||||
private readonly uiErrorHandler?: { error: (error: string) => void, },
|
||||
private readonly initialLists?: Partial<InitialLists>,
|
||||
private readonly uiErrorHandler?: { error: (error: string) => void }
|
||||
) {
|
||||
this.pagesManager = new PagesManager(screen, this.session.isMobile, this.setCSSLoading)
|
||||
this.mouseMoveManager = new MouseMoveManager(screen)
|
||||
|
||||
this.sessionStart = this.session.startedAt
|
||||
|
||||
this.lists = new Lists(initialLists)
|
||||
initialLists?.event?.forEach((e: Record<string, string>) => { // TODO: to one of "Movable" module
|
||||
if (e.type === EVENT_TYPES.LOCATION) {
|
||||
this.locationEventManager.append(e);
|
||||
}
|
||||
})
|
||||
|
||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live
|
||||
this.mouseMoveManager = new MouseMoveManager(screen);
|
||||
this.sessionStart = this.session.startedAt;
|
||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds); // only if not-live
|
||||
}
|
||||
|
||||
public getListsFullState = () => {
|
||||
return this.lists.getFullListsState()
|
||||
const fullState: Record<string, any> = {};
|
||||
for (let tab in Object.keys(this.tabs)) {
|
||||
fullState[tab] = this.tabs[tab].getListsFullState();
|
||||
}
|
||||
return Object.values(this.tabs)[0].getListsFullState();
|
||||
};
|
||||
|
||||
public updateLists(lists: Partial<InitialLists>) {
|
||||
Object.keys(lists).forEach((key: 'event' | 'stack' | 'exceptions') => {
|
||||
const currentList = this.lists.lists[key]
|
||||
lists[key]!.forEach(item => currentList.insert(item))
|
||||
public updateLists(lists: RawList) {
|
||||
Object.keys(this.tabs).forEach((tab) => {
|
||||
this.tabs[tab]!.updateLists(lists);
|
||||
// once upon a time we wanted to insert events for each tab individually
|
||||
// but then evil magician came and said "no, you don't want to do that"
|
||||
// because it was bad for database size
|
||||
// const list = {
|
||||
// event: lists.event.filter((e) => e.tabId === tab),
|
||||
// frustrations: lists.frustrations.filter((e) => e.tabId === tab),
|
||||
// stack: lists.stack.filter((e) => e.tabId === tab),
|
||||
// exceptions: lists.exceptions.filter((e) => e.tabId === tab),
|
||||
// };
|
||||
// // saving some microseconds here probably
|
||||
// if (Object.values(list).some((l) => l.length > 0)) {
|
||||
// this.tabs[tab]!.updateLists(list);
|
||||
// }
|
||||
})
|
||||
lists?.event?.forEach((e: Record<string, string>) => {
|
||||
if (e.type === EVENT_TYPES.LOCATION) {
|
||||
this.locationEventManager.append(e);
|
||||
}
|
||||
})
|
||||
|
||||
this.state.update({ ...this.lists.getFullListsState() });
|
||||
}
|
||||
|
||||
private setCSSLoading = (cssLoading: boolean) => {
|
||||
this.screen.displayFrame(!cssLoading)
|
||||
this.state.update({ cssLoading, ready: !this.state.get().messagesLoading && !cssLoading })
|
||||
}
|
||||
|
||||
public _sortMessagesHack = (msgs: Message[]) => {
|
||||
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
|
||||
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
|
||||
this.pagesManager.sortPages((m1, m2) => {
|
||||
if (m1.time === m2.time) {
|
||||
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
|
||||
if (headChildrenIds.includes(m1.id)) {
|
||||
return -1;
|
||||
}
|
||||
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
|
||||
if (headChildrenIds.includes(m2.id)) {
|
||||
return 1;
|
||||
}
|
||||
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
|
||||
const m1FromHead = headChildrenIds.includes(m1.id);
|
||||
const m2FromHead = headChildrenIds.includes(m2.id);
|
||||
if (m1FromHead && !m2FromHead) {
|
||||
return -1;
|
||||
} else if (m2FromHead && !m1FromHead) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
}
|
||||
Object.values(this.tabs).forEach((tab) => tab._sortMessagesHack(msgs));
|
||||
};
|
||||
|
||||
private waitingForFiles: boolean = false
|
||||
private waitingForFiles: boolean = false;
|
||||
public onFileReadSuccess = () => {
|
||||
const stateToUpdate : Partial<State>= {
|
||||
performanceChartData: this.performanceTrackManager.chartData,
|
||||
performanceAvailability: this.performanceTrackManager.availability,
|
||||
...this.lists.getFullListsState(),
|
||||
}
|
||||
if (this.activityManager) {
|
||||
this.activityManager.end()
|
||||
stateToUpdate.skipIntervals = this.activityManager.list
|
||||
}
|
||||
this.state.update(stateToUpdate)
|
||||
this.activityManager.end();
|
||||
this.state.update({ skipIntervals: this.activityManager.list });
|
||||
}
|
||||
Object.values(this.tabs).forEach((tab) => tab.onFileReadSuccess?.());
|
||||
};
|
||||
|
||||
public onFileReadFailed = (e: any) => {
|
||||
logger.error(e)
|
||||
this.state.update({ error: true })
|
||||
this.uiErrorHandler?.error('Error requesting a session file')
|
||||
}
|
||||
logger.error(e);
|
||||
this.state.update({ error: true });
|
||||
this.uiErrorHandler?.error('Error requesting a session file');
|
||||
};
|
||||
|
||||
public onFileReadFinally = () => {
|
||||
this.waitingForFiles = false
|
||||
this.state.update({ messagesProcessed: true })
|
||||
}
|
||||
this.waitingForFiles = false;
|
||||
this.state.update({ messagesProcessed: true });
|
||||
};
|
||||
|
||||
public startLoading = () => {
|
||||
this.waitingForFiles = true
|
||||
this.state.update({ messagesProcessed: false })
|
||||
this.setMessagesLoading(true)
|
||||
}
|
||||
this.waitingForFiles = true;
|
||||
this.state.update({ messagesProcessed: false });
|
||||
this.setMessagesLoading(true);
|
||||
};
|
||||
|
||||
resetMessageManagers() {
|
||||
this.locationEventManager = new ListWalker();
|
||||
this.locationManager = new ListWalker();
|
||||
this.loadedLocationManager = new ListWalker();
|
||||
this.connectionInfoManger = new ListWalker();
|
||||
this.clickManager = new ListWalker();
|
||||
this.scrollManager = new ListWalker();
|
||||
this.resizeManager = new ListWalker();
|
||||
|
||||
this.performanceTrackManager = new PerformanceTrackManager()
|
||||
this.windowNodeCounter = new WindowNodeCounter();
|
||||
this.pagesManager = new PagesManager(this.screen, this.session.isMobile, this.setCSSLoading)
|
||||
this.mouseMoveManager = new MouseMoveManager(this.screen);
|
||||
this.activityManager = new ActivityManager(this.session.duration.milliseconds);
|
||||
this.activeTabManager = new ActiveTabManager();
|
||||
|
||||
Object.values(this.tabs).forEach((tab) => tab.resetMessageManagers());
|
||||
}
|
||||
|
||||
move(t: number, index?: number): void {
|
||||
const stateToUpdate: Partial<State> = {};
|
||||
/* == REFACTOR_ME == */
|
||||
const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index);
|
||||
if (!!lastLoadedLocationMsg) {
|
||||
// TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time)
|
||||
this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart;
|
||||
}
|
||||
const llEvent = this.locationEventManager.moveGetLast(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 & SetPageLocation: add navigationStart to db)
|
||||
value: llEvent.domContentLoadedTime,
|
||||
}
|
||||
}
|
||||
if (llEvent.loadTime != null) {
|
||||
stateToUpdate.loadTime = {
|
||||
time: llEvent.loadTime + this.navigationStartOffset,
|
||||
value: llEvent.loadTime,
|
||||
}
|
||||
}
|
||||
if (llEvent.domBuildingTime != null) {
|
||||
stateToUpdate.domBuildingTime = llEvent.domBuildingTime;
|
||||
}
|
||||
}
|
||||
/* === */
|
||||
const lastLocationMsg = this.locationManager.moveGetLast(t, index);
|
||||
if (!!lastLocationMsg) {
|
||||
stateToUpdate.location = lastLocationMsg.url;
|
||||
}
|
||||
const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index);
|
||||
if (!!lastConnectionInfoMsg) {
|
||||
stateToUpdate.connType = lastConnectionInfoMsg.type;
|
||||
stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink;
|
||||
}
|
||||
const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index);
|
||||
if (!!lastPerformanceTrackMessage) {
|
||||
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
|
||||
}
|
||||
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t))
|
||||
Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate);
|
||||
|
||||
/* Sequence of the managers is important here */
|
||||
// Preparing the size of "screen"
|
||||
const lastResize = this.resizeManager.moveGetLast(t, index);
|
||||
if (!!lastResize) {
|
||||
this.setSize(lastResize)
|
||||
}
|
||||
this.pagesManager.moveReady(t).then(() => {
|
||||
|
||||
const lastScroll = this.scrollManager.moveGetLast(t, index);
|
||||
if (!!lastScroll && this.screen.window) {
|
||||
this.screen.window.scrollTo(lastScroll.x, lastScroll.y);
|
||||
}
|
||||
move(t: number): any {
|
||||
// usually means waiting for messages from live session
|
||||
if (Object.keys(this.tabs).length === 0) return;
|
||||
this.activeTabManager.moveReady(t).then((tabId) => {
|
||||
// Moving mouse and setting :hover classes on ready view
|
||||
this.mouseMoveManager.move(t);
|
||||
const lastClick = this.clickManager.moveGetLast(t);
|
||||
if (!!lastClick && t - lastClick.time < 600) { // happened during last 600ms
|
||||
if (!!lastClick && t - lastClick.time < 600) {
|
||||
// happened during last 600ms
|
||||
this.screen.cursor.click();
|
||||
}
|
||||
const lastThrashing = this.mouseThrashingManager.moveGetLast(t)
|
||||
const lastThrashing = this.mouseThrashingManager.moveGetLast(t);
|
||||
if (!!lastThrashing && t - lastThrashing.time < 300) {
|
||||
this.screen.cursor.shake();
|
||||
}
|
||||
})
|
||||
|
||||
if (this.waitingForFiles && this.lastMessageTime <= t && t !== this.session.duration.milliseconds) {
|
||||
this.setMessagesLoading(true)
|
||||
const activeTabs = this.state.get().tabs;
|
||||
if (tabId && !activeTabs.includes(tabId)) {
|
||||
this.state.update({ tabs: activeTabs.concat(tabId) });
|
||||
}
|
||||
|
||||
if (tabId && this.activeTab !== tabId) {
|
||||
this.state.update({ currentTab: tabId });
|
||||
this.activeTab = tabId;
|
||||
}
|
||||
|
||||
if (this.tabs[this.activeTab]) {
|
||||
this.tabs[this.activeTab].move(t);
|
||||
} else {
|
||||
console.error(
|
||||
'missing tab state',
|
||||
this.tabs,
|
||||
this.activeTab,
|
||||
tabId,
|
||||
this.activeTabManager.list
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
this.waitingForFiles &&
|
||||
this.lastMessageTime <= t &&
|
||||
t !== this.session.duration.milliseconds
|
||||
) {
|
||||
this.setMessagesLoading(true);
|
||||
}
|
||||
}
|
||||
|
||||
public changeTab(tabId: string) {
|
||||
this.activeTab = tabId;
|
||||
this.state.update({ currentTab: tabId });
|
||||
this.tabs[tabId].move(this.state.get().time);
|
||||
}
|
||||
|
||||
distributeMessage = (msg: Message): void => {
|
||||
const lastMessageTime = Math.max(msg.time, this.lastMessageTime)
|
||||
this.lastMessageTime = lastMessageTime
|
||||
this.state.update({ lastMessageTime })
|
||||
public updateChangeEvents() {
|
||||
this.state.update({ tabChangeEvents: this.tabChangeEvents });
|
||||
}
|
||||
|
||||
distributeMessage = (msg: Message & { tabId: string }): void => {
|
||||
if (!this.tabs[msg.tabId]) {
|
||||
this.tabs[msg.tabId] = new TabSessionManager(
|
||||
this.session,
|
||||
this.state,
|
||||
this.screen,
|
||||
msg.tabId,
|
||||
this.setSize,
|
||||
this.sessionStart,
|
||||
this.initialLists
|
||||
);
|
||||
}
|
||||
|
||||
const lastMessageTime = Math.max(msg.time, this.lastMessageTime);
|
||||
this.lastMessageTime = lastMessageTime;
|
||||
this.state.update({ lastMessageTime });
|
||||
if (visualChanges.includes(msg.tp)) {
|
||||
this.activityManager?.updateAcctivity(msg.time);
|
||||
}
|
||||
switch (msg.tp) {
|
||||
case MType.SetPageLocation:
|
||||
this.locationManager.append(msg);
|
||||
if (msg.navigationStart > 0) {
|
||||
this.loadedLocationManager.append(msg);
|
||||
case MType.TabChange:
|
||||
const prevChange = this.activeTabManager.last;
|
||||
if (!prevChange || prevChange.tabId !== msg.tabId) {
|
||||
this.tabChangeEvents.push({
|
||||
tabId: msg.tabId,
|
||||
timestamp: this.sessionStart + msg.time,
|
||||
toTab: mapTabs(this.tabs)[msg.tabId],
|
||||
fromTab: prevChange?.tabId ? mapTabs(this.tabs)[prevChange.tabId] : '',
|
||||
type: 'TABCHANGE',
|
||||
});
|
||||
this.activeTabManager.append(msg);
|
||||
}
|
||||
break;
|
||||
case MType.SetViewportSize:
|
||||
this.resizeManager.append(msg);
|
||||
break;
|
||||
case MType.MouseThrashing:
|
||||
this.mouseThrashingManager.append(msg);
|
||||
break;
|
||||
|
|
@ -334,103 +272,37 @@ export default class MessageManager {
|
|||
case MType.MouseClick:
|
||||
this.clickManager.append(msg);
|
||||
break;
|
||||
case MType.SetViewportScroll:
|
||||
this.scrollManager.append(msg);
|
||||
break;
|
||||
case MType.PerformanceTrack:
|
||||
this.performanceTrackManager.append(msg);
|
||||
break;
|
||||
case MType.SetPageVisibility:
|
||||
this.performanceTrackManager.handleVisibility(msg)
|
||||
break;
|
||||
case MType.ConnectionInformation:
|
||||
this.connectionInfoManger.append(msg);
|
||||
break;
|
||||
case MType.OTable:
|
||||
this.decoder.set(msg.key, msg.value);
|
||||
break;
|
||||
/* Lists: */
|
||||
case MType.ConsoleLog:
|
||||
if (msg.level === 'debug') break;
|
||||
this.lists.lists.log.append(
|
||||
// @ts-ignore : TODO: enums in the message schema
|
||||
Log(msg)
|
||||
)
|
||||
break;
|
||||
case MType.ResourceTimingDeprecated:
|
||||
case MType.ResourceTiming:
|
||||
// TODO: merge `resource` and `fetch` lists into one here instead of UI
|
||||
if (msg.initiator !== ResourceType.FETCH && msg.initiator !== ResourceType.XHR) {
|
||||
// @ts-ignore TODO: typing for lists
|
||||
this.lists.lists.resource.insert(getResourceFromResourceTiming(msg, this.sessionStart))
|
||||
}
|
||||
break;
|
||||
case MType.Fetch:
|
||||
case MType.NetworkRequest:
|
||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
|
||||
break;
|
||||
case MType.Redux:
|
||||
this.lists.lists.redux.append(msg);
|
||||
break;
|
||||
case MType.NgRx:
|
||||
this.lists.lists.ngrx.append(msg);
|
||||
break;
|
||||
case MType.Vuex:
|
||||
this.lists.lists.vuex.append(msg);
|
||||
break;
|
||||
case MType.Zustand:
|
||||
this.lists.lists.zustand.append(msg)
|
||||
break
|
||||
case MType.MobX:
|
||||
this.lists.lists.mobx.append(msg);
|
||||
break;
|
||||
case MType.GraphQl:
|
||||
this.lists.lists.graphql.append(msg);
|
||||
break;
|
||||
case MType.Profiler:
|
||||
this.lists.lists.profiles.append(msg);
|
||||
break;
|
||||
/* ===|=== */
|
||||
default:
|
||||
switch (msg.tp) {
|
||||
case MType.CreateDocument:
|
||||
if (!this.firstVisualEventSet) {
|
||||
this.state.update({ firstVisualEvent: msg.time });
|
||||
this.activeTabManager.append({ tp: MType.TabChange, tabId: msg.tabId, time: 0 });
|
||||
this.state.update({
|
||||
firstVisualEvent: msg.time,
|
||||
currentTab: msg.tabId,
|
||||
tabs: [msg.tabId],
|
||||
});
|
||||
this.firstVisualEventSet = true;
|
||||
}
|
||||
this.windowNodeCounter.reset();
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
case MType.CreateTextNode:
|
||||
case MType.CreateElementNode:
|
||||
this.windowNodeCounter.addNode(msg.id, msg.parentID);
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
case MType.MoveNode:
|
||||
this.windowNodeCounter.moveNode(msg.id, msg.parentID);
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
case MType.RemoveNode:
|
||||
this.windowNodeCounter.removeNode(msg.id);
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
}
|
||||
this.tabs[msg.tabId].distributeMessage(msg);
|
||||
break;
|
||||
}
|
||||
this.performanceTrackManager.addNodeCountPointIfNeed(msg.time)
|
||||
isDOMType(msg.tp) && this.pagesManager.appendMessage(msg)
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setMessagesLoading = (messagesLoading: boolean) => {
|
||||
if (!messagesLoading) {
|
||||
this.updateChangeEvents();
|
||||
}
|
||||
this.screen.display(!messagesLoading);
|
||||
this.state.update({ messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading });
|
||||
}
|
||||
};
|
||||
|
||||
decodeMessage(msg: Message) {
|
||||
return this.decoder.decode(msg)
|
||||
return this.tabs[this.activeTab].decodeMessage(msg);
|
||||
}
|
||||
|
||||
private setSize({ height, width }: { height: number, width: number }) {
|
||||
private setSize({ height, width }: { height: number; width: number }) {
|
||||
this.screen.scale({ height, width });
|
||||
this.state.update({ width, height });
|
||||
}
|
||||
|
|
@ -438,8 +310,15 @@ export default class MessageManager {
|
|||
// TODO: clean managers?
|
||||
clean() {
|
||||
this.state.update(MessageManager.INITIAL_STATE);
|
||||
// @ts-ignore
|
||||
this.pagesManager.reset();
|
||||
}
|
||||
}
|
||||
|
||||
function mapTabs(tabs: Record<string, TabState>) {
|
||||
const tabIds = Object.keys(tabs);
|
||||
const tabMap = {};
|
||||
tabIds.forEach((tabId) => {
|
||||
tabMap[tabId] = `Tab ${tabIds.indexOf(tabId)+1}`;
|
||||
});
|
||||
|
||||
return tabMap;
|
||||
}
|
||||
|
|
|
|||
327
frontend/app/player/web/TabManager.ts
Normal file
327
frontend/app/player/web/TabManager.ts
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import ListWalker from "Player/common/ListWalker";
|
||||
import {
|
||||
ConnectionInformation,
|
||||
Message, MType, ResourceTiming,
|
||||
SetPageLocation,
|
||||
SetViewportScroll,
|
||||
SetViewportSize
|
||||
} from "Player/web/messages";
|
||||
import PerformanceTrackManager from "Player/web/managers/PerformanceTrackManager";
|
||||
import WindowNodeCounter from "Player/web/managers/WindowNodeCounter";
|
||||
import PagesManager from "Player/web/managers/PagesManager";
|
||||
// @ts-ignore
|
||||
import { Decoder } from "syncod";
|
||||
import Lists, { InitialLists, INITIAL_STATE as LISTS_INITIAL_STATE, State as ListsState } from "Player/web/Lists";
|
||||
import type { Store } from 'Player';
|
||||
import Screen from "Player/web/Screen/Screen";
|
||||
import { TYPES as EVENT_TYPES } from "Types/session/event";
|
||||
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
|
||||
import { getResourceFromNetworkRequest, getResourceFromResourceTiming, Log, ResourceType } from "Player";
|
||||
import { isDOMType } from "Player/web/messages/filters.gen";
|
||||
|
||||
export interface TabState extends ListsState {
|
||||
performanceAvailability?: PerformanceTrackManager['availability']
|
||||
performanceChartData: PerformanceChartPoint[],
|
||||
performanceChartTime: PerformanceChartPoint[]
|
||||
cssLoading: boolean
|
||||
location: string
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT DELETE UNUSED METHODS
|
||||
* THEY'RE ALL USED IN MESSAGE MANAGER VIA this.tabs[id]
|
||||
* */
|
||||
|
||||
export default class TabSessionManager {
|
||||
static INITIAL_STATE: TabState = {
|
||||
...LISTS_INITIAL_STATE,
|
||||
performanceChartData: [],
|
||||
performanceChartTime: [],
|
||||
cssLoading: false,
|
||||
location: '',
|
||||
}
|
||||
|
||||
private locationEventManager: ListWalker<any>/*<LocationEvent>*/ = new ListWalker();
|
||||
private locationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
private loadedLocationManager: ListWalker<SetPageLocation> = new ListWalker();
|
||||
private connectionInfoManger: ListWalker<ConnectionInformation> = new ListWalker();
|
||||
private performanceTrackManager: PerformanceTrackManager = new PerformanceTrackManager();
|
||||
private windowNodeCounter: WindowNodeCounter = new WindowNodeCounter();
|
||||
|
||||
private resizeManager: ListWalker<SetViewportSize> = new ListWalker([]);
|
||||
private pagesManager: PagesManager;
|
||||
private scrollManager: ListWalker<SetViewportScroll> = new ListWalker();
|
||||
|
||||
public readonly decoder = new Decoder();
|
||||
private lists: Lists;
|
||||
private navigationStartOffset = 0
|
||||
|
||||
constructor(
|
||||
private readonly session: any,
|
||||
private readonly state: Store<{ tabStates: { [tabId: string]: TabState } }>,
|
||||
private readonly screen: Screen,
|
||||
private readonly id: string,
|
||||
private readonly setSize: ({ height, width }: { height: number, width: number }) => void,
|
||||
private readonly sessionStart: number,
|
||||
initialLists?: Partial<InitialLists>,
|
||||
) {
|
||||
this.pagesManager = new PagesManager(screen, this.session.isMobile, this.setCSSLoading)
|
||||
this.lists = new Lists(initialLists)
|
||||
initialLists?.event?.forEach((e: Record<string, string>) => { // TODO: to one of "Movable" module
|
||||
if (e.type === EVENT_TYPES.LOCATION) {
|
||||
this.locationEventManager.append(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public updateLists(lists: Partial<InitialLists>) {
|
||||
Object.keys(lists).forEach((key: 'event' | 'stack' | 'exceptions') => {
|
||||
const currentList = this.lists.lists[key]
|
||||
lists[key]!.forEach(item => currentList.insert(item))
|
||||
})
|
||||
lists?.event?.forEach((e: Record<string, string>) => {
|
||||
if (e.type === EVENT_TYPES.LOCATION) {
|
||||
this.locationEventManager.append(e);
|
||||
}
|
||||
})
|
||||
|
||||
this.updateLocalState({ ...this.lists.getFullListsState() });
|
||||
}
|
||||
|
||||
updateLocalState(state: Partial<TabState>) {
|
||||
this.state.update({
|
||||
tabStates: {
|
||||
...this.state.get().tabStates,
|
||||
[this.id]: {
|
||||
...this.state.get().tabStates[this.id],
|
||||
...state
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private setCSSLoading = (cssLoading: boolean) => {
|
||||
this.screen.displayFrame(!cssLoading)
|
||||
this.updateLocalState({
|
||||
cssLoading
|
||||
})
|
||||
this.state.update({
|
||||
// @ts-ignore
|
||||
ready: !this.state.get().messagesLoading && !cssLoading
|
||||
})
|
||||
}
|
||||
|
||||
public resetMessageManagers() {
|
||||
this.locationEventManager = new ListWalker();
|
||||
this.locationManager = new ListWalker();
|
||||
this.loadedLocationManager = new ListWalker();
|
||||
this.connectionInfoManger = new ListWalker();
|
||||
this.scrollManager = new ListWalker();
|
||||
this.resizeManager = new ListWalker();
|
||||
|
||||
this.performanceTrackManager = new PerformanceTrackManager()
|
||||
this.windowNodeCounter = new WindowNodeCounter();
|
||||
this.pagesManager = new PagesManager(this.screen, this.session.isMobile, this.setCSSLoading)
|
||||
}
|
||||
|
||||
|
||||
distributeMessage(msg: Message): void {
|
||||
switch (msg.tp) {
|
||||
case MType.SetPageLocation:
|
||||
this.locationManager.append(msg);
|
||||
if (msg.navigationStart > 0) {
|
||||
this.loadedLocationManager.append(msg);
|
||||
}
|
||||
break;
|
||||
case MType.SetViewportSize:
|
||||
this.resizeManager.append(msg);
|
||||
break;
|
||||
case MType.SetViewportScroll:
|
||||
this.scrollManager.append(msg);
|
||||
break;
|
||||
case MType.PerformanceTrack:
|
||||
this.performanceTrackManager.append(msg);
|
||||
break;
|
||||
case MType.SetPageVisibility:
|
||||
this.performanceTrackManager.handleVisibility(msg)
|
||||
break;
|
||||
case MType.ConnectionInformation:
|
||||
this.connectionInfoManger.append(msg);
|
||||
break;
|
||||
case MType.OTable:
|
||||
this.decoder.set(msg.key, msg.value);
|
||||
break;
|
||||
/* Lists: */
|
||||
case MType.ConsoleLog:
|
||||
if (msg.level === 'debug') break;
|
||||
this.lists.lists.log.append(
|
||||
// @ts-ignore : TODO: enums in the message schema
|
||||
Log(msg)
|
||||
)
|
||||
break;
|
||||
case MType.ResourceTimingDeprecated:
|
||||
case MType.ResourceTiming:
|
||||
// TODO: merge `resource` and `fetch` lists into one here instead of UI
|
||||
if (msg.initiator !== ResourceType.FETCH && msg.initiator !== ResourceType.XHR) {
|
||||
this.lists.lists.resource.insert(getResourceFromResourceTiming(msg as ResourceTiming, this.sessionStart))
|
||||
}
|
||||
break;
|
||||
case MType.Fetch:
|
||||
case MType.NetworkRequest:
|
||||
this.lists.lists.fetch.insert(getResourceFromNetworkRequest(msg, this.sessionStart))
|
||||
break;
|
||||
case MType.Redux:
|
||||
this.lists.lists.redux.append(msg);
|
||||
break;
|
||||
case MType.NgRx:
|
||||
this.lists.lists.ngrx.append(msg);
|
||||
break;
|
||||
case MType.Vuex:
|
||||
this.lists.lists.vuex.append(msg);
|
||||
break;
|
||||
case MType.Zustand:
|
||||
this.lists.lists.zustand.append(msg)
|
||||
break
|
||||
case MType.MobX:
|
||||
this.lists.lists.mobx.append(msg);
|
||||
break;
|
||||
case MType.GraphQl:
|
||||
this.lists.lists.graphql.append(msg);
|
||||
break;
|
||||
case MType.Profiler:
|
||||
this.lists.lists.profiles.append(msg);
|
||||
break;
|
||||
/* ===|=== */
|
||||
default:
|
||||
switch (msg.tp) {
|
||||
case MType.CreateDocument:
|
||||
this.windowNodeCounter.reset();
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
case MType.CreateTextNode:
|
||||
case MType.CreateElementNode:
|
||||
this.windowNodeCounter.addNode(msg.id, msg.parentID);
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
case MType.MoveNode:
|
||||
this.windowNodeCounter.moveNode(msg.id, msg.parentID);
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
case MType.RemoveNode:
|
||||
this.windowNodeCounter.removeNode(msg.id);
|
||||
this.performanceTrackManager.setCurrentNodesCount(this.windowNodeCounter.count);
|
||||
break;
|
||||
}
|
||||
this.performanceTrackManager.addNodeCountPointIfNeed(msg.time)
|
||||
isDOMType(msg.tp) && this.pagesManager.appendMessage(msg)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
move(t: number, index?: number): void {
|
||||
const stateToUpdate: Record<string, any> = {};
|
||||
/* == REFACTOR_ME == */
|
||||
const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index);
|
||||
if (!!lastLoadedLocationMsg) {
|
||||
// TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time)
|
||||
this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart;
|
||||
}
|
||||
const llEvent = this.locationEventManager.moveGetLast(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 & SetPageLocation: add navigationStart to db)
|
||||
value: llEvent.domContentLoadedTime,
|
||||
}
|
||||
}
|
||||
if (llEvent.loadTime != null) {
|
||||
stateToUpdate.loadTime = {
|
||||
time: llEvent.loadTime + this.navigationStartOffset,
|
||||
value: llEvent.loadTime,
|
||||
}
|
||||
}
|
||||
if (llEvent.domBuildingTime != null) {
|
||||
stateToUpdate.domBuildingTime = llEvent.domBuildingTime;
|
||||
}
|
||||
}
|
||||
/* === */
|
||||
const lastLocationMsg = this.locationManager.moveGetLast(t, index);
|
||||
if (!!lastLocationMsg) {
|
||||
stateToUpdate.location = lastLocationMsg.url;
|
||||
}
|
||||
const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index);
|
||||
if (!!lastConnectionInfoMsg) {
|
||||
stateToUpdate.connType = lastConnectionInfoMsg.type;
|
||||
stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink;
|
||||
}
|
||||
const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index);
|
||||
if (!!lastPerformanceTrackMessage) {
|
||||
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
|
||||
}
|
||||
|
||||
Object.assign(stateToUpdate, this.lists.moveGetState(t))
|
||||
Object.keys(stateToUpdate).length > 0 && this.updateLocalState(stateToUpdate);
|
||||
|
||||
/* Sequence of the managers is important here */
|
||||
// Preparing the size of "screen"
|
||||
const lastResize = this.resizeManager.moveGetLast(t, index);
|
||||
if (!!lastResize) {
|
||||
this.setSize(lastResize)
|
||||
}
|
||||
this.pagesManager.moveReady(t).then(() => {
|
||||
const lastScroll = this.scrollManager.moveGetLast(t, index);
|
||||
if (!!lastScroll && this.screen.window) {
|
||||
this.screen.window.scrollTo(lastScroll.x, lastScroll.y);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public decodeMessage(msg: Message) {
|
||||
return this.decoder.decode(msg)
|
||||
}
|
||||
|
||||
public _sortMessagesHack = (msgs: Message[]) => {
|
||||
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
|
||||
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
|
||||
this.pagesManager.sortPages((m1, m2) => {
|
||||
if (m1.time === m2.time) {
|
||||
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
|
||||
if (headChildrenIds.includes(m1.id)) {
|
||||
return -1;
|
||||
}
|
||||
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
|
||||
if (headChildrenIds.includes(m2.id)) {
|
||||
return 1;
|
||||
}
|
||||
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
|
||||
const m1FromHead = headChildrenIds.includes(m1.id);
|
||||
const m2FromHead = headChildrenIds.includes(m2.id);
|
||||
if (m1FromHead && !m2FromHead) {
|
||||
return -1;
|
||||
} else if (m2FromHead && !m1FromHead) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
}
|
||||
|
||||
public onFileReadSuccess = () => {
|
||||
const stateToUpdate : Partial<Record<string,any>> = {
|
||||
performanceChartData: this.performanceTrackManager.chartData,
|
||||
performanceAvailability: this.performanceTrackManager.availability,
|
||||
...this.lists.getFullListsState(),
|
||||
}
|
||||
|
||||
this.updateLocalState(stateToUpdate)
|
||||
}
|
||||
|
||||
public getListsFullState = () => {
|
||||
return this.lists.getFullListsState()
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.pagesManager.reset()
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,8 @@ export default class WebPlayer extends Player {
|
|||
session,
|
||||
wpState,
|
||||
messageManager,
|
||||
isClickMap
|
||||
isClickMap,
|
||||
uiErrorHandler
|
||||
)
|
||||
super(wpState, messageManager)
|
||||
this.screen = screen
|
||||
|
|
@ -82,7 +83,7 @@ export default class WebPlayer extends Player {
|
|||
}
|
||||
|
||||
updateLists = (session: any) => {
|
||||
let lists = {
|
||||
const lists = {
|
||||
event: session.events || [],
|
||||
frustrations: session.frustrations || [],
|
||||
stack: session.stackEvents || [],
|
||||
|
|
@ -162,6 +163,10 @@ export default class WebPlayer extends Player {
|
|||
this.screen.cursor.showTag(name)
|
||||
}
|
||||
|
||||
changeTab = (tab: string) => {
|
||||
this.messageManager.changeTab(tab)
|
||||
}
|
||||
|
||||
clean = () => {
|
||||
super.clean()
|
||||
this.screen.clean()
|
||||
|
|
|
|||
|
|
@ -166,9 +166,10 @@ export default class AssistManager {
|
|||
waitingForMessages = true
|
||||
this.setStatus(ConnectionStatus.WaitingMessages) // TODO: reconnect happens frequently on bad network
|
||||
})
|
||||
socket.on('messages', messages => {
|
||||
jmr.append(messages) // as RawMessage[]
|
||||
|
||||
let currentTab = ''
|
||||
socket.on('messages', messages => {
|
||||
jmr.append(messages.data) // as RawMessage[]
|
||||
if (waitingForMessages) {
|
||||
waitingForMessages = false // TODO: more explicit
|
||||
this.setStatus(ConnectionStatus.Connected)
|
||||
|
|
@ -185,9 +186,15 @@ export default class AssistManager {
|
|||
this.setStatus(ConnectionStatus.Connected)
|
||||
})
|
||||
|
||||
socket.on('UPDATE_SESSION', ({ active }) => {
|
||||
socket.on('UPDATE_SESSION', (evData) => {
|
||||
const { metadata = {}, data = {} } = evData
|
||||
const { tabId } = metadata
|
||||
const { active } = data
|
||||
this.clearDisconnectTimeout()
|
||||
!this.inactiveTimeout && this.setStatus(ConnectionStatus.Connected)
|
||||
if (tabId !== currentTab) {
|
||||
this.store.update({ currentTab: tabId })
|
||||
}
|
||||
if (typeof active === "boolean") {
|
||||
this.clearInactiveTimeout()
|
||||
if (active) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export enum CallingState {
|
|||
|
||||
export interface State {
|
||||
calling: CallingState;
|
||||
currentTab?: string;
|
||||
}
|
||||
|
||||
export default class Call {
|
||||
|
|
@ -158,7 +159,7 @@ export default class Call {
|
|||
}
|
||||
|
||||
initiateCallEnd = async () => {
|
||||
this.socket?.emit("call_end", appStore.getState().getIn([ 'user', 'account', 'name']))
|
||||
this.emitData("call_end", appStore.getState().getIn([ 'user', 'account', 'name']))
|
||||
this.handleCallEnd()
|
||||
// TODO: We have it separated, right? (check)
|
||||
// const remoteControl = this.store.get().remoteControl
|
||||
|
|
@ -168,6 +169,10 @@ export default class Call {
|
|||
// }
|
||||
}
|
||||
|
||||
private emitData = (event: string, data?: any) => {
|
||||
this.socket?.emit(event, { meta: { tabId: this.store.get().currentTab }, data })
|
||||
}
|
||||
|
||||
|
||||
private callArgs: {
|
||||
localStream: LocalStream,
|
||||
|
|
@ -206,7 +211,7 @@ export default class Call {
|
|||
|
||||
toggleVideoLocalStream(enabled: boolean) {
|
||||
this.getPeer().then((peer) => {
|
||||
this.socket.emit('videofeed', { streamId: peer.id, enabled })
|
||||
this.emitData('videofeed', { streamId: peer.id, enabled })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +228,7 @@ export default class Call {
|
|||
if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) { return }
|
||||
this.store.update({ calling: CallingState.Connecting })
|
||||
this._peerConnection(this.peerID);
|
||||
this.socket.emit("_agent_name", appStore.getState().getIn([ 'user', 'account', 'name']))
|
||||
this.emitData("_agent_name", appStore.getState().getIn([ 'user', 'account', 'name']))
|
||||
}
|
||||
|
||||
private async _peerConnection(remotePeerId: string) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export enum RemoteControlStatus {
|
|||
export interface State {
|
||||
annotating: boolean
|
||||
remoteControl: RemoteControlStatus
|
||||
currentTab?: string
|
||||
}
|
||||
|
||||
export default class RemoteControl {
|
||||
|
|
@ -28,11 +29,11 @@ export default class RemoteControl {
|
|||
private agentInfo: Object,
|
||||
private onToggle: (active: boolean) => void,
|
||||
){
|
||||
socket.on("control_granted", id => {
|
||||
this.toggleRemoteControl(id === socket.id)
|
||||
socket.on("control_granted", ({ meta, data }) => {
|
||||
this.toggleRemoteControl(data === socket.id)
|
||||
})
|
||||
socket.on("control_rejected", id => {
|
||||
id === socket.id && this.toggleRemoteControl(false)
|
||||
socket.on("control_rejected", ({ meta, data }) => {
|
||||
data === socket.id && this.toggleRemoteControl(false)
|
||||
this.onReject()
|
||||
})
|
||||
socket.on('SESSION_DISCONNECTED', () => {
|
||||
|
|
@ -50,14 +51,18 @@ export default class RemoteControl {
|
|||
|
||||
private onMouseMove = (e: MouseEvent): void => {
|
||||
const data = this.screen.getInternalCoordinates(e)
|
||||
this.socket.emit("move", [ data.x, data.y ])
|
||||
this.emitData("move", [ data.x, data.y ])
|
||||
}
|
||||
|
||||
private emitData = (event: string, data?: any) => {
|
||||
this.socket.emit(event, { meta: { tabId: this.store.get().currentTab }, data })
|
||||
}
|
||||
|
||||
private onWheel = (e: WheelEvent): void => {
|
||||
e.preventDefault()
|
||||
//throttling makes movements less smooth, so it is omitted
|
||||
//this.onMouseMove(e)
|
||||
this.socket.emit("scroll", [ e.deltaX, e.deltaY ])
|
||||
this.emitData("scroll", [ e.deltaX, e.deltaY ])
|
||||
}
|
||||
|
||||
public setCallbacks = ({ onReject }: { onReject: () => void }) => {
|
||||
|
|
@ -76,9 +81,9 @@ export default class RemoteControl {
|
|||
if (el instanceof HTMLTextAreaElement
|
||||
|| el instanceof HTMLInputElement
|
||||
) {
|
||||
this.socket && this.socket.emit("input", el.value)
|
||||
this.socket && this.emitData("input", el.value)
|
||||
} else if (el.isContentEditable) {
|
||||
this.socket && this.socket.emit("input", el.innerText)
|
||||
this.socket && this.emitData("input", el.innerText)
|
||||
}
|
||||
}
|
||||
// TODO: send "focus" event to assist with the nodeID
|
||||
|
|
@ -92,7 +97,7 @@ export default class RemoteControl {
|
|||
el.onblur = null
|
||||
}
|
||||
}
|
||||
this.socket.emit("click", [ data.x, data.y ]);
|
||||
this.emitData("click", [ data.x, data.y ]);
|
||||
}
|
||||
|
||||
private toggleRemoteControl(enable: boolean){
|
||||
|
|
@ -116,7 +121,7 @@ export default class RemoteControl {
|
|||
if (remoteControl === RemoteControlStatus.Requesting) { return }
|
||||
if (remoteControl === RemoteControlStatus.Disabled) {
|
||||
this.store.update({ remoteControl: RemoteControlStatus.Requesting })
|
||||
this.socket.emit("request_control", JSON.stringify({
|
||||
this.emitData("request_control", JSON.stringify({
|
||||
...this.agentInfo,
|
||||
query: document.location.search
|
||||
}))
|
||||
|
|
@ -126,7 +131,7 @@ export default class RemoteControl {
|
|||
}
|
||||
|
||||
releaseRemoteControl = () => {
|
||||
this.socket.emit("release_control")
|
||||
this.emitData("release_control",)
|
||||
this.toggleRemoteControl(false)
|
||||
}
|
||||
|
||||
|
|
@ -134,30 +139,30 @@ export default class RemoteControl {
|
|||
|
||||
toggleAnnotation(enable?: boolean) {
|
||||
if (typeof enable !== "boolean") {
|
||||
enable = !!this.store.get().annotating
|
||||
enable = this.store.get().annotating
|
||||
}
|
||||
if (enable && !this.annot) {
|
||||
const annot = this.annot = new AnnotationCanvas()
|
||||
annot.mount(this.screen.overlay)
|
||||
annot.canvas.addEventListener("mousedown", e => {
|
||||
const data = this.screen.getInternalViewportCoordinates(e)
|
||||
const data = this.screen.getInternalViewportCoordin1ates(e)
|
||||
annot.start([ data.x, data.y ])
|
||||
this.socket.emit("startAnnotation", [ data.x, data.y ])
|
||||
this.emitData("startAnnotation", [ data.x, data.y ])
|
||||
})
|
||||
annot.canvas.addEventListener("mouseleave", () => {
|
||||
annot.stop()
|
||||
this.socket.emit("stopAnnotation")
|
||||
this.emitData("stopAnnotation")
|
||||
})
|
||||
annot.canvas.addEventListener("mouseup", () => {
|
||||
annot.stop()
|
||||
this.socket.emit("stopAnnotation")
|
||||
this.emitData("stopAnnotation")
|
||||
})
|
||||
annot.canvas.addEventListener("mousemove", e => {
|
||||
if (!annot.isPainting()) { return }
|
||||
|
||||
const data = this.screen.getInternalViewportCoordinates(e)
|
||||
annot.move([ data.x, data.y ])
|
||||
this.socket.emit("moveAnnotation", [ data.x, data.y ])
|
||||
this.emitData("moveAnnotation", [ data.x, data.y ])
|
||||
})
|
||||
this.store.update({ annotating: true })
|
||||
} else if (!enable && !!this.annot) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export enum SessionRecordingStatus {
|
|||
|
||||
export interface State {
|
||||
recordingState: SessionRecordingStatus;
|
||||
currentTab?: string;
|
||||
}
|
||||
|
||||
export default class ScreenRecording {
|
||||
|
|
@ -46,14 +47,19 @@ export default class ScreenRecording {
|
|||
if (recordingState === SessionRecordingStatus.Requesting) return;
|
||||
|
||||
this.store.update({ recordingState: SessionRecordingStatus.Requesting })
|
||||
this.socket.emit("request_recording", JSON.stringify({
|
||||
this.emitData("request_recording", JSON.stringify({
|
||||
...this.agentInfo,
|
||||
query: document.location.search,
|
||||
}))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private emitData = (event: string, data?: any) => {
|
||||
this.socket.emit(event, { meta: { tabId: this.store.get().currentTab }, data })
|
||||
}
|
||||
|
||||
stopRecording = () => {
|
||||
this.socket.emit("stop_recording")
|
||||
this.emitData("stop_recording")
|
||||
this.toggleRecording(false)
|
||||
}
|
||||
|
||||
|
|
|
|||
18
frontend/app/player/web/managers/ActiveTabManager.ts
Normal file
18
frontend/app/player/web/managers/ActiveTabManager.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import ListWalker from '../../common/ListWalker';
|
||||
import type { TabChange } from '../messages';
|
||||
|
||||
export default class ActiveTabManager extends ListWalker<TabChange> {
|
||||
currentTime = 0;
|
||||
|
||||
moveReady(t: number): Promise<string | null> {
|
||||
|
||||
if (t < this.currentTime) {
|
||||
this.reset()
|
||||
}
|
||||
this.currentTime = t
|
||||
const msg = this.moveGetLastDebug(t)
|
||||
// console.log('move', t, msg, this.list)
|
||||
|
||||
return Promise.resolve(msg?.tabId || null)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
// 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff = no indexes + weird failover (don't ask)
|
||||
const skipIndexes = this.readCustomIndex(this.buf.slice(0, 8)) === 72057594037927940
|
||||
|| this.readCustomIndex(this.buf.slice(0, 9)) === 72057594037927940
|
||||
|
||||
if (skipIndexes) {
|
||||
this.noIndexes = true
|
||||
this.skip(8)
|
||||
|
|
@ -63,6 +64,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
}
|
||||
}
|
||||
|
||||
currentTab = 'back-compatability'
|
||||
readNext(): Message & { _index?: number } | null {
|
||||
if (this.error || !this.hasNextByte()) {
|
||||
return null
|
||||
|
|
@ -82,6 +84,10 @@ export default class MFileReader extends RawMessageReader {
|
|||
return null
|
||||
}
|
||||
|
||||
if (rMsg.tp === MType.TabData) {
|
||||
this.currentTab = rMsg.tabId
|
||||
return this.readNext()
|
||||
}
|
||||
if (rMsg.tp === MType.Timestamp) {
|
||||
if (!this.startTime) {
|
||||
this.startTime = rMsg.timestamp
|
||||
|
|
@ -93,6 +99,7 @@ export default class MFileReader extends RawMessageReader {
|
|||
const index = this.noIndexes ? 0 : this.getLastMessageID()
|
||||
const msg = Object.assign(rewriteMessage(rMsg), {
|
||||
time: this.currentTime,
|
||||
tabId: this.currentTab,
|
||||
}, !this.noIndexes ? { _index: index } : {})
|
||||
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -11,18 +11,28 @@ export default class MStreamReader {
|
|||
|
||||
private t: number = 0
|
||||
private idx: number = 0
|
||||
|
||||
currentTab = 'back-compatability'
|
||||
readNext(): Message & { _index: number } | null {
|
||||
let msg = this.r.readMessage()
|
||||
if (msg === null) { return null }
|
||||
if (msg.tp === MType.Timestamp) {
|
||||
this.startTs = this.startTs || msg.timestamp
|
||||
this.t = msg.timestamp - this.startTs
|
||||
const newT = msg.timestamp - this.startTs
|
||||
if (newT > this.t) {
|
||||
this.t = newT
|
||||
}
|
||||
return this.readNext()
|
||||
}
|
||||
if (msg.tp === MType.TabData) {
|
||||
this.currentTab = msg.tabId
|
||||
return this.readNext()
|
||||
}
|
||||
|
||||
return Object.assign(msg, {
|
||||
time: this.t,
|
||||
_index: this.idx++,
|
||||
tabId: this.currentTab,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -673,6 +673,22 @@ export default class RawMessageReader extends PrimitiveReader {
|
|||
};
|
||||
}
|
||||
|
||||
case 117: {
|
||||
const tabId = this.readString(); if (tabId === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.TabChange,
|
||||
tabId,
|
||||
};
|
||||
}
|
||||
|
||||
case 118: {
|
||||
const tabId = this.readString(); if (tabId === null) { return resetPointer() }
|
||||
return {
|
||||
tp: MType.TabData,
|
||||
tabId,
|
||||
};
|
||||
}
|
||||
|
||||
case 90: {
|
||||
const timestamp = this.readUint(); if (timestamp === null) { return resetPointer() }
|
||||
const projectID = this.readUint(); if (projectID === null) { return resetPointer() }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { MType } from './raw.gen'
|
||||
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,90,93,96,100,102,103,105]
|
||||
const DOM_TYPES = [0,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,37,38,49,50,51,54,55,57,58,59,60,61,67,69,70,71,72,73,74,75,76,77,113,114,117,118,90,93,96,100,102,103,105]
|
||||
export function isDOMType(t: MType) {
|
||||
return DOM_TYPES.includes(t)
|
||||
}
|
||||
|
|
@ -58,6 +58,8 @@ import type {
|
|||
RawSelectionChange,
|
||||
RawMouseThrashing,
|
||||
RawResourceTiming,
|
||||
RawTabChange,
|
||||
RawTabData,
|
||||
RawIosSessionStart,
|
||||
RawIosCustomEvent,
|
||||
RawIosScreenChanges,
|
||||
|
|
@ -178,6 +180,10 @@ export type MouseThrashing = RawMouseThrashing & Timed
|
|||
|
||||
export type ResourceTiming = RawResourceTiming & Timed
|
||||
|
||||
export type TabChange = RawTabChange & Timed
|
||||
|
||||
export type TabData = RawTabData & Timed
|
||||
|
||||
export type IosSessionStart = RawIosSessionStart & Timed
|
||||
|
||||
export type IosCustomEvent = RawIosCustomEvent & Timed
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export const enum MType {
|
|||
SelectionChange = 113,
|
||||
MouseThrashing = 114,
|
||||
ResourceTiming = 116,
|
||||
TabChange = 117,
|
||||
TabData = 118,
|
||||
IosSessionStart = 90,
|
||||
IosCustomEvent = 93,
|
||||
IosScreenChanges = 96,
|
||||
|
|
@ -447,6 +449,16 @@ export interface RawResourceTiming {
|
|||
cached: boolean,
|
||||
}
|
||||
|
||||
export interface RawTabChange {
|
||||
tp: MType.TabChange,
|
||||
tabId: string,
|
||||
}
|
||||
|
||||
export interface RawTabData {
|
||||
tp: MType.TabData,
|
||||
tabId: string,
|
||||
}
|
||||
|
||||
export interface RawIosSessionStart {
|
||||
tp: MType.IosSessionStart,
|
||||
timestamp: number,
|
||||
|
|
@ -518,4 +530,4 @@ export interface RawIosNetworkCall {
|
|||
}
|
||||
|
||||
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequest | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
|
||||
export type RawMessage = RawTimestamp | RawSetPageLocation | RawSetViewportSize | RawSetViewportScroll | RawCreateDocument | RawCreateElementNode | RawCreateTextNode | RawMoveNode | RawRemoveNode | RawSetNodeAttribute | RawRemoveNodeAttribute | RawSetNodeData | RawSetCssData | RawSetNodeScroll | RawSetInputValue | RawSetInputChecked | RawMouseMove | RawNetworkRequest | RawConsoleLog | RawCssInsertRule | RawCssDeleteRule | RawFetch | RawProfiler | RawOTable | RawRedux | RawVuex | RawMobX | RawNgRx | RawGraphQl | RawPerformanceTrack | RawStringDict | RawSetNodeAttributeDict | RawResourceTimingDeprecated | RawConnectionInformation | RawSetPageVisibility | RawLoadFontFace | RawSetNodeFocus | RawLongTask | RawSetNodeAttributeURLBased | RawSetCssDataURLBased | RawCssInsertRuleURLBased | RawMouseClick | RawCreateIFrameDocument | RawAdoptedSsReplaceURLBased | RawAdoptedSsReplace | RawAdoptedSsInsertRuleURLBased | RawAdoptedSsInsertRule | RawAdoptedSsDeleteRule | RawAdoptedSsAddOwner | RawAdoptedSsRemoveOwner | RawZustand | RawSelectionChange | RawMouseThrashing | RawResourceTiming | RawTabChange | RawTabData | RawIosSessionStart | RawIosCustomEvent | RawIosScreenChanges | RawIosClickEvent | RawIosPerformanceEvent | RawIosLog | RawIosNetworkCall;
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ export const TP_MAP = {
|
|||
113: MType.SelectionChange,
|
||||
114: MType.MouseThrashing,
|
||||
116: MType.ResourceTiming,
|
||||
117: MType.TabChange,
|
||||
118: MType.TabData,
|
||||
90: MType.IosSessionStart,
|
||||
93: MType.IosCustomEvent,
|
||||
96: MType.IosScreenChanges,
|
||||
|
|
|
|||
|
|
@ -470,8 +470,18 @@ type TrResourceTiming = [
|
|||
cached: boolean,
|
||||
]
|
||||
|
||||
type TrTabChange = [
|
||||
type: 117,
|
||||
tabId: string,
|
||||
]
|
||||
|
||||
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequest | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming
|
||||
type TrTabData = [
|
||||
type: 118,
|
||||
tabId: string,
|
||||
]
|
||||
|
||||
|
||||
export type TrackerMessage = TrTimestamp | TrSetPageLocation | TrSetViewportSize | TrSetViewportScroll | TrCreateDocument | TrCreateElementNode | TrCreateTextNode | TrMoveNode | TrRemoveNode | TrSetNodeAttribute | TrRemoveNodeAttribute | TrSetNodeData | TrSetNodeScroll | TrSetInputTarget | TrSetInputValue | TrSetInputChecked | TrMouseMove | TrNetworkRequest | TrConsoleLog | TrPageLoadTiming | TrPageRenderTiming | TrCustomEvent | TrUserID | TrUserAnonymousID | TrMetadata | TrCSSInsertRule | TrCSSDeleteRule | TrFetch | TrProfiler | TrOTable | TrStateAction | TrRedux | TrVuex | TrMobX | TrNgRx | TrGraphQL | TrPerformanceTrack | TrStringDict | TrSetNodeAttributeDict | TrResourceTimingDeprecated | TrConnectionInformation | TrSetPageVisibility | TrLoadFontFace | TrSetNodeFocus | TrLongTask | TrSetNodeAttributeURLBased | TrSetCSSDataURLBased | TrTechnicalInfo | TrCustomIssue | TrCSSInsertRuleURLBased | TrMouseClick | TrCreateIFrameDocument | TrAdoptedSSReplaceURLBased | TrAdoptedSSInsertRuleURLBased | TrAdoptedSSDeleteRule | TrAdoptedSSAddOwner | TrAdoptedSSRemoveOwner | TrJSException | TrZustand | TrBatchMetadata | TrPartitionedMessage | TrInputChange | TrSelectionChange | TrMouseThrashing | TrUnbindNodes | TrResourceTiming | TrTabChange | TrTabData
|
||||
|
||||
export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
||||
switch(tMsg[0]) {
|
||||
|
|
@ -940,6 +950,20 @@ export default function translate(tMsg: TrackerMessage): RawMessage | null {
|
|||
}
|
||||
}
|
||||
|
||||
case 117: {
|
||||
return {
|
||||
tp: MType.TabChange,
|
||||
tabId: tMsg[1],
|
||||
}
|
||||
}
|
||||
|
||||
case 118: {
|
||||
return {
|
||||
tp: MType.TabData,
|
||||
tabId: tMsg[1],
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,16 +12,15 @@ export enum StorageType {
|
|||
export const STORAGE_TYPES = StorageType // TODO: update name everywhere
|
||||
|
||||
export function selectStorageType(state: State): StorageType {
|
||||
if (!state.reduxList) return StorageType.NONE
|
||||
if (state.reduxList.length > 0) {
|
||||
if (state.reduxList?.length > 0) {
|
||||
return StorageType.REDUX
|
||||
} else if (state.vuexList.length > 0) {
|
||||
} else if (state.vuexList?.length > 0) {
|
||||
return StorageType.VUEX
|
||||
} else if (state.mobxList.length > 0) {
|
||||
} else if (state.mobxList?.length > 0) {
|
||||
return StorageType.MOBX
|
||||
} else if (state.ngrxList.length > 0) {
|
||||
} else if (state.ngrxList?.length > 0) {
|
||||
return StorageType.NGRX
|
||||
} else if (state.zustandList.length > 0) {
|
||||
} else if (state.zustandList?.length > 0) {
|
||||
return StorageType.ZUSTAND
|
||||
}
|
||||
return StorageType.NONE
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface ILog {
|
|||
time: number
|
||||
index?: number
|
||||
errorId?: string
|
||||
tabId?: string
|
||||
}
|
||||
|
||||
export const Log = (log: ILog) => ({
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface IEvent {
|
|||
key: number;
|
||||
label: string;
|
||||
targetPath: string;
|
||||
tabId?: string;
|
||||
target: {
|
||||
path: string;
|
||||
label: string;
|
||||
|
|
@ -69,12 +70,14 @@ class Event {
|
|||
time: IEvent['time'];
|
||||
label: IEvent['label'];
|
||||
target: IEvent['target'];
|
||||
tabId: IEvent['tabId'];
|
||||
|
||||
constructor(event: IEvent) {
|
||||
Object.assign(this, {
|
||||
time: event.time,
|
||||
label: event.label,
|
||||
key: event.key,
|
||||
tabId: event.tabId,
|
||||
target: {
|
||||
path: event.target?.path || event.targetPath,
|
||||
label: event.target?.label,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './session';
|
||||
export { default, mergeEventLists } from './session';
|
||||
|
|
@ -9,7 +9,7 @@ import { toJS } from 'mobx';
|
|||
const HASH_MOD = 1610612741;
|
||||
const HASH_P = 53;
|
||||
|
||||
function mergeEventLists<T extends Record<string, any>, Y extends Record<string, any>>(arr1: T[], arr2: Y[]): Array<T | Y> {
|
||||
export function mergeEventLists<T extends Record<string, any>, Y extends Record<string, any>>(arr1: T[], arr2: Y[]): Array<T | Y> {
|
||||
let merged = [];
|
||||
let index1 = 0;
|
||||
let index2 = 0;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ describe(
|
|||
cy.get('#redcounter').click().click().click();
|
||||
cy.get('#test-api').click().click();
|
||||
cy.get('#test-event').click().click();
|
||||
cy.wait(SECOND * 3);
|
||||
cy.wait(SECOND * 15);
|
||||
|
||||
cy.log('finished generating a session')
|
||||
|
||||
|
|
|
|||
|
|
@ -484,6 +484,14 @@ message 116, 'ResourceTiming', :replayer => :devtools do
|
|||
boolean 'Cached'
|
||||
end
|
||||
|
||||
message 117, 'TabChange' do
|
||||
string 'TabId'
|
||||
end
|
||||
|
||||
message 118, 'TabData' do
|
||||
string 'TabId'
|
||||
end
|
||||
|
||||
## Backend-only
|
||||
message 125, 'IssueEvent', :replayer => false, :tracker => false do
|
||||
uint 'MessageID'
|
||||
|
|
|
|||
1
tracker/tracker-assist/.gitignore
vendored
1
tracker/tracker-assist/.gitignore
vendored
|
|
@ -6,3 +6,4 @@ cjs
|
|||
.cache
|
||||
*.cache
|
||||
*.DS_Store
|
||||
coverage
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
## 6.0.0
|
||||
|
||||
- added support for multi tab assist session
|
||||
|
||||
## 5.0.2
|
||||
|
||||
- Added `onCallDeny`, `onRemoteControlDeny` and `onRecordingDeny` callbacks to signal denial of user's consent to call/control/recording
|
||||
|
|
|
|||
13
tracker/tracker-assist/jest.config.js
Normal file
13
tracker/tracker-assist/jest.config.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
const config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts',],
|
||||
// .js file extension fix
|
||||
moduleNameMapper: {
|
||||
'(.+)\\.js': '$1',
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker-assist",
|
||||
"description": "Tracker plugin for screen assistance through the WebRTC",
|
||||
"version": "5.0.2",
|
||||
"version": "6.0.0-beta.1",
|
||||
"keywords": [
|
||||
"WebRTC",
|
||||
"assistance",
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"type": "module",
|
||||
"main": "./lib/index.js",
|
||||
"scripts": {
|
||||
"tsrun": "tsc",
|
||||
"lint": "eslint src --ext .ts,.js --fix --quiet",
|
||||
"build": "npm run build-es && npm run build-cjs",
|
||||
"build-es": "rm -Rf lib && tsc && npm run replace-versions",
|
||||
|
|
@ -23,7 +24,10 @@
|
|||
"replace-req-version": "replace-in-files lib/* cjs/* --string='REQUIRED_TRACKER_VERSION' --replacement='3.5.14'",
|
||||
"prepublishOnly": "npm run build",
|
||||
"prepare": "cd ../../ && husky install tracker/.husky/",
|
||||
"lint-front": "lint-staged"
|
||||
"lint-front": "lint-staged",
|
||||
"test": "jest --coverage=false",
|
||||
"test:ci": "jest --coverage=true",
|
||||
"postversion": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.10",
|
||||
|
|
@ -31,7 +35,7 @@
|
|||
"socket.io-client": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@openreplay/tracker": ">=5.0.0"
|
||||
"@openreplay/tracker": ">=8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openreplay/tracker": "file:../tracker",
|
||||
|
|
@ -41,9 +45,12 @@
|
|||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.1",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"replace-in-files-cli": "^1.0.0",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.6.0-dev.20211126"
|
||||
},
|
||||
"husky": {
|
||||
|
|
|
|||
|
|
@ -127,8 +127,8 @@ export default class Assist {
|
|||
app.session.attachUpdateCallback(sessInfo => this.emit('UPDATE_SESSION', sessInfo))
|
||||
}
|
||||
|
||||
private emit(ev: string, ...args): void {
|
||||
this.socket && this.socket.emit(ev, ...args)
|
||||
private emit(ev: string, args?: any): void {
|
||||
this.socket && this.socket.emit(ev, { meta: { tabId: this.app.getTabId(), }, data: args, })
|
||||
}
|
||||
|
||||
private get agentsConnected(): boolean {
|
||||
|
|
@ -164,7 +164,7 @@ export default class Assist {
|
|||
if (!sessionId) {
|
||||
return app.debug.error('No session ID')
|
||||
}
|
||||
const peerID = `${app.getProjectKey()}-${sessionId}`
|
||||
const peerID = `${app.getProjectKey()}-${sessionId}-${this.app.getTabId()}`
|
||||
|
||||
// SocketIO
|
||||
const socket = this.socket = connect(this.getHost(), {
|
||||
|
|
@ -172,6 +172,7 @@ export default class Assist {
|
|||
query: {
|
||||
'peerId': peerID,
|
||||
'identity': 'session',
|
||||
'tabId': this.app.getTabId(),
|
||||
'sessionInfo': JSON.stringify({
|
||||
pageTitle: document.title,
|
||||
active: true,
|
||||
|
|
@ -180,7 +181,12 @@ export default class Assist {
|
|||
},
|
||||
transports: ['websocket',],
|
||||
})
|
||||
socket.onAny((...args) => app.debug.log('Socket:', ...args))
|
||||
socket.onAny((...args) => {
|
||||
if (args[0] === 'messages' || args[0] === 'UPDATE_SESSION') {
|
||||
return
|
||||
}
|
||||
app.debug.log('Socket:', ...args)
|
||||
})
|
||||
|
||||
this.remoteControl = new RemoteControl(
|
||||
this.options,
|
||||
|
|
@ -197,7 +203,11 @@ export default class Assist {
|
|||
annot.mount()
|
||||
return callingAgents.get(id)
|
||||
},
|
||||
(id, isDenied) => {
|
||||
(id, isDenied) => onRelease(id, isDenied),
|
||||
)
|
||||
|
||||
const onRelease = (id, isDenied) => {
|
||||
{
|
||||
if (id) {
|
||||
const cb = this.agents[id].onControlReleased
|
||||
delete this.agents[id].onControlReleased
|
||||
|
|
@ -217,8 +227,8 @@ export default class Assist {
|
|||
const info = id ? this.agents[id]?.agentInfo : {}
|
||||
this.options.onRemoteControlDeny?.(info || {})
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onAcceptRecording = () => {
|
||||
socket.emit('recording_accepted')
|
||||
|
|
@ -230,24 +240,37 @@ export default class Assist {
|
|||
}
|
||||
const recordingState = new ScreenRecordingState(this.options.recordingConfirm)
|
||||
|
||||
// TODO: check incoming args
|
||||
socket.on('request_control', this.remoteControl.requestControl)
|
||||
socket.on('release_control', this.remoteControl.releaseControl)
|
||||
socket.on('scroll', this.remoteControl.scroll)
|
||||
socket.on('click', this.remoteControl.click)
|
||||
socket.on('move', this.remoteControl.move)
|
||||
socket.on('focus', (clientID, nodeID) => {
|
||||
function processEvent(agentId: string, event: { meta: { tabId: string }, data?: any }, callback?: (id: string, data: any) => void) {
|
||||
if (app.getTabId() === event.meta.tabId) {
|
||||
return callback?.(agentId, event.data)
|
||||
}
|
||||
}
|
||||
if (this.remoteControl !== null) {
|
||||
socket.on('request_control', (agentId, dataObj) => {
|
||||
processEvent(agentId, dataObj, this.remoteControl?.requestControl)
|
||||
})
|
||||
socket.on('release_control', (agentId, dataObj) => {
|
||||
processEvent(agentId, dataObj, (_, data) =>
|
||||
this.remoteControl?.releaseControl(data)
|
||||
)
|
||||
})
|
||||
socket.on('scroll', (id, event) => processEvent(id, event, this.remoteControl?.scroll))
|
||||
socket.on('click', (id, event) => processEvent(id, event, this.remoteControl?.click))
|
||||
socket.on('move', (id, event) => processEvent(id, event, this.remoteControl?.move))
|
||||
socket.on('focus', (id, event) => processEvent(id, event, (clientID, nodeID) => {
|
||||
const el = app.nodes.getNode(nodeID)
|
||||
if (el instanceof HTMLElement && this.remoteControl) {
|
||||
this.remoteControl.focus(clientID, el)
|
||||
}
|
||||
})
|
||||
socket.on('input', this.remoteControl.input)
|
||||
}))
|
||||
socket.on('input', (id, event) => processEvent(id, event, this.remoteControl?.input))
|
||||
}
|
||||
|
||||
|
||||
socket.on('moveAnnotation', (_, p) => annot && annot.move(p)) // TODO: restrict by id
|
||||
socket.on('startAnnotation', (_, p) => annot && annot.start(p))
|
||||
socket.on('stopAnnotation', () => annot && annot.stop())
|
||||
// TODO: restrict by id
|
||||
socket.on('moveAnnotation', (id, event) => processEvent(id, event, (_, d) => annot && annot.move(d)))
|
||||
socket.on('startAnnotation', (id, event) => processEvent(id, event, (_, d) => annot?.start(d)))
|
||||
socket.on('stopAnnotation', (id, event) => processEvent(id, event, annot?.stop))
|
||||
|
||||
socket.on('NEW_AGENT', (id: string, info) => {
|
||||
this.agents[id] = {
|
||||
|
|
@ -256,7 +279,10 @@ export default class Assist {
|
|||
}
|
||||
this.assistDemandedRestart = true
|
||||
this.app.stop()
|
||||
setTimeout(() => {
|
||||
this.app.start().then(() => { this.assistDemandedRestart = false }).catch(e => app.debug.error(e))
|
||||
// TODO: check if it's needed; basically allowing some time for the app to finish everything before starting again
|
||||
}, 500)
|
||||
})
|
||||
socket.on('AGENTS_CONNECTED', (ids: string[]) => {
|
||||
ids.forEach(id =>{
|
||||
|
|
@ -268,7 +294,10 @@ export default class Assist {
|
|||
})
|
||||
this.assistDemandedRestart = true
|
||||
this.app.stop()
|
||||
setTimeout(() => {
|
||||
this.app.start().then(() => { this.assistDemandedRestart = false }).catch(e => app.debug.error(e))
|
||||
// TODO: check if it's needed; basically allowing some time for the app to finish everything before starting again
|
||||
}, 500)
|
||||
|
||||
this.remoteControl?.reconnect(ids)
|
||||
})
|
||||
|
|
@ -287,7 +316,8 @@ export default class Assist {
|
|||
this.agents = {}
|
||||
if (recordingState.isActive) recordingState.stopRecording()
|
||||
})
|
||||
socket.on('call_end', (id) => {
|
||||
socket.on('call_end', (info) => {
|
||||
const id = info.data
|
||||
if (!callingAgents.has(id)) {
|
||||
app.debug.warn('Received call_end from unknown agent', id)
|
||||
return
|
||||
|
|
@ -295,14 +325,20 @@ export default class Assist {
|
|||
endAgentCall(id)
|
||||
})
|
||||
|
||||
socket.on('_agent_name', (id, name) => {
|
||||
socket.on('_agent_name', (id, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const name = info.data
|
||||
callingAgents.set(id, name)
|
||||
updateCallerNames()
|
||||
})
|
||||
socket.on('videofeed', (_, feedState) => {
|
||||
socket.on('videofeed', (_, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const feedState = info.data
|
||||
callUI?.toggleVideoStream(feedState)
|
||||
})
|
||||
socket.on('request_recording', (id, agentData) => {
|
||||
socket.on('request_recording', (id, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
const agentData = info.data
|
||||
if (!recordingState.isActive) {
|
||||
this.options.onRecordingRequest?.(JSON.parse(agentData))
|
||||
recordingState.requestRecording(id, onAcceptRecording, () => onRejectRecording(agentData))
|
||||
|
|
@ -310,7 +346,8 @@ export default class Assist {
|
|||
this.emit('recording_busy')
|
||||
}
|
||||
})
|
||||
socket.on('stop_recording', (id) => {
|
||||
socket.on('stop_recording', (id, info) => {
|
||||
if (app.getTabId() !== info.meta.tabId) return
|
||||
if (recordingState.isActive) {
|
||||
recordingState.stopAgentRecording(id)
|
||||
}
|
||||
|
|
@ -482,6 +519,11 @@ export default class Assist {
|
|||
})
|
||||
|
||||
call.answer(lStreams[call.peer].stream)
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
initiateCallEnd()
|
||||
})
|
||||
|
||||
this.setCallingState(CallingState.True)
|
||||
if (!callEndCallback) { callEndCallback = this.options.onCallStart?.() }
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ if (nativeInputValueDescriptor && nativeInputValueDescriptor.set) {
|
|||
|
||||
|
||||
export default class RemoteControl {
|
||||
private mouse: Mouse | null
|
||||
status: RCStatus = RCStatus.Disabled
|
||||
private mouse: Mouse | null = null
|
||||
public status: RCStatus = RCStatus.Disabled
|
||||
private agentID: string | null = null
|
||||
|
||||
constructor(
|
||||
|
|
@ -89,6 +89,9 @@ export default class RemoteControl {
|
|||
}
|
||||
this.mouse = new Mouse(agentName)
|
||||
this.mouse.mount()
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) this.releaseControl(false)
|
||||
})
|
||||
}
|
||||
|
||||
resetMouse = () => {
|
||||
|
|
@ -97,7 +100,9 @@ export default class RemoteControl {
|
|||
}
|
||||
|
||||
scroll = (id, d) => { id === this.agentID && this.mouse?.scroll(d) }
|
||||
move = (id, xy) => { id === this.agentID && this.mouse?.move(xy) }
|
||||
move = (id, xy) => {
|
||||
return id === this.agentID && this.mouse?.move(xy)
|
||||
}
|
||||
private focused: HTMLElement | null = null
|
||||
click = (id, xy) => {
|
||||
if (id !== this.agentID || !this.mouse) { return }
|
||||
|
|
@ -109,7 +114,9 @@ export default class RemoteControl {
|
|||
input = (id, value: string) => {
|
||||
if (id !== this.agentID || !this.mouse || !this.focused) { return }
|
||||
if (this.focused instanceof HTMLTextAreaElement
|
||||
|| this.focused instanceof HTMLInputElement) {
|
||||
|| this.focused instanceof HTMLInputElement
|
||||
|| this.focused.tagName === 'INPUT'
|
||||
|| this.focused.tagName === 'TEXTAREA') {
|
||||
setInputValue.call(this.focused, value)
|
||||
const ev = new Event('input', { bubbles: true,})
|
||||
this.focused.dispatchEvent(ev)
|
||||
|
|
|
|||
148
tracker/tracker-assist/tests/AnnotationCanvas.test.ts
Normal file
148
tracker/tracker-assist/tests/AnnotationCanvas.test.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import AnnotationCanvas from '../src/AnnotationCanvas'
|
||||
import { describe, expect, test, it, jest, beforeEach, afterEach, } from '@jest/globals'
|
||||
|
||||
|
||||
describe('AnnotationCanvas', () => {
|
||||
let annotationCanvas
|
||||
let documentBody
|
||||
let canvasMock
|
||||
let contextMock
|
||||
|
||||
beforeEach(() => {
|
||||
canvasMock = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
style: {},
|
||||
getContext: jest.fn(() => contextMock as unknown as HTMLCanvasElement),
|
||||
parentNode: document,
|
||||
}
|
||||
|
||||
contextMock = {
|
||||
globalAlpha: 1.0,
|
||||
beginPath: jest.fn(),
|
||||
moveTo: jest.fn(),
|
||||
lineTo: jest.fn(),
|
||||
lineWidth: 8,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
strokeStyle: 'red',
|
||||
stroke: jest.fn(),
|
||||
globalCompositeOperation: '',
|
||||
fillStyle: '',
|
||||
fillRect: jest.fn(),
|
||||
clearRect: jest.fn(),
|
||||
}
|
||||
|
||||
documentBody = document.body
|
||||
// @ts-ignore
|
||||
document['removeChild'] = (el) => jest.fn(el)
|
||||
// @ts-ignore
|
||||
document['createElement'] = () => canvasMock
|
||||
|
||||
jest.spyOn(documentBody, 'appendChild').mockImplementation(jest.fn())
|
||||
jest.spyOn(documentBody, 'removeChild').mockImplementation(jest.fn())
|
||||
jest.spyOn(window, 'addEventListener').mockImplementation(jest.fn())
|
||||
jest.spyOn(window, 'removeEventListener').mockImplementation(jest.fn())
|
||||
annotationCanvas = new AnnotationCanvas()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should create a canvas element with correct styles when initialized', () => {
|
||||
const createElSpy = jest.spyOn(document, 'createElement')
|
||||
annotationCanvas = new AnnotationCanvas()
|
||||
expect(createElSpy).toHaveBeenCalledWith('canvas')
|
||||
expect(canvasMock.style.position).toBe('fixed')
|
||||
expect(canvasMock.style.left).toBe(0)
|
||||
expect(canvasMock.style.top).toBe(0)
|
||||
expect(canvasMock.style.pointerEvents).toBe('none')
|
||||
expect(canvasMock.style.zIndex).toBe(2147483647 - 2)
|
||||
})
|
||||
|
||||
it('should resize the canvas when calling resizeCanvas method', () => {
|
||||
annotationCanvas.resizeCanvas()
|
||||
|
||||
expect(canvasMock.width).toBe(window.innerWidth)
|
||||
expect(canvasMock.height).toBe(window.innerHeight)
|
||||
})
|
||||
|
||||
it('should start painting and set the last position when calling start method', () => {
|
||||
const position = [10, 20,]
|
||||
|
||||
annotationCanvas.start(position)
|
||||
|
||||
expect(annotationCanvas.painting).toBe(true)
|
||||
expect(annotationCanvas.clrTmID).toBeNull()
|
||||
expect(annotationCanvas.lastPosition).toEqual(position)
|
||||
})
|
||||
|
||||
it('should stop painting and call fadeOut method when calling stop method', () => {
|
||||
annotationCanvas.painting = true
|
||||
const fadeOutSpy = jest.spyOn(annotationCanvas, 'fadeOut')
|
||||
|
||||
annotationCanvas.stop()
|
||||
|
||||
expect(annotationCanvas.painting).toBe(false)
|
||||
expect(fadeOutSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not stop painting or call fadeOut method when calling stop method while not painting', () => {
|
||||
annotationCanvas.painting = false
|
||||
const fadeOutSpy = jest.spyOn(annotationCanvas, 'fadeOut')
|
||||
annotationCanvas.stop()
|
||||
|
||||
expect(fadeOutSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should draw a line on the canvas when calling move method', () => {
|
||||
annotationCanvas.painting = true
|
||||
annotationCanvas.ctx = contextMock
|
||||
const initialLastPosition = [0, 0,]
|
||||
const position = [10, 20,]
|
||||
|
||||
annotationCanvas.move(position)
|
||||
|
||||
expect(contextMock.globalAlpha).toBe(1.0)
|
||||
expect(contextMock.beginPath).toHaveBeenCalled()
|
||||
expect(contextMock.moveTo).toHaveBeenCalledWith(initialLastPosition[0], initialLastPosition[1])
|
||||
expect(contextMock.lineTo).toHaveBeenCalledWith(position[0], position[1])
|
||||
expect(contextMock.stroke).toHaveBeenCalled()
|
||||
expect(annotationCanvas.lastPosition).toEqual(position)
|
||||
})
|
||||
|
||||
it('should not draw a line on the canvas when calling move method while not painting', () => {
|
||||
annotationCanvas.painting = false
|
||||
annotationCanvas.ctx = contextMock
|
||||
const position = [10, 20,]
|
||||
|
||||
annotationCanvas.move(position)
|
||||
|
||||
expect(contextMock.beginPath).not.toHaveBeenCalled()
|
||||
expect(contextMock.stroke).not.toHaveBeenCalled()
|
||||
expect(annotationCanvas.lastPosition).toEqual([0, 0,])
|
||||
})
|
||||
|
||||
it('should fade out the canvas when calling fadeOut method', () => {
|
||||
annotationCanvas.ctx = contextMock
|
||||
jest.useFakeTimers()
|
||||
const timerSpy = jest.spyOn(window, 'setTimeout')
|
||||
annotationCanvas.fadeOut()
|
||||
|
||||
expect(timerSpy).toHaveBeenCalledTimes(2)
|
||||
expect(contextMock.globalCompositeOperation).toBe('source-over')
|
||||
expect(contextMock.fillStyle).toBe('rgba(255, 255, 255, 0.1)')
|
||||
expect(contextMock.fillRect).toHaveBeenCalledWith(0, 0, canvasMock.width, canvasMock.height)
|
||||
jest.runOnlyPendingTimers()
|
||||
expect(contextMock.clearRect).toHaveBeenCalledWith(0, 0, canvasMock.width, canvasMock.height)
|
||||
})
|
||||
|
||||
it('should remove the canvas element when calling remove method', () => {
|
||||
const spyOnRemove = jest.spyOn(document, 'removeChild')
|
||||
annotationCanvas.remove()
|
||||
|
||||
expect(spyOnRemove).toHaveBeenCalledWith(canvasMock)
|
||||
expect(window.removeEventListener).toHaveBeenCalledWith('resize', annotationCanvas.resizeCanvas)
|
||||
})
|
||||
})
|
||||
208
tracker/tracker-assist/tests/RemoteControl.test.ts
Normal file
208
tracker/tracker-assist/tests/RemoteControl.test.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import RemoteControl, { RCStatus, } from '../src/RemoteControl'
|
||||
import ConfirmWindow from '../src/ConfirmWindow/ConfirmWindow'
|
||||
import { describe, expect, test, jest, beforeEach, afterEach, } from '@jest/globals'
|
||||
|
||||
describe('RemoteControl', () => {
|
||||
let remoteControl
|
||||
let options
|
||||
let onGrand
|
||||
let onRelease
|
||||
let confirmWindowMountMock
|
||||
let confirmWindowRemoveMock
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
/* mock options */
|
||||
}
|
||||
onGrand = jest.fn()
|
||||
onRelease = jest.fn()
|
||||
confirmWindowMountMock = jest.fn(() => Promise.resolve(true))
|
||||
confirmWindowRemoveMock = jest.fn()
|
||||
|
||||
jest.spyOn(window, 'HTMLInputElement').mockImplementation((): any => ({
|
||||
value: '',
|
||||
dispatchEvent: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.spyOn(window, 'HTMLTextAreaElement').mockImplementation((): any => ({
|
||||
value: '',
|
||||
dispatchEvent: jest.fn(),
|
||||
}))
|
||||
|
||||
jest
|
||||
.spyOn(ConfirmWindow.prototype, 'mount')
|
||||
.mockImplementation(confirmWindowMountMock)
|
||||
jest
|
||||
.spyOn(ConfirmWindow.prototype, 'remove')
|
||||
.mockImplementation(confirmWindowRemoveMock)
|
||||
|
||||
remoteControl = new RemoteControl(options, onGrand, onRelease)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('should initialize with disabled status', () => {
|
||||
expect(remoteControl.status).toBe(RCStatus.Disabled)
|
||||
expect(remoteControl.agentID).toBeNull()
|
||||
expect(remoteControl.confirm).toBeNull()
|
||||
expect(remoteControl.mouse).toBeNull()
|
||||
})
|
||||
|
||||
test('should request control when calling requestControl method', () => {
|
||||
const id = 'agent123'
|
||||
remoteControl.requestControl(id)
|
||||
|
||||
expect(remoteControl.agentID).toBe(id)
|
||||
expect(remoteControl.status).toBe(RCStatus.Requesting)
|
||||
expect(confirmWindowMountMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should grant control when calling grantControl method', () => {
|
||||
const id = 'agent123'
|
||||
remoteControl.grantControl(id)
|
||||
|
||||
expect(remoteControl.agentID).toBe(id)
|
||||
expect(remoteControl.status).toBe(RCStatus.Enabled)
|
||||
expect(onGrand).toHaveBeenCalledWith(id)
|
||||
expect(remoteControl.mouse).toBeDefined()
|
||||
})
|
||||
|
||||
test('should release control when calling releaseControl method', () => {
|
||||
const isDenied = true
|
||||
remoteControl['confirm'] = { remove: jest.fn(), } as unknown as ConfirmWindow
|
||||
const confirmSpy = jest.spyOn(remoteControl['confirm'], 'remove')
|
||||
|
||||
remoteControl.releaseControl(isDenied)
|
||||
expect(remoteControl.agentID).toBeNull()
|
||||
expect(remoteControl.status).toBe(RCStatus.Disabled)
|
||||
expect(onRelease).toHaveBeenCalledWith(null, isDenied)
|
||||
expect(confirmSpy).toHaveBeenCalled()
|
||||
expect(remoteControl.mouse).toBeNull()
|
||||
})
|
||||
|
||||
test('should reset mouse when calling resetMouse method', () => {
|
||||
remoteControl.resetMouse()
|
||||
|
||||
expect(remoteControl.mouse).toBeNull()
|
||||
})
|
||||
|
||||
test('should call mouse.scroll when calling scroll method with correct agentID', () => {
|
||||
const id = 'agent123'
|
||||
const d = 10
|
||||
remoteControl.agentID = id
|
||||
remoteControl.mouse = {
|
||||
scroll: jest.fn(),
|
||||
}
|
||||
|
||||
remoteControl.scroll(id, d)
|
||||
|
||||
expect(remoteControl.mouse.scroll).toHaveBeenCalledWith(d)
|
||||
})
|
||||
|
||||
test('should not call mouse.scroll when calling scroll method with incorrect agentID', () => {
|
||||
const id = 'agent123'
|
||||
const d = 10
|
||||
remoteControl.agentID = 'anotherAgent'
|
||||
remoteControl.mouse = {
|
||||
scroll: jest.fn(),
|
||||
}
|
||||
|
||||
remoteControl.scroll(id, d)
|
||||
|
||||
expect(remoteControl.mouse.scroll).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should call mouse.move when calling move method with correct agentID', () => {
|
||||
const id = 'agent123'
|
||||
const xy = { x: 10, y: 20, }
|
||||
remoteControl.agentID = id
|
||||
remoteControl.mouse = {
|
||||
move: jest.fn(),
|
||||
}
|
||||
|
||||
remoteControl.move(id, xy)
|
||||
|
||||
expect(remoteControl.mouse.move).toHaveBeenCalledWith(xy)
|
||||
})
|
||||
|
||||
test('should not call mouse.move when calling move method with incorrect agentID', () => {
|
||||
const id = 'agent123'
|
||||
const xy = { x: 10, y: 20, }
|
||||
remoteControl.agentID = 'anotherAgent'
|
||||
remoteControl.mouse = {
|
||||
move: jest.fn(),
|
||||
}
|
||||
|
||||
remoteControl.move(id, xy)
|
||||
|
||||
expect(remoteControl.mouse.move).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should call mouse.click when calling click method with correct agentID', () => {
|
||||
const id = 'agent123'
|
||||
const xy = { x: 10, y: 20, }
|
||||
remoteControl.agentID = id
|
||||
remoteControl.mouse = {
|
||||
click: jest.fn(),
|
||||
}
|
||||
|
||||
remoteControl.click(id, xy)
|
||||
|
||||
expect(remoteControl.mouse.click).toHaveBeenCalledWith(xy)
|
||||
})
|
||||
|
||||
test('should not call mouse.click when calling click method with incorrect agentID', () => {
|
||||
const id = 'agent123'
|
||||
const xy = { x: 10, y: 20, }
|
||||
remoteControl.agentID = 'anotherAgent'
|
||||
remoteControl.mouse = {
|
||||
click: jest.fn(),
|
||||
}
|
||||
|
||||
remoteControl.click(id, xy)
|
||||
|
||||
expect(remoteControl.mouse.click).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should set the focused element when calling focus method', () => {
|
||||
const id = 'agent123'
|
||||
const element = document.createElement('div')
|
||||
|
||||
remoteControl.focus(id, element)
|
||||
|
||||
expect(remoteControl.focused).toBe(element)
|
||||
})
|
||||
|
||||
test('should call setInputValue and dispatch input event when calling input method with HTMLInputElement', () => {
|
||||
const id = 'agent1234'
|
||||
const value = 'test_test'
|
||||
const element = document.createElement('input')
|
||||
const dispatchSpy = jest.spyOn(element, 'dispatchEvent')
|
||||
remoteControl.agentID = id
|
||||
remoteControl.mouse = true
|
||||
remoteControl.focused = element
|
||||
|
||||
remoteControl.input(id, value)
|
||||
|
||||
expect(element.value).toBe(value)
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
new Event('input', { bubbles: true, })
|
||||
)
|
||||
})
|
||||
|
||||
test('should update innerText when calling input method with content editable element', () => {
|
||||
const id = 'agent123'
|
||||
const value = 'test'
|
||||
const element = document.createElement('div')
|
||||
// @ts-ignore
|
||||
element['isContentEditable'] = true
|
||||
remoteControl.agentID = id
|
||||
remoteControl.mouse = true
|
||||
remoteControl.focused = element
|
||||
|
||||
remoteControl.input(id, value)
|
||||
expect(element.innerText).toBe(value)
|
||||
})
|
||||
})
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "./cjs"
|
||||
"outDir": "./cjs",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": ["**/*.test.ts"]
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@
|
|||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"outDir": "./lib"
|
||||
}
|
||||
"outDir": "./lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": ["**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
# 8.0.0
|
||||
|
||||
- **[breaking]** support for multi-tab sessions
|
||||
|
||||
# 7.0.3
|
||||
|
||||
- Prevent auto restart after manual stop
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "7.0.3",
|
||||
"version": "8.0.0-beta.1",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
@ -22,7 +22,10 @@
|
|||
"build": "npm run clean && npm run tscRun && npm run rollup && npm run compile",
|
||||
"prepare": "cd ../../ && husky install tracker/.husky/",
|
||||
"lint-front": "lint-staged",
|
||||
"test": "jest"
|
||||
"test": "jest --coverage=false",
|
||||
"test:ci": "jest --coverage=true",
|
||||
"postversion": "npm run build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.2",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type Start = {
|
|||
pageNo: number
|
||||
timestamp: number
|
||||
url: string
|
||||
tabId: string
|
||||
} & Options
|
||||
|
||||
type Auth = {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ export declare const enum Type {
|
|||
MouseThrashing = 114,
|
||||
UnbindNodes = 115,
|
||||
ResourceTiming = 116,
|
||||
TabChange = 117,
|
||||
TabData = 118,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -536,6 +538,16 @@ export type ResourceTiming = [
|
|||
/*cached:*/ boolean,
|
||||
]
|
||||
|
||||
export type TabChange = [
|
||||
/*type:*/ Type.TabChange,
|
||||
/*tabId:*/ string,
|
||||
]
|
||||
|
||||
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequest | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming
|
||||
export type TabData = [
|
||||
/*type:*/ Type.TabData,
|
||||
/*tabId:*/ string,
|
||||
]
|
||||
|
||||
|
||||
type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetNodeScroll | SetInputTarget | SetInputValue | SetInputChecked | MouseMove | NetworkRequest | ConsoleLog | PageLoadTiming | PageRenderTiming | CustomEvent | UserID | UserAnonymousID | Metadata | CSSInsertRule | CSSDeleteRule | Fetch | Profiler | OTable | StateAction | Redux | Vuex | MobX | NgRx | GraphQL | PerformanceTrack | StringDict | SetNodeAttributeDict | ResourceTimingDeprecated | ConnectionInformation | SetPageVisibility | LoadFontFace | SetNodeFocus | LongTask | SetNodeAttributeURLBased | SetCSSDataURLBased | TechnicalInfo | CustomIssue | CSSInsertRuleURLBased | MouseClick | CreateIFrameDocument | AdoptedSSReplaceURLBased | AdoptedSSInsertRuleURLBased | AdoptedSSDeleteRule | AdoptedSSAddOwner | AdoptedSSRemoveOwner | JSException | Zustand | BatchMetadata | PartitionedMessage | InputChange | SelectionChange | MouseThrashing | UnbindNodes | ResourceTiming | TabChange | TabData
|
||||
export default Message
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type Message from './messages.gen.js'
|
||||
import { Timestamp, Metadata, UserID, Type as MType } from './messages.gen.js'
|
||||
import { Timestamp, Metadata, UserID, Type as MType, TabChange, TabData } from './messages.gen.js'
|
||||
import { now, adjustTimeOrigin, deprecationWarn } from '../utils.js'
|
||||
import Nodes from './nodes.js'
|
||||
import Observer from './observer/top_observer.js'
|
||||
|
|
@ -46,6 +46,12 @@ type UnsuccessfulStart = {
|
|||
reason: typeof CANCELED | string
|
||||
success: false
|
||||
}
|
||||
|
||||
type RickRoll = { source: string } & (
|
||||
| { line: 'never-gonna-give-you-up' }
|
||||
| { line: 'never-gonna-let-you-down'; token: string }
|
||||
)
|
||||
|
||||
const UnsuccessfulStart = (reason: string): UnsuccessfulStart => ({ reason, success: false })
|
||||
const SuccessfulStart = (body: OnStartInfo): SuccessfulStart => ({ ...body, success: true })
|
||||
export type StartPromiseReturn = SuccessfulStart | UnsuccessfulStart
|
||||
|
|
@ -64,6 +70,7 @@ type AppOptions = {
|
|||
session_reset_key: string
|
||||
session_token_key: string
|
||||
session_pageno_key: string
|
||||
session_tabid_key: string
|
||||
local_uuid_key: string
|
||||
ingestPoint: string
|
||||
resourceBaseHref: string | null // resourceHref?
|
||||
|
|
@ -74,6 +81,7 @@ type AppOptions = {
|
|||
__debug__?: LoggerOptions
|
||||
localStorage: Storage | null
|
||||
sessionStorage: Storage | null
|
||||
forceSingleTab?: boolean
|
||||
|
||||
// @deprecated
|
||||
onStart?: StartCallback
|
||||
|
|
@ -109,6 +117,7 @@ export default class App {
|
|||
private readonly worker?: TypedWorker
|
||||
private compressionThreshold = 24 * 1000
|
||||
private restartAttempts = 0
|
||||
private readonly bc: BroadcastChannel = new BroadcastChannel('rick')
|
||||
|
||||
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>) {
|
||||
// if (options.onStart !== undefined) {
|
||||
|
|
@ -124,6 +133,7 @@ export default class App {
|
|||
session_token_key: '__openreplay_token',
|
||||
session_pageno_key: '__openreplay_pageno',
|
||||
session_reset_key: '__openreplay_reset',
|
||||
session_tabid_key: '__openreplay_tabid',
|
||||
local_uuid_key: '__openreplay_uuid',
|
||||
ingestPoint: DEFAULT_INGEST_POINT,
|
||||
resourceBaseHref: null,
|
||||
|
|
@ -132,6 +142,7 @@ export default class App {
|
|||
__debug_report_edp: null,
|
||||
localStorage: null,
|
||||
sessionStorage: null,
|
||||
forceSingleTab: false,
|
||||
},
|
||||
options,
|
||||
)
|
||||
|
|
@ -212,6 +223,30 @@ export default class App {
|
|||
} catch (e) {
|
||||
this._debug('worker_start', e)
|
||||
}
|
||||
|
||||
const thisTab = this.session.getTabId()
|
||||
|
||||
if (!this.session.getSessionToken() && !this.options.forceSingleTab) {
|
||||
this.bc.postMessage({ line: 'never-gonna-give-you-up', source: thisTab })
|
||||
}
|
||||
|
||||
this.bc.onmessage = (ev: MessageEvent<RickRoll>) => {
|
||||
if (ev.data.source === thisTab) return
|
||||
if (ev.data.line === 'never-gonna-let-you-down') {
|
||||
const sessionToken = ev.data.token
|
||||
this.session.setSessionToken(sessionToken)
|
||||
}
|
||||
if (ev.data.line === 'never-gonna-give-you-up') {
|
||||
const token = this.session.getSessionToken()
|
||||
if (token) {
|
||||
this.bc.postMessage({
|
||||
line: 'never-gonna-let-you-down',
|
||||
token,
|
||||
source: thisTab,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _debug(context: string, e: any) {
|
||||
|
|
@ -257,6 +292,7 @@ export default class App {
|
|||
}
|
||||
private commit(): void {
|
||||
if (this.worker && this.messages.length) {
|
||||
this.messages.unshift(TabData(this.session.getTabId()))
|
||||
this.messages.unshift(Timestamp(this.timestamp()))
|
||||
this.worker.postMessage(this.messages)
|
||||
this.commitCallbacks.forEach((cb) => cb(this.messages))
|
||||
|
|
@ -455,12 +491,16 @@ export default class App {
|
|||
url: document.URL,
|
||||
connAttemptCount: this.options.connAttemptCount,
|
||||
connAttemptGap: this.options.connAttemptGap,
|
||||
tabId: this.session.getTabId(),
|
||||
})
|
||||
|
||||
const lsReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null
|
||||
this.sessionStorage.removeItem(this.options.session_reset_key)
|
||||
const needNewSessionID = startOpts.forceNew || lsReset || resetByWorker
|
||||
const sessionToken = this.session.getSessionToken()
|
||||
const isNewSession = needNewSessionID || !sessionToken
|
||||
|
||||
console.log('OpenReplay: starting session', needNewSessionID, sessionToken)
|
||||
return window
|
||||
.fetch(this.options.ingestPoint + '/v1/web/start', {
|
||||
method: 'POST',
|
||||
|
|
@ -471,7 +511,7 @@ export default class App {
|
|||
...this.getTrackerInfo(),
|
||||
timestamp,
|
||||
userID: this.session.getInfo().userID,
|
||||
token: needNewSessionID ? undefined : this.session.getSessionToken(),
|
||||
token: isNewSession ? undefined : sessionToken,
|
||||
deviceMemory,
|
||||
jsHeapSizeLimit,
|
||||
}),
|
||||
|
|
@ -523,6 +563,11 @@ export default class App {
|
|||
timestamp: startTimestamp || timestamp,
|
||||
projectID,
|
||||
})
|
||||
if (!isNewSession && token === sessionToken) {
|
||||
console.log('continuing session on new tab', this.session.getTabId())
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
this.send(TabChange(this.session.getTabId()))
|
||||
}
|
||||
// (Re)send Metadata for the case of a new session
|
||||
Object.entries(this.session.getInfo().metadata).forEach(([key, value]) =>
|
||||
this.send(Metadata(key, value)),
|
||||
|
|
@ -566,21 +611,35 @@ export default class App {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* basically we ask other tabs during constructor
|
||||
* and here we just apply 10ms delay just in case
|
||||
* */
|
||||
start(...args: Parameters<App['_start']>): Promise<StartPromiseReturn> {
|
||||
if (!document.hidden) {
|
||||
return this._start(...args)
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(this._start(...args))
|
||||
}, 10)
|
||||
})
|
||||
} else {
|
||||
return new Promise((resolve) => {
|
||||
const onVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
setTimeout(() => {
|
||||
resolve(this._start(...args))
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getTabId() {
|
||||
return this.session.getTabId()
|
||||
}
|
||||
stop(stopWorker = true): void {
|
||||
if (this.activityState !== ActivityState.NotActive) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -869,3 +869,21 @@ export function ResourceTiming(
|
|||
]
|
||||
}
|
||||
|
||||
export function TabChange(
|
||||
tabId: string,
|
||||
): Messages.TabChange {
|
||||
return [
|
||||
Messages.Type.TabChange,
|
||||
tabId,
|
||||
]
|
||||
}
|
||||
|
||||
export function TabData(
|
||||
tabId: string,
|
||||
): Messages.TabData {
|
||||
return [
|
||||
Messages.Type.TabData,
|
||||
tabId,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ export interface Options {
|
|||
domSanitizer?: (node: Element) => SanitizeLevel
|
||||
}
|
||||
|
||||
export const stringWiper = (input: string) =>
|
||||
input
|
||||
.trim()
|
||||
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█')
|
||||
|
||||
export default class Sanitizer {
|
||||
private readonly obscured: Set<number> = new Set()
|
||||
private readonly hidden: Set<number> = new Set()
|
||||
|
|
@ -59,10 +64,9 @@ export default class Sanitizer {
|
|||
sanitize(id: number, data: string): string {
|
||||
if (this.obscured.has(id)) {
|
||||
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
|
||||
return data
|
||||
.trim()
|
||||
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█')
|
||||
return stringWiper(data)
|
||||
}
|
||||
|
||||
if (this.options.obscureTextNumbers) {
|
||||
data = data.replace(/\d/g, '0')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type App from './index.js'
|
||||
import { generateRandomId } from '../utils.js'
|
||||
|
||||
interface SessionInfo {
|
||||
sessionID: string | undefined
|
||||
|
|
@ -12,6 +13,7 @@ type OnUpdateCallback = (i: Partial<SessionInfo>) => void
|
|||
export type Options = {
|
||||
session_token_key: string
|
||||
session_pageno_key: string
|
||||
session_tabid_key: string
|
||||
}
|
||||
|
||||
export default class Session {
|
||||
|
|
@ -21,8 +23,11 @@ export default class Session {
|
|||
private readonly callbacks: OnUpdateCallback[] = []
|
||||
private timestamp = 0
|
||||
private projectID: string | undefined
|
||||
private tabId: string
|
||||
|
||||
constructor(private readonly app: App, private readonly options: Options) {}
|
||||
constructor(private readonly app: App, private readonly options: Options) {
|
||||
this.createTabId()
|
||||
}
|
||||
|
||||
attachUpdateCallback(cb: OnUpdateCallback) {
|
||||
this.callbacks.push(cb)
|
||||
|
|
@ -61,6 +66,7 @@ export default class Session {
|
|||
this.metadata[key] = value
|
||||
this.handleUpdate({ metadata: { [key]: value } })
|
||||
}
|
||||
|
||||
setUserID(userID: string) {
|
||||
this.userID = userID
|
||||
this.handleUpdate({ userID })
|
||||
|
|
@ -88,6 +94,7 @@ export default class Session {
|
|||
getSessionToken(): string | undefined {
|
||||
return this.app.sessionStorage.getItem(this.options.session_token_key) || undefined
|
||||
}
|
||||
|
||||
setSessionToken(token: string): void {
|
||||
this.app.sessionStorage.setItem(this.options.session_token_key, token)
|
||||
}
|
||||
|
|
@ -115,6 +122,22 @@ export default class Session {
|
|||
return encodeURI(String(pageNo) + '&' + token)
|
||||
}
|
||||
|
||||
public getTabId(): string {
|
||||
if (!this.tabId) this.createTabId()
|
||||
return this.tabId
|
||||
}
|
||||
|
||||
private createTabId() {
|
||||
const localId = this.app.sessionStorage.getItem(this.options.session_tabid_key)
|
||||
if (localId) {
|
||||
this.tabId = localId
|
||||
} else {
|
||||
const randomId = generateRandomId(12)
|
||||
this.app.sessionStorage.setItem(this.options.session_tabid_key, randomId)
|
||||
this.tabId = randomId
|
||||
}
|
||||
}
|
||||
|
||||
getInfo(): SessionInfo {
|
||||
return {
|
||||
sessionID: this.sessionID,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import Fonts from './modules/fonts.js'
|
|||
import Network from './modules/network.js'
|
||||
import ConstructedStyleSheets from './modules/constructedStyleSheets.js'
|
||||
import Selection from './modules/selection.js'
|
||||
import Tabs from './modules/tabs.js'
|
||||
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js'
|
||||
|
||||
import type { Options as AppOptions } from './app/index.js'
|
||||
|
|
@ -136,6 +137,7 @@ export default class API {
|
|||
Fonts(app)
|
||||
Network(app, options.network)
|
||||
Selection(app)
|
||||
Tabs(app)
|
||||
;(window as any).__OPENREPLAY__ = this
|
||||
|
||||
if (options.autoResetOnWindowOpen) {
|
||||
|
|
@ -216,6 +218,13 @@ export default class API {
|
|||
}
|
||||
return this.app.getSessionID()
|
||||
}
|
||||
|
||||
getTabId() {
|
||||
if (this.app === null) {
|
||||
return null
|
||||
}
|
||||
return this.app.getTabId()
|
||||
}
|
||||
sessionID(): string | null | undefined {
|
||||
deprecationWarn("'sessionID' method", "'getSessionID' method", '/')
|
||||
return this.getSessionID()
|
||||
|
|
|
|||
13
tracker/tracker/src/main/modules/tabs.ts
Normal file
13
tracker/tracker/src/main/modules/tabs.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type App from '../app/index.js'
|
||||
import { TabChange } from '../app/messages.gen.js'
|
||||
|
||||
export default function (app: App): void {
|
||||
function changeTab() {
|
||||
if (!document.hidden) {
|
||||
app.debug.log('Openreplay: tab change to' + app.session.getTabId())
|
||||
app.send(TabChange(app.session.getTabId()))
|
||||
}
|
||||
}
|
||||
|
||||
app.attachEventListener(window, 'focus', changeTab as EventListener, false, false)
|
||||
}
|
||||
|
|
@ -82,14 +82,6 @@ export function hasOpenreplayAttribute(e: Element, attr: string): boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
export function isIframeCrossdomain(e: HTMLIFrameElement): boolean {
|
||||
try {
|
||||
return e.contentWindow?.location.href !== window.location.href
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if iframe is accessible
|
||||
**/
|
||||
|
|
@ -100,3 +92,16 @@ export function canAccessIframe(iframe: HTMLIFrameElement) {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function dec2hex(dec: number) {
|
||||
return dec.toString(16).padStart(2, '0')
|
||||
}
|
||||
|
||||
export function generateRandomId(len?: number) {
|
||||
const arr: Uint8Array = new Uint8Array((len || 40) / 2)
|
||||
// msCrypto = IE11
|
||||
// @ts-ignore
|
||||
const safeCrypto = window.crypto || window.msCrypto
|
||||
safeCrypto.getRandomValues(arr)
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
|
|
|
|||
113
tracker/tracker/src/tests/guards.unit.test.ts
Normal file
113
tracker/tracker/src/tests/guards.unit.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { describe, expect, test } from '@jest/globals'
|
||||
import {
|
||||
isNode,
|
||||
isSVGElement,
|
||||
isElementNode,
|
||||
isCommentNode,
|
||||
isTextNode,
|
||||
isDocument,
|
||||
isRootNode,
|
||||
hasTag,
|
||||
} from '../main/app/guards.js'
|
||||
|
||||
describe('isNode', () => {
|
||||
test('returns true for a valid Node object', () => {
|
||||
const node = document.createElement('div')
|
||||
expect(isNode(node)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-Node object', () => {
|
||||
const obj = { foo: 'bar' }
|
||||
expect(isNode(obj)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSVGElement', () => {
|
||||
test('returns true for an SVGElement object', () => {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
expect(isSVGElement(svg)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-SVGElement object', () => {
|
||||
const div = document.createElement('div')
|
||||
expect(isSVGElement(div)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isElementNode', () => {
|
||||
test('returns true for an Element object', () => {
|
||||
const element = document.createElement('div')
|
||||
expect(isElementNode(element)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-Element object', () => {
|
||||
const textNode = document.createTextNode('Hello')
|
||||
expect(isElementNode(textNode)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isCommentNode', () => {
|
||||
test('returns true for a Comment object', () => {
|
||||
const comment = document.createComment('This is a comment')
|
||||
expect(isCommentNode(comment)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-Comment object', () => {
|
||||
const div = document.createElement('div')
|
||||
expect(isCommentNode(div)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isTextNode', () => {
|
||||
test('returns true for a Text object', () => {
|
||||
const textNode = document.createTextNode('Hello')
|
||||
expect(isTextNode(textNode)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-Text object', () => {
|
||||
const div = document.createElement('div')
|
||||
expect(isTextNode(div)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isDocument', () => {
|
||||
test('returns true for a Document object', () => {
|
||||
const documentObj = document.implementation.createHTMLDocument('Test')
|
||||
expect(isDocument(documentObj)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-Document object', () => {
|
||||
const div = document.createElement('div')
|
||||
expect(isDocument(div)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRootNode', () => {
|
||||
test('returns true for a Document object', () => {
|
||||
const documentObj = document.implementation.createHTMLDocument('Test')
|
||||
expect(isRootNode(documentObj)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for a DocumentFragment object', () => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
expect(isRootNode(fragment)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a non-root Node object', () => {
|
||||
const div = document.createElement('div')
|
||||
expect(isRootNode(div)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasTag', () => {
|
||||
test('returns true if the element has the specified tag name', () => {
|
||||
const element = document.createElement('input')
|
||||
expect(hasTag(element, 'input')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false if the element does not have the specified tag name', () => {
|
||||
const element = document.createElement('div')
|
||||
// @ts-expect-error
|
||||
expect(hasTag(element, 'span')).toBe(false)
|
||||
})
|
||||
})
|
||||
135
tracker/tracker/src/tests/sanitizer.unit.test.ts
Normal file
135
tracker/tracker/src/tests/sanitizer.unit.test.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
|
||||
import Sanitizer, { SanitizeLevel, Options, stringWiper } from '../main/app/sanitizer.js'
|
||||
|
||||
describe('stringWiper', () => {
|
||||
test('should replace all characters with █', () => {
|
||||
expect(stringWiper('Sensitive Data')).toBe('██████████████')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sanitizer', () => {
|
||||
let sanitizer: Sanitizer
|
||||
|
||||
beforeEach(() => {
|
||||
const options: Options = {
|
||||
obscureTextEmails: true,
|
||||
obscureTextNumbers: false,
|
||||
domSanitizer: undefined,
|
||||
}
|
||||
const app = {
|
||||
nodes: {
|
||||
getID: (el: { mockId: number }) => el.mockId,
|
||||
},
|
||||
}
|
||||
// @ts-expect-error
|
||||
sanitizer = new Sanitizer(app, options)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sanitizer.clear()
|
||||
})
|
||||
|
||||
test('should handle node and mark it as obscured if parent is obscured', () => {
|
||||
sanitizer['obscured'].add(2)
|
||||
sanitizer.handleNode(1, 2, document.createElement('div'))
|
||||
expect(sanitizer.isObscured(1)).toBe(true)
|
||||
})
|
||||
|
||||
test('should handle node and mark it as obscured if it has "masked" or "obscured" attribute', () => {
|
||||
const node = document.createElement('div')
|
||||
node.setAttribute('data-openreplay-obscured', '')
|
||||
sanitizer.handleNode(1, 2, node)
|
||||
expect(sanitizer.isObscured(1)).toBe(true)
|
||||
})
|
||||
|
||||
test('should handle node and mark it as hidden if parent is hidden', () => {
|
||||
sanitizer['hidden'].add(2)
|
||||
sanitizer.handleNode(1, 2, document.createElement('div'))
|
||||
expect(sanitizer.isHidden(1)).toBe(true)
|
||||
})
|
||||
|
||||
test('should handle node and mark it as hidden if it has "htmlmasked" or "hidden" attribute', () => {
|
||||
const node = document.createElement('div')
|
||||
node.setAttribute('data-openreplay-hidden', '')
|
||||
sanitizer.handleNode(1, 2, node)
|
||||
expect(sanitizer.isHidden(1)).toBe(true)
|
||||
})
|
||||
|
||||
test('should handle node and sanitize based on custom domSanitizer function', () => {
|
||||
const domSanitizer = (node: Element): SanitizeLevel => {
|
||||
if (node.tagName === 'SPAN') {
|
||||
return SanitizeLevel.Obscured
|
||||
}
|
||||
if (node.tagName === 'DIV') {
|
||||
return SanitizeLevel.Hidden
|
||||
}
|
||||
return SanitizeLevel.Plain
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
obscureTextEmails: true,
|
||||
obscureTextNumbers: false,
|
||||
domSanitizer,
|
||||
}
|
||||
const app = {
|
||||
nodes: {
|
||||
getID: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
sanitizer = new Sanitizer(app, options)
|
||||
|
||||
const spanNode = document.createElement('span')
|
||||
const divNode = document.createElement('div')
|
||||
const plainNode = document.createElement('p')
|
||||
|
||||
sanitizer.handleNode(1, 2, spanNode)
|
||||
sanitizer.handleNode(3, 4, divNode)
|
||||
sanitizer.handleNode(5, 6, plainNode)
|
||||
|
||||
expect(sanitizer.isObscured(1)).toBe(true)
|
||||
expect(sanitizer.isHidden(3)).toBe(true)
|
||||
expect(sanitizer.isObscured(5)).toBe(false)
|
||||
expect(sanitizer.isHidden(5)).toBe(false)
|
||||
})
|
||||
|
||||
test('should sanitize data as obscured if node is marked as obscured', () => {
|
||||
sanitizer['obscured'].add(1)
|
||||
const data = 'Sensitive Data'
|
||||
|
||||
const sanitizedData = sanitizer.sanitize(1, data)
|
||||
expect(sanitizedData).toEqual(stringWiper(data))
|
||||
})
|
||||
|
||||
test('should sanitize data by obscuring text numbers if enabled', () => {
|
||||
sanitizer['options'].obscureTextNumbers = true
|
||||
const data = 'Phone: 123-456-7890'
|
||||
const sanitizedData = sanitizer.sanitize(1, data)
|
||||
expect(sanitizedData).toEqual('Phone: 000-000-0000')
|
||||
})
|
||||
|
||||
test('should sanitize data by obscuring text emails if enabled', () => {
|
||||
sanitizer['options'].obscureTextEmails = true
|
||||
const data = 'john.doe@example.com'
|
||||
const sanitizedData = sanitizer.sanitize(1, data)
|
||||
expect(sanitizedData).toEqual('********@*******.***')
|
||||
})
|
||||
|
||||
test('should return inner text of an element securely by sanitizing it', () => {
|
||||
const element = document.createElement('div')
|
||||
sanitizer['obscured'].add(1)
|
||||
// @ts-expect-error
|
||||
element.mockId = 1
|
||||
element.innerText = 'Sensitive Data'
|
||||
const sanitizedText = sanitizer.getInnerTextSecure(element)
|
||||
expect(sanitizedText).toEqual('██████████████')
|
||||
})
|
||||
|
||||
test('should return empty string if node element does not exist', () => {
|
||||
const element = document.createElement('div')
|
||||
element.innerText = 'Sensitive Data'
|
||||
const sanitizedText = sanitizer.getInnerTextSecure(element)
|
||||
expect(sanitizedText).toEqual('')
|
||||
})
|
||||
})
|
||||
186
tracker/tracker/src/tests/utils.unit.test.ts
Normal file
186
tracker/tracker/src/tests/utils.unit.test.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { describe, expect, test, jest, afterEach, beforeEach } from '@jest/globals'
|
||||
import {
|
||||
adjustTimeOrigin,
|
||||
getTimeOrigin,
|
||||
now,
|
||||
stars,
|
||||
normSpaces,
|
||||
isURL,
|
||||
deprecationWarn,
|
||||
getLabelAttribute,
|
||||
hasOpenreplayAttribute,
|
||||
canAccessIframe,
|
||||
generateRandomId,
|
||||
} from '../main/utils.js'
|
||||
|
||||
describe('adjustTimeOrigin', () => {
|
||||
test('adjusts the time origin based on performance.now', () => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(1000)
|
||||
jest.spyOn(performance, 'now').mockReturnValue(1000)
|
||||
adjustTimeOrigin()
|
||||
|
||||
expect(getTimeOrigin()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('now', () => {
|
||||
test('returns the current timestamp in milliseconds', () => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(2550)
|
||||
jest.spyOn(performance, 'now').mockReturnValue(2550)
|
||||
|
||||
adjustTimeOrigin()
|
||||
|
||||
expect(now()).toBe(2550)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stars', () => {
|
||||
test('returns a string of asterisks with the same length as the input string', () => {
|
||||
expect(stars('hello')).toBe('*****')
|
||||
})
|
||||
|
||||
test('returns an empty string if the input string is empty', () => {
|
||||
expect(stars('')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normSpaces', () => {
|
||||
test('trims the string and replaces multiple spaces with a single space', () => {
|
||||
expect(normSpaces(' hello world ')).toBe('hello world')
|
||||
})
|
||||
|
||||
test('returns an empty string if the input string is empty', () => {
|
||||
expect(normSpaces('')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isURL', () => {
|
||||
test('returns true for a valid URL starting with "https://"', () => {
|
||||
expect(isURL('https://example.com')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for a valid URL starting with "http://"', () => {
|
||||
expect(isURL('http://example.com')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for a URL without a valid protocol', () => {
|
||||
expect(isURL('example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for an empty string', () => {
|
||||
expect(isURL('')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deprecationWarn', () => {
|
||||
let consoleWarnSpy: jest.SpiedFunction<(args: any) => void>
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((args) => args)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
test('prints a warning message for a deprecated feature', () => {
|
||||
deprecationWarn('oldFeature', 'newFeature')
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'OpenReplay: oldFeature is deprecated. Please, use newFeature instead. Visit https://docs.openreplay.com/ for more information.',
|
||||
)
|
||||
})
|
||||
|
||||
test('does not print a warning message for a deprecated feature that has already been warned', () => {
|
||||
deprecationWarn('oldFeature2', 'newFeature')
|
||||
deprecationWarn('oldFeature2', 'newFeature')
|
||||
expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLabelAttribute', () => {
|
||||
test('returns the value of "data-openreplay-label" attribute if present', () => {
|
||||
const element = document.createElement('div')
|
||||
element.setAttribute('data-openreplay-label', 'Label')
|
||||
expect(getLabelAttribute(element)).toBe('Label')
|
||||
})
|
||||
|
||||
test('returns the value of "data-asayer-label" attribute if "data-openreplay-label" is not present (with deprecation warning)', () => {
|
||||
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((args) => args)
|
||||
const element = document.createElement('div')
|
||||
element.setAttribute('data-asayer-label', 'Label')
|
||||
expect(getLabelAttribute(element)).toBe('Label')
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'OpenReplay: "data-asayer-label" attribute is deprecated. Please, use "data-openreplay-label" attribute instead. Visit https://docs.openreplay.com/ for more information.',
|
||||
)
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
test('returns null if neither "data-openreplay-label" nor "data-asayer-label" are present', () => {
|
||||
const element = document.createElement('div')
|
||||
expect(getLabelAttribute(element)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('hasOpenreplayAttribute', () => {
|
||||
let consoleWarnSpy: jest.SpiedFunction<(args: any) => void>
|
||||
|
||||
beforeEach(() => {
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((args) => args)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
test('returns true and prints a deprecation warning for a deprecated openreplay attribute', () => {
|
||||
const element = document.createElement('div')
|
||||
element.setAttribute('data-openreplay-htmlmasked', 'true')
|
||||
const result = hasOpenreplayAttribute(element, 'htmlmasked')
|
||||
expect(result).toBe(true)
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'OpenReplay: "data-openreplay-htmlmasked" attribute is deprecated. Please, use "hidden" attribute instead. Visit https://docs.openreplay.com/installation/sanitize-data for more information.',
|
||||
)
|
||||
})
|
||||
|
||||
test('returns false for a non-existent openreplay attribute', () => {
|
||||
const element = document.createElement('div')
|
||||
const result = hasOpenreplayAttribute(element, 'nonexistent')
|
||||
expect(result).toBe(false)
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('canAccessIframe', () => {
|
||||
test('returns true if the iframe has a contentDocument', () => {
|
||||
const iframe = document.createElement('iframe')
|
||||
Object.defineProperty(iframe, 'contentDocument', {
|
||||
get: () => document.createElement('div'),
|
||||
})
|
||||
expect(canAccessIframe(iframe)).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false if the iframe does not have a contentDocument', () => {
|
||||
const iframe = document.createElement('iframe')
|
||||
// Mock iframe.contentDocument to throw an error
|
||||
Object.defineProperty(iframe, 'contentDocument', {
|
||||
get: () => {
|
||||
throw new Error('securityError')
|
||||
},
|
||||
})
|
||||
expect(canAccessIframe(iframe)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateRandomId', () => {
|
||||
test('generates a random ID with the specified length', () => {
|
||||
const id = generateRandomId(10)
|
||||
expect(id).toHaveLength(10)
|
||||
expect(/^[0-9a-f]+$/.test(id)).toBe(true)
|
||||
})
|
||||
|
||||
test('generates a random ID with the default length if no length is specified', () => {
|
||||
const id = generateRandomId()
|
||||
expect(id).toHaveLength(40)
|
||||
expect(/^[0-9a-f]+$/.test(id)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -19,6 +19,7 @@ export default class BatchWriter {
|
|||
private timestamp: number,
|
||||
private url: string,
|
||||
private readonly onBatch: (batch: Uint8Array) => void,
|
||||
private tabId: string,
|
||||
) {
|
||||
this.prepare()
|
||||
}
|
||||
|
|
@ -51,8 +52,12 @@ export default class BatchWriter {
|
|||
this.timestamp,
|
||||
this.url,
|
||||
]
|
||||
|
||||
const tabData: Messages.TabData = [Messages.Type.TabData, this.tabId]
|
||||
|
||||
this.writeType(batchMetadata)
|
||||
this.writeFields(batchMetadata)
|
||||
this.writeWithSize(tabData as Message)
|
||||
this.isEmpty = true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ describe('BatchWriter', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
onBatchMock = jest.fn()
|
||||
batchWriter = new BatchWriter(1, 123456789, 'example.com', onBatchMock)
|
||||
batchWriter = new BatchWriter(1, 123456789, 'example.com', onBatchMock, '123')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -21,7 +21,8 @@ describe('BatchWriter', () => {
|
|||
expect(batchWriter['timestamp']).toBe(123456789)
|
||||
expect(batchWriter['url']).toBe('example.com')
|
||||
expect(batchWriter['onBatch']).toBe(onBatchMock)
|
||||
expect(batchWriter['nextIndex']).toBe(0)
|
||||
// we add tab id as first in the batch
|
||||
expect(batchWriter['nextIndex']).toBe(1)
|
||||
expect(batchWriter['beaconSize']).toBe(200000)
|
||||
expect(batchWriter['encoder']).toBeDefined()
|
||||
expect(batchWriter['strDict']).toBeDefined()
|
||||
|
|
@ -30,12 +31,14 @@ describe('BatchWriter', () => {
|
|||
})
|
||||
|
||||
test('writeType writes the type of the message', () => {
|
||||
// @ts-ignore
|
||||
const message = [Messages.Type.BatchMetadata, 1, 2, 3, 4, 'example.com']
|
||||
const result = batchWriter['writeType'](message as Message)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test('writeFields encodes the message fields', () => {
|
||||
// @ts-ignore
|
||||
const message = [Messages.Type.BatchMetadata, 1, 2, 3, 4, 'example.com']
|
||||
const result = batchWriter['writeFields'](message as Message)
|
||||
expect(result).toBe(true)
|
||||
|
|
@ -52,6 +55,7 @@ describe('BatchWriter', () => {
|
|||
})
|
||||
|
||||
test('writeWithSize writes the message with its size', () => {
|
||||
// @ts-ignore
|
||||
const message = [Messages.Type.BatchMetadata, 1, 2, 3, 4, 'example.com']
|
||||
const result = batchWriter['writeWithSize'](message as Message)
|
||||
expect(result).toBe(true)
|
||||
|
|
@ -72,6 +76,7 @@ describe('BatchWriter', () => {
|
|||
})
|
||||
|
||||
test('writeMessage writes the given message', () => {
|
||||
// @ts-ignore
|
||||
const message = [Messages.Type.Timestamp, 987654321]
|
||||
// @ts-ignore
|
||||
batchWriter['writeWithSize'] = jest.fn().mockReturnValue(true)
|
||||
|
|
|
|||
|
|
@ -274,6 +274,14 @@ export default class MessageEncoder extends PrimitiveEncoder {
|
|||
return this.uint(msg[1]) && this.uint(msg[2]) && this.uint(msg[3]) && this.uint(msg[4]) && this.uint(msg[5]) && this.uint(msg[6]) && this.string(msg[7]) && this.string(msg[8]) && this.uint(msg[9]) && this.boolean(msg[10])
|
||||
break
|
||||
|
||||
case Messages.Type.TabChange:
|
||||
return this.string(msg[1])
|
||||
break
|
||||
|
||||
case Messages.Type.TabData:
|
||||
return this.string(msg[1])
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,6 @@ export default class QueueSender {
|
|||
setTimeout(() => {
|
||||
this.token = null
|
||||
this.queue.length = 0
|
||||
}, 100)
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import QueueSender from './QueueSender.js'
|
|||
global.fetch = () => Promise.resolve(new Response()) // jsdom does not have it
|
||||
|
||||
function mockFetch(status: number, headers?: Record<string, string>) {
|
||||
return jest
|
||||
.spyOn(global, 'fetch')
|
||||
.mockImplementation((request) =>
|
||||
return jest.spyOn(global, 'fetch').mockImplementation((request) =>
|
||||
Promise.resolve({ status, headers, request } as unknown as Response & {
|
||||
request: RequestInfo
|
||||
}),
|
||||
|
|
@ -40,6 +38,7 @@ function defaultQueueSender({
|
|||
describe('QueueSender', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
// Test fetch first parameter + authorization header to be present
|
||||
|
|
@ -93,9 +92,10 @@ describe('QueueSender', () => {
|
|||
test("Doesn't call fetch on push() after clean()", () => {
|
||||
const queueSender = defaultQueueSender()
|
||||
const fetchMock = mockFetch(200)
|
||||
|
||||
jest.useFakeTimers()
|
||||
queueSender.authorise(randomToken)
|
||||
queueSender.clean()
|
||||
jest.runAllTimers()
|
||||
queueSender.push(sampleArray)
|
||||
expect(fetchMock).not.toBeCalled()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ const AUTO_SEND_INTERVAL = 10 * 1000
|
|||
|
||||
let sender: QueueSender | null = null
|
||||
let writer: BatchWriter | null = null
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let workerStatus: WorkerStatus = WorkerStatus.NotActive
|
||||
// let afterSleepRestarts = 0
|
||||
|
||||
function finalize(): void {
|
||||
if (!writer) {
|
||||
return
|
||||
|
|
@ -44,7 +45,7 @@ function resetSender(): void {
|
|||
// allowing some time to send last batch
|
||||
setTimeout(() => {
|
||||
sender = null
|
||||
}, 500)
|
||||
}, 20)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +57,9 @@ function reset(): void {
|
|||
}
|
||||
resetWriter()
|
||||
resetSender()
|
||||
setTimeout(() => {
|
||||
workerStatus = WorkerStatus.NotActive
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function initiateRestart(): void {
|
||||
|
|
@ -73,7 +76,7 @@ let sendIntervalID: ReturnType<typeof setInterval> | null = null
|
|||
let restartTimeoutID: ReturnType<typeof setTimeout>
|
||||
|
||||
// @ts-ignore
|
||||
self.onmessage = ({ data }: any): any => {
|
||||
self.onmessage = ({ data }: { data: ToWorkerData }): any => {
|
||||
if (data == null) {
|
||||
finalize()
|
||||
return
|
||||
|
|
@ -146,6 +149,7 @@ self.onmessage = ({ data }: any): any => {
|
|||
data.timestamp,
|
||||
data.url,
|
||||
(batch) => sender && sender.push(batch),
|
||||
data.tabId,
|
||||
)
|
||||
if (sendIntervalID === null) {
|
||||
sendIntervalID = setInterval(finalize, AUTO_SEND_INTERVAL)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue