Merge branch 'dev' into live-se-red

This commit is contained in:
nick-delirium 2024-12-09 17:22:37 +01:00
commit e1bb2e8bcb
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
39 changed files with 547 additions and 375 deletions

View file

@ -5,7 +5,7 @@ name = "pypi"
[packages]
sqlparse = "==0.5.2"
urllib3 = "==1.26.16"
urllib3 = "==2.2.3"
requests = "==2.32.3"
boto3 = "==1.35.76"
pyjwt = "==2.10.1"
@ -21,7 +21,7 @@ uvicorn = {extras = ["standard"], version = "==0.32.1"}
python-decouple = "==3.8"
pydantic = {extras = ["email"], version = "==2.10.3"}
apscheduler = "==3.11.0"
redis = "==5.2.0"
redis = "==5.2.1"
[dev-packages]

View file

@ -45,8 +45,6 @@ class JWTAuth(HTTPBearer):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid authentication scheme.")
jwt_payload = authorizers.jwt_authorizer(scheme=credentials.scheme, token=credentials.credentials)
logger.info("------ jwt_payload ------")
logger.info(jwt_payload)
auth_exists = jwt_payload is not None and users.auth_exists(user_id=jwt_payload.get("userId", -1),
jwt_iat=jwt_payload.get("iat", 100))
if jwt_payload is None \
@ -120,8 +118,7 @@ class JWTAuth(HTTPBearer):
jwt_payload = None
else:
jwt_payload = authorizers.jwt_refresh_authorizer(scheme="Bearer", token=request.cookies["spotRefreshToken"])
logger.info("__process_spot_refresh_call")
logger.info(jwt_payload)
if jwt_payload is None or jwt_payload.get("jti") is None:
logger.warning("Null spotRefreshToken's payload, or null JTI.")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,

View file

