diff --git a/api/Pipfile b/api/Pipfile index d5c52995c..ffd5906f5 100644 --- a/api/Pipfile +++ b/api/Pipfile @@ -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] diff --git a/api/auth/auth_jwt.py b/api/auth/auth_jwt.py index fd4d145b1..2e30e6975 100644 --- a/api/auth/auth_jwt.py +++ b/api/auth/auth_jwt.py @@ -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, diff --git a/api/chalicelib/core/custom_metrics.py b/api/chalicelib/core/custom_metrics.py index c3ac00b69..01f04803a 100644 --- a/api/chalicelib/core/custom_metrics.py +++ b/api/chalicelib/core/custom_metrics.py @@ -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: diff --git a/api/chalicelib/utils/ch_client_exp.py b/api/chalicelib/utils/ch_client_exp.py index edb666d09..8bdb4c20b 100644 --- a/api/chalicelib/utils/ch_client_exp.py +++ b/api/chalicelib/utils/ch_client_exp.py @@ -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() diff --git a/api/chalicelib/utils/pg_client.py b/api/chalicelib/utils/pg_client.py index b97ab005e..29ea84873 100644 --- a/api/chalicelib/utils/pg_client.py +++ b/api/chalicelib/utils/pg_client.py @@ -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() diff --git a/api/env.default b/api/env.default index f6ca1872b..4e52e5c57 100644 --- a/api/env.default +++ b/api/env.default @@ -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 diff --git a/api/requirements-alerts.txt b/api/requirements-alerts.txt index 4f356e46c..ee8bbc950 100644 --- a/api/requirements-alerts.txt +++ b/api/requirements-alerts.txt @@ -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 diff --git a/api/requirements.txt b/api/requirements.txt index 943a36e40..d643061f1 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -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 diff --git a/backend/pkg/integrations/clients/elastic.go b/backend/pkg/integrations/clients/elastic.go index e854860b3..2d3ffc4e2 100644 --- a/backend/pkg/integrations/clients/elastic.go +++ b/backend/pkg/integrations/clients/elastic.go @@ -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") diff --git a/backend/pkg/integrations/clients/sentry.go b/backend/pkg/integrations/clients/sentry.go index a115e9558..99ddc995c 100644 --- a/backend/pkg/integrations/clients/sentry.go +++ b/backend/pkg/integrations/clients/sentry.go @@ -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") diff --git a/ee/api/Pipfile b/ee/api/Pipfile index ee2617767..a32b99a8e 100644 --- a/ee/api/Pipfile +++ b/ee/api/Pipfile @@ -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] diff --git a/ee/api/env.default b/ee/api/env.default index 9d30b0e0b..527bb56e3 100644 --- a/ee/api/env.default +++ b/ee/api/env.default @@ -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 diff --git a/ee/api/requirements-alerts.txt b/ee/api/requirements-alerts.txt index ebf1aea0d..adfc42bba 100644 --- a/ee/api/requirements-alerts.txt +++ b/ee/api/requirements-alerts.txt @@ -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 \ No newline at end of file +azure-storage-blob==12.24.0 \ No newline at end of file diff --git a/ee/api/requirements-crons.txt b/ee/api/requirements-crons.txt index 6aa2370d8..68c115f1c 100644 --- a/ee/api/requirements-crons.txt +++ b/ee/api/requirements-crons.txt @@ -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 diff --git a/ee/api/requirements.txt b/ee/api/requirements.txt index 0c0b2902a..538364050 100644 --- a/ee/api/requirements.txt +++ b/ee/api/requirements.txt @@ -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 diff --git a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx index 78f9d47d4..275f3e6e2 100644 --- a/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx +++ b/frontend/app/components/Session/Player/MobilePlayer/MobileControls.tsx @@ -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 = { diff --git a/frontend/app/components/Session/Player/SharedComponents/BackendLogs/BackendLogsPanel.tsx b/frontend/app/components/Session/Player/SharedComponents/BackendLogs/BackendLogsPanel.tsx index bc3880dc7..45807f5b2 100644 --- a/frontend/app/components/Session/Player/SharedComponents/BackendLogs/BackendLogsPanel.tsx +++ b/frontend/app/components/Session/Player/SharedComponents/BackendLogs/BackendLogsPanel.tsx @@ -91,6 +91,7 @@ function BackendLogsPanel() { ) : null}
+ { + 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 ( @@ -281,6 +344,7 @@ function PanelComponent({
{isSpot ? null : (
+ ( { - 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 (
{
{isGraph ? ( - + ) : _list.length > 0 ? ( _list.map((item: { items: any[], left: number, isGrouped: boolean }, index: number) => { const left = item.left diff --git a/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx b/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx index 2e719f377..f46d89d60 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/PerformanceGraph/PerformanceGraph.tsx @@ -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 ( - - - - - - - - - {/* */} - - - + 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 ( +
+ {disabled ? ( +
+
Disabled for all tabs
+
+ ) : null} + + + + + + + + + {/* */} + + + +
+ ); }); export default PerformanceGraph; diff --git a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx index cc7f13f63..4518ddf1c 100644 --- a/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx +++ b/frontend/app/components/Session_/OverviewPanel/components/TimelinePointer/TimelinePointer.tsx @@ -168,7 +168,7 @@ function GroupedIssue({
{items.length} diff --git a/frontend/app/components/Session_/Performance/Performance.tsx b/frontend/app/components/Session_/Performance/Performance.tsx index c0cdceb39..4227c234f 100644 --- a/frontend/app/components/Session_/Performance/Performance.tsx +++ b/frontend/app/components/Session_/Performance/Performance.tsx @@ -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() {
Performance
- - - +
+ + + + +
diff --git a/frontend/app/components/Session_/Player/Controls/Controls.tsx b/frontend/app/components/Session_/Player/Controls/Controls.tsx index 0bddb0a23..d2dd042c0 100644 --- a/frontend/app/components/Session_/Player/Controls/Controls.tsx +++ b/frontend/app/components/Session_/Player/Controls/Controls.tsx @@ -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'; diff --git a/frontend/app/components/Session_/Player/Controls/EventsList.tsx b/frontend/app/components/Session_/Player/Controls/EventsList.tsx index 41010c766..e9994771a 100644 --- a/frontend/app/components/Session_/Player/Controls/EventsList.tsx +++ b/frontend/app/components/Session_/Player/Controls/EventsList.tsx @@ -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) => ( diff --git a/frontend/app/components/Session_/Player/Controls/Timeline.tsx b/frontend/app/components/Session_/Player/Controls/Timeline.tsx index 0888df62b..1d5d56467 100644 --- a/frontend/app/components/Session_/Player/Controls/Timeline.tsx +++ b/frontend/app/components/Session_/Player/Controls/Timeline.tsx @@ -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 (
: null}
- {props.isMobile ? : } + {isMobile ? : } diff --git a/frontend/app/components/Session_/Storage/Storage.tsx b/frontend/app/components/Session_/Storage/Storage.tsx index 2b138728e..43c5fd851 100644 --- a/frontend/app/components/Session_/Storage/Storage.tsx +++ b/frontend/app/components/Session_/Storage/Storage.tsx @@ -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, prevItem?: Record) => { + const renderDiff = ( + item: Record, + prevItem?: Record + ) => { if (!showDiffs) { return; } @@ -113,7 +121,10 @@ function Storage() { if (!stateDiff) { return ( -
+
No diff
); @@ -121,13 +132,15 @@ function Storage() { return (
- {stateDiff.map((d: Record, i: number) => renderDiffs(d, i))} + {stateDiff.map((d: Record, i: number) => + renderDiffs(d, i) + )}
); }; const renderDiffs = (diff: Record, i: number) => { - const path = diff.path.join('.') + const path = diff.path.join('.'); return ( @@ -145,12 +158,16 @@ function Storage() { player.jump(list[listNow.length].time); }; - const renderItem = (item: Record, i: number, prevItem?: Record) => { + const renderItem = ( + item: Record, + i: number, + prevItem?: Record + ) => { 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 (
{src === null ? ( @@ -187,7 +207,10 @@ function Storage() { ) : ( <> {renderDiff(itemD, prevItemD)} -
+
{typeof item?.duration === 'number' && ( -
{formatMs(itemD.duration)}
+
+ {formatMs(itemD.duration)} +
)}
{i + 1 < listNow.length && ( - )} @@ -222,31 +250,36 @@ function Storage() { }; if (type === STORAGE_TYPES.REDUX) { - return + return ; } return ( {/*@ts-ignore*/} <> - {list.length > 0 && ( -
-

- {'STATE'} -

- {showDiffs ? ( -

- DIFFS -

- ) : null} -

- {getActionsName(type)} -

-

- TTE -

+
+
+

{'STATE'}

- )} + {showDiffs ? ( +

+ DIFFS +

+ ) : null} +

+ {getActionsName(type)} +

+

+ TTE +

+ +

- @@ -322,8 +358,7 @@ function Storage() { {'Empty state.'}
) : ( - + )}
@@ -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]) * } * - * */ \ No newline at end of file + * */ diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index b96b94e07..193c7556f 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -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({ Console
- +
+ + +
{/* @ts-ignore */} {/* @ts-ignore */} @@ -211,6 +226,8 @@ function ConsolePanel({ iconProps={getIconProps(log.level)} renderWithNL={renderWithNL} onClick={() => showDetails(log)} + showSingleTab={showSingleTab} + getTabNum={getTabNum} /> ))} diff --git a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx index f61e04a9c..9581dd87f 100644 --- a/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx +++ b/frontend/app/components/shared/DevTools/ConsoleRow/ConsoleRow.tsx @@ -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 (
(!!log.errorId ? props.onClick?.() : toggleExpand()) : undefined} > -
- -
+ {logSource !== -1 && } +
-
+
{canExpand && ( diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index bb00633e6..8d15b713c 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -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 ( {r.type}
}> @@ -79,14 +73,6 @@ export function renderName(r: any) { ); } -export function renderStart(r: any) { - return ( -
- {Duration.fromMillis(r.time).toFormat('mm:ss.SSS')} -
- ); -} - 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 ( ); } @@ -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(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) =>
Tab {getTabNum?.(r.tabId) ?? 0}
, + }) + } + return cols + }, [showSingleTab]) + return ( )}
- +
+ + +
@@ -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} {selectedWsChannel ? ( setSelectedWsChannel(null)} /> diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx index 1ee227ae1..711504672 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx +++ b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx @@ -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} />
- +
+ + +
{ + uiPlayerStore.changeDataSource(value) + } + return ( + + ) +} + +export default observer(TabSelector) \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/TabTag.tsx b/frontend/app/components/shared/DevTools/TabTag.tsx new file mode 100644 index 000000000..a2e6f33b1 --- /dev/null +++ b/frontend/app/components/shared/DevTools/TabTag.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +function TabTag({ tabNum }: { tabNum?: React.ReactNode }) { + return ( +
+ {tabNum} +
+ ) +} + +export default TabTag \ No newline at end of file diff --git a/frontend/app/mstore/uiPlayerStore.ts b/frontend/app/mstore/uiPlayerStore.ts index dd642f047..d79e13c45 100644 --- a/frontend/app/mstore/uiPlayerStore.ts +++ b/frontend/app/mstore/uiPlayerStore.ts @@ -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; } diff --git a/frontend/app/player/common/SimpleStore.ts b/frontend/app/player/common/SimpleStore.ts index 82ddd3433..c1194df96 100644 --- a/frontend/app/player/common/SimpleStore.ts +++ b/frontend/app/player/common/SimpleStore.ts @@ -1,13 +1,18 @@ import { Store } from './types' -export default class SimpleSore implements Store { +export default class SimpleStore, S extends Record = G> implements Store { constructor(private state: G){} get(): G { return this.state } - update(newState: Partial) { + update = (newState: Partial) => { Object.assign(this.state, newState) } + updateTabStates = (id: string, newState: Partial) => { + try { + Object.assign(this.state.tabStates[id], newState) + } catch (e) { + console.log('Error updating tab state', e, id, newState, this.state, this) + } + } } - - diff --git a/frontend/app/player/common/types.ts b/frontend/app/player/common/types.ts index ad34097cb..e3f06f1dc 100644 --- a/frontend/app/player/common/types.ts +++ b/frontend/app/player/common/types.ts @@ -27,6 +27,7 @@ export interface Interval { export interface Store { get(): G update(state: Partial): void + updateTabStates(id: string, state: Partial): void } diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts index aa4bf58ad..4ff59cbf6 100644 --- a/frontend/app/player/web/MessageLoader.ts +++ b/frontend/app/player/web/MessageLoader.ts @@ -236,6 +236,7 @@ export default class MessageLoader { try { await this.loadMobs(); } catch (sessionLoadError) { + console.info('!', sessionLoadError); try { await this.loadEFSMobs(); } catch (unprocessedLoadError) { diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index d6ed2ed92..ba074a1cc 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -99,7 +99,7 @@ export default class MessageManager { closedTabs: [], sessionStart: 0, tabNames: {}, - }; +}; private clickManager: ListWalker = new ListWalker(); private mouseThrashingManager: ListWalker = 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, diff --git a/frontend/app/player/web/TabManager.ts b/frontend/app/player/web/TabManager.ts index baa0176fb..200d5f9b6 100644 --- a/frontend/app/player/web/TabManager.ts +++ b/frontend/app/player/web/TabManager.ts @@ -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) { - 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); diff --git a/scripts/helmcharts/vars.yaml b/scripts/helmcharts/vars.yaml index fa74a65fa..d2e89e1ec 100644 --- a/scripts/helmcharts/vars.yaml +++ b/scripts/helmcharts/vars.yaml @@ -29,6 +29,7 @@ clickhouse: &clickhouse password: "" service: webPort: 9000 + dataPort: 8123 # For enterpriseEdition quickwit: &quickwit