@ -12,20 +12,6 @@ from chalicelib.utils.TimeUTC import TimeUTC
logger = logging.getLogger(__name__)
# TODO: refactor this to split
# timeseries /
# table of errors / table of issues / table of browsers / table of devices / table of countries / table of URLs
# remove "table of" calls from this function
def __try_live(project_id, data: schemas.CardSchema):
results = []
for i, s in enumerate(data.series):
results.append(sessions.search2_series(data=s.filter, project_id=project_id, density=data.density,
view_type=data.view_type, metric_type=data.metric_type,
metric_of=data.metric_of, metric_value=data.metric_value))
return results
def __get_table_of_series(project_id, data: schemas.CardSchema):
results = []
for i, s in enumerate(data.series):
@ -43,9 +29,6 @@ def __get_funnel_chart(project: schemas.ProjectContext, data: schemas.CardFunnel
"totalDropDueToIssues": 0
}
# return funnels.get_top_insights_on_the_fly_widget(project_id=project_id,
# data=data.series[0].filter,
# metric_format=data.metric_format)
return funnels.get_simple_funnel(project=project,
data=data.series[0].filter,
metric_format=data.metric_format)
@ -93,7 +76,12 @@ def __get_path_analysis_chart(project: schemas.ProjectContext, user_id: int, dat
def __get_timeseries_chart(project: schemas.ProjectContext, data: schemas.CardTimeSeries, user_id: int = None):
series_charts = __try_live(project_id=project.project_id, data=data)
series_charts = []
for i, s in enumerate(data.series):
series_charts.append(sessions.search2_series(data=s.filter, project_id=project.project_id, density=data.density,
view_type=data.view_type, metric_type=data.metric_type,
metric_of=data.metric_of, metric_value=data.metric_value))
results = [{}] * len(series_charts[0])
for i in range(len(results)):
for j, series_chart in enumerate(series_charts):
@ -179,12 +167,6 @@ def get_chart(project: schemas.ProjectContext, data: schemas.CardSchema, user_id
def get_sessions_by_card_id(project_id, user_id, metric_id, data: schemas.CardSessionsSchema):
# No need for this because UI is sending the full payload
# card: dict = get_card(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
# if card is None:
# return None
# metric: schemas.CardSchema = schemas.CardSchema(**card)
# metric: schemas.CardSchema = __merge_metric_with_data(metric=metric, data=data)
if not card_exists(metric_id=metric_id, project_id=project_id, user_id=user_id):
return None
results = []
@ -553,17 +535,7 @@ def change_state(project_id, metric_id, user_id, status):
def get_funnel_sessions_by_issue(user_id, project_id, metric_id, issue_id,
data: schemas.CardSessionsSchema
# , range_value=None, start_date=None, end_date=None
):
# No need for this because UI is sending the full payload
# card: dict = get_card(metric_id=metric_id, project_id=project_id, user_id=user_id, flatten=False)
# if card is None:
# return None
# metric: schemas.CardSchema = schemas.CardSchema(**card)
# metric: schemas.CardSchema = __merge_metric_with_data(metric=metric, data=data)
# if metric is None:
# return None
data: schemas.CardSessionsSchema):
if not card_exists(metric_id=metric_id, project_id=project_id, user_id=user_id):
return None
for s in data.series:

View file

@ -2,13 +2,22 @@ import logging
import threading
import time
from functools import wraps
from queue import Queue
from queue import Queue, Empty
import clickhouse_connect
from clickhouse_connect.driver.query import QueryContext
from clickhouse_connect.driver.exceptions import DatabaseError
from decouple import config
logger = logging.getLogger(__name__)
_CH_CONFIG = {"host": config("ch_host"),
"user": config("ch_user", default="default"),
"password": config("ch_password", default=""),
"port": config("ch_port_http", cast=int),
"client_name": config("APP_NAME", default="PY")}
CH_CONFIG = dict(_CH_CONFIG)
settings = {}
if config('ch_timeout', cast=int, default=-1) > 0:
logging.info(f"CH-max_execution_time set to {config('ch_timeout')}s")
@ -26,6 +35,7 @@ if config("CH_COMPRESSION", cast=bool, default=True):
def transform_result(original_function):
@wraps(original_function)
def wrapper(*args, **kwargs):
logger.info("Executing query on CH")
result = original_function(*args, **kwargs)
if isinstance(result, clickhouse_connect.driver.query.QueryResult):
column_names = result.column_names
@ -38,21 +48,17 @@ def transform_result(original_function):
class ClickHouseConnectionPool:
def __init__(self, min_size, max_size, settings):
def __init__(self, min_size, max_size):
self.min_size = min_size
self.max_size = max_size
self.pool = Queue()
self.lock = threading.Lock()
self.total_connections = 0
self.settings = settings
# Initialize the pool with min_size connections
for _ in range(self.min_size):
client = clickhouse_connect.get_client(host=config("ch_host"),
client = clickhouse_connect.get_client(**CH_CONFIG,
database=config("ch_database", default="default"),
user=config("ch_user", default="default"),
password=config("ch_password", default=""),
port=config("ch_port_http", cast=int),
settings=settings,
**extra_args)
self.pool.put(client)
@ -66,15 +72,10 @@ class ClickHouseConnectionPool:
except Empty:
with self.lock:
if self.total_connections < self.max_size:
client = clickhouse_connect.get_client(
host=config("ch_host"),
database=config("ch_database", default="default"),
user=config("ch_user", default="default"),
password=config("ch_password", default=""),
port=config("ch_port_http", cast=int),
settings=settings,
**extra_args
)
client = clickhouse_connect.get_client(**CH_CONFIG,
database=config("ch_database", default="default"),
settings=settings,
**extra_args)
self.total_connections += 1
return client
# If max_size reached, wait until a connection is available
@ -110,12 +111,11 @@ def make_pool():
except Exception as error:
logger.error("Error while closing all connexions to CH", error)
try:
CH_pool = ClickHouseConnectionPool(min_size=config("PG_MINCONN", cast=int, default=4),
max_size=config("PG_MAXCONN", cast=int, default=8),
settings=settings)
CH_pool = ClickHouseConnectionPool(min_size=config("CH_MINCONN", cast=int, default=4),
max_size=config("CH_MAXCONN", cast=int, default=8))
if CH_pool is not None:
logger.info("Connection pool created successfully for CH")
except Exception as error:
except ConnectionError as error:
logger.error("Error while connecting to CH", error)
if RETRY < RETRY_MAX:
RETRY += 1
@ -131,15 +131,12 @@ class ClickHouseClient:
def __init__(self, database=None):
if self.__client is None:
if config('CH_POOL', cast=bool, default=True):
if database is None and config('CH_POOL', cast=bool, default=True):
self.__client = CH_pool.get_connection()
else:
self.__client = clickhouse_connect.get_client(host=config("ch_host"),
self.__client = clickhouse_connect.get_client(**CH_CONFIG,
database=database if database else config("ch_database",
default="default"),
user=config("ch_user", default="default"),
password=config("ch_password", default=""),
port=config("ch_port_http", cast=int),
settings=settings,
**extra_args)
self.__client.execute = transform_result(self.__client.query)
@ -164,7 +161,7 @@ class ClickHouseClient:
async def init():
logger.info(f">CH_POOL:{config('CH_POOL', default=None)}")
logger.info(f">use CH_POOL:{config('CH_POOL', default=True)}")
if config('CH_POOL', cast=bool, default=True):
make_pool()

View file

@ -166,7 +166,7 @@ class PostgresClient:
async def init():
logger.info(f">PG_POOL:{config('PG_POOL', default=None)}")
logger.info(f">use PG_POOL:{config('PG_POOL', default=True)}")
if config('PG_POOL', cast=bool, default=True):
make_pool()

View file

@ -10,8 +10,8 @@ captcha_key=
captcha_server=
CH_COMPRESSION=true
ch_host=
ch_port=
ch_port_http=
ch_port=9000
ch_port_http=8123
ch_receive_timeout=10
ch_timeout=30
change_password_link=/reset-password?invitation=%s&&pass=%s

View file

@ -1,5 +1,4 @@
# Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16
urllib3==2.2.3
requests==2.32.3
boto3==1.35.76
pyjwt==2.10.1

View file

@ -1,5 +1,4 @@
# Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16
urllib3==2.2.3
requests==2.32.3
boto3==1.35.76
pyjwt==2.10.1
@ -19,4 +18,4 @@ python-decouple==3.8
pydantic[email]==2.10.3
apscheduler==3.11.0
redis==5.2.0
redis==5.2.1

View file

@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/elastic/go-elasticsearch/v8"
@ -55,7 +54,7 @@ func (e *elasticsearchClient) FetchSessionData(credentials interface{}, sessionI
// Create Elasticsearch client
es, err := elasticsearch.NewClient(clientCfg)
if err != nil {
log.Fatalf("Error creating the client: %s", err)
return nil, fmt.Errorf("error creating the client: %s", err)
}
var buf strings.Builder
@ -79,17 +78,17 @@ func (e *elasticsearchClient) FetchSessionData(credentials interface{}, sessionI
es.Search.WithTrackTotalHits(true),
)
if err != nil {
log.Fatalf("Error getting response: %s", err)
return nil, fmt.Errorf("error getting response: %s", err)
}
defer res.Body.Close()
if res.IsError() {
log.Fatalf("Error: %s", res.String())
return nil, fmt.Errorf("error: %s", res.String())
}
var r map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
log.Fatalf("Error parsing the response body: %s", err)
return nil, fmt.Errorf("error parsing the response body: %s", err)
}
if r["hits"] == nil {
return nil, fmt.Errorf("no logs found")

View file

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
)
@ -62,7 +61,7 @@ func (s *sentryClient) FetchSessionData(credentials interface{}, sessionID uint6
// Create a new request
req, err := http.NewRequest("GET", requestUrl, nil)
if err != nil {
log.Fatalf("Failed to create request: %v", err)
return nil, fmt.Errorf("failed to create request: %v", err)
}
// Add Authorization header
@ -72,26 +71,26 @@ func (s *sentryClient) FetchSessionData(credentials interface{}, sessionID uint6
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Failed to send request: %v", err)
return nil, fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()
// Check if the response status is OK
if resp.StatusCode != http.StatusOK {
log.Fatalf("Failed to fetch logs, status code: %v", resp.StatusCode)
return nil, fmt.Errorf("failed to fetch logs, status code: %v", resp.StatusCode)
}
// Read the response body
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Failed to read response body: %v", err)
return nil, fmt.Errorf("failed to read response body: %v", err)
}
// Parse the JSON response
var events []SentryEvent
err = json.Unmarshal(body, &events)
if err != nil {
log.Fatalf("Failed to parse JSON: %v", err)
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
if events == nil || len(events) == 0 {
return nil, fmt.Errorf("no logs found")

View file

@ -4,12 +4,12 @@ verify_ssl = true
name = "pypi"
[packages]
urllib3 = "==1.26.16"
urllib3 = "==2.2.3"
requests = "==2.32.3"
boto3 = "==1.35.76"
pyjwt = "==2.10.1"
psycopg2-binary = "==2.9.10"
psycopg = {extras = ["binary", "pool"], version = "==3.2.3"}
psycopg = {extras = ["pool", "binary"], version = "==3.2.3"}
clickhouse-driver = {extras = ["lz4"], version = "==0.2.9"}
clickhouse-connect = "==0.8.9"
elasticsearch = "==8.16.0"
@ -21,10 +21,10 @@ gunicorn = "==23.0.0"
python-decouple = "==3.8"
pydantic = {extras = ["email"], version = "==2.10.3"}
apscheduler = "==3.11.0"
redis = "==5.2.1"
python3-saml = "==1.16.0"
python-multipart = "==0.0.17"
redis = "==5.2.0"
azure-storage-blob = "==12.23.1"
azure-storage-blob = "==12.24.0"
[dev-packages]

View file

@ -11,8 +11,8 @@ captcha_key=
captcha_server=
CH_COMPRESSION=true
ch_host=
ch_port=
ch_port_http=
ch_port=9000
ch_port_http=8123
ch_receive_timeout=10
ch_timeout=30
change_password_link=/reset-password?invitation=%s&&pass=%s

View file

@ -1,5 +1,4 @@
# Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16
urllib3==2.2.3
requests==2.32.3
boto3==1.35.76
pyjwt==2.10.1
@ -19,4 +18,4 @@ python-decouple==3.8
pydantic[email]==2.10.3
apscheduler==3.11.0
azure-storage-blob==12.23.1
azure-storage-blob==12.24.0

View file

@ -1,5 +1,4 @@
# Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16
urllib3==2.2.3
requests==2.32.3
boto3==1.35.76
pyjwt==2.10.1
@ -19,4 +18,4 @@ pydantic[email]==2.10.3
apscheduler==3.11.0
redis==5.2.0
azure-storage-blob==12.23.1
azure-storage-blob==12.24.0

View file

@ -1,5 +1,4 @@
# Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16
urllib3==2.2.3
requests==2.32.3
boto3==1.35.76
pyjwt==2.10.1
@ -20,12 +19,13 @@ python-decouple==3.8
pydantic[email]==2.10.3
apscheduler==3.11.0
redis==5.2.1
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
#--no-binary is used to avoid libxml2 library version incompatibilities between xmlsec and lxml
python3-saml==1.16.0
--no-binary=lxml
python-multipart==0.0.17
python-multipart==0.0.18
redis==5.2.0
#confluent-kafka==2.1.0
azure-storage-blob==12.23.1
azure-storage-blob==12.24.0

View file

@ -28,6 +28,7 @@ import {
import { useStore } from 'App/mstore';
import { session as sessionRoute, withSiteId } from 'App/routes';
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
import { MobEventsList, WebEventsList } from "../../../Session_/Player/Controls/EventsList";
import useShortcuts from '../ReplayPlayer/useShortcuts';
export const SKIP_INTERVALS = {

View file

@ -91,6 +91,7 @@ function BackendLogsPanel() {
) : null}
<div className={'ml-auto'} />
<Segmented options={[{ label: 'All Tabs', value: 'all' }]} />
<Input
className="input-small h-8"
placeholder="Filter by keyword"

View file

@ -12,6 +12,7 @@ import SummaryBlock from 'Components/Session/Player/ReplayPlayer/SummaryBlock';
import { SummaryButton } from 'Components/Session_/Player/Controls/Controls';
import TimelineZoomButton from 'Components/Session_/Player/Controls/components/TimelineZoomButton';
import { Icon, NoContent } from 'UI';
import TabSelector from "../../shared/DevTools/TabSelector";
import BottomBlock from '../BottomBlock';
import EventRow from './components/EventRow';
@ -133,17 +134,66 @@ function WebOverviewPanelCont() {
'ERRORS',
'NETWORK',
]);
const globalTabs = ['FRUSTRATIONS', 'ERRORS']
const { endTime, currentTab, tabStates } = store.get();
const stackEventList = tabStates[currentTab]?.stackList || [];
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 tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const {
stackEventList = [],
frustrationsList = [],
exceptionsList = [],
resourceListUnmap = [],
fetchList = [],
graphqlList = [],
performanceChartData = [],
} = React.useMemo(() => {
if (showSingleTab) {
const stackEventList = tabStates[currentTab].stackList;
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;
return {
stackEventList,
frustrationsList,
exceptionsList,
resourceListUnmap,
fetchList,
graphqlList,
performanceChartData,
}
} else {
const stackEventList = tabValues.flatMap((tab) => tab.stackList);
// these two are global
const frustrationsList = tabValues[0].frustrationsList;
const exceptionsList = tabValues[0].exceptionsList;
// we can't compute global chart data because some tabs coexist
const performanceChartData: any = [];
const resourceListUnmap = tabValues.flatMap((tab) => tab.resourceList);
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
const graphqlList = tabValues.flatMap((tab) => tab.graphqlList);
return {
stackEventList,
frustrationsList,
exceptionsList,
resourceListUnmap,
fetchList,
graphqlList,
performanceChartData,
}
}
}, [tabStates, currentTab, dataSource, tabValues]);
console.log(showSingleTab, frustrationsList, performanceChartData);
const fetchPresented = fetchList.length > 0;
const resourceList = resourceListUnmap
@ -168,7 +218,18 @@ function WebOverviewPanelCont() {
PERFORMANCE: checkInZoomRange(performanceChartData),
FRUSTRATIONS: checkInZoomRange(frustrationsList),
};
}, [tabStates, currentTab, zoomEnabled, zoomStartTs, zoomEndTs]);
}, [
tabStates,
currentTab,
zoomEnabled,
zoomStartTs,
zoomEndTs,
resourceList.length,
exceptionsList.length,
stackEventList.length,
performanceChartData.length,
frustrationsList.length,
]);
const originStr = window.env.ORIGIN || window.location.origin;
const isSaas = /app\.openreplay\.com/.test(originStr);
@ -187,6 +248,7 @@ function WebOverviewPanelCont() {
sessionId={sessionId}
setZoomTab={setZoomTab}
zoomTab={zoomTab}
showSingleTab={showSingleTab}
/>
);
}
@ -238,6 +300,7 @@ function PanelComponent({
spotTime,
spotEndTime,
onClose,
showSingleTab,
}: any) {
return (
<React.Fragment>
@ -281,6 +344,7 @@ function PanelComponent({
</div>
{isSpot ? null : (
<div className="flex items-center h-20 mr-4 gap-2">
<TabSelector />
<TimelineZoomButton />
<FeatureSelection
list={selectedFeatures}
@ -318,6 +382,7 @@ function PanelComponent({
<EventRow
isGraph={feature === 'PERFORMANCE'}
title={feature}
disabled={!showSingleTab}
list={resources[feature]}
renderElement={(pointer: any[], isGrouped: boolean) => (
<TimelinePointer

View file

@ -13,9 +13,10 @@ interface Props {
isGraph?: boolean;
zIndex?: number;
noMargin?: boolean;
disabled?: boolean;
}
const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0, isGraph = false, message = '' } = props;
const { title, className, list = [], endTime = 0, isGraph = false, message = '', disabled } = props;
const scale = 100 / endTime;
const _list =
isGraph ? [] :
@ -82,7 +83,7 @@ const EventRow = React.memo((props: Props) => {
}
return groupedItems;
}, [list]);
}, [list.length]);
return (
<div
@ -105,7 +106,7 @@ const EventRow = React.memo((props: Props) => {
</div>
<div className="relative w-full" style={{ zIndex: props.zIndex ? props.zIndex : undefined }}>
{isGraph ? (
<PerformanceGraph list={list} />
<PerformanceGraph disabled={disabled} list={list} />
) : _list.length > 0 ? (
_list.map((item: { items: any[], left: number, isGrouped: boolean }, index: number) => {
const left = item.left

View file

@ -2,81 +2,103 @@ import React from 'react';
import { AreaChart, Area, ResponsiveContainer } from 'recharts';
interface Props {
list: any;
list: any;
disabled?: boolean;
}
const PerformanceGraph = React.memo((props: Props) => {
const { list } = props;
const { list, disabled } = props;
const finalValues = React.useMemo(() => {
const cpuMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.cpu);
}, 0);
const cpuMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.cpu);
}, Infinity);
const finalValues = React.useMemo(() => {
const cpuMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.cpu);
}, 0);
const cpuMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.cpu);
}, Infinity);
const memoryMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.usedHeap);
}, Infinity);
const memoryMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.usedHeap);
}, 0);
const memoryMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.usedHeap);
}, Infinity);
const memoryMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.usedHeap);
}, 0);
const convertToPercentage = (val: number, max: number, min: number) => {
return ((val - min) / (max - min)) * 100;
};
const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin));
const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin));
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
result.push(num > 60 ? num : 1);
}
return result;
};
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
return finalValues;
}, []);
const data = list.map((item: any, index: number) => {
return {
time: item.time,
cpu: finalValues[index],
};
});
return (
<ResponsiveContainer height={35}>
<AreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient id="cpuGradientTimeline" x1="0" y1="0" x2="0" y2="1">
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
</linearGradient>
</defs>
{/* <Tooltip filterNull={false} /> */}
<Area
dataKey="cpu"
baseValue={5}
type="monotone"
stroke="none"
activeDot={false}
fill="url(#cpuGradientTimeline)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
const convertToPercentage = (val: number, max: number, min: number) => {
return ((val - min) / (max - min)) * 100;
};
const cpuValues = list.map((item: any) =>
convertToPercentage(item.cpu, cpuMax, cpuMin)
);
const memoryValues = list.map((item: any) =>
convertToPercentage(item.usedHeap, memoryMax, memoryMin)
);
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
result.push(num > 60 ? num : 1);
}
return result;
};
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
return finalValues;
}, [list.length]);
const data = list.map((item: any, index: number) => {
return {
time: item.time,
cpu: finalValues[index],
};
});
return (
<div className={'relative'}>
{disabled ? (
<div
className={
'absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center'
}
>
<div className={'text-disabled-text decoration-dotted'}>Disabled for all tabs</div>
</div>
) : null}
<ResponsiveContainer height={35}>
<AreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient
id="cpuGradientTimeline"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
</linearGradient>
</defs>
{/* <Tooltip filterNull={false} /> */}
<Area
dataKey="cpu"
baseValue={5}
type="monotone"
stroke="none"
activeDot={false}
fill="url(#cpuGradientTimeline)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
});
export default PerformanceGraph;

View file

@ -168,7 +168,7 @@ function GroupedIssue({
<div
onClick={onClick}
className={
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-sm'
'h-5 w-5 cursor-pointer rounded-full bg-red text-white font-bold flex items-center justify-center text-xs'
}
>
{items.length}

View file

@ -23,6 +23,7 @@ import stl from './performance.module.css';
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import { useStore } from 'App/mstore'
import { Segmented } from 'antd'
const CPU_VISUAL_OFFSET = 10;
@ -459,13 +460,16 @@ function Performance() {
<BottomBlock.Header>
<div className="flex items-center w-full">
<div className="font-semibold color-gray-medium mr-auto">Performance</div>
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
</InfoLine>
<div className={'flex items-center gap-2'}>
<Segmented options={[{ label: 'Current Tab', value: 'all' }]} />
<InfoLine>
<InfoLine.Point
label="Device Heap Size"
value={formatBytes(userDeviceHeapSize)}
display={true}
/>
</InfoLine>
</div>
</div>
</BottomBlock.Header>
<BottomBlock.Content>

View file

@ -34,6 +34,7 @@ import { Icon } from 'UI';
import LogsButton from 'App/components/Session/Player/SharedComponents/BackendLogs/LogsButton';
import ControlButton from './ControlButton';
import { WebEventsList } from "./EventsList";
import Timeline from './Timeline';
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';

View file

@ -4,10 +4,12 @@ import { PlayerContext, MobilePlayerContext } from 'Components/Session/playerCon
import { observer } from 'mobx-react-lite';
import { getTimelinePosition } from './getTimelinePosition'
function EventsList({ scale }: { scale: number }) {
function EventsList() {
const { store } = useContext(PlayerContext);
const { tabStates, eventCount } = store.get();
const { eventCount, endTime } = store.get();
const tabStates = store.get().tabStates;
const scale = 100 / endTime;
const events = React.useMemo(() => {
return Object.values(tabStates)[0]?.eventList.filter(e => e.time) || [];
}, [eventCount]);
@ -34,11 +36,12 @@ function EventsList({ scale }: { scale: number }) {
);
}
function MobileEventsList({ scale }: { scale: number }) {
function MobileEventsList() {
const { store } = useContext(MobilePlayerContext);
const { eventList } = store.get();
const { eventList, endTime } = store.get();
const events = eventList.filter(e => e.type !== 'SWIPE')
const scale = 100/endTime;
return (
<>
{events.map((e) => (

View file

@ -13,11 +13,7 @@ import NotesList from './NotesList';
import SkipIntervalsList from './SkipIntervalsList';
import TimelineTracker from 'Components/Session_/Player/Controls/TimelineTracker';
interface IProps {
isMobile?: boolean;
}
function Timeline(props: IProps) {
function Timeline({ isMobile }: { isMobile: boolean }) {
const { player, store } = useContext(PlayerContext);
const [wasPlaying, setWasPlaying] = useState(false);
const [maxWidth, setMaxWidth] = useState(0);
@ -126,6 +122,7 @@ function Timeline(props: IProps) {
return Math.max(Math.round(p * targetTime), 0);
};
console.log(devtoolsLoading , domLoading, !ready)
return (
<div
className="flex items-center absolute w-full"
@ -158,7 +155,7 @@ function Timeline(props: IProps) {
{devtoolsLoading || domLoading || !ready ? <div className={stl.stripes} /> : null}
</div>
{props.isMobile ? <MobEventsList scale={scale} /> : <WebEventsList scale={scale} />}
{isMobile ? <MobEventsList /> : <WebEventsList />}
<NotesList scale={scale} />
<SkipIntervalsList scale={scale} />

View file

@ -1,18 +1,24 @@
import React from 'react';
import { useStore } from 'App/mstore'
import { useStore } from 'App/mstore';
import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { JSONTree, NoContent, Tooltip } from 'UI';
import { formatMs } from 'App/date';
import diff from 'microdiff'
import { STORAGE_TYPES, selectStorageList, selectStorageListNow, selectStorageType } from 'Player';
import diff from 'microdiff';
import {
STORAGE_TYPES,
selectStorageList,
selectStorageListNow,
selectStorageType,
} from 'Player';
import Autoscroll from '../Autoscroll';
import BottomBlock from '../BottomBlock/index';
import DiffRow from './DiffRow';
import cn from 'classnames';
import stl from './storage.module.css';
import logger from "App/logger";
import ReduxViewer from './ReduxViewer'
import logger from 'App/logger';
import ReduxViewer from './ReduxViewer';
import { Segmented } from 'antd'
function getActionsName(type: string) {
switch (type) {
@ -31,7 +37,7 @@ const storageDecodeKeys = {
[STORAGE_TYPES.ZUSTAND]: ['state', 'mutation'],
[STORAGE_TYPES.MOBX]: ['payload'],
[STORAGE_TYPES.NONE]: ['state, action', 'payload', 'mutation'],
}
};
function Storage() {
const { uiPlayerStore } = useStore();
@ -42,49 +48,48 @@ function Storage() {
const [stateObject, setState] = React.useState({});
const { player, store } = React.useContext(PlayerContext);
const { tabStates, currentTab } = store.get()
const state = tabStates[currentTab] || {}
const { tabStates, currentTab } = store.get();
const state = tabStates[currentTab] || {};
const listNow = selectStorageListNow(state) || [];
const list = selectStorageList(state) || [];
const type = selectStorageType(state) || STORAGE_TYPES.NONE
const type = selectStorageType(state) || STORAGE_TYPES.NONE;
React.useEffect(() => {
let currentState;
if (listNow.length === 0) {
currentState = decodeMessage(list[0])
currentState = decodeMessage(list[0]);
} else {
currentState = decodeMessage(listNow[listNow.length - 1])
currentState = decodeMessage(listNow[listNow.length - 1]);
}
const stateObj = currentState?.state || currentState?.payload?.state || {}
const stateObj = currentState?.state || currentState?.payload?.state || {};
const newState = Object.assign(stateObject, stateObj);
setState(newState);
}, [listNow.length]);
const decodeMessage = (msg: any) => {
const decoded = {};
const pureMSG = { ...msg }
const pureMSG = { ...msg };
const keys = storageDecodeKeys[type];
try {
keys.forEach(key => {
keys.forEach((key) => {
if (pureMSG[key]) {
// @ts-ignore TODO: types for decoder
decoded[key] = player.decodeMessage(pureMSG[key]);
}
});
} catch (e) {
logger.error("Error on message decoding: ", e, pureMSG);
logger.error('Error on message decoding: ', e, pureMSG);
return null;
}
return { ...pureMSG, ...decoded };
}
};
const decodedList = React.useMemo(() => {
return listNow.map(msg => {
return decodeMessage(msg)
})
}, [listNow.length])
return listNow.map((msg) => {
return decodeMessage(msg);
});
}, [listNow.length]);
const focusNextButton = () => {
if (lastBtnRef.current) {
@ -99,7 +104,10 @@ function Storage() {
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;
}
@ -113,7 +121,10 @@ function Storage() {
if (!stateDiff) {
return (
<div style={{ flex: 3 }} className="flex flex-col p-2 pr-0 font-mono text-disabled-text">
<div
style={{ flex: 3 }}
className="flex flex-col p-2 pr-0 font-mono text-disabled-text"
>
No diff
</div>
);
@ -121,13 +132,15 @@ function Storage() {
return (
<div style={{ flex: 3 }} className="flex flex-col p-1 font-mono">
{stateDiff.map((d: Record<string, any>, i: number) => renderDiffs(d, i))}
{stateDiff.map((d: Record<string, any>, i: number) =>
renderDiffs(d, i)
)}
</div>
);
};
const renderDiffs = (diff: Record<string, any>, i: number) => {
const path = diff.path.join('.')
const path = diff.path.join('.');
return (
<React.Fragment key={i}>
<DiffRow path={path} diff={diff} />
@ -145,12 +158,16 @@ function Storage() {
player.jump(list[listNow.length].time);
};
const renderItem = (item: Record<string, any>, i: number, prevItem?: Record<string, any>) => {
const renderItem = (
item: Record<string, any>,
i: number,
prevItem?: Record<string, any>
) => {
let src;
let name;
const itemD = item
const prevItemD = prevItem ? prevItem : undefined
const itemD = item;
const prevItemD = prevItem ? prevItem : undefined;
switch (type) {
case STORAGE_TYPES.REDUX:
@ -177,7 +194,10 @@ function Storage() {
return (
<div
className={cn('flex justify-between items-start', src !== null ? 'border-b' : '')}
className={cn(
'flex justify-between items-start',
src !== null ? 'border-b' : ''
)}
key={`store-${i}`}
>
{src === null ? (
@ -187,7 +207,10 @@ function Storage() {
) : (
<>
{renderDiff(itemD, prevItemD)}
<div style={{ flex: 2 }} className={cn("flex pt-2", showDiffs && 'pl-10')}>
<div
style={{ flex: 2 }}
className={cn('flex pt-2', showDiffs && 'pl-10')}
>
<JSONTree
name={ensureString(name)}
src={src}
@ -202,11 +225,16 @@ function Storage() {
className="flex-1 flex gap-2 pt-2 items-center justify-end self-start"
>
{typeof item?.duration === 'number' && (
<div className="font-size-12 color-gray-medium">{formatMs(itemD.duration)}</div>
<div className="font-size-12 color-gray-medium">
{formatMs(itemD.duration)}
</div>
)}
<div className="w-12">
{i + 1 < listNow.length && (
<button className={stl.button} onClick={() => player.jump(item.time)}>
<button
className={stl.button}
onClick={() => player.jump(item.time)}
>
{'JUMP'}
</button>
)}
@ -222,31 +250,36 @@ function Storage() {
};
if (type === STORAGE_TYPES.REDUX) {
return <ReduxViewer />
return <ReduxViewer />;
}
return (
<BottomBlock>
{/*@ts-ignore*/}
<>
<BottomBlock.Header>
{list.length > 0 && (
<div className="flex w-full">
<h3 style={{ width: '25%', marginRight: 20 }} className="font-semibold">
{'STATE'}
</h3>
{showDiffs ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3 style={{ paddingRight: 30, marginLeft: 'auto' }} className="font-semibold">
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
<div className="flex w-full items-center">
<div
style={{ width: '25%', marginRight: 20 }}
className="font-semibold flex items-center gap-2"
>
<h3>{'STATE'}</h3>
</div>
)}
{showDiffs ? (
<h3 style={{ width: '39%' }} className="font-semibold">
DIFFS
</h3>
) : null}
<h3 style={{ width: '30%' }} className="font-semibold">
{getActionsName(type)}
</h3>
<h3
style={{ paddingRight: 30, marginLeft: 'auto' }}
className="font-semibold"
>
<Tooltip title="Time to execute">TTE</Tooltip>
</h3>
<Segmented options={[{ label: 'Current Tab', value: 'all' }]} />
</div>
</BottomBlock.Header>
<BottomBlock.Content className="flex">
<NoContent
@ -307,7 +340,10 @@ function Storage() {
.
<br />
<br />
<button className="color-teal" onClick={() => hideHint('storage')}>
<button
className="color-teal"
onClick={() => hideHint('storage')}
>
Got It!
</button>
</>
@ -322,8 +358,7 @@ function Storage() {
{'Empty state.'}
</div>
) : (
<JSONTree collapsed={2} src={stateObject}
/>
<JSONTree collapsed={2} src={stateObject} />
)}
</div>
<div className="flex" style={{ width: '75%' }}>
@ -342,7 +377,6 @@ function Storage() {
export default observer(Storage);
/**
* TODO: compute diff and only decode the required parts
* WIP example
@ -384,4 +418,4 @@ export default observer(Storage);
* }, [list.length])
* }
*
* */
* */

View file

@ -9,6 +9,7 @@ import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
import { useModal } from 'App/components/Modal';
import TabSelector from "../TabSelector";
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import { VList, VListHandle } from "virtua";
@ -93,6 +94,7 @@ function ConsolePanel({
sessionStore: { devTools },
uiPlayerStore,
} = useStore();
const zoomEnabled = uiPlayerStore.timelineZoom.enabled;
const zoomStartTs = uiPlayerStore.timelineZoom.startTs;
const zoomEndTs = uiPlayerStore.timelineZoom.endTs;
@ -109,12 +111,22 @@ function ConsolePanel({
const jump = (t: number) => player.jump(t);
const { currentTab, tabStates } = store.get();
const {
logList = [],
exceptionsList = [],
logListNow = [],
exceptionsListNow = [],
} = tabStates[currentTab] ?? {};
const tabsArr = Object.keys(tabStates);
const tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const { logList = [], exceptionsList = [], logListNow = [], exceptionsListNow = [] } = React.useMemo(() => {
if (showSingleTab) {
return tabStates[currentTab] ?? {};
} else {
const logList = tabValues.flatMap(tab => tab.logList);
const exceptionsList = tabValues.flatMap(tab => tab.exceptionsList);
const logListNow = isLive ? tabValues.flatMap(tab => tab.logListNow) : [];
const exceptionsListNow = isLive ? tabValues.flatMap(tab => tab.exceptionsListNow) : [];
return { logList, exceptionsList, logListNow, exceptionsListNow }
}
}, [currentTab, tabStates, dataSource, tabValues, isLive])
const getTabNum = (tab: string) => (tabsArr.findIndex((t) => t === tab) + 1);
const list = isLive
? (useMemo(
@ -180,15 +192,18 @@ function ConsolePanel({
<span className="font-semibold color-gray-medium mr-4">Console</span>
<Tabs tabs={TABS} active={activeTab} onClick={onTabClick} border={false} />
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
<div className={'flex items-center gap-2'}>
<TabSelector />
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
</div>
{/* @ts-ignore */}
</BottomBlock.Header>
{/* @ts-ignore */}
@ -211,6 +226,8 @@ function ConsolePanel({
iconProps={getIconProps(log.level)}
renderWithNL={renderWithNL}
onClick={() => showDetails(log)}
showSingleTab={showSingleTab}
getTabNum={getTabNum}
/>
))}
</VList>

View file

@ -2,6 +2,8 @@ import React, { useState } from 'react';
import cn from 'classnames';
import { Icon } from 'UI';
import JumpButton from 'Shared/DevTools/JumpButton';
import { Tag } from 'antd';
import TabTag from "../TabTag";
interface Props {
log: any;
@ -10,6 +12,8 @@ interface Props {
renderWithNL?: any;
style?: any;
onClick?: () => void;
getTabNum: (tab: string) => number;
showSingleTab: boolean;
}
function ConsoleRow(props: Props) {
const { log, iconProps, jump, renderWithNL, style } = props;
@ -41,11 +45,12 @@ function ConsoleRow(props: Props) {
const titleLine = lines[0];
const restLines = lines.slice(1);
const logSource = props.showSingleTab ? -1 : props.getTabNum(log.tabId);
return (
<div
style={style}
className={cn(
'border-b flex items-start py-1 px-4 pe-8 overflow-hidden group relative',
'border-b flex items-start gap-1 py-1 px-4 pe-8 overflow-hidden group relative',
{
info: !log.isYellow && !log.isRed,
warn: log.isYellow,
@ -55,11 +60,10 @@ function ConsoleRow(props: Props) {
)}
onClick={clickable ? () => (!!log.errorId ? props.onClick?.() : toggleExpand()) : undefined}
>
<div className="mr-2">
<Icon size="14" {...iconProps} />
</div>
{logSource !== -1 && <TabTag tabNum={logSource} />}
<Icon size="14" {...iconProps} />
<div key={log.key} data-scroll-item={log.isRed}>
<div className="flex items-start text-sm ">
<div className="flex items-start text-sm">
<div className={cn('flex items-start', { 'cursor-pointer underline decoration-dotted decoration-gray-400': !!log.errorId })}>
{canExpand && (
<Icon name={expanded ? 'caret-down-fill' : 'caret-right-fill'} className="mr-2" />

View file

@ -1,7 +1,7 @@
import { ResourceType, Timed } from 'Player';
import MobilePlayer from 'Player/mobile/IOSPlayer';
import WebPlayer from 'Player/web/WebPlayer';
import { Duration } from 'luxon';
import TabTag from "../TabTag";
import { observer } from 'mobx-react-lite';
import React, { useMemo, useState } from 'react';
@ -20,10 +20,10 @@ import { WsChannel } from "App/player/web/messages";
import BottomBlock from '../BottomBlock';
import InfoLine from '../BottomBlock/InfoLine';
import TabSelector from "../TabSelector";
import TimeTable from '../TimeTable';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import WSModal from './WSModal';
import WSPanel from './WSPanel';
const INDEX_KEY = 'network';
@ -57,12 +57,6 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
const DOM_LOADED_TIME_COLOR = 'teal';
const LOAD_TIME_COLOR = 'red';
function compare(a: any, b: any, key: string) {
if (a[key] > b[key]) return 1;
if (a[key] < b[key]) return -1;
return 0;
}
export function renderType(r: any) {
return (
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
@ -79,14 +73,6 @@ export function renderName(r: any) {
);
}
export function renderStart(r: any) {
return (
<div className="flex justify-between items-center grow-0 w-full">
<span>{Duration.fromMillis(r.time).toFormat('mm:ss.SSS')}</span>
</div>
);
}
function renderSize(r: any) {
if (r.responseBodySize) return formatBytes(r.responseBodySize);
let triggerText;
@ -125,13 +111,10 @@ export function renderDuration(r: any) {
if (!r.isRed && !r.isYellow) return text;
let tooltipText;
let className = 'w-full h-full flex items-center ';
if (r.isYellow) {
tooltipText = 'Slower than average';
className += 'warn color-orange';
} else {
tooltipText = 'Much slower than average';
className += 'error color-red';
}
return (
@ -184,7 +167,8 @@ function NetworkPanelCont({
panelHeight: number;
}) {
const { player, store } = React.useContext(PlayerContext);
const { sessionStore } = useStore();
const { sessionStore, uiPlayerStore } = useStore();
const startedAt = sessionStore.current.startedAt;
const {
domContentLoadedTime,
@ -193,6 +177,10 @@ function NetworkPanelCont({
tabStates,
currentTab,
} = store.get();
const tabsArr = Object.keys(tabStates);
const tabValues = Object.values(tabStates);
const dataSource = uiPlayerStore.dataSource;
const showSingleTab = dataSource === 'current';
const {
fetchList = [],
resourceList = [],
@ -200,7 +188,20 @@ function NetworkPanelCont({
resourceListNow = [],
websocketList = [],
websocketListNow = [],
} = tabStates[currentTab];
} = React.useMemo(() => {
if (showSingleTab) {
return tabStates[currentTab] ?? {};
} else {
const fetchList = tabValues.flatMap((tab) => tab.fetchList);
const resourceList = tabValues.flatMap((tab) => tab.resourceList);
const fetchListNow = tabValues.flatMap((tab) => tab.fetchListNow).filter(Boolean);
const resourceListNow = tabValues.flatMap((tab) => tab.resourceListNow).filter(Boolean);
const websocketList = tabValues.flatMap((tab) => tab.websocketList);
const websocketListNow = tabValues.flatMap((tab) => tab.websocketListNow).filter(Boolean);
return { fetchList, resourceList, fetchListNow, resourceListNow, websocketList, websocketListNow };
}
}, [currentTab, tabStates, dataSource, tabValues]);
const getTabNum = (tab: string) => (tabsArr.findIndex((t) => t === tab) + 1);
return (
<NetworkPanelComp
@ -216,6 +217,8 @@ function NetworkPanelCont({
startedAt={startedAt}
websocketList={websocketList as WSMessage[]}
websocketListNow={websocketListNow as WSMessage[]}
getTabNum={getTabNum}
showSingleTab={showSingleTab}
/>
);
}
@ -301,6 +304,8 @@ interface Props {
onClose?: () => void;
activeOutsideIndex?: number;
isSpot?: boolean;
getTabNum?: (tab: string) => number;
showSingleTab?: boolean;
}
export const NetworkPanelComp = observer(
@ -323,6 +328,8 @@ export const NetworkPanelComp = observer(
onClose,
activeOutsideIndex,
isSpot,
getTabNum,
showSingleTab,
}: Props) => {
const [selectedWsChannel, setSelectedWsChannel] = React.useState<WsChannel[] | null>(null)
const { showModal } = useModal();
@ -507,6 +514,55 @@ export const NetworkPanelComp = observer(
stopAutoscroll();
};
const tableCols = React.useMemo(() => {
const cols: any[] = [
{
label: 'Status',
dataKey: 'status',
width: 90,
render: renderStatus,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Method',
width: 80,
dataKey: 'method',
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
hidden: activeTab === XHR,
},
{
label: 'Duration',
width: 80,
dataKey: 'duration',
render: renderDuration,
},
]
if (!showSingleTab) {
cols.unshift({
label: 'Source',
width: 64,
render: (r: Record<string, any>) => <div>Tab {getTabNum?.(r.tabId) ?? 0}</div>,
})
}
return cols
}, [showSingleTab])
return (
<BottomBlock
style={{ height: '100%' }}
@ -529,16 +585,19 @@ export const NetworkPanelComp = observer(
/>
)}
</div>
<Input
className="input-small"
placeholder="Filter by name, type, method or value"
icon="search"
name="filter"
onChange={onFilterChange}
height={28}
width={280}
value={filter}
/>
<div className={'flex items-center gap-2'}>
<TabSelector />
<Input
className="input-small"
placeholder="Filter by name, type, method or value"
icon="search"
name="filter"
onChange={onFilterChange}
height={28}
width={280}
value={filter}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
@ -613,49 +672,7 @@ export const NetworkPanelComp = observer(
}}
activeIndex={activeIndex}
>
{[
// {
// label: 'Start',
// width: 120,
// render: renderStart,
// },
{
label: 'Status',
dataKey: 'status',
width: 90,
render: renderStatus,
},
{
label: 'Type',
dataKey: 'type',
width: 90,
render: renderType,
},
{
label: 'Method',
width: 80,
dataKey: 'method',
},
{
label: 'Name',
width: 240,
dataKey: 'name',
render: renderName,
},
{
label: 'Size',
width: 80,
dataKey: 'decodedBodySize',
render: renderSize,
hidden: activeTab === XHR,
},
{
label: 'Duration',
width: 80,
dataKey: 'duration',
render: renderDuration,
},
]}
{tableCols}
</TimeTable>
{selectedWsChannel ? (
<WSPanel socketMsgList={selectedWsChannel} onClose={() => setSelectedWsChannel(null)} />

View file

@ -10,6 +10,7 @@ import { typeList } from 'Types/session/stackEvent';
import StackEventRow from 'Shared/DevTools/StackEventRow';
import StackEventModal from '../StackEventModal';
import { Segmented } from 'antd'
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
import { VList, VListHandle } from 'virtua';
@ -175,15 +176,18 @@ const EventsPanel = observer(({
border={false}
/>
</div>
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
<div className={'flex items-center gap-2'}>
<Segmented options={[{ label: 'All Tabs', value: 'all' }]} />
<Input
className="input-small h-8"
placeholder="Filter by keyword"
icon="search"
name="filter"
height={28}
onChange={onFilterChange}
value={filter}
/>
</div>
</BottomBlock.Header>
<BottomBlock.Content className="overflow-y-auto">
<NoContent

View file

@ -0,0 +1,22 @@
import React from 'react'
import { Segmented } from 'antd'
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
function TabSelector() {
const { uiPlayerStore } = useStore();
const currentValue = uiPlayerStore.dataSource;
const options = [
{ label: 'All Tabs', value: 'all' },
{ label: 'Current Tab', value: 'current' }
]
const onChange = (value: 'all' | 'current') => {
uiPlayerStore.changeDataSource(value)
}
return (
<Segmented options={options} value={currentValue} onChange={onChange} />
)
}
export default observer(TabSelector)

View file

@ -0,0 +1,11 @@
import React from 'react'
function TabTag({ tabNum }: { tabNum?: React.ReactNode }) {
return (
<div className={'w-fit px-2 border border-gray-light rounded text-sm whitespace-nowrap'}>
{tabNum}
</div>
)
}
export default TabTag

View file

@ -66,11 +66,16 @@ export default class UiPlayerStore {
endTs: 0,
}
zoomTab: 'overview' | 'journey' | 'issues' | 'errors' = 'overview'
dataSource: 'all' | 'current' = 'all'
constructor() {
makeAutoObservable(this);
}
changeDataSource = (source: 'all' | 'current') => {
this.dataSource = source;
}
toggleFullscreen = (val?: boolean) => {
this.fullscreen = val ?? !this.fullscreen;
}

View file

@ -1,13 +1,18 @@
import { Store } from './types'
export default class SimpleSore<G extends Object, S extends Object = G> implements Store<G, S> {
export default class SimpleStore<G extends Record<string, any>, S extends Record<string, any> = G> implements Store<G, S> {
constructor(private state: G){}
get(): G {
return this.state
}
update(newState: Partial<S>) {
update = (newState: Partial<S>) => {
Object.assign(this.state, newState)
}
updateTabStates = (id: string, newState: Partial<S>) => {
try {
Object.assign(this.state.tabStates[id], newState)
} catch (e) {
console.log('Error updating tab state', e, id, newState, this.state, this)
}
}
}

View file

@ -27,6 +27,7 @@ export interface Interval {
export interface Store<G extends Object, S extends Object = G> {
get(): G
update(state: Partial<S>): void
updateTabStates(id: string, state: Partial<S>): void
}

View file

@ -236,6 +236,7 @@ export default class MessageLoader {
try {
await this.loadMobs();
} catch (sessionLoadError) {
console.info('!', sessionLoadError);
try {
await this.loadEFSMobs();
} catch (unprocessedLoadError) {

View file

@ -99,7 +99,7 @@ export default class MessageManager {
closedTabs: [],
sessionStart: 0,
tabNames: {},
};
};
private clickManager: ListWalker<MouseClick> = new ListWalker();
private mouseThrashingManager: ListWalker<MouseThrashing> = new ListWalker();
@ -179,6 +179,7 @@ export default class MessageManager {
this.activityManager.end();
this.state.update({ skipIntervals: this.activityManager.list });
}
Object.values(this.tabs).forEach((tab) => tab.onFileReadSuccess?.());
};
@ -317,6 +318,7 @@ export default class MessageManager {
if (msg.tp === 9999) return;
if (!this.tabs[msg.tabId]) {
this.tabsAmount++;
this.state.update({ tabStates: { ...this.state.get().tabStates, [msg.tabId]: TabSessionManager.INITIAL_STATE } });
this.tabs[msg.tabId] = new TabSessionManager(
this.session,
this.state,

View file

@ -163,15 +163,7 @@ export default class TabSessionManager {
* Because we use main state (from messageManager), we have to update it this way
* */
updateLocalState(state: Partial<TabState>) {
this.state.update({
tabStates: {
...this.state.get().tabStates,
[this.id]: {
...this.state.get().tabStates[this.id],
...state,
},
},
});
this.state.updateTabStates(this.id, state);
}
private setCSSLoading = (cssLoading: boolean) => {
@ -414,8 +406,9 @@ export default class TabSessionManager {
}
Object.assign(stateToUpdate, this.lists.moveGetState(t));
Object.keys(stateToUpdate).length > 0 &&
if (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);

View file

@ -29,6 +29,7 @@ clickhouse: &clickhouse
password: ""
service:
webPort: 9000
dataPort: 8123
# For enterpriseEdition
quickwit: &quickwit